Kubernetes快速编排WordPress高可用案例

今天我们来部署一个 WordPress 应用示例,我们需要达到的目的是让 WordPress 应用具有高可用、滚动更新的过程中不能中断服务、数据要持久化不能丢失、当应用负载太高的时候能够自动进行扩容、当然还有 HTTPS 访问等等,这些是我们的应用部署到线上环境基本上要具备的一些能力,接下来我们就来一步一步完成这些需求。

1、基本原理

要部署 WordPress 应用,我们需要知道 WordPress 是如何运行起来的。Wordpress 是一个基于 PHP 和 MySQL 的流行开源内容管理系统,拥有丰富的插件和模板系统。好了、现在我们就清楚应该如何去运行 WordPress 了,只需要一个能够解析 PHP 的程序 和 MySQL 数据库就可以了。

但是我们要想在 Kubernetes 系统中来运行,肯定需要使用到 Docker 镜像了,对于 WordPress 应用程序本身官方提供了镜像 https://hub.docker.com/_/wordpress,也给出了运行说明。我们可以通过一系列环境变量去指定 MySQL 数据库的配置,只需要将这些参数配置上直接运行即可。但是 WordPress 应用本身会频繁的和 MySQL 数据库进行交互,这种情况下如果将二者用容器部署在同一个 Pod 下面是不是要高效很多;因为一个 Pod 下面的所有容器是共享同一个 network和namespace 的;下面我们就来部署 WordPress 应用。

我们将 WordPress 应用部署到 image-z0ukun-com 这个命名空间下面,所有我们首先需要讲一个命名空间(namespace.yaml):

apiVersion: v1
kind: Namespace
metadata:
  name: image-z0ukun-com

然后我们直接创建这个ns:

[root@kubernetes-01 wordpress]# kubectl apply -f namespace.yaml 
namespace/image-z0ukun-com created
[root@kubernetes-01 wordpress]# kubectl get ns
NAME              STATUS   AGE
image-z0ukun-com   Active   2s
default           Active   4d7h
kube-node-lease   Active   4d7h
kube-public       Active   4d7h
kube-system       Active   4d7h
monitoring        Active   3d
rook-ceph         Active   3d14h
[root@kubernetes-01 wordpress]# 

