K8s如何实现滚动更新、健康检查与探测机制

K8s如何实现滚动更新

一、什么是滚动更新

当某个服务需要升级时,传统的做法是,先将要更新的服务下线,业务停止后再更新版本和配置,然后重新启动服务。如果业务集群规模较大时,这个工作就变成了一个挑战,而且全部停止了服务,再逐步升级的方式会导致服务较长时间不可用。针对这个问题,k8s提供了滚动更新(rolling-update)的方式来解决上述问题。

滚动更新是针对pod来操作的,它通过一次只更新一小部分副本,成功后,再更新更多的副本,最终完成所有副本的更新。滚动更新的最大的好处是零停机,整个更新过程始终有副本在运行,从而保证了服务的连续性。

二、实例分析滚动更新实现逻辑

下面我们部署一个http应用,三个副本,初始镜像为 httpd:2.4.33,然后将其更新到 httpd:2.4.38。编写一个Deployment文件http.yml,内容如下:

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpd2.4.33-deployment
spec:
  replicas: 3
  selector:
   matchLabels:
     app: httpd
  template:
    metadata:
      labels:
        app: httpd
    spec:
      containers:
      - name: httpd
        image: httpd:2.4.33
        ports:
        - containerPort: 80

然后执行这个文件,并查看状态,执行如下命令:

复制代码
[root@master k8s]# kubectl apply -f httpd.yml
[root@master k8s]# kubectl get deployment -o wide
[root@master k8s]# kubectl get replicaset -o wide
[root@master k8s]# kubectl get pod -o wide

然后,将http.yml文件中,镜像的版本改为httpd:2.4.38,再次执行更新操作,

复制代码
[root@master k8s]# kubectl apply -f httpd.yml
[root@master k8s]#  kubectl get replicaset -o wide

从中可以发现:新创建的ReplicaSet镜像为httpd:2.4.38,并且管理了三个新的 Pod。而老的ReplicaSet里面已经没有任何Pod。结论是老的ReplicaSet的三个httpd:2.4.33 Pod 已经被新的ReplicaSet的三个httpd:2.4.38 Pod逐渐替换了。

具体替换过程可以通过如下命令查看:

具体替换过程可以通过如下命令查看:

复制代码
[root@master k8s]# kubectl describe deployment httpd

从图中,可以看出更新规则如下:

每次只更新替换一个 Pod:

复制代码
ReplicaSet httpd2.4.33-deployment-65996464c7 增加一个 Pod,总数为 1。
ReplicaSet httpd2.4.33-deployment-67cc966cb4 减少一个 Pod,总数为 2。
ReplicaSet httpd2.4.33-deployment-65996464c7 增加一个 Pod,总数为 2。
ReplicaSet httpd2.4.33-deployment-67cc966cb4 减少一个 Pod,总数为 1。
ReplicaSet httpd2.4.33-deployment-65996464c7 增加一个 Pod,总数为 3。
ReplicaSet httpd2.4.33-deployment-67cc966cb4 减少一个 Pod,总数为 0。

三、k8s中版本回滚方法

在执行kubectl apply命令更新应用时,K8s都会记录下当前的配置,保存为一个revision(版本),通过这个版本记录,就可以回滚到某个特定的revision。默认配置下,K8s只会保留最近的几个版本,不过可以在Deployment配置文件中通过revisionHistoryLimit属性增加revision的数量。

下面具体来演示一下K8s中版本回滚的方法。编写三个Deployment文件http-2.4.39.yml、http-2.4.41.yml、http-2.4.43.yml,分别对应的httpd镜像为2.4.39、2.4.41和2.4.43,文件内容如下:

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpd-roll-deployment
spec:
  revisionHistoryLimit: 10
  replicas: 3
  selector:
   matchLabels:
     app: httpd
  template:
    metadata:
      labels:
        app: httpd
    spec:
      containers:
      - name: httpd
        image: httpd:2.4.39
        ports:
        - containerPort: 80

这是第一个文件http-2.4.39.yml,其它两个文件中,对应的httpd镜像分别为2.4.41和2.4.43,其它配置均一样。

然后,依次执行这三个文件,操作如下:

复制代码
[root@master roll]# kubectl apply -f http-2.4.39.yml --record
[root@master roll]# kubectl apply -f http-2.4.41.yml --record
[root@master roll]# kubectl apply -f http-2.4.43.yml --record

