Cloud Foundry和Kubernetes结合的过去与未来

过去的几年中,在云计算领域的开源社区中最有争议的话题莫过于Cloud Foundry和Kubernetes的关系。大家的疑问紧紧围绕着三个问题:“它们会互相取代对方吗?”,“它们是互斥的吗?” ,“还是说它们是可以融合的?”。放眼望去在目前的商业产品中,两者几乎没什么关联和集成,都可以运行在各种IaaS之上。

多年以来,我们一直从事Cloud Foundry部署的相关工作,从原来传统的BOSH CPI到现在的容器化Cloud Foundry的工作。在这期间,我们对两者结合的多种技术选型的进行探索,包括可行性分析、最佳实践、经验总结和价值意义等方面,旨在最大程度上利用和发挥两者的优势,并提升开发者体验。

本文将从Cloud Foundry部署的层面,来介绍业界和我们如何去把两套系统进行整合的。当然任何整合方案都会有各自的优缺点,而且所有的整合还在一个快速发展的阶段,所以在这里算是给大家抛砖引玉,为以后的工作和学习提供一些帮助。

Cloud Foundry和Kubernetes简介

Cloud Foundry

Cloud Foundry是一个独立于云的平台即服务解决方案,也是业界最成功的PaaS平台。Cloud Foundry提供了一个可轻松运行、扩展和维护应用程序的环境和快速便捷的开发者体验。Cloud Foundry支持Java、NodeJS、Ruby、Python等大多数语言和环境。

开源的Cloud Foundry由Cloud Foundry基金会开发并支持,基金会包括Pivotal、IBM、VMware以及其它许多厂商。商业版本的Cloud Foundry,如IBM Bluemix和Pivotal Cloud Foundry,是基于开源的Cloud Foundry项目并在其基础上提供企业级的支持。

Kubernetes

Kubernetes是一个来源于谷歌Borg项目的开源云平台。它由Cloud Native Computing Foundation发起,该基金会的成员包括了许多行业巨头,如AWS、Azure、Intel、IBM、RedHat、Pivotal等许多公司。

Kubernetes首要的功能是一个容器编排和容器生命周期的管理。尽管不限于此,但它通常是被用来运行Docker容器,它的受众人群更广泛一些,比如想要构建在容器服务之上的应用和服务开发人员。有一些解决方案基于Kubernetes提供了PaaS体验,比如IBM的Container Service和RedHat的OpenShift等。

两个平台的比较

两个平台都很成熟和完整,而且随着不断的开发和改进,所以两个平台的相同和不同之处也会随之变。当然两大项目有很多相似之处,包括容器化,命名空间和身份验证等机制。而两者也有各自的优势:

Cloud Foundry的优势在于:

  1. 成熟的身份验证系统UAA,用户组和multi-tenancy的支持
  2. 方便快捷的cf push
  3. 自带负载均衡Router
  4. 强大的日志和metrics整合
  5. 成熟的部署工具BOSH

Kubernetes的优势在于:

  1. 大量社区和第三方支持,提供强大的扩展性
  2. 完善的容器生命周期和自动伸缩管理
  3. 方便快捷的容器化应用部署
  4. 良好、多样的持久层支持
  5. 多种开源UI支持

现阶段两者的关系及结合情况

从上面的比较,我们可以看到,两大平台各有各的优势及强大之处。业界有很多厂商都想尝试将两者融合,从而将优势发挥到最大化。从现在来看两者整合主要有以下三种方式:

Kubernetes CPI

我们知道BOSH是Cloud Foundry官方指定的部署工具,它是一个针对大规模分布式系统的部署和生命周期管理的开源工具。但是BOSH不仅仅局限于部署Cloud Foundry,也可以应用于别的分布式系统,只需要其提供符合要求的Release即可。CPI全称Cloud Provider Interface,是BOSH用来与IaaS通信完成虚拟机实例和模板的创建和管理的一个API接口,CPI目前能够支持Amazon的AWS、微软的Azure、IBM的SoftLayer等IaaS平台,国内阿里云也提供了CPI的支持。BOSH的主控制器Director通过CPI与底层的IaaS层交互,将BOSH manifest.yaml 文件定义任务及组件部署到IaaS层的VM上,如下图所示:

未分类

所以,第一种整合,也就是开发一套Kubernetes的CPI,通过BOSH和manifest.yaml的配置将Cloud Foundry部署到Kubernetes上。现在有一些大的厂商如IBM、SAP在开发相应的Kubernetes CPI,大家可以在GitHub中搜索到。我个人觉得,这种方式虽然容易上手,但还是以IaaS的角度来看待Kubernetes,底层还是通过BOSH来管理的,没能最大地发挥Kubernetes平台的优势的。

Cloud Foundry Container Runtime(CFCR)

Cloud Foundry基金会在2017年底宣布把Pivotal和谷歌捐献的Kubo项目改名为CFCR(Cloud Foundry Container Runtime)。总体来说是利用BOSH来部署Cloud Foundry和Kubernetes,并通过Application Runtime管理Cloud Foundry的Application Service;通过Container Runtime管理Kubernetes的Container Service,比如一些无法部署在Cloud Foundry内的服务,如数据库、监控等。然后通过Open Service Broker将两者连接起来,如下图所示:

未分类

容器化Cloud Foundry

将Cloud Foundry所有组件容器化,并部署到Kubernetes上是一种比较新型的整合方式。现在有一些大的厂商做这方面的工作,比如SUSE、IBM和SAP。其核心就是通过将Cloud Foundry的BOSH Release转化成Docker镜像,然后通过Kubernetes的部署工具Helm来将Cloud Foundry更加自然地部署到Kubernetes上。只有在生成Cloud Foundry组件的Docker镜像是需要用到BOSH Cli去转化BOSH release,部署及部署之后的管理,都不需要BOSH,而是交给Kubernetes来对所有Cloud Foundry组件的Pod、服务及相应配置资源的生命周期进行管理。这样既享受到了Cloud Foundry带来的良好的开发者体验,又用到了Kubernetes强大的管理和扩展能力:

未分类

IBM在两者结合上做的工作和成果

IBM主要是和SUSE合作,在上面第三种方案的基础上,创建一套可以在IBM Cloud上快速部署的企业级Cloud Foundry。这个新的服务名称叫做IBM Cloud Foundry Enterprise Environment(CFEE),他的主要构件流程如下:

  1. 首先我们需要将BOSH的Release通过SUSE的转换工具Fissile将其编译并制作成CF组件相应的Docker镜像。
  2. 之后我们需要通过Fissile将预设的参数转换成Helm或者Kubernetes的配置资源文件。
  3. 利用IBM Cloud强大的服务资源,增加企业级服务的支持,比如数据库服务,安全服务和负载均衡服务。
  4. 最后发布新的IBM CF版本,用户可以直接在IBM Cloud的Catalog里面找到并自动部署出一套CFEE环境。

和传统的BOSH CPI的部署方式的比较,新的Cloud Foundry版本发布后,管理员只需执行一次来构建任务来创建新版本Docker镜像和Helm配置,之后就可以重复使用了。这种部署Cloud Foundry的方式时间大概在15-20分钟,和之前BOSH部署4-6个小时相比,快了很多。

IBM已于2018上半年的Cloud Foundry Summit上发布了Experimental版本:

未分类

在刚过去的2018年10月初,IBM又发布了CFEE的GA版,主要为企业级客户提供更好的服务、带来更大的价值。部署出来的CFEE环境不但与其他环境是相互隔离的,而且CFEE环境中部署的Cloud Foundry应用还可以很轻松地使用IBM Cloud上丰富的服务资源,如AI,大数据,IoT和Blockchain等:

未分类

感兴趣的用户可以通过IBM Cloud的Catalog找到CFEE的介绍:https://console.bluemix.net/docs/cloud-foundry/index.html#creating。

在部署页面中,用户可以配置CFEE的域名,Diego-cell的个数以及CF版本等信息,然后CFEE可以实现全自动化一键部署,大概1个小时左右,一个企业级的CF环境就部署成功了:

未分类

当然在部署完成后,CFEE还提供了强大的管理,监控和命令行等功能,如图所示,用户可以很轻松地在页面上查看Cloud Foundry和应用的使用情况,创建相应的资源或者绑定IBM的外部服务等:

未分类

未来两者结合的发展方向

当然两者结合的探索工作不止于此,现在越来越多的厂商和开发者加入到两者整合的研究中。其中比较火的有IBM主导的Cloud Foundry孵化项目Eirini,其想法是想将Cloud Foundry中的Diego-cell容器Garden替换成Kubernetes的容器,从而将两者更紧密地连接在一起:

未分类

还有像SUSE主导的Fissile项目,未来其想法是想通过BOSH命令可以直接生成CF的Docker镜像和Helm配置,从而可以更好地和社区融合到一起。

总结

综上所述,Kubernetes之于Cloud Foundry的关系不是挑战也不是竞争关系,Cloud Foundry希望与其更好地融合,就像Cloud Foundry Foundation执行董事Abby Kearns说的:“Cloud Foundry是结构化的PaaS平台,其他平台是非结构化的,用户的需求是多元化的,并不是一定要如何容器化,而是希望平台能够更开放、支持更多的类型。”

不管未来两者怎么发展,两者的结合绝对是适应市场的趋势,同时又能给用户带来多方面的价值提升。未来两者还能碰撞出什么样的火花?让我们拭目以待!

Kubernetes使用过程中遇到的一些问题与解决方法

说明

