描述

用 Wordpress 示例尽可能将前面知识点串联起来,目的是让 Wordpress 应用具有高可用、滚动更新、数据持久化、自动扩容、 HTTPS 访问等等。

实现

Wordpress 是一个基于 PHP 和 MySQL 的开源内容管理系统。此处采用将 wordpress 应用 和 mysql 部署到同一个 Pod 里的方案。

namespace.yaml

  1. apiVersion: v1
  2. kind: Namespace
  3. metadata:
  4. name: kube-example


deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: kube-example
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - name: wordpress
        image: wordpress:5.3.2-apache
        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.7
        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

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: kube-example
spec:
  selector:
    app: wordpress
  type: NodePort
  ports:
  - name: web
    port: 80
    targetPort: wdport
# 应用
$ kubectl apply -f namespace.yaml
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

# 查看
$ kubectl get pods -n kube-example
NAME                         READY   STATUS    RESTARTS   AGE
wordpress-77dcdb64c6-zdlb8   2/2     Running   0          12m

$ kubectl get svc -n kube-example
NAME        TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)  AGE
wordpress   NodePort   10.106.237.157   80:30892/TCP  2m2s


MySQL 和 Wordpress 在同一个 Pod 里,故在 Wordpress 中指定数据库地址可使用 localhost:3306 。当 Pod 启动完成后,通过 http://<任意节点IP>:30892 来访问。

高可用

实现后存在的问题:

  • 一个 Pod 中的所有容器没有启动的先后顺序,所以可能当 wordpress 容器启动时,mysql 还没有正常启动;
  • 存在单点故障和性能瓶颈。Wordpress 应用本身是无状态的,可以增加副本数。MySQL 是有状态的。

所以目前高可用方案是增加 Wordpress 副本数,而数据库 MySQL 还是一个实例。MySQL 也可以通过其它集群手段实现高可用,此处内容略。

mysql.yaml

apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  namespace: kube-example
  labels:
    app: wordpress
spec:
  ports:
  - port: 3306
    targetPort: dbport
  selector:
    app: wordpress
    tier: mysql
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  namespace: kube-example
  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.7
        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


wordpress.yaml

apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: kube-example
  labels:
    app: wordpress
spec:
  selector:
    app: wordpress
    tier: frontend
  type: NodePort
  ports:
  - name: web
    port: 80
    targetPort: wdport
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: kube-example
  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.3.2-apache
        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


环境变量 WORDPRESS_DB_HOST 的值将由 localhost 更改成 MySQL 服务的 DNS 地址,完整域名是 wordpress-mysql.kube-example.svc.cluster.local:3306,因两 Pod 都处于同一个命名空间,故简写 wordpress-mysql:3306

# 应用资源
$ kubectl apply -f mysql.yaml
$ kubectl apply -f wordpress.yaml

# 查看
$ kubectl get pods -l app=wordpress -n kube-example
NAME                               READY   STATUS    RESTARTS   AGE
wordpress-7f8d5bdb7d-8hs8r         1/1     Running   0          11s
wordpress-7f8d5bdb7d-gxsfd         1/1     Running   0          11s
wordpress-7f8d5bdb7d-qprw2         1/1     Running   0          11s
wordpress-mysql-5d664b6f54-kxlmr   1/1     Running   0          11s


#验证

$ kubectl get svc -l app=wordpress -n kube-example
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
wordpress         NodePort    10.96.90.167     <none>        80:32543/TCP   55s
wordpress-mysql   ClusterIP   10.106.183.216   <none>        3306/TCP       55s

通过 http://<任意节点的NodeIP>:32543 访问应用,页面如下:
image.png

稳定性

健康检查

健康检查是提高应用健壮性的手段,当检测到应用不健康时可以自动重启容器,当应用还没有准备好时暂时不要对外提供服务。添加 liveness probe 和 rediness probe 健康检测探针。

只添加可读性探针,不添加存活性探针的原因:
考虑到线上错误排查,当应用出现问题就自动重启去掩盖错误,可能这个错误会被永远忽略掉。所以不使用存活性探针,而是结合监控报警,保留错误现场,方便错误排查。

readinessProbe:
  tcpSocket:
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 5

资源限额

当节点资源不足时, 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,用计算资源换取业务性能和稳定性,减少排查问题时间和成本。

给应用设置资源大小,数值该如何确定?
使用 Apache Bench(AB Test) 或者 Fortio(Istio 测试工具) 等测试工具压测。

每秒1000个请求和8个并发连接的测试命令