其中,--record的作用是将当前命令记录到revision记录中,这样就可以知道每个revison对应的是哪个配置文件了。

接着,查看revison历史记录,执行如下命令:

复制代码
[root@master roll]# kubectl rollout history deployment httpd-roll-deployment
deployment.apps/httpd-roll-deployment 
REVISION  CHANGE-CAUSE
1         kubectl apply --filename=http-2.4.39.yml --record=true
2         kubectl apply --filename=http-2.4.41.yml --record=true
3         kubectl apply --filename=http-2.4.43.yml --record=true
[root@master roll]# kubectl  get deploy -o wide
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE     CONTAINERS    IMAGES         SELECTOR
httpd-roll-deployment   3/3     3            	3                2m19s   httpd        	httpd:2.4.43   app=httpd

例如要回滚到某个版本,比如revision 2,可以执行如下命令:

复制代码
[root@master roll]#  kubectl rollout undo deployment httpd-roll-deployment --to-revision=2
deployment.apps/httpd-roll-deployment rolled back
[root@master roll]# kubectl  get deploy -o wide
NAME                    	     READY   UP-TO-DATE   AVAILABLE          AGE      CONTAINERS   IMAGES         SELECTOR
httpd-roll-deployment   3/3                3            	3               3m59s   	httpd        httpd:2.4.41   app=httpd

回滚执行完成后,revison历史记录也会发生相应变化,执行如下命令查看:

复制代码
[root@master roll]#  kubectl rollout history deployment httpd-roll-deployment
deployment.apps/httpd-roll-deployment 
REVISION  CHANGE-CAUSE
1         kubectl apply --filename=http-2.4.39.yml --record=true
3         kubectl apply --filename=http-2.4.43.yml --record=true
4         kubectl apply --filename=http-2.4.41.yml --record=true

从输出可在,revison 2变成了revison 4。因此,有回滚需求的话,一定要在执行kubectl apply时加上 --record参数。

K8s中的健康检查与探测机制

一、k8s中默认的健康检查机制

k8s有强大的自愈能力,默认的自愈实现方式是自动重启发生故障的容器。那么k8s是如何发现容器故障的呢?

每个容器启动时都会执行一个进程,此进程由Dockerfile的CMD或ENTRYPOINT指定的。如果进程退出时返回码非零,则认为容器发生故障,K8s就会根据restartPolicy策略重启容器。

下面通过一个例子演示一下这种情况:先编写一个创建pod的文件,内容如下:

复制代码
[root@master pod]# more healthcheck-pod.yml 
apiVersion: v1
kind: Pod
metadata:
 name: pod-healthcheck
 labels:
   test: healthcheck-test
spec:
  restartPolicy: OnFailure
  containers:
  - name: healthcheck
    image: busybox
    args:
    - /bin/sh
    - -c
    - sleep 10; exit 1   	#sleep 10; exit 1 模拟容器启动 10 秒后发生故障

接着,执行这个pod,查看pod状态:

复制代码
[root@master pod]# kubectl  apply -f healthcheck-pod.yml
[root@master pod]# kubectl  get pod
NAME                READY              STATUS                  RESTARTS         AGE
pod-healthcheck     0/1       CrashLoopBackOff        5                    8m/

从输出可在,此pod已经自动重启了5次,由于容器进程返回值非零(1),K8s则认为容器发生故障,需要重启。此pod会一直这样重启下去。

这是最简单的健康检查机制,但是这种机制太不智能了,因为很多情况下,应用发生故障,并不一定进程就会退出,比如访问http服务器显示500/503等内部错误,可能是系统超载,也可能是资源不足导致,但此时httpd进程并没有异常退出,如果此时还是用默认的检查机制,那么将无法检查到这种情况。

此时我们可以利用Liveness和Readiness探测机制设置更精细的健康检查。

二、通过Liveness探测容器状态

Liveness探测允许让用户设置一些条件,来判断容器是否健康,如果探测失败,K8s就会重启容器。

下面看一个例子,编写一个pod脚本,内容如下:

复制代码
apiVersion: v1
kind: Pod
metadata:
 name: pod-liveness
 labels:
   test: liveness-test