这里记录Kubernetes使用过程中遇到的一些比较简单的问题与解决方法,比较复杂的问题会单独分析:https://www.lijiaocn.com/tags/kubernetes.html

kubectl exec登陆容器后,shell终端窗口宽度太小

这个其实不是kubectl的问题,而是shell终端的问题,在shell终端中可以通过变量COLUMNS修改宽度:

export COLUMNS=210

这个变量可以进入容器后设置,也可以在执行exec命令时传入,例如:

kubectl exec -ti busybox -- env COLUMNS=500 LINES=100 bash

使用 Kubeadm 升级 Kubernetes 版本

升级最新版 kubelet kubeadm kubectl (阿里云镜像)

cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
setenforce 0
yum install -y kubelet kubeadm kubectl
systemctl enable kubelet && systemctl start kubelet

查看此版本的容器镜像版本

$ kubeadm config images list

k8s.gcr.io/kube-apiserver:v1.12.1
k8s.gcr.io/kube-controller-manager:v1.12.1
k8s.gcr.io/kube-scheduler:v1.12.1
k8s.gcr.io/kube-proxy:v1.12.1
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.2.24
k8s.gcr.io/coredns:1.2.2

查询可用的版本

$ yum list --showduplicates | grep 'kubeadm|kubectl|kubelet'

拉取容器镜像(github)

echo ""
echo "=========================================================="
echo "Pull Kubernetes v1.12.1 Images from aliyuncs.com ......"
echo "=========================================================="
echo ""

MY_REGISTRY=registry.cn-hangzhou.aliyuncs.com/openthings

## 拉取镜像
docker pull ${MY_REGISTRY}/k8s-gcr-io-kube-apiserver:v1.12.1
docker pull ${MY_REGISTRY}/k8s-gcr-io-kube-controller-manager:v1.12.1
docker pull ${MY_REGISTRY}/k8s-gcr-io-kube-scheduler:v1.12.1
docker pull ${MY_REGISTRY}/k8s-gcr-io-kube-proxy:v1.12.1
docker pull ${MY_REGISTRY}/k8s-gcr-io-etcd:3.2.24
docker pull ${MY_REGISTRY}/k8s-gcr-io-pause:3.1
docker pull ${MY_REGISTRY}/k8s-gcr-io-coredns:1.2.2


## 添加Tag
docker tag ${MY_REGISTRY}/k8s-gcr-io-kube-apiserver:v1.12.1 k8s.gcr.io/kube-apiserver:v1.12.1
docker tag ${MY_REGISTRY}/k8s-gcr-io-kube-scheduler:v1.12.1 k8s.gcr.io/kube-scheduler:v1.12.1
docker tag ${MY_REGISTRY}/k8s-gcr-io-kube-controller-manager:v1.12.1 k8s.gcr.io/kube-controller-manager:v1.12.1
docker tag ${MY_REGISTRY}/k8s-gcr-io-kube-proxy:v1.12.1 k8s.gcr.io/kube-proxy:v1.12.1
docker tag ${MY_REGISTRY}/k8s-gcr-io-etcd:3.2.24 k8s.gcr.io/etcd:3.2.24
docker tag ${MY_REGISTRY}/k8s-gcr-io-pause:3.1 k8s.gcr.io/pause:3.1
docker tag ${MY_REGISTRY}/k8s-gcr-io-coredns:1.2.2 k8s.gcr.io/coredns:1.2.2

echo ""
echo "=========================================================="
echo "Pull Kubernetes v1.12.1 Images FINISHED."
echo "into registry.cn-hangzhou.aliyuncs.com/openthings, "
echo "           by openthings@https://my.oschina.net/u/2306127."
echo "=========================================================="

echo ""

备注:或参考https://www.cnblogs.com/Irving/p/9818440.html的镜像脚本

查询需要升级的信息

$ kubeadm upgrade plan

升级 Master 节点

$ kubeadm upgrade apply v1.12.1

Node 节点升级

升级对应的 kubelet kubeadm kubectl 的版本,拉取对应版本的镜像即可。

查询各节点信息与 pod 信息

$ kubectl get nodes
$ kubectl get pod --all-namespaces -o wide

k8s(十三)、企业级docker仓库Harbor在kubernetes上搭建使用

前言

Harbor是Docker镜像仓库管理系统,用于企业级管理容器镜像,支持对接ldap进行权限管理,由VMVare中国团队开发。

项目地址:
https://github.com/goharbor/harbor/

安装方式:
1.Docker Compose方式部署:
https://github.com/goharbor/harbor/blob/master/docs/installation_guide.md

2.通过kubernetes直接部署:
新版本已废弃

3.通过helm/kubernetes方式部署:
https://github.com/goharbor/harbor-helm/blob/master/README.md
推荐使用,本文将采用此方式

Helm安装

helm简介

Helm是一个用于Kubernetes的包管理工具,每一个安装包构成一个chart基本单位.想象一下,一个完整的部署,可能包含前端后端中间件DB的部署实例,以及k8s管理对应的secret/configmap/svc/ing/pv/pvc等众多资源类型,在部署管理或者卸载之前,如果逐一编辑管理这些资源的yaml文件,工作量比较庞大且容易疏漏.helm的出现很好的解决了这个问题,使用模板以及变量替换的方式,按需生成部署yaml文件包,并且向kubernetes api发送调度请求,虽然额外地引入了一些学习成本,但对于部署包的管理无疑是带来了很大的便利性的.

helm组件

Helm

Helm是一个cli客户端,可以完成如下内容:

1.创建/打包/调试Chart
2.创建本地Chart仓库,管理本地和远程Chart仓库
3.与Tiller交互并完成Chart的安装,升级,删除,回滚,查看等操作

Tiller

1.监听helm客户端的请求,根据chart生成相应的release
2.将release的资源信息发送给kubernetes api,实现资源增删改查.
3.追踪release的状态,实现资源的更新/回滚操作

安装:
安装较为简单,参考官方文档,不做复述:
https://docs.helm.sh/using_helm/#installing-helm

Harbor安装

这里采用次新版本的v1.5.3版本

root@h1:# wget https://github.com/goharbor/harbor/archive/v1.5.3.tar.gz
root@h1:# tar -xf https://github.com/goharbor/harbor/archive/v1.5.3.tar.gz 
root@h1:# cd harbor-1.5.3/contrib/helm/harbor/

helm方式安装的harbor默认是https的,因此需要k8s集群ingress网关开启https协议,traefik开启https参考此前文章:
https://blog.csdn.net/ywq935/article/details/79886793

更新helm dependency

harbor的helm部署依赖了postgresql的helm,在官方的安装文档没有明确说明,直接按照官方文档说明安装,就会缺失postgresql的部署,导致helm install failed

helm dependency update

修改value.yaml文件,添加pvc持久存储并将相应组件上下文中的volume部分注释打开:

mysql:
  volumes:
    data:
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce

adminserver:
  volumes:
    config:
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce
    size: 1G

registry:
  volumes:
    data:
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce
    size: 5G

完整value.yaml文件如下:

# Configure persisten Volumes per application
## Applications that require storage have a `volumes` definition which will be used
## when `persistence.enabled` is set to true.
## example
# mysql:
#   volumes:
#     data:
## Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
##   set, choosing the default provisioner.  (gp2 on AWS, standard on
##   GKE, AWS & OpenStack)
##
#     storageClass: "-"
#     accessMode: ReadWriteOnce
#     size: 1Gi

mysql:
  volumes:
    data:
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce
adminserver:
  volumes:
    config:
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce
    size: 1G

registry:
  volumes:
    data:
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce
    size: 5G

## Configure resource requests and limits per application
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
##
# mysql:
#   resources:
#     requests:
#       memory: 256Mi
#       cpu: 100m

persistence:
  enabled: true

# The tag for Harbor docker images.
harborImageTag: &harbor_image_tag v1.4.0

# The FQDN for Harbor service.
externalDomain: harbor.my.domain
# If set to true, you don't need to set tlsCrt/tlsKey/caCrt, but must add
# Harbor FQDN as insecure-registries for your docker client.
insecureRegistry: false
#insecureRegistry: true
# The TLS certificate for Harbor. The common name of tlsCrt must match the externalDomain above.
tlsCrt: 
#tlsCrt: root/traefik/ssl/kokoerp.crt
tlsKey:
#tlsKey: root/traefik/ssl/kokoerp.key
caCrt:

# The secret key used for encryption. Must be a string of 16 chars.
secretKey: not-a-secure-key

# These annotations allow the registry to work behind the nginx
# ingress controller.
ingress:
  annotations:
    ingress.kubernetes.io/ssl-redirect: "true"
    ingress.kubernetes.io/body-size: "0"
    ingress.kubernetes.io/proxy-body-size: "0"

adminserver:
  image:
    repository: vmware/harbor-adminserver
    tag: *harbor_image_tag
    pullPolicy: IfNotPresent
  emailHost: "smtp.mydomain.com"
  emailPort: "25"
  emailUser: "[email protected]"
  emailSsl: "false"
  emailFrom: "admin <[email protected]>"
  emailIdentity: ""
  emailInsecure: "False"
  emailPwd: not-a-secure-password
  adminPassword: Harbor12345
  authenticationMode: "db_auth"
  selfRegistration: "on"
  ldap:
    url: "ldaps://ldapserver"
    searchDN: ""
    baseDN: ""
    filter: "(objectClass=person)"
    uid: "uid"
    scope: "2"
    timeout: "5"
    verifyCert: "True"
  ## Persist data to a persistent volume
  volumes:
    config:
      storageClass: "cephrbd"
      accessMode: ReadWriteOnce
      size: 1Gi
  # resources:
  #  requests:
  #    memory: 256Mi
  #    cpu: 100m