创建完成以后我们开始编写deployment资源清单文件(deployment-test.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: image-z0ukun-com
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - name: wordpress
        image: wordpress:5.4.1
        ports:
        - containerPort: 80
          name: wdport
        env:
        - name: WORDPRESS_DB_HOST
          value: localhost:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress
      - name: mysql
        image: mysql:5.6.48
        imagePullPolicy: IfNotPresent
        args:  # 新版本镜像有更新,需要使用下面的认证插件环境变量配置才会生效
        - --default_authentication_plugin=mysql_native_password
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
        ports:
        - containerPort: 3306
          name: dbport
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: rootPassW0rd
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          value: wordpress

这里我们把 MySQL 和 WordPress 放在在同一个 Pod 下面,所以在 WordPress 中我们指定数据库地址的时候是用的 localhost:3306,因为这两个容器已经共享同一个 network和namespace 了。这点很重要,如果我们想把这个服务暴露给外部用户,我们还需要创建一个 Service 或者 Ingress 对象,这里我们一步一步来,暂时先创建一个 NodePort 类型的 Service:(service-test.yaml)

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: image-z0ukun-com
spec:
  selector:
    app: wordpress
  type: NodePort
  ports:
  - name: web
    port: 80
    targetPort: wdport

因为只需要暴露 WordPress 这个应用,所以只匹配了一个名为 wdport 的端口,现在我们来创建上面的几个资源对象:

kubectl apply -f deployment-test.yaml 
kubectl apply -f service-test.yaml 

这里我们提前把需要的镜像拉取到各个主机节点上面,接下来就是等待拉取镜像,启动 Pod:

[root@kubernetes-01 ~]# kubectl get pod -n image-z0ukun-com -o wide
NAME                        READY   STATUS    RESTARTS   AGE     IP              NODE            NOMINATED NODE   READINESS GATES
wordpress-dbcd48bbd-w79sn   2/2     Running   0          2m41s   172.30.31.115   kubernetes-01   <none>           <none>
[root@kubernetes-01 ~]# kubectl get svc -n image-z0ukun-com
NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
wordpress   NodePort   10.254.39.166   <none>        80:30038/TCP   2m44s
[root@kubernetes-01 ~]# 

当 Pod 启动完成后,我们就可以通过上面的 http://<任意节点IP>:30038 这个 NodePort 端口来访问应用了。

但是我们仔细想一想这种方式有什么问题呢?首先一个 Pod 中的所有容器并没有启动的先后顺序,所以很有可能当 wordpress 这个容器启动起来后去连接 mysql 的时候,mysql 还没有启动;另外一个问题是现在wordpress应用只有一个副本?会有单点问题,应用的性能也是一个问题。由于 WordPress 应用本身是无状态应用,所以这种情况下一般我们只需要多部署几个副本即可,比如我们可以在 Deployment 的 YAML 文件中加上 replicas:3 这个属性。

但是新的问题又产生了:由于 MySQL 是有状态应用,每一个 Pod 里面的数据库的数据都是独立的,他们并没有共享,也就是说这3个 Pod 相当于是独立的3个 WordPress 实例,所以应该怎么办呢?拆分?对的、我们只需要把 WordPress 和 MySQL 这两个容器都部署成独立的 Pod 就可以了,这样我们只需要对 WordPress 应用增加副本,而数据库 MySQL 还是一个实例,所有的应用都连接到这一个数据库上面,是不是就可以解决这个问题呢?下面我们就一起来试试看。

2、WordPress高可用

现在我们对 Pod 中的两个容器进行拆分,将 WordPress 和 MySQL 分别部署;然后 WordPress 用多个副本进行部署就可以实现应用的高可用了。由于 MySQL 是有状态应用,一般来说需要用 StatefulSet 来进行管理,但是我们这里部署的 MySQL 并不是集群模式,而是单副本的,所以用 Deployment 也是没有问题的,当然如果要真正用于生产环境还是需要集群模式的(mysql-deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  namespace: image-z0ukun-com
  labels:
    app: wordpress
    tier: mysql
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.6.48
        imagePullPolicy: IfNotPresent
        args:  # 新版本镜像有更新,需要使用下面的认证插件环境变量配置才会生效
        - --default_authentication_plugin=mysql_native_password
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
        ports:
        - containerPort: 3306
          name: dbport
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: rootPassW0rd
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          value: wordpress

我们给 MySQL 应用添加一个 Service 对象,因为 WordPress 应用需要来连接数据库,之前在同一个 Pod 中我们用 localhost 即可,现在需要通过 Service 的 DNS 形式的域名进行连接(mysql-service.yaml):

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  namespace: image-z0ukun-com
  labels:
    app: wordpress
spec:
  ports:
  - port: 3306
    targetPort: dbport
  selector:
    app: wordpress
    tier: mysql

然后我们直接创建这两个资源对象即可:

kubectl apply -f namespace.yaml
kubectl apply -f mysql-service.yaml
kubectl apply -f mysql-deployment.yaml

接下来创建独立的 WordPress 服务,首先我们需要来创建一个Deployment的资源对象:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: image-z0ukun-com
  labels:
    app: wordpress
    tier: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - name: wordpress
        image: wordpress:5.4.1
        ports:
        - containerPort: 80
          name: wdport
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress

然后我们通过NodePort模式把wordpress服务暴露出来:

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: image-z0ukun-com
  labels:
    app: wordpress
spec:
  selector:
    app: wordpress
    tier: frontend
  type: NodePort
  ports:
  - name: web
    port: 80
    targetPort: wdport

注:这里环境变量 WORDPRESS_DB_HOST 的值是将之前的 localhost 地址更改成了上面 MySQL 服务的 DNS 地址,完整的域名应该是 wordpress-mysql.kube-example.svc.cluster.local:3306,由于这两个应该都处于同一个命名空间,所以直接简写成 wordpress-mysql:3306 也是可以的。

然后我们创建上面的资源对象:

kubectl apply -f wordpress-service.yaml
kubectl apply -f wordpress-deployment.yaml

[root@kubernetes-01]# kubectl get pod -n image-z0ukun-com -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP               NODE            NOMINATED NODE   READINESS GATES
wordpress-99bc4dd8b-5z6jx          1/1     Running   0          4m57s   172.30.31.91     kubernetes-01   <none>           <none>
wordpress-99bc4dd8b-jrnhp          1/1     Running   0          4m57s   172.30.218.147   kubernetes-02   <none>           <none>
wordpress-99bc4dd8b-nnsv4          1/1     Running   0          4m57s   172.30.11.38     kubernetes-03   <none>           <none>
wordpress-mysql-74ccbd5d66-9wptb   1/1     Running   0          12m     172.30.31.86     kubernetes-01   <none>           <none>

创建完成之后我们可以看到所有Pod都已经是 Running 状态了,但是我们怎么验证呢?上面我们采用的是 NodePort 模式来暴露的服务,我们可以来查看一下服务端口并通过浏览器访问一下:

[root@kubernetes-01]# kubectl get svc -n image-z0ukun-com -o wide
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE     SELECTOR
wordpress         NodePort    10.254.115.36   <none>        80:31875/TCP   4m59s   app=wordpress,tier=frontend
wordpress-mysql   ClusterIP   10.254.13.228   <none>        3306/TCP       13m     app=wordpress,tier=mysql
[root@kubernetes-01]#

可以看到 wordpress 服务产生了一个 31875 的端口,现在我们就可以通过 http://<任意节点的NodeIP>:31875 来访问 wordpress 应用了。如果我们在浏览器中打开,可以看到 wordpress 跳转到了安装页面,证明我们的安装是正确的,如果没有出现预期的效果,那么就需要去查看下 Pod 的日志来排查问题了,根据页面提示,填上对应的信息,点击“安装”即可,最终安装成功后,我们就可以看到熟悉的首页界面了:

img

3、WordPress稳定性

现在 Wodpress 应用已经部署成功了,那么就万事大吉了吗?如果网站访问量突然变大了怎么办,如果我们要更新镜像该怎么办?所以要保证我们的网站能够非常稳定的提供服务,我们做得还不够,我们可以通过做些什么事情来提高网站的稳定性呢?

3.1、避免单点故障

为什么会有单点故障的问题呢?我们不是部署了多个副本的 WordPress 应用吗?当我们设置 replicas=1 的时候肯定会存在单点故障问题,如果大于 1 但是所有副本都调度到了同一个节点的是不是同样就会存在单点问题了,这个节点挂了所有副本就都挂了,所以我们不仅需要设置多个副本数量,还需要让这些副本调度到不同的节点上,来避免单点故障,这个利用 Pod 反亲和性来实现了,我们可以加上如下所示的配置:

apiVersion: apps/v1
......
spec:
  replicas: 3
  ......
    spec:
      containers:
      - name: wordpress
        image: wordpress:5.4.1
      ......
      # 增加反亲和性配置,避免单点故障
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:  # 软策略
          - weight: 1
            podAffinityTerm:
              topologyKey: kubernetes.io/hostname
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - wordpress

这里的意思就是如果一个节点上面有 app=wordpress 这样的 Pod ,那么我们的 Pod 就尽可能别调度到这个节点上面来,因为我们这里的节点并不多,所以我使用的是软策略。如果使用硬策略的话,应用副本数超过了节点数就必然会有 Pod 调度不成功,如果你线上节点非常多的话(节点数大于 Pod 副本数),建议使用硬策略,更新后我们可以查看下 3 个副本被分散在了不同的节点上。添加完成之后我们直接apply更新上面的资源对象即可。

3.2、PDB可用性

有些时候线上的某些节点需要做一些维护操作,比如要升级内核,这个时候我们就需要将要维护的节点进行驱逐操作,驱逐节点首先是将节点设置为不可调度,这样可以避免有新的 Pod 调度上来,然后将该节点上的 Pod 全部删除,ReplicaSet 控制器检测到 Pod 数量减少了就会重新创建一个新的 Pod,调度到其他节点上面的,这个过程是先删除,再创建,并非是滚动更新,因此更新过程中,如果一个服务的所有副本都在被驱逐的节点上,则可能导致该服务不可用。

如果服务本身存在单点故障,所有副本都在同一个节点,驱逐的时候肯定就会造成服务不可用了,这种情况我们使用上面的反亲和性和多副本就可以解决这个问题。但是如果我们的服务本身就被打散在多个节点上,这些节点如果都被同时驱逐的话,那么这个服务的所有实例都会被同时删除,这个时候也会造成服务不可用了,这种情况下我们可以通过配置 PDB(PodDisruptionBudget)对象来避免所有副本同时被删除,比如我们可以设置在驱逐的时候 wordpress 应用最多只有一个副本不可用,其实就相当于逐个删除并在其它节点上重建(wordpress-pdb.yaml):

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: wordpress-pdb
  namespace: image-z0ukun-com
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: wordpress
      tier: frontend

我们直接创建上面这个资源对象,关于 PDB 的更多详细信息可以查看官方文档:https://kubernetes.io/docs/tasks/run-application/configure-pdb/。

[root@kubernetes-01]# vi wordpress-pdb.yaml 
[root@kubernetes-01]# kubectl apply -f wordpress-pdb.yaml 
poddisruptionbudget.policy/wordpress-pdb created
[root@kubernetes-01]# kubectl get pdb -n image-z0ukun-com
NAME            MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
wordpress-pdb   N/A             1                 1                     25s
[root@kubernetes-01]# 
3.3、健康检查

我们的应用现在还有一个非常重要的功能没有提供,那就是健康检查,我们知道健康检查是提高应用健壮性非常重要的手段,当我们检测到应用不健康的时候我们希望可以自动重启容器,当应用还没有准备好的时候我们也希望暂时不要对外提供服务,所以我们需要添加我们前面经常提到的 liveness probe 和 rediness probe 两个健康检测探针,检查探针的方式有很多,我们这里当然可以认为如果容器的 80 端口可以成功访问那么就是健康的,对于一般的应用提供一个健康检查的 URL 会更好,这里我们添加一个如下所示的可读性探针,为什么不添加存活性探针呢?这里其实是考虑到线上错误排查的一个问题,如果当我们的应用出现了问题,然后就自动重启去掩盖错误的话,可能这个错误就会被永远忽略掉了,所以其实这是一个折衷的做法,不使用存活性探针,而是结合监控报警,保留错误现场,方便错误排查,但是可读写探针是一定需要添加的:

apiVersion: apps/v1
......
    spec:
      containers:
      - name: wordpress
        image: wordpress:5.4.1
        ports:
        - containerPort: 80
          name: wdport
        # 增加健康检查
        readinessProbe:
          tcpSocket:
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        env:
......

增加上面的探针,每 5s 检测一次应用是否可读,这样只有当 readinessProbe 探针检测成功后才表示准备好接收流量了,这个时候才会更新 Service 的 Endpoints 对象。

3.4、QOS服务质量

QoS 是 Quality of Service 的缩写,即服务质量。为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes 针对不同服务质量的预期,通过 QoS 来对 Pod 进行服务质量管理。对于一个 Pod 来说,服务质量体现在两个具体的指标:CPU 和内存。当节点上内存资源紧张时,Kubernetes 会根据预先设置的不同 QoS 类别进行相应处理。 QoS 主要分为 Guaranteed、Burstable 和 Best-Effort三类,优先级从高到低。我们先分别来介绍下这三种服务类型的定义。

Guaranteed(有保证的)

属于该级别的 Pod 有以下两种:Pod 中的所有容器都且仅设置了 CPU 和内存的 limits;Pod 中的所有容器都设置了 CPU 和内存的 requests 和 limits ,且单个容器内的requestslimits(requests不等于0)

Pod 中的所有容器都且仅设置了 limits,示例如下:

containers:
  - name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
  - name: bar
    resources:
      limits:
        cpu: 100m
        memory: 100Mi

Pod 中的所有容器都设置了 requests 和 limits,且单个容器内的 requestslimits 的情况,示例如下:

containers:
  - name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
      requests:
        cpu: 10m
        memory: 1Gi
  - name: bar
    resources:
      limits:
        cpu: 100m
        memory: 100Mi
      requests:
        cpu: 100m
        memory: 100Mi

容器 foo 和 bar 内 resources 的 requests 和 limits 均相等,该 Pod 的 QoS 级别属于 Guaranteed。

Burstable(不稳定的)

Pod 中只要有一个容器的 requests 和 limits 的设置不相同,那么该 Pod 的 QoS 即为 Burstable。

容器 foo 指定了 resource,而容器 bar 未指定,示例如下:

containers:
  - name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
      requests:
        cpu: 10m
        memory: 1Gi
  - name: bar

容器 foo 设置了内存 limits,而容器 bar 设置了CPU limits,示例如下:

containers:
  - name: foo
    resources:
      limits:
        memory: 1Gi
  - name: bar
    resources:
      limits:
        cpu: 100m

需要注意的是如果容器指定了 requests 而未指定 limits,则 limits 的值等于节点资源的最大值,如果容器指定了 limits 而未指定 requests,则 requests 的值等于 limits。

Best-Effort(尽最大努力)

如果 Pod 中所有容器的 resources 均未设置 requests 与 limits,该 Pod 的 QoS 即为 Best-Effort。

容器 foo 和容器 bar 均未设置requests 和 limits,示例如下:

containers:
  - name: foo
    resources:
  - name: bar
    resources:

资源回收策略

Kubernetes 通过 CGroup 给 Pod设置 QoS 级别,当资源不足时会优先 kill 掉优先级低的 Pod,在实际使用过程中,通过 OOM 分数值来实现,OOM 分数值范围为 0-1000,OOM 分数值根据 OOM_ADJ参数计算得出。

对于 Guaranteed 级别的 Pod,OOM_ADJ 参数设置成了-998,对于 Best-Effort 级别的 Pod,OOM_ADJ 参数设置成了1000,对于 Burstable 级别的 Pod,OOM_ADJ 参数取值从 2 到 999。

QoS Pods 被 kill 掉的场景和顺序如下所示:

Best-Effort Pods:系统用完了全部内存时,该类型 Pods 会最先被 kill 掉;

Burstable Pods:系统用完了全部内存,且没有 Best-Effort 类型的容器可以被 kill 时,该类型的 Pods 会被 kill 掉;

Guaranteed Pods:系统用完了全部内存,且没有 Burstable 与 Best-Effort 类型的容器可以被 kill 时,该类型的 pods 会被 kill 掉。

所以如果资源充足,可将 QoS Pods 类型设置为 Guaranteed,用计算资源换业务性能和稳定性,减少排查问题时间和成本。如果想更好的提高资源利用率,业务服务可以设置为 Guaranteed,而其他服务根据重要程度可分别设置为 Burstable 或 Best-Effort,这就要看具体的场景了。 比如我们这里如果想要尽可能提高 WordPress 应用的稳定性,我们可以将其设置为 Guaranteed 类型的 Pod,我们现在没有设置 resources 资源,所以现在是 Best-Effort 类型的 Pod。

现在如果要想给应用设置资源大小,就又有一个新的问题了,应该如何设置合适的资源大小呢?其实这就需要我们对自己的应用非常了解才行了,一般情况下我们可以先不设置资源,然后可以根据我们的应用的并发和访问量来进行压力测试,基本上可以大概计算出应用的资源使用量,我们这里可以使用 Apache Bench(AB Test) 或者 Fortio(Istio 测试工具) 这样的测试工具来测试,我们这里使用 Fortio 这个测试工具,比如每秒 1000 个请求和 8 个并发的连接的测试命令如下所示:

[root@ZabbixServer ~]# fortio load -a -c 8 -qps 1000 -t 60s "http://172.16.200.11:31875/"
Fortio 1.3.1 running at 1000 queries per second, 8->8 procs, for 1m0s: http://172.16.200.11:31875/
13:09:38 I httprunner.go:82> Starting http test for http://172.16.200.11:31875/ with 8 threads at 1000.0 qps
Starting at 1000 qps with 8 thread(s) [gomax 8] for 1m0s : 7500 calls each (total 60000)
......
Sockets used: 96 (for perfect keepalive, would be 8)
Code 200 : 9480 (100.0 %)
Response Header Sizes : count 9480 avg 276.17838 +/- 1.832 min 276 max 295 sum 2618171
Response Body/Total Sizes : count 9480 avg 26312.178 +/- 1.832 min 26312 max 26331 sum 249439451
All done 9480 calls (plus 8 warmup) 50.547 ms avg, 157.9 qps
Successfully wrote 4585 bytes of Json data to 2020-06-09-130938_172_16_200_11_31875_ZabbixServer.json
[root@ZabbixServer ~]# 

也可以通过浏览器查看到最终测试结果:

img

在测试期间我们可以用如下所示的命令查看应用的资源使用情况:

[root@kubernetes-01 wordpress]# kubectl top pod -n image-z0ukun-com
NAME                               CPU(cores)   MEMORY(bytes)   
wordpress-75cb574c7b-9phmb         1719m        82Mi            
wordpress-75cb574c7b-pcmh6         1839m        82Mi            
wordpress-75cb574c7b-wq5mv         1409m        88Mi            
wordpress-mysql-74ccbd5d66-9wptb   1509m        478Mi           
[root@kubernetes-01 wordpress]# 

我们可以看到内存基本上都是处于 100Mi 以内,而 CPU 消耗就非常大了,但是由于 CPU 是可压缩资源,也就是说超过了限制应用也不会挂掉的,只是会变慢而已。所以我们这里可以给 WordPress 应用添加如下所示的资源配置,如果你集群资源足够的话可以适当多分配一些资源:

apiVersion: apps/v1
......
    spec:
      containers:
      - name: wordpress
        resources:
          limits:
            cpu: 500m
            memory: 100Mi
          requests:
            cpu: 500m
            memory: 100Mi
        image: wordpress:5.4.1
......

4、滚动更新

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

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

apiVersion: apps/v1
......
spec:
  replicas: 3
  # 滚动更新
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
......

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

[root@ZabbixServer ~]# fortio load -a -c 8 -qps 10000 -t 60s "http://172.16.200.11:31875/"
Fortio 1.3.1 running at 10000 queries per second, 8->8 procs, for 1m0s: http://172.16.200.11:31875/
......
Sockets used: 35 (for perfect keepalive, would be 8)
Code  -1 : 3 (0.1 %)
Code 200 : 2530 (99.9 %)
Response Header Sizes : count 2533 avg 275.81563 +/- 9.638 min 0 max 295 sum 698641
Response Body/Total Sizes : count 2533 avg 26280.979 +/- 905 min 0 max 26331 sum 66569721
All done 2533 calls (plus 8 warmup) 189.641 ms avg, 42.1 qps
Successfully wrote 5417 bytes of Json data to 2020-06-09-153658_172_16_200_11_31875_ZabbixServer.json
[root@ZabbixServer ~]# 

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

4.1、失败原因

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

img

Kubernetes 会根据 Pods 的状态去更新 Endpoints 对象,这样就可以保证 Endpoints 中包含的都是准备好处理请求的 Pod。一旦新的 Pod 处于活动状态并准备就绪后,Kubernetes 就将会停止就的 Pod,从而将 Pod 的状态更新为 “Terminating”,然后从 Endpoints 对象中移除,并且发送一个 SIGTERM 信号给 Pod 的主进程。

SIGTERM 信号就会让容器以正常的方式关闭,并且不接受任何新的连接。Pod 从 Endpoints 对象中被移除后,前面的负载均衡器就会将流量路由到其他(新的)Pod 中去。因为在负载均衡器注意到变更并更新其配置之前,终止信号就会去停用 Pod,而这个重新配置过程又是异步发生的,并不能保证正确的顺序,所以就可能导致很少的请求会被路由到已经终止的 Pod 上去了,也就出现了上面我们说的情况。

4.2、零宕机

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

首先,要实现这个目标的先决条件是我们的容器要正确处理终止信号,在 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 重新测试下,则会看到零失败请求的理想状态。

5、HPA弹性伸缩

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

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

[root@kubernetes-01 wordpress]# kubectl autoscale deployment wordpress --namespace image-z0ukun-com --cpu-percent=20 --min=3 --max=6
horizontalpodautoscaler.autoscaling/wordpress autoscaled
[root@kubernetes-01 wordpress]# kubectl get hpa -n image-z0ukun-com
NAME        REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   0%/20%    3         6         3          20s
[root@kubernetes-01 wordpress]# 

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

fortio load -a -c 8 -qps 10000 -t 60s "http://172.16.200.11:31875/"

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

[root@kubernetes-01 wordpress]# kubectl get hpa -n image-z0ukun-com
NAME        REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   82%/20%   3         6         6          2m5s
[root@kubernetes-01 wordpress]# kubectl get pod -n image-z0ukun-com
NAME                               READY   STATUS    RESTARTS   AGE
wordpress-76947db5fb-5mrdv         1/1     Running   0          28m
wordpress-76947db5fb-8nxpn         1/1     Running   0          28m
wordpress-76947db5fb-8zk6m         1/1     Running   0          27s
wordpress-76947db5fb-lz46k         1/1     Running   0          27s
wordpress-76947db5fb-q7r94         1/1     Running   0          28m
wordpress-76947db5fb-s9zzt         1/1     Running   0          27s
wordpress-mysql-74ccbd5d66-9wptb   1/1     Running   0          6h5m
[root@kubernetes-01 wordpress]# 

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

6、安全性保障

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

kubectl create secret generic wordpress-db-pwd --from-literal=dbpwd=wordpress -n image-z0ukun-com

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

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

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

7、数据持久化

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

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
  namespace: image-z0ukun-com
  labels:
    app: wordpress
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi

但是由于 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:
      size: 3
  dataPools:
  - replicated:
      size: 3
  metadataServer:
    activeCount: 1
    activeStandby: true

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

[root@kubernetes-01]# kubectl get pods -n rook-ceph |grep myfs
rook-ceph-mds-myfs-a-6bb88745fd-sgjhk                     1/1     Running       0          11s
rook-ceph-mds-myfs-b-79f5847bcc-sf8j2                     1/1     Running       0          10s
[root@kubernetes-01]# 

这个时候我们就可以创建 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: image-z0ukun-com
  labels:
    app: wordpress
spec:
  storageClassName: csi-cephfs
  accessModes:
  - ReadWriteMany  # 由于是多个Pod所以要用 RWM
  resources:
    requests:
      storage: 50Gi

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

[root@kubernetes-01]# kubectl get pvc -n image-z0ukun-com
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mysql-pvc       Bound    pvc-9e569e37-1891-426f-81de-25dbde5aae1c   50Gi       RWO            rook-ceph-block   3m11s
wordpress-pvc   Bound    pvc-8f33ef27-7731-4fed-adcd-77a0b42f1538   50Gi       RWX            csi-cephfs        9s
[root@kubernetes-01]# 

我们来检查一下CSI的创建情况:

[root@kubernetes-01]# kubectl get pods -n rook-ceph |grep csi-cephfsplugin-provisioner
csi-cephfsplugin-provisioner-7b8fbf88b4-thnrj             4/4     Running     136        4d9h
csi-cephfsplugin-provisioner-7b8fbf88b4-tw4h8             4/4     Running     81         4d9h
[root@kubernetes-01]# 

[root@kubernetes-01]# kubectl get pvc -n image-z0ukun-com                              
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mysql-pvc       Bound    pvc-9e569e37-1891-426f-81de-25dbde5aae1c   50Gi       RWO            rook-ceph-block   7m54s
wordpress-pvc   Bound    pvc-8f33ef27-7731-4fed-adcd-77a0b42f1538   50Gi       RWX            csi-cephfs        4m52s
[root@kubernetes-01]# kubectl get pv -n image-z0ukun-com                               
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                           STORAGECLASS      REASON   AGE
pvc-8f33ef27-7731-4fed-adcd-77a0b42f1538   50Gi       RWX            Retain           Bound    image-z0ukun-com/wordpress-pvc   csi-cephfs                 4m48s
pvc-9e569e37-1891-426f-81de-25dbde5aae1c   50Gi       RWO            Delete           Bound    image-z0ukun-com/mysql-pvc       rook-ceph-block            7m53s
[root@kubernetes-01]# 

可以看到这里有两个副本的 Provisioner,正常应该是有一个提供服务,另外一个作为备用的,通过获取到分布式锁来表示当前的 Pod 是否是 leader,这里 PVC 和 PV 也已经成功绑定了 。接下来我们就可以在Wordpress 应用上添加对 /var/www/html 目录的挂载声明:

apiVersion: apps/v1
......
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress
        volumeMounts:
        - name: wordpress-data
          mountPath: /var/www/html
      volumes:
      - name: wordpress-data
        persistentVolumeClaim:
          claimName: wordpress-pvc
......

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

apiVersion: apps/v1
......
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: rootPassW0rd
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          value: wordpress
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql-pvc

添加完成之后我们直接更新上面的两个资源对象即可。如果想验证数据持久化是否已经正常生效、我们可以去Wordpress后台新增一篇文章,然后采用下面的命令强制重启一下Pod。然后再刷新浏览器看看新增的文章是否还在。

kubectl replace --force -f wordpress-deployment.yaml

img

推荐文章