spec:
  restartPolicy: OnFailure
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/check; sleep 30; rm -rf /tmp/check; sleep 600
    livenessProbe:
      exec:
       command:
       - cat
       - /tmp/check
      initialDelaySeconds: 10
      periodSeconds: 5

说明:

这个pod的执行过程是,首先启动一个busybox镜像,然后在容器中创建一个/tmp/check文件,等待30秒后,再次删除这个文件,然后继续运行600秒。

livenessProbe部分定义了如何执行Liveness探测:探测的方法是通过cat命令检查/tmp/check文件是否存在。如果命令执行成功,返回值为零,K8s则认为本次Liveness探测成功;如果命令返回值非零,本次Liveness探测失败。

另外,initialDelaySeconds: 10 表示指定容器启动10秒之后开始执行Liveness探测,由于容器启动需要一段时间,因此我们一般会根据应用启动的准备时间来设置这个值。比如某个应用正常启动要花20秒,那么initialDelaySeconds的值就应该大于20。

periodSeconds: 5 表示指定每5秒执行一次Liveness探测。K8s如果连续执行3次Liveness探测均失败,则会杀掉并重启容器。

接着,开始测试这个功能,执行如下命令:

复制代码
[root@master pod]#  kubectl apply  -f  healthcheck-liveness-pod.yml

执行如下命令,看pod启动日志:

复制代码
[root@master pod]#  kubectl describe pod pod-liveness

从输出可知,最开始的30秒,/tmp/check存在,cat命令返回0,Liveness探测成功,之后,日志会显示/tmp/check已经不存在,Liveness探测失败。再过几十秒,几次探测都失败后,容器会被重启。

复制代码
[root@master pod]# kubectl  get pod
NAME             READY     STATUS               RESTARTS     AGE
pod-liveness     0/1         CrashLoopBackOff            6            12m

三、通过Readiness探测容器状态

从上面的分析可知,通过Liveness探测可以实现重启容器实现自愈,而接下来要介绍的Readiness探测实现的是,在容器故障时,将容器设置为不可用,不接收Service转发的请求。注意这两个的区别。

Readiness 探测的配置语法与 Liveness 探测完全一样,看下面一个例子:

复制代码
[root@master pod]# more healthcheck-readiness.yml 
apiVersion: v1
kind: Pod
metadata:
 name: pod-readiness
 labels:
   test: readiness-test
spec:
  restartPolicy: OnFailure
  containers:
  - name: readiness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/check; sleep 30; rm -rf /tmp/check; sleep 600
    readinessProbe:
      exec:
       command:
       - cat
       - /tmp/check
      initialDelaySeconds: 10
      periodSeconds: 5

这个配置文件只是将前面例子中的liveness替换为了readiness,其它没有任何变化。

接着,开始测试这个功能,执行如下命令:

复制代码
[root@master pod]# kubectl  apply  -f  healthcheck-readiness.yml

Pod readiness的READY状态经历了如下变化:

(1)、刚被创建时,READY 状态为不可用。

(2)、15 秒后(initialDelaySeconds + periodSeconds),第一次进行 Readiness 探测并成功返回,设置 READY 为可用。

(3)、30 秒后,/tmp/check被删除,连续3次Readiness探测均失败后,READY又被设置为不可用。

最后,执行如下命令,看pod启动日志:

复制代码
[root@master pod]# kubectl describe pod pod-liveness

下面对 Liveness 探测和 Readiness 探测做个比较:

    • Liveness 探测和Readiness探测是两种健康检查机制,如果不特意配置,K8s将对两种探测采取相同的默认行为,即通过判断容器启动进程的返回值是否为零来判断探测是否成功。
    • 两种探测的配置方法完全一样,支持的配置参数也一样。不同之处在于探测失败后的行为:Liveness探测失败,会重启容器;而Readiness探测失败,会将容器设置为不可用,Liveness 探测和 Readiness 探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用。

四、健康检查在更新副本中的应用

对于多副本的应用,在执行副本升级操作时,新副本会作为backend被添加到Service的负载均衡中,与已有的副本一起处理客户的请求。

但这里需要考虑一个问题,那就是新副本的启动通常都需要一个准备阶段,比如加载缓存数据,连接数据库等,从容器启动到正真能够提供服务是需要一段时间的。