## jobservice
#
jobservice:
  image:
    repository: vmware/harbor-jobservice
    tag: *harbor_image_tag
    pullPolicy: IfNotPresent
  secret: not-a-secure-secret
# resources:
#   requests:
#     memory: 256Mi
#     cpu: 100m

## UI
#
ui:
  image:
    repository: vmware/harbor-ui
    tag: *harbor_image_tag
    pullPolicy: IfNotPresent
  secret: not-a-secure-secret
  privateKeyPem: |
    -----BEGIN RSA PRIVATE KEY-----
    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
    -----END RSA PRIVATE KEY-----
# resources:
#  requests:
#    memory: 256Mi
#    cpu: 100m

## MySQL Settings. Currently Harbor does not support an external
## MySQL server, only their own image. Until this is fixed, do not
## Change the settings below.
#
mysql:
  image:
    repository: vmware/harbor-db
    tag: *harbor_image_tag
    pullPolicy: IfNotPresent
  # If left blank will use the included mysql service name.
  host: ~
  port: 3306
  user: "root"
  pass: "registry"
  database: "registry"
  volumes:
    data:
      storageClass: "cephrbd"
      accessMode: ReadWriteOnce
      size: 1Gi
  # resources:
  #  requests:
  #    memory: 256Mi
  #    cpu: 100m

registry:
  image:
    repository: vmware/registry-photon
    tag: v2.6.2-v1.4.0
    pullPolicy: IfNotPresent
  httpSecret: not-a-secure-secret
  logLevel:
# comment out one of the below to use your cloud's object storage.
#  objectStorage:
#    gcs:
#      keyfile: ""
#      bucket: ""
#      chunksize: "5242880"
#    s3:
#      region: ""
#      accesskey: ""
#      secretkey: ""
#      bucket: ""
#      encrypt: "true"
#    azure:
#      accountname: ""
#      accountkey: ""
#      container: ""
  rootCrt: |
    -----BEGIN CERTIFICATE-----
    MIIE0zCCArugAwIBAgIJAIgs3S+hsjhmMA0GCSqGSIb3DQEBCwUAMAAwHhcNMTcx
    MTA5MTcyNzQ5WhcNMjcxMTA3MTcyNzQ5WjAAMIICIjANBgkqhkiG9w0BAQEFAAOC
    Ag8AMIICCgKCAgEA4WYbxdrFGG6RnfyYKlHYML3lEqtA9cYWWOynE9BeaEr/cMnM
    bBr1dd91/Nm6RiYhQvTDU2Kc6NejqjdliW5B9xUoVKayri8OU81a8ViXeNgKwCPR
    AiTTla1zoX5DnvoxpO9G3lxyNvTKXc0cw8NjQDAXpaDbzJYLkshCeuyD9bco8R96
    /zrpBEX8tADN3+3yA3fMcZzVXsBm4BTpHJRk/qBpHYEPSHzxyH3iGMNKk3vMUBZz
    e0EYkK8NCA2CuEKMnC3acx9IdRwkx10abGvHQCLRCVY7rGoak+b0oZ99RJIRQ9Iq
    YXsn8fsMBQly6xxvSeY5XuSP7Xb6JKDt3y8Spi4gR1M/5aEzhuOyu201rMna7Rs/
    GPfaKjBlbX0jiLDa7v4zjsBPsPaf/c4uooz3ICLsdukaom+E538R0EiOkXt/wyw2
    2YmaWNCsYlEpke7cVC33e/0dPBq4IHsVflawSF9OWS23ikVAs/n+76KjuucEDmbT
    aKUYAJjvAmZL14j+EKc/CoplhCe6pKhavjmNIOfCSdlreIPBhOVbf1f817wKoSIZ
    qVyCA1AYNkI9RYS00axtJGBGMlKbdQqCNpLL58c6To2awmckIZCEcATKOp++NoGm
    Ib0bhdSasdGB5VCtwZVluN8bLl13zBKoxTGjNlEatUGDRnDAnLdZbXXffjsCAwEA
    AaNQME4wHQYDVR0OBBYEFCMYYMOL0E/Uyj5wseDfIl7o4ELsMB8GA1UdIwQYMBaA
    FCMYYMOL0E/Uyj5wseDfIl7o4ELsMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL
    BQADggIBABG8fPvrrR+erpwQFuB/56j2i6sO+qoOJPpAMYwkzICrT0eerWAavwoy
    f0UAKN7cUeEJXjIR7s7CogGFijWdaWaQsXUD0zJq5aotLYZLimEc1O0uAmJEsfYC
    v7mG07eU6ge22sSo5hxhVplGt52hnXnT0DdgSRZpq2mvgd9lcopAidM+KHlaasXk
    IecHKM99KX9D8smr0AcQ6M/Ygbf2qjO9YRmpBIjyQWEake4y/4LWm+3+v08ecg4B
    g+iMC0Rw1QcPqgwaGaWu71RtYhyTg7SnAknb5nBcHIbLb0hdLgQTa3ZdtXgqchIi
    GuFlEBmHFZP6bLJORRUQ0ari5wpXIsYfrB4T8PybTzva3OCMlEsMjuysFr9ewhzM
    9UGLiSQNDyKA10J8WwlzbeD0AAW944hW4Dbg6SWv4gAo51T+6AukRdup5y6lfQ5a
    h4Lbo6pzaA369IsJBntvKvia6hUf/SghnbG7pCHX/AEilcgTb13HndF/G+7aZgKR
    mi9qvNRSDsE/BrgZawovp81+j6aL4y6UtXYspHr+SuWsKYsaH7pl5HspNCyJ5vV6
    dpJAwosFBqSEnI333wAunpMYmi/jKHH/j4WqjLnCInp0/wouzYu42l8Pmz591BSp
    Jag500bEBxqI2RLELgMt/bUdjp4N2M7mrxdrN+2579HTzb6Hviu9
    -----END CERTIFICATE-----
  ## Persist data to a persistent volume
  volumes:
    data:
      storageClass: "cephrbd"
      accessMode: ReadWriteOnce
      size: 5Gi
  # resources:
  #  requests:
  #    memory: 256Mi
  #    cpu: 100m

clair:
  enabled: true
  image:
    repository: vmware/clair-photon
    tag: v2.0.1-v1.4.0
    pullPolicy: IfNotPresent
## The following needs to match the credentials
## in the `postgresql` configuration under the
## `postgresql` namespace below.
  postgresPassword: not-a-secure-password
  postgresUser: clair
  postgresDatabase: clair
# resources:
#  requests:
#    memory: 256Mi
#    cpu: 100m
# pgResources:
#  requests:
#    memory: 256Mi
#    cpu: 100m
#  volumes:
#    pgData:
#      storageClass: "cephrbd"
#      accessMode: ReadWriteOnce
#      size: 1Gi
  # resources:
  #  requests:
  #    memory: 256Mi
  #    cpu: 100m

## Notary support is not yet fully implemented in the Helm Charts
## Enabling it will just break things.
#
notary:
  enabled: false

## Settings for postgresql dependency.
## see https://github.com/kubernetes/charts/tree/master/stable/postgresql
## for further configurables.
postgresql:
  postgresUser: clair
  postgresPassword: not-a-secure-password
  postgresDatabase: clair
  persistence:
    # enabled: false
    enabled: true
    storageClass: "cephrbd"
    accessMode: ReadWriteOnce
    size: 8Gi

部署

helm install . --debug --name hub --set externalDomain=hub.test.com

等待一段时间的镜像拉取后,查看pod:

hub-harbor-adminserver-0                 1/1       Running            1          3h
hub-harbor-clair-6c7d9dcdb7-q4lv4        1/1       Running            2          3h
hub-harbor-jobservice-75f7fbcc9c-ggwp4   1/1       Running            2          3h
hub-harbor-mysql-0                       1/1       Running            0          3h
hub-harbor-registry-0                    1/1       Running            1          3h
hub-harbor-ui-57b4674ff9-kcfq6           1/1       Running            0          3h
hub-postgresql-ccf8d56d5-jg4wq           1/1       Running            1          3h

添加dns,指向ingress gateway(即traefik的node ip):

echo "192.168.1.238  hub.test.com"  >> /etc/hosts

浏览器打开测试,默认登录口令为admin/Harbor12345

未分类

可以看到默认有一个公开项目library,尝试往这里推送镜像

Docker推送测试

找一台安装了docker的机器,修改docker服务脚本:

~# cat /lib/systemd/system/docker.service

#ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd  --insecure-registry hub.kokoerp.com

重启docker并登录测试:

~# systemctl daemon-reload
~# systemctl restart docker

root@h2:~# docker login hub.test.com
Username (admin): admin
Password: 
Login Succeeded

推送镜像测试:

root@h2:~# docker tag busybox hub.test.com/library/busybox:test1
root@h2:~# docker push hub.test.com/library/busybox:test1
The push refers to a repository [hub.test.com/library/busybox]
8a788232037e: Pushed 
test1: digest: sha256:915f390a8912e16d4beb8689720a17348f3f6d1a7b659697df850ab625ea29d5 size: 527

可以看到推送成功

ldap配置

未分类

harbor可以基于企业内部的ldap来获取人员信息,可以在ui上创建非公开项目,将ldap中获取的人员加入项目并赋予相应pull push权限,ui操作界面比较友好,这里就不展示了.

