如何部署一个生产级别的 Kubernetes 应用(2/2)

部署一个生产级别的 Kubernetes 应用

滚动更新

Deployment 控制器默认的就是滚动更新的更新策略,该策略可以在任何时间点更新应用的时候保证某些实例依然可以正常运行来防止应用 down 掉,当新部署的 Pod 启动并可以处理流量之后,才会去杀掉旧的 Pod。在使用过程中我们还可以指定 Kubernetes 在更新期间如何处理多个副本的切换方式,比如我们有一个3副本的应用,在更新的过程中是否应该立即创建这3个新的 Pod 并等待他们全部启动,或者杀掉一个之外的所有旧的 Pod,或者还是要一个一个的 Pod 进行替换?

如果我们从旧版本到新版本进行滚动更新,只是简单的通过输出显示来判断哪些 Pod 是存活并准备就绪的,那么这个滚动更新的行为看上去肯定就是有效的,但是往往实际情况就是从旧版本到新版本的切换的过程并不总是十分顺畅的,应用程序很有可能会丢弃掉某些客户端的请求。比如我们在 Wordpress 应用中添加上如下的滚动更新策略,随便更改以下 Pod Template 中的参数,比如容器名更改为 blog:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge1
    maxUnavailable0

然后更新应用,同时用 Fortio 工具在滚动更新过程中来测试应用是否可用:

$ kubectl apply -f wordpress.yaml
$ fortio load -a -c 8 -qps 1000 -t 60s "http://k8s.qikqiak.com:30012"
Starting at 1000 qps with 8 thread(s) [gomax 2] for 1m0s : 7500 calls each (total 60000)
Ended after 1m0.006243654s : 5485 calls. qps=91.407
Aggregated Sleep Time : count 5485 avg -17.626081 +/- 15 min -54.753398956 max 0.000709054 sum -96679.0518
[...]
Code 200 : 5463 (99.6 %)
Code 502 : 20 (0.4 %)
Response Header Sizes : count 5485 avg 213.14166 +/- 13.53 min 0 max 214 sum 1169082
Response Body/Total Sizes : count 5485 avg 823.18651 +/- 44.41 min 0 max 826 sum 4515178
[...]

从上面的输出可以看出有部分请求处理失败了(502),要弄清楚失败的原因就需要弄明白当应用在滚动更新期间重新路由流量时,从旧的 Pod 实例到新的实例究竟会发生什么,首先让我们先看看 Kubernetes 是如何管理工作负载连接的。

失败原因

我们这里通过 NodePort 去访问应用,实际上也是通过每个节点上面的 kube-proxy通过更新 iptables 规则来实现的。

如何部署一个生产级别的 Kubernetes 应用

kubernetes kube-proxy

Kubernetes 会根据 Pods 的状态去更新 Endpoints 对象,这样就可以保证 Endpoints 中包含的都是准备好处理请求的 Pod。一旦新的 Pod 处于活动状态并准备就绪后,Kubernetes 就将会停止就的 Pod,从而将 Pod 的状态更新为 “Terminating”,然后从 Endpoints 对象中移除,并且发送一个 SIGTERM 信号给 Pod 的主进程。SIGTERM 信号就会让容器以正常的方式关闭,并且不接受任何新的连接。Pod 从 Endpoints 对象中被移除后,前面的负载均衡器就会将流量路由到其他(新的)Pod 中去。因为在负载均衡器注意到变更并更新其配置之前,终止信号就会去停用 Pod,而这个重新配置过程又是异步发生的,并不能保证正确的顺序,所以就可能导致很少的请求会被路由到已经终止的 Pod 上去了,也就出现了上面我们说的情况。

零宕机

那么如何增强我们的应用程序以实现真正的零宕机迁移更新呢?

首先,要实现这个目标的先决条件是我们的容器要正确处理终止信号,在 SIGTERM信号上实现优雅关闭。下一步需要添加 readiness 可读探针,来检查我们的应用程序是否已经准备好来处理流量了。为了解决 Pod 停止的时候不会阻塞并等到负载均衡器重新配置的问题,我们还需要使用 preStop 这个生命周期的钩子,在容器终止之前调用该钩子。