此时,我们可以通过Readiness探测,判断容器是否就绪,避免将请求发送到还没有ready的backend上。下面看一个例子,配置文件内容如下:

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myweb
spec:
  replicas: 3
  selector:
   matchLabels:
     run: web
  template:
   metadata:
     labels:
       run: web
   spec:
    containers:
    - name: web
      image: httpd
      ports:
      - containerPort: 80
      readinessProbe:
        httpGet:
          scheme: HTTP
          path: /index.html
          port: 80
        initialDelaySeconds: 10
        periodSeconds: 5 

apiVersion: v1
kind: Service
metadata:
   name: web-service
spec:
  selector:
    run: web
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80

这个配置文件,将Deployment和Service放在了一起,这样写也是可以的,重点看readinessProbe部分。这里使用另一种探测方法httpGet。K8s对于该方法探测成功的判断条件是http请求的返回代码在200-400之间。

schema:指定协议,支持HTTP(默认值)和HTTPS。

Path:指定探测路径。

port :指定探测端口。

上面这段readiness配置的作用是:

容器启动10秒之后开始探测。如果访问http://container_ip:80/index.html返回代码不是200-400之间,表示容器没有就绪,那么不接收Service的请求。

接着,每隔5秒再探测一次。直到返回代码为200-400之间,表明容器已经就绪,然后将其加入到web-service的负责均衡中,开始接收客户端请求。

最后,探测会继续以5秒的间隔执行,如果连续发生3次探测失败,容器又会从负载均衡中移除,直到下次探测成功,重新加入。

五、健康检查在滚动更新中的应用

在对应用进行版本升级时,k8s会依次启动新副本,然后逐渐删除旧副本,在这个过程中,可能会发生如下问题:

  • 一个新副本需要10秒左右完成准备工作,在此之前无法响应业务请求。
  • 如果出现人为配置错误,导致副本始终无法准备就绪(例如无法连接数据库)

这种情况下,如果没有自定义配置健康检查机制,采用了默认检查机制,此时,由于新副本本身没有异常退出,就会出现旧副本逐渐被新副本替换,导致整个应用无法处理请求,无法对外提供服务,

而如果配置了Readiness探测,新副本只有通过了检测,才会被添加到 Service,如果没有通过探测,现有副本不会被全部替换,业务仍然正常进行。

这就是Readiness的用途。

下面看个例子,演示一下这个过程:

首先看第一个部署文件appv1.yml ,内容如下:

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
spec:
  replicas: 10
  selector:
   matchLabels:
     run: app
  template:
    metadata:
     labels:
       run: app
    spec:
     containers:
     - name: app
       image: busybox
       args:
       - /bin/sh
       - -c
       - sleep 10; touch /tmp/check; sleep 30000
       readinessProbe:
         exec:
           command:
            - cat
            - /tmp/check
         initialDelaySeconds: 10
         periodSeconds: 5

这个deployment配置了10个副本,10秒钟后,Readiness开始探测,从上面的条件来看,Readiness探测是可以通过的。

接着,再写一个部署文件appv2.yml,模拟一个滚动更新失败的场景,内容如下:

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
spec:
  replicas: 10
  selector:
   matchLabels:
     run: app
  template:
    metadata:
     labels:
       run: app
    spec:
     containers:
     - name: app
       image: busybox
       args:
       - /bin/sh
       - -c
       - sleep 30000
       readinessProbe:
         exec:
           command:
            - cat
            - /tmp/check
         initialDelaySeconds: 10
         periodSeconds: 5

这个部署文件中,很明显,探测的条件不满足(/tmp/check不存在),所以无法通过Readiness探测。

那么会发生什么情况呢,演示如下:

复制代码
[root@master check]# kubectl apply -f  appv1.yml
[root@master check]# kubectl  get deploy
[root@master check]# kubectl  get pod
[root@master check]# kubectl  apply -f appv2.yml
[root@master check]# kubectl  get deploy
[root@master check]# kubectl  get pod

首先看kubectl get deploy的输出:

  • READY列中的10表示期望值,也就是10个READY的副本。左边的8表示目前READY的副本数。
  • UP-TO-DATE表示当前已经完成更新的副本数, 5表示5个新副本。
  • AVAILABLE表示当前处于READY状态的副本数,即8个旧副本。