在Kubernetes上搭建RabbitMQ Cluster

为了在Kubernetes上搭建RabbitMQ3.7.X Cluster,踩爆无数坑,官方整合了第三方开源项目但没有完整demo,网上的post都是RabbitMQ 3.6.X旧版的部署方案,几经周折,最终弄明白在Kubernetes集群下,基于Kubernetes Discovery,使用hostname方式部署RabbitMQ3.7.X Cluster,总结如下:

1. IP模式

rabbitmq-peer-discovery-k8s是RabbitMQ官方基于第三方开源项目rabbitmq-autocluster开发,对3.7.X版本提供的Kubernetes下的同行发现插件,但官方只提供了一个基于IP模式的demo

kind: Service
apiVersion: v1
metadata:
  namespace: test-rabbitmq
  name: rabbitmq
  labels:
    app: rabbitmq
    type: LoadBalancer  
spec:
  type: NodePort
  ports:
   - name: http
     protocol: TCP
     port: 15672
     targetPort: 15672
     nodePort: 31672
   - name: amqp
     protocol: TCP
     port: 5672
     targetPort: 5672
     nodePort: 30672
  selector:
    app: rabbitmq
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-config
  namespace: test-rabbitmq
data:
  enabled_plugins: |
      [rabbitmq_management,rabbitmq_peer_discovery_k8s].
  rabbitmq.conf: |
      ## Cluster formation. See http://www.rabbitmq.com/cluster-formation.html to learn more.
      cluster_formation.peer_discovery_backend  = rabbit_peer_discovery_k8s
      cluster_formation.k8s.host = kubernetes.default.svc.cluster.local
      ## Should RabbitMQ node name be computed from the pod's hostname or IP address?
      ## IP addresses are not stable, so using [stable] hostnames is recommended when possible.
      ## Set to "hostname" to use pod hostnames.
      ## When this value is changed, so should the variable used to set the RABBITMQ_NODENAME
      ## environment variable.
      cluster_formation.k8s.address_type = ip
      ## How often should node cleanup checks run?
      cluster_formation.node_cleanup.interval = 30
      ## Set to false if automatic removal of unknown/absent nodes
      ## is desired. This can be dangerous, see
      ##  * http://www.rabbitmq.com/cluster-formation.html#node-health-checks-and-cleanup
      ##  * https://groups.google.com/forum/#!msg/rabbitmq-users/wuOfzEywHXo/k8z_HWIkBgAJ
      cluster_formation.node_cleanup.only_log_warning = true
      cluster_partition_handling = autoheal
      ## See http://www.rabbitmq.com/ha.html#master-migration-data-locality
      queue_master_locator=min-masters
      ## See http://www.rabbitmq.com/access-control.html#loopback-users
      loopback_users.guest = false

---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: rabbitmq
  namespace: test-rabbitmq
spec:
  serviceName: rabbitmq
  replicas: 3
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      serviceAccountName: rabbitmq
      terminationGracePeriodSeconds: 10
      containers:        
      - name: rabbitmq-k8s
        image: rabbitmq:3.7
        volumeMounts:
          - name: config-volume
            mountPath: /etc/rabbitmq
        ports:
          - name: http
            protocol: TCP
            containerPort: 15672
          - name: amqp
            protocol: TCP
            containerPort: 5672
        livenessProbe:
          exec:
            command: ["rabbitmqctl", "status"]
          initialDelaySeconds: 60
          periodSeconds: 60
          timeoutSeconds: 10
        readinessProbe:
          exec:
            command: ["rabbitmqctl", "status"]
          initialDelaySeconds: 20
          periodSeconds: 60
          timeoutSeconds: 10
        imagePullPolicy: Always
        env:
          - name: MY_POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
          - name: RABBITMQ_USE_LONGNAME
            value: "true"
          # See a note on cluster_formation.k8s.address_type in the config file section
          - name: RABBITMQ_NODENAME
            value: "rabbit@$(MY_POD_IP)"
          - name: K8S_SERVICE_NAME
            value: "rabbitmq"
          - name: RABBITMQ_ERLANG_COOKIE
            value: "mycookie" 
      volumes:
        - name: config-volume
          configMap:
            name: rabbitmq-config
            items:
            - key: rabbitmq.conf
              path: rabbitmq.conf
            - key: enabled_plugins
              path: enabled_plugins

在ConfigMap配置项中,指明 cluster_formation.k8s.address_type = ip,也就是说RabbitMQ Node的命名和访问地址是以IP地址作为区分,如[email protected]

但这样的配置会产生比较大的问题,如果我们使用pv和pvc去做数据的持久化,那么每个节点的配置和数据存储都会放在[email protected]这样的文件夹下,而Kubernetes集群中,Pod的IP都是不稳定的,当有RabbitMQ Node的Pod挂掉后,重新创建的Pod IP可能会变,这就会使得节点的配置和数据全部丢失。

所以我们更希望RabbitMQ Node的命名是以一定规则编写的相对稳定的名称,如rabbit@rabbit-0,这就需要修改 cluster_formation.k8s.address_type = hostname,以启用hostname模式。

但直接修改address_type 并不能满足要求,注释部分也描述了“Set to hostname to use pod hostnames. When this value is changed, so should the variable used to set the RABBITMQ_NODENAME”。那么RABBITMQ_NODENAME该如何设置,就必须先要了解如何用hostname访问pod

2. Pod与Service的DNS

Kubernetes官方讲述了如何用hostname访问service和pod:https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/

其中对于service,可以直接使用my-svc.my-namespace.svc.cluster.local进行访问;而对于pod,则需使用pod-ip-address.my-namespace.pod.cluster.local进行访问,但这里却仍显式的应用到了pod的ip。我们希望脱离ip对pod进行访问,很不幸的是,pod确实无法直接通过hostname访问,不过却有个曲线救国的方案。

apiVersion: v1
kind: Service
metadata:
  name: default-subdomain # 和pod的subdomain相同
spec:
  selector:
    name: busybox
  clusterIP: None  # clusterIP: None表示这是一个headless service
  ports:
  - name: foo # 没啥用
    port: 1234
    targetPort: 1234
---
apiVersion: v1
kind: Pod
metadata:
  name: busybox1
  labels:
    name: busybox
spec:
  hostname: busybox-1 # 默认使用metadata.name作为hostname,也可指定设置
  subdomain: default-subdomain
  containers:
  - image: busybox
    command:
      - sleep
      - "3600"
    name: busybox

如上面代码所示,我们需要一个headless service来作为中介,这样就可以使用busybox-1.default-subdomain.default.svc.cluster.local来访问pod了(hostname.subdomain.my-namespace.svc.cluster.local)

3. Statefulset 与Headless Service

了解了如何用hostname访问Pod还不足以解决问题,在RabbitMQ的配置中,我们使用的是StatefulSet,那么StatefulSet如何用Headless Service去做Pod的hostname访问呢?

Kubernetes(StatefulSets在1.9版本后已经是一个稳定功能)官方也给出了详细的说明:https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/

Demo和注释如下:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None # 是一个headless service
  selector:
    app: nginx
---
apiVersion: apps/v1 # 需要注意如果是apps/v1,.spec.selector.matchLabels和.spec.template.metadata.labels要相同;如果是apps/v1beta,可以省略.spec.selector.matchLabels
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx # 需要与 .spec.template.metadata.labels 相同,但无需与headless service name相同
  serviceName: "nginx" # 需要与headless service name相同
  replicas: 3 
  template:
    metadata:
      labels:
        app: nginx #  需要与 .spec.selector.matchLabels 相同,但无需与headless service name相同
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web

需要特别注意的是,网上很多例子的StatefulSet用的apps/v1beta

4. hostname模式

在我查找的众多资料中,在Kubernetes中

讲RabbitMQ 3.6.X部署的,www.kubernetes.org.cn/2629.html 这篇讲的比较清楚

讲RabbitMQ 3.7.X部署的,https://habr.com/company/eastbanctech/blog/419817/ 这篇俄文的Post讲的比较清楚,但它也是用的apps/v1beta,同时有大量的重复配置,不知道哪些可用哪些无用,还有一个最致命的问题是按照它的配置部署后,readinessProbe老报错,说DNS解析出现问题。几经折腾,才明白因为用Headless Service去做Pod的hostname访问,需要等Pod和Service都启动后才能访问,而readiness探针还没等DNS正常就去探查服务是否可用,所以才会误认为服务不可达,最终无法启动Pod。解决办法是给Headless Service设置publishNotReadyAddresses: true

我的配置文件如下所示:

apiVersion: v1
kind: Namespace
metadata:
  name: rabbitmq
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rabbitmq 
  namespace: rabbitmq 
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: endpoint-reader
  namespace: rabbitmq 
rules:
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: endpoint-reader
  namespace: rabbitmq
subjects:
- kind: ServiceAccount
  name: rabbitmq
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: endpoint-reader
---
apiVersion: v1
kind: PersistentVolume
metadata:
    name: rabbitmq-data
    labels:
      release: rabbitmq-data
    namespace: rabbitmq
spec:
    capacity:
      storage: 10Gi
    accessModes:
      - ReadWriteMany
    persistentVolumeReclaimPolicy: Retain
    nfs:
      path: /rabbit
      server: xxxxx  # nas地址
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rabbitmq-data-claim
  namespace: rabbitmq
