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来控制副本替换的数量。
- maxSurge:此参数控制滚动更新过程中副本总数可以超过期望副本数的上限。maxSurge可以是具体的整数(比如 2),也可以是百分百,向上取整。maxSurge默认值为25%。
在上面的例子中,期望副本数为10,那么副本总数的最大值为:
roundUp(10 + 10 * 25%) = 13,所以我们看到总共有13个pod。
- 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%
下面内容省略。。