接着再来看看kubectl get pod输出:

  • 状态是ContainerCreating的5个Pod是新创建的副本,目前处于NOT READY状态。而处于正常状态的旧副本从最初10个减少到8个。还有2个Terminating状态的pod正在销毁。
  • 当前总共有13个新、旧副本。也就是8个旧副本+5个新副本。

在我们部署文件配置中,新副本始终都无法通过Readiness探测,所以这个状态会一直持续下去。

从这个过程可知,k8s的健康检查机制帮我们屏蔽了有缺陷的副本,同时保留了大部分旧副本,业务没有因更新失败受到影响。

接下来,在深入探讨下上面关于副本数的问题,为什么新创建的副本数是5个,同时只销毁了2个旧副本呢?

原因是这样的:

滚动更新通过参数maxSurge和maxUnavailable来控制副本替换的数量。

  1. maxSurge:此参数控制滚动更新过程中副本总数可以超过期望副本数的上限。maxSurge可以是具体的整数(比如 2),也可以是百分百,向上取整。maxSurge默认值为25%。

在上面的例子中,期望副本数为10,那么副本总数的最大值为:

复制代码
roundUp(10 + 10 * 25%) = 13,所以我们看到总共有13个pod。
  1. maxUnavailable:此参数控制滚动更新过程中,不可用的副本占期望副本数的最大比例。 maxUnavailable可以是具体的整数(比如 2),也可以是百分百,向下取整。maxUnavailable默认值为25%。

在上面的例子中,期望副本数为10,那么可用的副本数至少要为:

复制代码
10 -- roundDown(10 * 25%) = 8,所以我们看到pod的AVAILABLE就是8。

可以发现:maxSurge值越大,初始创建的新副本数量就越多;maxUnavailable值越大,初始销毁的旧副本数量就越多。

总结一下:

上面这个案例滚动更新的过程是这样的:

首先创建3个新副本使副本总数达到13个。然后销毁2个旧副本,使可用的副本数降到8个。当这2个旧副本成功销毁后,可再创建2个新副本,使副本总数保持为13个。

而当新副本通过Readiness探测后,会使可用副本(新副本+旧副本)数增加,超过8。那么此时就可以继续销毁更多的旧副本,使可用副本(新副本+旧副本)数回到8。

旧副本的销毁使副本总数低于13,这样就允许创建更多的新副本。这个过程会持续进行,最终所有的旧副本都会被新副本替换,滚动更新完成。

由于上面故意模拟了一个更新失败的场景,要恢复这个失败,需要回滚到之前版本即可,执行如下命令:

复制代码
[root@master check]# kubectl rollout history deployment
[root@master check]# kubectl rollout undo deployment app-deployment --to-revision=1
[root@master check]# kubectl  get deploy
[root@master check]# kubectl  get pod 

执行完毕后,发现已经回滚到appv1版本,10个副本已经正常运行了。

如果要定制 maxSurge 和 maxUnavailable,可以如下配置:

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
spec:
  strategy:
    rollingUpdate:
      maxSurge: 35%
      maxUnavailable: 35%
下面内容省略。。
相关推荐
kaisun641 小时前
Docker 构建网络问题排查
网络·docker·eureka
楼田莉子2 小时前
Docker学习:Docker介绍及其架构介绍
运维·后端·学习·docker·容器·架构
SpikeKing2 小时前
LLM - 集成 Hermes Agent 与 WebUI 至同一个 Docker 镜像配置
docker·webui·vibecoding·hermes agent
杨浦老苏3 小时前
网络连接实时可视化利器TapMap
网络·docker·可视化·监控·群晖
张忠琳4 小时前
【kubernetes v1.21】(一)Kubernetes 总览架构深度分析
云原生·架构·kubernetes
香气袭人知骤暖4 小时前
PG数据库 Docker 容器自动备份方案
数据库·docker·容器
AI服务老曹4 小时前
解耦异构算力:基于 Docker 与 GB28181/RTSP 的边缘计算 AI 视频管理平台架构设计与源码交付实践
人工智能·docker·边缘计算
weixin_468466854 小时前
Prometheus监控服务部署与实战指南
服务器·后端·python·docker·自动化·prometheus
maomao大哥闯天下4 小时前
K8s对象deployment、job、service应用详解
java·容器·kubernetes