spec:
  accessModes:
    - ReadWriteMany
  resources:  
    requests:
      storage: 10Gi
  selector:
    matchLabels:
      release: rabbitmq-data
---
# headless service 用于使用hostname访问pod
kind: Service
apiVersion: v1
metadata:
  name: rabbitmq-headless
  namespace: rabbitmq
spec:
  clusterIP: None
  # publishNotReadyAddresses, when set to true, indicates that DNS implementations must publish the notReadyAddresses of subsets for the Endpoints associated with the Service. The default value is false. The primary use case for setting this field is to use a StatefulSet's Headless Service to propagate SRV records for its Pods without respect to their readiness for purpose of peer discovery. This field will replace the service.alpha.kubernetes.io/tolerate-unready-endpoints when that annotation is deprecated and all clients have been converted to use this field.
  # 由于使用DNS访问Pod需Pod和Headless service启动之后才能访问,publishNotReadyAddresses设置成true,防止readinessProbe在服务没启动时找不到DNS
  publishNotReadyAddresses: true 
  ports: 
   - name: amqp
     port: 5672
   - name: http
     port: 15672
  selector:
    app: rabbitmq
---
# 用于暴露dashboard到外网
kind: Service
apiVersion: v1
metadata:
  namespace: rabbitmq
  name: rabbitmq-service
spec:
  type: NodePort
  ports:
   - name: http
     protocol: TCP
     port: 15672
     targetPort: 15672 
     nodePort: 15672   # 注意k8s默认情况下,nodeport要在30000~32767之间,可以自行修改
   - name: amqp
     protocol: TCP
     port: 5672
     targetPort: 5672  # 注意如果你想在外网下访问mq,需要增配nodeport
  selector:
    app: rabbitmq
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-config
  namespace: rabbitmq
data:
  enabled_plugins: |
      [rabbitmq_management,rabbitmq_peer_discovery_k8s].
  rabbitmq.conf: |
      cluster_formation.peer_discovery_backend  = rabbit_peer_discovery_k8s
      cluster_formation.k8s.host = kubernetes.default.svc.cluster.local
      cluster_formation.k8s.address_type = hostname
      cluster_formation.node_cleanup.interval = 10
      cluster_formation.node_cleanup.only_log_warning = true
      cluster_partition_handling = autoheal
      queue_master_locator=min-masters
      loopback_users.guest = false
​
      cluster_formation.randomized_startup_delay_range.min = 0
      cluster_formation.randomized_startup_delay_range.max = 2
      # 必须设置service_name,否则Pod无法正常启动,这里设置后可以不设置statefulset下env中的K8S_SERVICE_NAME变量
      cluster_formation.k8s.service_name = rabbitmq-headless
      # 必须设置hostname_suffix,否则节点不能成为集群
      cluster_formation.k8s.hostname_suffix = .rabbitmq-headless.rabbitmq.svc.cluster.local
      # 内存上限
      vm_memory_high_watermark.absolute = 1.6GB
      # 硬盘上限
      disk_free_limit.absolute = 2GB
---
# 使用apps/v1版本代替apps/v1beta
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: rabbitmq
  namespace: rabbitmq
spec:
  serviceName: rabbitmq-headless   # 必须与headless service的name相同,用于hostname传播访问pod
  selector:
    matchLabels:
      app: rabbitmq # 在apps/v1中,需与 .spec.template.metadata.label 相同,用于hostname传播访问pod,而在apps/v1beta中无需这样做
  replicas: 3
  template:
    metadata:
      labels:
        app: rabbitmq  # 在apps/v1中,需与 .spec.selector.matchLabels 相同
      # 设置podAntiAffinity
      annotations:
        scheduler.alpha.kubernetes.io/affinity: >
            {
              "podAntiAffinity": {
                "requiredDuringSchedulingIgnoredDuringExecution": [{
                  "labelSelector": {
                    "matchExpressions": [{
                      "key": "app",
                      "operator": "In",
                      "values": ["rabbitmq"]
                    }]
                  },
                  "topologyKey": "kubernetes.io/hostname"
                }]
              }
            }
    spec:
      serviceAccountName: rabbitmq
      terminationGracePeriodSeconds: 10
      containers:        
      - name: rabbitmq
        image: registry-vpc.cn-shenzhen.aliyuncs.com/heygears/rabbitmq:3.7
        resources:
          limits:
            cpu: 0.5
            memory: 2Gi
          requests:
            cpu: 0.3
            memory: 2Gi
        volumeMounts:
          - name: config-volume
            mountPath: /etc/rabbitmq
          - name: rabbitmq-data
            mountPath: /var/lib/rabbitmq/mnesia
        ports:
          - name: http
            protocol: TCP
            containerPort: 15672
          - name: amqp
            protocol: TCP
            containerPort: 5672
        livenessProbe:
          exec:
            command: ["rabbitmqctl", "status"]
          initialDelaySeconds: 60
          periodSeconds: 60
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command: ["rabbitmqctl", "status"]
          initialDelaySeconds: 20
          periodSeconds: 60
          timeoutSeconds: 5
        imagePullPolicy: Always
        env:
          - name: HOSTNAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: RABBITMQ_USE_LONGNAME
            value: "true"
          - name: RABBITMQ_NODENAME
            value: "rabbit@$(HOSTNAME).rabbitmq-headless.rabbitmq.svc.cluster.local"
          # 若在ConfigMap中设置了service_name,则此处无需再次设置
          # - name: K8S_SERVICE_NAME
          #   value: "rabbitmq-headless"
          - name: RABBITMQ_ERLANG_COOKIE
            value: "mycookie" 
      volumes:
        - name: config-volume
          configMap:
            name: rabbitmq-config
            items:
            - key: rabbitmq.conf
              path: rabbitmq.conf
            - key: enabled_plugins
              path: enabled_plugins
        - name: rabbitmq-data
          persistentVolumeClaim:
            claimName: rabbitmq-data-claim

至此,终于在Kubernetes上部署完成RabbitMQ Cluster 3.7.X

容器服务kubernetes弹性伸缩高级用法

前言

近期,阿里云容器服务kubernetes发布了cluster-autoscaler的支持,开发者可以通过页面简单快捷的配置节点的弹性伸缩,支持普通实例、GPU实例以及竞价实例帮助开发者实现架构弹性和运营成本之间的博弈。阿里云容器服务kubernetes的cluster-autoscaler的能力还有很多会陆续通过控制台开放出来,对于cluster-autoscaler高级功能有强需求的开发者,也提供手动配置的方式进行实现,那么接下来我们一起来看一下cluster-autoscaler支持的高级功能。

高级功能解析

在解析高级特性之前,我们首先要了解的是弹性伸缩的主要原理,在阿里云容器服务kubernetes中,通过页面配置的伸缩规则会转换为ESS(弹性伸缩服务)中的伸缩组,主动下发组件cluster-autoscaler,并将组ID作为参数传递给cluster-autoscaler,然后cluster-autoscaler会根据相应的配置实现伸缩组的选择以及具体弹出的实例规格。下发的cluster-autoscaler在命名空间kube-system下面,模板内容如下:

未分类

可以看到在cluster-autoscaler的启动参数中包含了–node的参数,里面配置了伸缩组的ID,cluster-autoscaler就是通过这个组ID来识别伸缩组信息并实现伸缩的。在了解了这些原理后,我们来看下怎么使用阿里云容器服务kubernetes提供的高级特性。阿里云容器服务kubernetes的cluster-autoscaler支持如下高级特性:

未分类

接下来我们针对上述的高级特性进行一一解析:

单可用区、多可用区支持

阿里云容器服务kubernetes集群支持单可用区与多可用区两种形式,多可用区的kubernetes集群可以具备更好的集群鲁棒性,不会因为单一可用区机房的宕机造成整个集群的不可用。那么多可用区的cluster-autoscaler有什么好处呢?多可用区的cluster-autoscaler可以提高实例弹性伸缩的成功率。因为云资源是动态调整的,每个地域每个可用区的库存都会根据不同的时间不同的资源状态进行调整,同样规格的实例可能在可用区A中可以生产,但是在可用区B中无法生产。如果配置多个可用区,那么就拥有了在多个可用区中弹出实例的可能,提高了弹性伸缩的成功率。

目前在控制台上只支持单可用区的伸缩组配置,那么怎么创建一个具有多可用区的弹性伸缩组并使用呢,从上文我们了解到cluster-autoscaler只需要识别伸缩组ID即可,那么只需要创建一个新的伸缩组,并配置给cluster-autoscaler即可。

未分类

伸缩组中的其他配置,建议拷贝一个已有的伸缩做来设置,降低配置的难度。最后将这个伸缩组的ID配置到yaml中即可

未分类

多实例规格的支持

多实例规格可以获得更好的伸缩成功率,而且结合竞价实例可以获得更优的运营成本节约,对于竞价实例不了解的开发者,可以先参考下这篇文档。多实例规格的支持方式非常简单,我们可以无需新建伸缩组,只需修改已有的配置即可。通过容器服务弹性伸缩的页面点击进入ESS的伸缩组配置。

未分类

点击左侧菜单的伸缩配置以及右侧配置的修改按钮,并添加希望加入的其他配置,此处需要特别注意的是容器的配置一定要保证规格一致,比如CPU和内存的大小必须保持一致

未分类

未分类

配置完成点击确认配置即可生效。

定时伸缩与报警伸缩