生命周期钩子函数是同步的,所以必须在将最终停止信号发送到容器之前完成,在我们的示例中,我们使用该钩子简单的等待,然后 SIGTERM 信号将停止应用程序进程。同时,Kubernetes 将从 Endpoints 对象中删除该 Pod,所以该 Pod 将会从我们的负载均衡器中排除,基本上来说我们的生命周期钩子函数等待的时间可以确保在应用程序停止之前重新配置负载均衡器:

readinessProbe:
  # ...
lifecycle:
  preStop:
    exec:
      command: ["/bin/bash""-c""sleep 20"]

我们这里使用 preStop 设置了一个 20s 的宽限期,Pod 在真正销毁前会先 sleep 等待 20s,这就相当于留了时间给 Endpoints 控制器和 kube-proxy 更新去 Endpoints 对象和转发规则,这段时间 Pod 虽然处于 Terminating 状态,即便在转发规则更新完全之前有请求被转发到这个 Terminating 的 Pod,依然可以被正常处理,因为它还在 sleep,没有被真正销毁。

现在,当我们去查看滚动更新期间的 Pod 行为时,我们将看到正在终止的 Pod 处于 Terminating 状态,但是在等待时间结束之前不会关闭的,如果我们使用 Fortio 重新测试下,则会看到零失败请求的理想状态。

HPA

现在应用是固定的3个副本,但是往往在生产环境流量是不可控的,很有可能一次活动就会有大量的流量,3个副本很有可能抗不住大量的用户请求,这个时候我们就希望能够自动对 Pod 进行伸缩,直接使用前面我们学习的 HPA 这个资源对象就可以满足我们的需求了。

直接使用kubectl autoscale命令来创建一个 HPA 对象

$ kubectl autoscale deployment wordpress --namespace kube-example --cpu-percent=20 --min=3 --max=6
horizontalpodautoscaler.autoscaling/hpa-demo autoscaled
$ kubectl get hpa -n kube-example
NAME        REFERENCE              TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   <unknown>/20%   3         6         0          13s

此命令创建了一个关联资源 wordpress 的 HPA,最小的 Pod 副本数为3,最大为6。HPA 会根据设定的 cpu 使用率(20%)动态的增加或者减少 Pod 数量。同样,使用上面的 Fortio 工具来进行压测一次,看下能否进行自动的扩缩容:

$ fortio load -a -c 8 -qps 1000 -t 60"http://k8s.qikqiak.com:30012"

在压测的过程中我们可以看到 HPA 的状态变化以及 Pod 数量也变成了6个:

$ kubectl get hpa -n kube-example
NAME        REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   98%/20%   3         6         6          2m40s
$ kubectl get pods -n kube-example                
NAME                              READY   STATUS    RESTARTS   AGE
wordpress-79d756cbc8-f6kfm        1/1     Running   0          21m
wordpress-79d756cbc8-kspch        1/1     Running   0          32s
wordpress-79d756cbc8-sf5rm        1/1     Running   0          32s
wordpress-79d756cbc8-tsjmf        1/1     Running   0          20m
wordpress-79d756cbc8-v9p7n        1/1     Running   0          32s
wordpress-79d756cbc8-z4wpp        1/1     Running   0          21m
wordpress-mysql-5756ccc8b-zqstp   1/1     Running   0          3d19h

当压测停止以后正常5分钟后就会自动进行缩容,变成最小的3个 Pod 副本。

安全性

安全性这个和具体的业务应用有关系,比如我们这里的 Wordpress 也就是数据库的密码属于比较私密的信息,我们可以使用 Kubernetes 中的 Secret 资源对象来存储比较私密的信息:

$ kubectl create secret generic wordpress-db-pwd --from-literal=dbpwd=wordpress -n kube-example
secret/wordpress-db-pwd created

然后将 Deployment 资源对象中的数据库密码环境变量通过 Secret 对象读取:

env:
- name: WORDPRESS_DB_HOST
  valuewordpress-mysql:3306name: WORDPRESS_DB_USER
  value: wordpress
- name: WORDPRESS_DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: wordpress-db-pwd
      key: dbpwd