$ fortio load -a -c 8 -qps 1000 -t 60s "http://ip:30012"
Starting at 1000 qps with 8 thread(s) [gomax 2] for 1m0s : 7500 calls each (total 60000)
Ended after 1m0.687224615s : 5005 calls. qps=82.472
Aggregated Sleep Time : count 5005 avg -27.128368 +/- 16 min -55.964246789 max -0.050576982 sum -135777.482
[......]
Sockets used: 53 (for perfect keepalive, would be 8)
Code 200 : 5005 (100.0 %)
Response Header Sizes : count 5005 avg 292.17083 +/- 1.793 min 292 max 311 sum 1462315
Response Body/Total Sizes : count 5005 avg 27641.171 +/- 1.793 min 27641 max 27660 sum 138344060
Saved result to data/2020-02-15-125121_Fortio.json (graph link)
All done 5005 calls 95.872 ms avg, 82.5 qps

压测期间查看应用的资源使用情况

$ kubectl top pods -l app=wordpress -n kube-example
NAME                              CPU(cores)   MEMORY(bytes)
wordpress-5cc66f986b-2jv7h        569m         72Mi
wordpress-5cc66f986b-nf79l        997m         71Mi
wordpress-d4c885d5d-gtvhd         895m         87Mi

结果显示内存处于 100Mi 以内,而 CPU 消耗较大。由于 CPU 是可压缩资源,超过了限制应用也不会挂掉,只会变慢。所以添加如下配额信息:

resources:
  limits:
    cpu: 200m
    memory: 100Mi
  requests:
    cpu: 200m
    memory: 100Mi

滚动更新

Deployment 控制器默认的就是滚动更新的更新策略,该策略可以在任何时间点更新应用时保证某些实例依然可以正常运行来防止应用 down 掉,当新部署的 Pod 启动并可以处理流量之后,才会去杀掉旧的 Pod。

Wordpress 应用中添加滚动更新策略:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0


更新应用时进行压测,输出信息可得部分请求 502

$ 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
[...]

失败原因

image.png
通过 NodePort 访问应用,实际上是通过 kube-proxy 更新 iptables 规则来实现。Kubernetes 会根据 Pods 的状态去更新 Endpoints 对象,保证 Endpoints 中包含的都是准备好处理请求的 Pod。

当新 Pod 处于活动状态并准备就绪后,Kubernetes 就将会停止旧 Pod,将其状态更新为 Terminating ,然后从 Endpoints 列表中移除,并且发送一个 SIGTERM 信号给 Pod 的主进程。SIGTERM 信号就会让容器以正常的方式关闭,且不接受任何新的连接。

当新 Pod 处于活动状态并准备就绪时,并不一定代表服务可正常访问,因为 Kubernetes 中 Pod 是 running 状态时,可能 Pod 中的容器还在运行加载所需内容以便能够正常提供服务。所以可能导致新 Pod 并未能正常提供服务就被加到 Endpoints 列表中,当请求路由到此 Pod 时,则出现 502 。

在负载均衡器注意到有信息更新时,会去更新配置,同时终止信号会去停用 Pod,而此重新配置过程是异步发生,不能保证正确的顺序。所以可能导致很少的请求会被路由到已经终止的 Pod 上去,则出现 502 。

零宕机

针对以上两种失败原因,可分别进行如下处理:

  • 添加 readiness 可读探针,检查应用程序是否已经准备好来处理流量
  • 使用 preStop ,在容器终止之前调用该钩子。生命周期钩子函数是同步的,所以必须在将最终停止信号发送到容器之前完成。

使用该钩子进行 sleep 操作,一般来说 20s 可确保在 Pod 停止之前,负载均衡器已经重新配置好(即 Pod 从Endpoints 列表中移除)

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


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

现在更新资源时,使用 Fortio 测试,会看到零失败请求的理想状态。

HPA

在生产环境流量是不可控的,很有可能一次活动就会有大量的流量,3个副本很可能抗不住大量的用户请求,此时期望 Pod 自动扩缩容。

HPA 会根据设定的 cpu 使用率(20%)动态增减 Pod 数量,最小副本数 3,最大 6

# 创建hpa对象
$ kubectl autoscale deployment wordpress --namespace kube-example --cpu-percent=20 --min=3 --max=6

$ kubectl get hpa -n kube-example
NAME        REFERENCE              TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
wordpress   Deployment/wordpress   /20%   3         6         0        3          13s
$ fortio load -a -c 8 -qps 1000 -t 60s "http://ip:30012"

# 压测过程中hpa状态变化,副本数变为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副本。

安全性

数据库密码属于私密信息,使用 Secret 资源对象存储

$ kubectl create secret generic wordpress-db-pwd --from-literal=dbpwd=wordpress -n kube-example
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

持久化

要做一个合格的线上应用,MySQL 数据库,Wordpress 应用本身都需要做数据持久化。

参考链接 进阶存储类

对外访问

线上生产环境要通过 Ingress 对象来暴露服务。

参考链接 集群服务外部访问(Ingress)