定时伸缩是一个非常常见的伸缩场景,但是定时伸缩与cluster-autoscaler的伸缩策略是不完全相同的,那么如何实现呢。此处我们只需要依赖ESS(弹性伸缩服务)即可,首先参考多可用的配置,先创建一个伸缩组。然后在这个伸缩组中设置弹出的伸缩规则。

未分类

在定时任务中设置任务配置,选择伸缩组与伸缩规则,并设置执行时间

未分类

此时,就设置完成了一个定时的伸缩,如果需要周期性设置,那么可以勾选下放的重复周期设置。同理,可以设置缩容的规则,以及缩容的时间。对于报警伸缩而言,和定时伸缩配置方法是一致的,他们都无需依赖cluster-autoscaler来实现。

自定义安装脚本

在讲解如何定义安装脚本之前,需要额外讲解下一个ECS的机器是如何加入到集群中的,在伸缩配置的高级选项中有一个base64的自定义数据,我们通过base64的解码工具进行解析,可以看到里面内容如下:

#!/bin/sh
curl http://aliacs-k8s-cn-shenzhen.oss-cn-shenzhen.aliyuncs.com/public/pkg/run/attach/1.10.4/attach_node.sh | bash -s -- --openapi-token [secret_token] --ess true --labels workload_type=spot

上述的脚本的作用就是将一个ECS的节点加入到集群中的,我们自定义的安装脚本可以添加到上述脚本后面,然后通过base64工具进行加密,并贴回原来的自定义数据框内即可。

最后