这样我们就不会在 YAML 文件中看到明文的数据库密码了,当然安全性都是相对的,Secret 资源对象也只是简单的将密码做了一次 Base64 编码而已,对于一些特殊场景安全性要求非常高的应用,就需要使用其他功能更加强大的密码系统来进行管理了,比如 Vault。

持久化

现在还有一个比较大的问题就是我们的数据还没有做持久化,MySQL 数据库没有做,Wordpress 应用本身也没有做,这显然不是一个合格的线上应用。这里我们直接使用前面章节中创建的 rook-ceph-block 这个 StorageClass 来创建我们的数据库存储后端:(pvc.yaml)

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
  namespace: kube-example
  labels:
    app: wordpress
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage20Gi

但是由于 Wordpress 应用是多个副本,所以需要同时在多个节点进行读写,也就是 accessModes 需要 ReadWriteMany 模式,而 Ceph RBD 模式是不支持 RWM 的,所以需要使用 CephFS,首先需要在 Ceph 中创建一个 Filesystem,这里我们可以通过 Rook 的 CephFilesystem 资源对象创建,如下所示:

apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
  name: myfs
  namespace: rook-ceph
spec:
  metadataPool:
    replicated:
      size3
  dataPools:
  - replicated:
      size3
  metadataServer:
    activeCount1
    activeStandby: true

创建完成后还会生成一个名为 myfs-data0 的存储池,也会自动生成两个 MDS 的 Pod 服务:

$ kubectl get pods -n rook-ceph |grep myfs
rook-ceph-mds-myfs-a-7948557994-44c4f                  1/1     Running     0          11m
rook-ceph-mds-myfs-b-5976b868cc-gl86g                  1/1     Running     0          11m

这个时候就可以创建我们的 StorageClass 对象了:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-cephfs
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  # 上面创建的 CephFS 文件系统名称
  fsName: myfs
  # 自动生成的
  pool: myfs-data0 
  # Root path of an existing CephFS volume
  # Required for provisionVolume"false"
  # rootPath: /absolute/path
  # The secrets contain Ceph admin credentials. These are generated automatically by the operator
  # in the same namespace as the cluster.
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph

reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:

同样直接创建上面的 StorageClass 资源对象即可,现在 Wordpress 的 PVC 对象使用我们这里的 csi-cephfs 这个 StorageClass 对象:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-pvc
  namespace: kube-example
  labels:
    app: wordpress
spec:
  storageClassName: csi-cephfs
  accessModes:
  - ReadWriteMany  # 由于是多个Pod所以要用 RWM
  resources:
    requests:
      storage2Gi

直接创建上面的资源对象:

$ kubectl get pvc -n kube-example
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mysql-pvc       Bound    pvc-93e0b186-da20-4e5e-8414-8cc73e00bf64   20Gi       RWO            rook-ceph-block   45m
wordpress-pvc   Bound    pvc-87675c58-407a-4b7e-be9d-0733f67c4835   2Gi        RWX            csi-cephfs        5s

在使用 CephFS 的过程中遇到了上面的 PVC 一直处于 Pending 状态,然后 describe 后发现有如下所示的错误信息:

如何部署一个生产级别的 Kubernetes 应用

从错误信息上面来看是在通过 StorageClass 去自动创建 PV 的时候就出现了问题,所以我们需要去检查 CSI 的 attach 阶段:

$ kubectl get pods -n rook-ceph |grep csi-cephfsplugin-provisioner
csi-cephfsplugin-provisioner-56c8b7ddf4-4s7fd          4/4     Running     0          18m
csi-cephfsplugin-provisioner-56c8b7ddf4-55sg6          4/4     Running     0          39m

然后查看这两个 Pod 的日志发现都是类似于下面的错误:

I0304 10:04:04.823171       1 leaderelection.go:246failed to acquire lease rook-ceph/rook-ceph-cephfs-csi-ceph-com
I0304 10:04:14.344109       1 leaderelection.go:350lock is held by csi-cephfsplugin-provisioner-56c8b7ddf4-rq4t6 and has not yet expired