在本文中,给大家讲解了如何使用cluster-autoscaler的高级特性来支持不同维度和场景的弹性伸缩,cluster-autoscaler也会在近期开源并提交给社区,有需求或者问题可以提交issues到github(https://github.com/AliyunContainerService/autoscaler)

构建生产环境可用的高可用kubernetes集群

kubernetes集群三步安装:https://sealyun.com/pro/products/

sealos项目地址:https://github.com/fanux/sealos

特性

  • [x] 支持任意节点的etcd集群自动构建,且etcd集群使用安全证书,通过static pod方式启动,这样可以通过监控pod来监控etcd集群健康状态
  • [x] 支持多master节点,允许任意一台master宕机集群功能不受影响
  • [x] calico使用etcd集群,配置安全证书,网络管控数据无单点故障
  • [x] 包含dashboard, heapster coreDNS addons, coreDNS双副本,无单点故障
  • [x] 使用haproxy负载master节点,同样是用static pod,这样可通过统一监控pod状态来监控haproxy是否健康
  • [x] haproxy节点使用keepalived提供虚拟IP,任意一个节点宕机虚拟IP可实现漂移,不影响node连接master
  • [x] node节点与kube-proxy配置使用虚拟IP
  • [ ] 集群健康检测功能
  • [ ] promethus 监控功能,一键安装,无需配置
  • [ ] EFK 日志收集功能
  • [ ] 分布式HA模式,不用keepalived,减少集群构建出错概率,无VIP切换时间
  • [x] istio 微服务支持:https://sealyun.com/pro/istio/

ship on docker

你必须已经有了sealyun kubernetes离线安装包https://sealyun.com/pro/products/ (默认支持kubernetes版本v1.12.x,针对特殊版本的适配会切分支处理)

针对后续高版本会有更多优化

大概原理是为了减少大家搭建ansible和sealos的环境,客户端的东西都放到docker里,把安装包挂载到容器中,然后ansible脚本会把包分发到你在hosts文件中配置的所有服务器上

所以大概分成三步:

  1. 配置免密钥,把docker里的公钥分发给你所有的服务器
  2. 配置ansible playbook的hosts文件
  3. 执行ansible

下面逐一说明:

启动ansible容器与免密钥设置

找台宿主机如你的PC,或者一台服务器,把下载好的离线包拷贝到/data目录,启动sealos容器,把离线包挂载进去:

docker run --rm -v /data/kube1.12.0.tar.gz:/data/kube1.12.0.tar.gz -it -w /etc/ansible fanux/sealos:v1.12.0-beta bash

在容器里面执行:

mkdir ~/.ssh
cd ~/.ssh
ssh-keygen -t rsa

ssh public key:

cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7fTirP9zPcx7wIjhsF+Dyu0A2sV5llC8jsmp/xtiyuJirE3mclpNEqgrzHC26f+ckfzwoE0HPU0wDPxbWFl3B0K89EwJSBsVZSZ0VLYnZp0u2JgwCLZzZzKfY0018yoqoL9KHz/68RpqtG2bWVf0/WSj+4hN7xTRpRTtXJHBOQRQBfqVSIcfMBSEnO15buUbDaLol/HvQd0YBrWwafQtMacmBlqDG0Z6/yeY4sTNRVRV2Uu5TeaHfzgYgmY9+NxtvPn8Td6tgZtq7cVU//kSsbzkUzDSD8zsh8kPUm4yljT5tYM1cPFLGM4m/zqAjAZN2YaEdFckJFAQ7TWAK857d root@8682294b9464

这样公钥就生成了

在其它所有要安装k8s的服务器上执行

cd ~/.ssh
echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7fTirP9zPcx7wIjhsF+Dyu0A2sV5llC8jsmp/xtiyuJirE3mclpNEqgrzHC26f+ckfzwoE0HPU0wDPxbWFl3B0K89EwJSBsVZSZ0VLYnZp0u2JgwCLZzZzKfY0018yoqoL9KHz/68RpqtG2bWVf0/WSj+4hN7xTRpRTtXJHBOQRQBfqVSIcfMBSEnO15buUbDaLol/HvQd0YBrWwafQtMacmBlqDG0Z6/yeY4sTNRVRV2Uu5TeaHfzgYgmY9+NxtvPn8Td6tgZtq7cVU//kSsbzkUzDSD8zsh8kPUm4yljT5tYM1cPFLGM4m/zqAjAZN2YaEdFckJFAQ7TWAK857d root@8682294b9464" >> authorized_keys

这样公钥分发工作完成了,所有的机器直接ssh无需输入密码即可登录

修改配置

Config your own hosts

# cd /etc/ansible
# vim hosts

配置说明:

[k8s-master]
10.1.86.204 name=node01 order=1 role=master lb=MASTER lbname=lbmaster priority=100
10.1.86.205 name=node02 order=2 role=master lb=BACKUP lbname=lbbackup priority=80
10.1.86.206 name=node03 order=3 role=master 

[k8s-node]
10.1.86.207 name=node04 role=node

[k8s-all:children]
k8s-master
k8s-node

[all:vars]
vip=10.1.86.209   # 同网段未被占用IP
k8s_version=1.12.0  # kubernetes版本
ip_interface=eth.*
etcd_crts=["ca-key.pem","ca.pem","client-key.pem","client.pem","member1-key.pem","member1.pem","server-key.pem","server.pem","ca.csr","client.csr","member1.csr","server.csr"]
k8s_crts=["apiserver.crt","apiserver-kubelet-client.crt","ca.crt", "front-proxy-ca.key","front-proxy-client.key","sa.pub", "apiserver.key","apiserver-kubelet-client.key",  "ca.key",  "front-proxy-ca.crt",  "front-proxy-client.crt" , "sa.key"]

注意role=master的会装etcd与kubernetes控制节点,role=node即k8s node节点,配置比较简单,除了改IP和版本,其它基本不用动

启动安装

# ansible-playbook roles/install-all.yaml

uninstall all

# ansible-playbook roles/uninstall-all.yaml

k8s v1.11.1高可用集群的问题排查

k8s集群环境基于阿里云VPC网络,采用kubeadm工具快速部署k8s高可用集群。

服务器:

  • ubuntu 16.04.4 LTS (Xenial Xerus),4.4.0-117-generic

  • kubelet –version: Kubernetes v1.11.1

  • docker -v: Docker version 17.03.3-ce, build e19b718

问题现象是通过kubectl get nodes -w监控node节点的状态时,发现偶尔会出现NotReady状态。这时候集群访问正常,外部访问应用服务正常,只是自动化接口测试时,偶尔报出 http接口请求的错误:java.net.ConnectException: 拒绝连接 (Connection refused)。初步怀疑是Node如果是NotReady状态,则对应节点上的服务会受到影响,导致请求被拒绝。

未分类

未分类

通过kubectl describe node查看node的信息,发现Conditions status Unknown, Kubelet stopped posting node status信息。

通过google得知,node挂了后到底多久会notready 多久开始调度? 40s判死刑,缓期5min善后!

注意:这个由kube-controller-manager的两个参数决定的

  • –pod-eviction-timeout:缺省为 5m,五分钟,在 Pod 驱逐行为的超时时间。
  • –node-monitor-grace-period:缺省为 40s,也就是 40 秒,无响应 Node 在标记为 NotReady 之前的等候时间。
    初步怀疑原因是,node的notready状态,触发了Kubernetes的Pod重调度流程。

首先分析一下该重调度流程:

  1. kubelet周期性的调用rest apiserver接口将Node状态更新至etcd,周期为node-status-update-frequency,默认值10s

  2. controller manager每node-monitor-period通过apiserver查询etcd,检查kubelet上报上来的状态,默认值为5s

  3. node的状态将会在node-monitor-grace-period周期内更新,controller manager中设置默认值为40s

  4. kubelet在更新自身状态的时候有nodeStatusUpdateRetry次重试,nodeStatusUpdateRetry的默认值为5次

说明: controller manager会在node-monitor-grace-period 40s之内获取etcd中的node状态,而期间kubelet更新了40s/10s=4次node状态。

通过查看/etc/kubernetes/manifests/kube-controoller-manager.yaml,发现配置是

- --node-monitor-grace-period=10s
- --pod-eviction-timeout=10s

因为kubelet默认更新Node状态的时间是10s,而controller manager判断Node是否health的时间也是10s,但是考虑到kubelet,controller manager通过apiserver,操作etcd之间是异步访问,实际更新Node的时间应该可能超过10s,他们之间的delay将会受到network latency, apiserver latency ,etcd latency,以及master node上的负载的影响。根据Kubernetes的list-watch机制,每次watch之前需要按照本地最大version list所有etcd中更新的logs,所以在delay的情况下,controller manager可能获取不到Node的状态更新,从而认为其Unhealthy,且10s之后根据eviction 10s,如果Node仍然为Unhealthy,则触发Pod的重调度,从而导致服务震荡,出现以上的connection refuse的问题。

通过上面解释,我们知道导致服务偶尔中断的原因是controller manager设置Node状态的时间和kubelet更新时间设置的太短,解决办法也很简单,去除这两个配置,采用默认值即可,经测试,没有发现NotReady现象,且接口自动化测试也通过。

社区默认的配置

未分类

快速更新和快速反应

该模式下会产生频繁的node update事件,加重etcd的负担

未分类

这种场景下20s之后,会认为node down了,接着–pod-eviction-timeout=30s之后,pod将会被驱逐,也就是50s会发生evict。但是会增加etcd的负载,每个node将会每2s更新一次etcd的状态。

如果环境有1000个节点,1分钟内将有15000个节点更新,这可能需要大型的etcd容器甚至是etcd的专用节点。

中等更新和平均反应

未分类

将会20s更新一次node状态,也就是说controller manager认为node状态不正常之前,会有2m60/205=30次更新node的状态。Node状态为down之后1m,就会触发evict。
如果1000node的场景,1分钟内将会有60s/20s*1000=3000次 etcd node状态的更新。

低更新和慢反应

未分类

Kubelet将会1m更新一次node的状态,在认为不健康之后会有5m/1m*5=25次重试更新的机会。Node为不健康的时候,1m之后pod开始evict。

目前我们采用的是默认的配置。

这里需要先了解kubelet和apiserver之间的交互过程。

k8s集群采用kubeadm init –config kubeadm-init.yaml进行高可用部署。

apiserver的高可用和etcd的高可用,采用的是static pod方式运行,基于容器方式运行k8s相关组件。

附录!

Kubernetes作为容器化应用集群管理系统,关键系统服务有 api-server, scheduler, controller-manager 三个。api-server 一方面作为 Kubernetes 系统的访问入口点,一方面作为背后 etcd 存储的代理服务器,Kubernetes 里的所有资源对象,包括 Service、Deployment、ReplicaSet、DaemonSet,Pod、Endpoint、ConfigMap、Secret 等等,都是通过 api-server 检查格式后,以 protobuf 格式序列化并存入 etcd。

未分类

Kubernetes为容器化应用提供了便利的资源调度, 部署运行,服务发现, 扩容缩容,自动运维等功能。

  1. 系统各个组件分工明确(APIServer是所有请求入口,CM是控制中枢,Scheduler主管调度,而Kubelet负责运行),配合流畅,整个运行机制一气呵成。

  2. 可以看出除了配置管理和持久化组件ETCD,其他组件并不保存数据。意味除ETCD外其他组件都是无状态的。因此从架构设计上对kubernetes系统高可用部署提供了支撑。

  3. 同时因为组件无状态,组件的升级,重启,故障等并不影响集群最终状态,只要组件恢复后就可以从中断处继续运行。

  4. 各个组件和kube-apiserver之间的数据推送都是通过list-watch机制来实现。所以理解list-watch机制是深入理解kubernetes的关键。

  5. 整体运行机制基于声明式数据(如nginx-depolyment.yaml)而非用户输入各种命令来工作,这是kubernetes最核心的设计理念。

list-watch机制分析

kubernetes中结合watch请求增加了list请求,主要做如下两件事情:

  1. watch请求开始之前,先发起一次list请求,获取集群中当前所有该类数据(同时得到最新的ResourceVersion),之后基于最新的ResourceVersion发起watch请求。

  2. 当watch出错时(比如说网络闪断造成客户端和服务端数据不同步),重新发起一次list请求获取所有数据,再重新基于最新ResourceVersion来watch。

kubernetes中基于ResourceVersion信息采用list-watch(http streaming)机制来保证组件间的数据实时可靠传送。

2 个 K8S 命令轻松部署深度学习模型

如今利用 Keras 构建深度学习模型已然成为一种风尚。Kubernetes 无需人工干预即可全部自动化完成任务的特点,使之成为部署学习模型的绝佳选择。今天我们就从 Keras 构建深度学习模型角度出发,通过 Flask 提供 REST API 服务,看看如何利用 Kubernetes 两个命令轻松部署深度学习模型。接下来,我将在 Google Cloud 上进行全程部署。

主要内容包括以下四部分:

  • 使用 Google Cloud 创建环境;
  • 使用 Keras、Flask 和 Docker 作为 API 提供深度学习模型;
  • 使用 Kubernetes 部署所述模型;
  • 结语。

使用 Google Cloud 创建环境

首先在 Google Compute Engine 上使用一个小型虚拟机来构建、服务和定位深度学习模型。我曾试图在 Windows 10 笔记本电脑上安装最新版本的 Docker CE(Community Edition),但我失败了。

所以我决定利用 Google Cloud 来进行部署,这要比弄清如何安装 Docker 更高效。

未分类

启动 Google Cloud VM,打开屏幕左侧的功能区,选择 “Select Compute” 再选择 “Create Instance”,你将看到已经运行了一个实例。

未分类

下一步选择使用的计算资源大小。默认(read: cheapest)设置可以正常工作,但鉴于我们最多只需要这个 VM 约 1 小时,我选择了 4vCPU 和 15GB 内存。

未分类

接下来,选择使用的操作系统和磁盘空间。我们选择 “Boot Disk” 以编辑默认值,将 CentOS 7 作为操作系统,并将磁盘容量从 10GB 增加到 100GB(CentOS 操作系统不是必要的)。我建议将磁盘大小增加到 10GB 以上,因为我们创建的 Docker 容器每个大约 1GB。

未分类

创建 VM 前需要将防火墙规则设置为:Allow HTTP traffic、Allow HTTPS traffic 。在模型部署到 Kubernetes 之前,我将进行防火墙设置,保证在 VM 上能测试到我们的 API。因此,检查下图这些方框是不够的 —— 还有更多工作要做。

未分类

现在点击 “Create”, Google Cloud 创建环境成功!

未分类

使用 Keras 构建深度学习模型

通过 SSH 进入到我们的 VM 并开始构建模型。最简单的方法是只需单击 VM 旁边的 SSH 图标(如下所示),浏览器会打开一个终端。

未分类

删除现有版本 Docker

sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine

请注意,如果选择 CentOS 7 以外的操作系统,这些命令会有所不同。

安装最新版本 Docker

sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager — add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce

启动 Docker 并运行测试脚本

sudo systemctl start docker
sudo docker run hello-world

如果你看到的输出如下所示,则完成设置。

Hello from Docker!
This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.    (amd64) 3. The Docker daemon created a new container from that image which runs the    executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it    to your terminal.

创建深度学习模型

对 Adrian Rosebrock 编写的脚本进行复制。我对 Adrian 脚本进行两次编辑才能让它运行起来。如果你不关心 Docker 和 Tensorflow 的细节,请跳过这两段。

与 Docker 有关问题:在本地运行应用程序的默认行为是在本地主机(127.0.0 …)上提供应用程序,但应用程序在 Docker 容器内运行时会出现问题。解决方法:当调用 app.run() 时,将 url 指定为 0.0.0.0,类似于 app.run( host=’0.0.0.0′ )。现在,我们的应用程序可在本地和外部 IP 上使用。

与 Tensorflow 有关问题:当我运行 Adrian 原始脚本时,我无法成功调用模型。利用下面的解决方案,对代码进行后续更改。

global graph
graph = tf.get_default_graph()
...
with graph.as_default():
    preds = model.predict(image)

创建一个名为 “keras-app” 的新目录并将模型移动到该目录中。

mkdir keras-app
cd keras-app

创建一个名为 “app.py” 的文件。你可以选择你喜欢的文本编辑器。我更喜欢使用 Vim,先创建并打开 app.py 类型:

vim app.py

从文件内部,点击 “i” 键进入插入模式。将此代码进行粘贴:

# USAGE
# Start the server:
#     python app.py
# Submit a request via cURL:
#     curl -X POST -F [email protected] 'http://localhost:5000/predict'# import the necessary packages
from keras.applications import ResNet50
from keras.preprocessing.image import img_to_array
from keras.applications import imagenet_utils
from PIL import Image
import numpy as np
import flask
import io
import tensorflow as tf

# initialize our Flask application and the Keras model
app = flask.Flask(__name__)
model = None

def load_model():
    # load the pre-trained Keras model (here we are using a model
    # pre-trained on ImageNet and provided by Keras, but you can
    # substitute in your own networks just as easily)
    global model
    model = ResNet50(weights="imagenet")
    global graph
    graph = tf.get_default_graph()

def prepare_image(image, target):
    # if the image mode is not RGB, convert it    if image.mode != "RGB":
        image = image.convert("RGB")

    # resize the input image and preprocess it
    image = image.resize(target)
    image = img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image = imagenet_utils.preprocess_input(image)

    # return the processed image    return image

@app.route("/predict", methods=["POST"])
def predict():
    # initialize the data dictionary that will be returned from the
    # view
    data = {"success": False}

    # ensure an image was properly uploaded to our endpoint    if flask.request.method == "POST":        if flask.request.files.get("image"):
            # read the image in PIL format
            image = flask.request.files["image"].read()
            image = Image.open(io.BytesIO(image))

            # preprocess the image and prepare it for classification
            image = prepare_image(image, target=(224, 224))

            # classify the input image and then initialize the list
            # of predictions to return to the client            with graph.as_default():
                preds = model.predict(image)
                results = imagenet_utils.decode_predictions(preds)
                data["predictions"] = []

                # loop over the results and add them to the list of
                # returned predictions                for (imagenetID, label, prob) in results[0]:
                    r = {"label": label, "probability": float(prob)}
                    data["predictions"].append(r)

                # indicate that the request was a success
                data["success"] = True

    # return the data dictionary as a JSON response    return flask.jsonify(data)

# if this is the main thread of execution first load the model and
# then start the serverif __name__ == "__main__":
    print(("* Loading Keras model and Flask starting server..."
        "please wait until server has fully started"))
    load_model()
    app.run(host='0.0.0.0')
view rawapp.py hosted with ❤ by GitHub

复制上述代码后,按 “Esc” 键退出编辑模式,键入保存并关闭文件 :x。

创建 requirements.txt 文件

我们将在 Docker 容器中运行上述代码。现在先创建一个 requirements.txt 文件。该文件将包含代码所需的运行包,例如 Keras、Flask 等。这样保证无论我们将 Docker 容器放置在哪,底层服务器都能安装代码所需依赖项。

创建并打开一个名为 “requirements” 的文件。通过键入 vim requirements.txt,用 vim 创建 txt 。将以下内容复制到 requirements.txt 中,进行保存并关闭。

keras
tensorflow
flask
gevent
pillow
requests

创建 Dockerfile

对 Dockerfile 进行创建,如下是 Docker 为构建和运行项目而读取的文件:

FROM python:3.6
WORKDIR /app
COPY requirements.txt /app
RUN pip install -r ./requirements.txt
COPY app.py /app
CMD ["python", "app.py"]~

现在我们指示 Docker 下载 Python 3 的基本镜像。完成后,我们要求 Docker 使用 Python 包管理器 pip 安装 requirements.txt 中的详细的包,再通知 Docker 运行脚本 python app.py。

构建 Docker 容器

构建并测试应用程序,首先要构建 Docker 容器,请运行:

sudo docker build -t keras-app:latest .

这条代码指示 Docker 为当前工作中的代码构建容器 director keras-app。此命令将需要一两分钟才能完成。后台的 Docker 也会同时下载 Python 3.6 的镜像并安装requirements.txt 中列出的软件包。

运行 Docker 容器

运行 Docker 容器来测试应用:

sudo docker run -d -p 5000:5000 keras-app

关于 5000:5000 的简单说明 ——设置的 Docker 让外部端口 5000 可用,并将本地应用程序转发到该端口(也在本地端口 5000 上运行),通过检查运行容器的状态,运行命令 sudo docker ps -a 可看到类似如下内容:

[gustafcavanaugh@instance-3 ~]$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS                    NAMES
d82f65802166        keras-app           "python app.py"     About an hour ago   Up About an hour          0.0.0.0:5000->5000/tcp   nervous_northcutt

测试模型

让该模型接收一张狗的照片作为输入,命令返回狗的品种作为测试。

未分类

从终端运行:

curl -X POST -F [email protected] 'http://localhost:5000/predict'

确保 “dog.jpg” 在你当前的目录中(或提供该文件的相应路径)

显示结果如下:

{"predictions":[{"label":"beagle","probability":0.987775444984436},
{"label":"pot","probability":0.0020967808086425066},
{"label":"Cardigan","probability":0.001351703773252666},
{"label":"Walker_hound","probability":0.0012711131712421775},
{"label":"Brittany_spaniel","probability":0.0010085132671520114}],"
success":true}

我们的模型正确地将狗归类为比格犬。这说明你已成功运行了一个训练有素的深度学习模型与 Keras、Flask 一起服务,并用 Docker 对它进行包装。

使用 Kubernetes 部署模型

创建 Docker Hub 帐户

将构建的模型上传到 Docker Hub。(如果你没有 Docker Hub 帐户,请立即创建一个帐户)。我们不会将容器移动到 Kubernetes 集群中,而是指示 Kubernetes 从集中托管的服务器中(即 Docker Hub)安装我们的容器。

登录 Docker Hub 帐户

创建 Docker Hub 帐户后,从命令行登录 sudo docker login。你需要提供用户名和密码,就像登录网站一样。

如果你看到如下消息:

Login Succeeded

登录成功!

标记容器

在上传之前标记容器的命名。运行 sudo docker images 并找到 keras-app 容器的镜像 ID。

输出如下所示:

REPOSITORY          TAG                 IMAGE ID            
CREATED             SIZE keras-app           latest              
ddb507b8a017        About an hour ago   1.61GB

标记 keras-app,请务必遵循我的格式,将 image id 和 docker hub id 的值替换为你的特定值。

#Format
sudo docker tag <your image id> <your docker hub id>/<app name>
#My Exact Command - Make Sure To Use Your Inputs
sudo docker tag ddb507b8a017 gcav66/keras-app

容器传送到 Docker Hub

从 shell 运行容器:

#Format
sudo docker push <your docker hub name>/<app-name>
#My exact command
sudo docker push gcav66/keras-app

当你导航回 Docker Hub 网站,将会看到 keras-app 存储库。

创建 Kubernetes 集群

在 Google Cloud 主屏幕上,选择 “Kubernetes Engine”。

未分类

再创建一个新的 Kubernetes 集群。

未分类

接下来,我们将自定义集群中的 Node 大小。我将选择 4vCPU 和 15 GB 的 RAM。你可以使用较小的集群尝试此操作。请记住,默认设置会启动 3 个 Node,因此你的集群将拥有你提供资源的 3 倍,即在我的情况下,45 GB 的 RAM。

未分类

单击进行创建,你需要一到两分钟才能启动集群。单击 “Click Run in Cloud Shell” 以显示 Kubernetes 集群的控制台。请注意,在你创建和测试的 Docker 容器中,这是一个独立于 VM 的 shell 环境。我们可以在 VM 上安装 Kubernetes,但 Google 的 Kubernetes 服务可以为我们提供自动化安装。

未分类

在 Kubernetes 上运行我们的 Docker 容器。请注意,镜像标记仅指向 Docker Hub 上的托管 Docker 镜像。另外,指定 port 在端口 5000 运行应用程序。

kubectl run keras-app --image=gcav66/keras-app --port 5000

在 Kubernetes 中,容器都在 Pod 内部运行。我们可以通过输入 kubectl get pods 验证 Pod 是否运行。结果如下则输入正确:

gustafcavanaugh@cloudshell:~ (basic-web-app-test)$ kubectl get pods
NAME                         READY     STATUS    RESTARTS   AGE
keras-app-79568b5f57-5qxqk   1/1       Running   0          1m

现在 Pod 正在运行,我们需要将端口 80 上的 Pod 暴露给外网。这意味着访问我们部署的 IP 地址的同时也可以访问我们的 API。也就是说我们不必在网址之后再指定一个端口号(告别 :5000)。

kubectl expose deployment keras-app --type=LoadBalancer --port 
80 --target-port 5000

通过运行 kubectl get service 来确定部署状态(以及确定需要调用 API 的 URL) 。若命令输入如下则运行正确:

gustafcavanaugh@cloudshell:~ (basic-web-app-test)$ kubectl get service
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
keras-app    LoadBalancer   10.11.250.71   35.225.226.94   80:30271/TCP   4m
kubernetes   ClusterIP      10.11.240.1    <none>          443/TCP        18m

抓住 Keras 应用程序的 cluster-ip,打开本地终端(或任何有狗照片的地方)并运行以下命令来调用 API curl -X POST -F [email protected] 'http://<your service IP>/predict'

结果如下所示,API 正确返回图片的 beagle 标签:

$ curl -X POST -F [email protected] 'http://35.225.226.94/predict'
{"predictions":
[{"label":"beagle","probability":0.987775444984436},
{"label":"pot","probability":0.0020967808086425066},
{"label":"Cardigan","probability":0.001351703773252666},
{"label":"Walker_hound","probability":0.0012711131712421775},
{"label":"Brittany_spaniel","probability":0.0010085132671520114}],
"success":true}

结语

在本教程中,我们使用 Keras 和 Flask 作为 REST API 提供深度学习模型,而后我们将该应用程序放在 Docker 容器中,将容器上传到 Docker Hub,并使用 Kubernetes 进行部署。

只需两个命令,Kubernetes 就可以部署我们的应用程序并将其暴露给外网。是不是感觉很自豪?当然这个方案还有很多可以改进的空间。比如,我们可以将 Flask 中的 Python Web 服务器从本地 Python 服务器改为类似 gunicorn 产品级别的服务器;我们还可以探索 Kubernetes 的扩展性和管理功能等等。

备份 k8s yaml 配置

备份 k8s yaml 配置

for n in $(kubectl get -o=name pvc,configmap,serviceaccount,secret,ingress,service,deployment,statefulset,hpa,job,cronjob)
do
    mkdir -p $(dirname $n)
    kubectl get -o=yaml --export $n > $n.yaml
done