因为我们这里有两个副本的 Provisioner,正常应该是有一个提供服务,另外一个作为备用的,通过获取到分布式锁来表示当前的 Pod 是否是 leader,这里两个 Pod 都没获取到,应该就是出现了通信问题,然后将两个 Pod 都重建后,其中一个 Pod 便获取到了 lease 对象,然后 PVC 也成功绑定上了 PV。

可以看到上面的 PVC 已经自动绑定到 PV 上面去了,这个就是上面的 StorageClass 完成的工作。然后在 Wordpress 应用上添加对 /var/www/html 目录的挂载声明:

  volumeMounts:
  - name: wordpress-data
    mountPath: /var/www/html
volumes:
- name: wordpress-data
  persistentVolumeClaim:
    claimName: wordpress-pvc

在 MySQL 应用上添加对 /var/lib/mysql 目录的挂载声明:

  volumeMounts:
  - name: mysql-data
    mountPath: /var/lib/mysql
volumes:
- name: mysql-data
  persistentVolumeClaim:
    claimName: mysql-pvc

重新更新应用即可,在更新的过程中发现 MySQL 启动失败了,报如下所示的错误:

......
[ERROR] --initialize specified but the data directory has files in it. Aborting.

意思就是 /var/lib/mysql 目录下面已经有数据了,当然可以清空该目录然后重新创建即可,这里可能是 mysql:5.7 这个镜像的 BUG,所以我们更改成 mysql:5.6 这个镜像,去掉之前添加的一个认证参数:

containers:
image: mysql:5.6
  name: mysql
  imagePullPolicy: IfNotPresent
  args:
  - --character-set-server=utf8mb4
  - --collation-server=utf8mb4_unicode_ci

重新更新就可以正常启动了。

Ingress

对于一个线上的应用对外暴露服务用一个域名显然是更加合适的,上面我们使用的 NodePort 类型的服务不适合用于线上生产环境,这里我们通过 Ingress 对象来暴露服务,由于我们使用的是 Traefik2.1 这个 Ingress 控制器,所以通过 IngressRoute 对象来暴露我们的服务,此外为了安全我们还使用了 ACME 来自动获取 https 的证书,并且通过一个中间件将 http 强制跳转到 https 服务:(ingressroute.yaml)

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: wordpress-https
  namespace: kube-example
spec:
  entryPoints:
    - websecure
  routes:
  - match: Host(`wordpress.qikqiak.com`)
    kind: Rule
    services:
    - name: wordpress
      port80
  tls:
    certResolver: ali
    domains:
    - main"*.qikqiak.com"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: redirect-https
  namespace: kube-example
spec:
  redirectScheme:
    scheme: https
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: wordpress-http
  namespace: kube-example
spec:
  entryPoints:
    - web
  routes:
  - match: Host(`wordpress.qikqiak.com`)
    kind: Rule
    services:
    - name: wordpress
      port80
    middlewares: 
    - name: redirect-https

直接创建上面的资源对象即可:

$ kubectl apply -f ingressroute.yaml 
ingressroute.traefik.containo.us/wordpress-https created
middleware.traefik.containo.us/redirect-https created
ingressroute.traefik.containo.us/wordpress-http created

然后对域名 wordpress.qikqiak.com 加上对应的 DNS 解析即可正常访问了,这样即使我们的数据库或者 Wordpress 应用挂掉了也不会丢失数据了,到这里就完成了我们一个生产级别应用的部署,虽然应用本身很简单,但是如果真的要部署到生产环境我们需要关注的内容就比较多了,当然对于线上应用除了上面我们提到的还有很多值得我们关注的地方,比如监控报警、日志收集等方面都是我们关注的层面,这些在后面的课程中我们再慢慢去了解。

如何部署一个生产级别的 Kubernetes 应用






本文作者: k8s技术圈
转发链接: 原文地址





  • 发表于 2021-05-30 11:33
  • 阅读 ( 45 )
  • 分类:Kubenetes

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
zhik8sadm9
zhik8sadm9

运维工程师

14 篇文章

作家榜 »

  1. zhik8sadm9 14 文章
  2. DonaldBraks 0 文章
  3. hengjunyin 0 文章
  4. hebergemWob 0 文章
  5. liangjj 0 文章
  6. Lai Wei 0 文章
  7. noveluser 0 文章
  8. z1x2c34 0 文章