27 滚动更新:如何做到平滑的应用升级降级?
你好,我是Chrono。
上次课里我们学习了管理有状态应用的对象StatefulSet,再加上管理无状态应用的Deployment和DaemonSet,我们就能在Kubernetes里部署任意形式的应用了。
不过,只是把应用发布到集群里是远远不够的,要让应用稳定可靠地运行,还需要有持续的运维工作。
如果你还记得在第18节课里,我们学过Deployment的“应用伸缩”功能就是一种常见的运维操作,在Kubernetes里,使用命令 kubectl scale
,我们就可以轻松调整Deployment下属的Pod数量,因为StatefulSet是Deployment的一种特例,所以它也可以使用 kubectl scale
来实现“应用伸缩”。
除了“应用伸缩”,其他的运维操作比如应用更新、版本回退等工作,该怎么做呢?这些也是我们日常运维中经常会遇到的问题。
今天我就以Deployment为例,来讲讲Kubernetes在应用管理方面的高级操作:滚动更新,使用 kubectl rollout
实现用户无感知的应用升级和降级。
Kubernetes如何定义应用版本
应用的版本更新,大家都知道是怎么回事,比如我们发布了V1版,过了几天加了新功能,要发布V2版。
不过说起来简单,版本更新实际做起来是一个相当棘手的事。因为系统已经上线运行,必须要保证不间断地对外提供服务,通俗地说就是“给空中的飞机换引擎”。尤其在以前,需要开发、测试、运维、监控、网络等各个部门的一大堆人来协同工作,费时又费力。
但是,应用的版本更新其实是有章可循的,现在我们有了Kubernetes这个强大的自动化运维管理系统,就可以把它的过程抽象出来,让计算机去完成那些复杂繁琐的人工操作。
在Kubernetes里,版本更新使用的不是API对象,而是两个命令:kubectl apply
和 kubectl rollout
,当然它们也要搭配部署应用所需要的Deployment、DaemonSet等YAML文件。
不过在我们信心满满开始操作之前,首先要理解在Kubernetes里,所谓的“版本”到底是什么?
我们常常会简单地认为“版本”就是应用程序的“版本号”,或者是容器镜像的“标签”,但不要忘了,在Kubernetes里应用都是以Pod的形式运行的,而Pod通常又会被Deployment等对象来管理,所以应用的“版本更新”实际上更新的是整个Pod。
那Pod又是由什么来决定的呢?
仔细回忆一下之前我们创建的那么多个对象,你就会发现,Pod是由YAML描述文件来确定的,更准确地说,是Deployment等对象里的字段 template
。
所以,在Kubernetes里应用的版本变化就是 template
里Pod的变化,哪怕 template
里只变动了一个字段,那也会形成一个新的版本,也算是版本变化。
但 template
里的内容太多了,拿这么长的字符串来当做“版本号”不太现实,所以Kubernetes就使用了“摘要”功能,用摘要算法计算 template
的Hash值作为“版本号”,虽然不太方便识别,但是很实用。
我们就拿第18讲里的Nginx Deployment作为例子吧,创建对象之后,使用 kubectl get
来查看Pod的状态:
Pod名字里的那串随机数“6796……”就是Pod模板的Hash值,也就是Pod的“版本号”。
如果你变动了Pod YAML描述,比如把镜像改成 nginx:stable-alpine
,或者把容器名字改成 nginx-test
,都会生成一个新的应用版本,kubectl apply
后就会重新创建Pod:
你可以看到,Pod名字里的Hash值变成了“7c6c……”,这就表示Pod的版本更新了。
Kubernetes如何实现应用更新
为了更仔细地研究Kubernetes的应用更新过程,让我们来略微改造一下Nginx Deployment对象,看看Kubernetes到底是怎么实现版本更新的。
首先修改ConfigMap,让它输出Nginx的版本号,方便我们用curl查看版本:
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location / {
default_type text/plain;
return 200
'ver : $nginx_version\nsrv : $server_addr:$server_port\nhost: $hostname\n';
}
}
然后我们修改Pod镜像,明确地指定版本号是 1.21-alpine
,实例数设置为4个:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
replicas: 4
... ...
containers:
- image: nginx:1.21-alpine
... ...
把它命名为 ngx-v1.yml
,然后执行命令 kubectl apply
部署这个应用:
我们还可以为它创建Service对象,再用 kubectl port-forward
转发请求来查看状态:
从curl命令的输出中可以看到,现在应用的版本是 1.21.6
。
现在,让我们编写一个新版本对象 ngx-v2.yml
,把镜像升级到 nginx:1.22-alpine
,其他的都不变。
因为Kubernetes的动作太快了,为了能够观察到应用更新的过程,我们还需要添加一个字段 minReadySeconds
,让Kubernetes在更新过程中等待一点时间,确认Pod没问题才继续其余Pod的创建工作。
要提醒你注意的是,minReadySeconds
这个字段不属于Pod模板,所以它不会影响Pod版本:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
minReadySeconds: 15 # 确认Pod就绪的等待时间
replicas: 4
... ...
containers:
- image: nginx:1.22-alpine
... ...
现在我们执行命令 kubectl apply
来更新应用,因为改动了镜像名,Pod模板变了,就会触发“版本更新”,然后用一个新命令:kubectl rollout status
,来查看应用更新的状态:
更新完成后,你再执行 kubectl get pod
,就会看到Pod已经全部替换成了新版本“d575……”,用curl访问Nginx,输出信息也变成了“1.22.0”:
仔细查看 kubectl rollout status
的输出信息,你可以发现,Kubernetes不是把旧Pod全部销毁再一次性创建出新Pod,而是在逐个地创建新Pod,同时也在销毁旧Pod,保证系统里始终有足够数量的Pod在运行,不会有“空窗期”中断服务。
新Pod数量增加的过程有点像是“滚雪球”,从零开始,越滚越大,所以这就是所谓的“滚动更新”(rolling update)。
使用命令 kubectl describe
可以更清楚地看到Pod的变化情况:
- 一开始的时候V1 Pod(即ngx-dep-54b865d75)的数量是4;
- 当“滚动更新”开始的时候,Kubernetes创建1个 V2 Pod(即ngx-dep-d575d5776),并且把V1 Pod数量减少到3;
- 接着再增加V2 Pod的数量到2,同时V1 Pod的数量变成了1;
- 最后V2 Pod的数量达到预期值4,V1 Pod的数量变成了0,整个更新过程就结束了。
看到这里你是不是有点明白了呢,其实“滚动更新”就是由Deployment控制的两个同步进行的“应用伸缩”操作,老版本缩容到0,同时新版本扩容到指定值,是一个“此消彼长”的过程。
这个滚动更新的过程我画了一张图,你可以参考它来进一步体会:
Kubernetes如何管理应用更新
Kubernetes的“滚动更新”功能确实非常方便,不需要任何人工干预就能简单地把应用升级到新版本,也不会中断服务,不过如果更新过程中发生了错误或者更新后发现有Bug该怎么办呢?
要解决这两个问题,我们还是要用 kubectl rollout
命令。
在应用更新的过程中,你可以随时使用 kubectl rollout pause
来暂停更新,检查、修改Pod,或者测试验证,如果确认没问题,再用 kubectl rollout resume
来继续更新。
这两个命令比较简单,我就不多做介绍了,要注意的是它们只支持Deployment,不能用在DaemonSet、StatefulSet上(最新的1.24支持了StatefulSet的滚动更新)。
对于更新后出现的问题,Kubernetes为我们提供了“后悔药”,也就是更新历史,你可以查看之前的每次更新记录,并且回退到任何位置,和我们开发常用的Git等版本控制软件非常类似。
查看更新历史使用的命令是 kubectl rollout history
:
它会输出一个版本列表,因为我们创建Nginx Deployment是一个版本,更新又是一个版本,所以这里就会有两条历史记录。
但 kubectl rollout history
的列表输出的有用信息太少,你可以在命令后加上参数 --revision
来查看每个版本的详细信息,包括标签、镜像名、环境变量、存储卷等等,通过这些就可以大致了解每次都变动了哪些关键字段:
假设我们认为刚刚更新的 nginx:1.22-alpine
不好,想要回退到上一个版本,就可以使用命令 kubectl rollout undo
,也可以加上参数 --to-revision
回退到任意一个历史版本:
kubectl rollout undo
的操作过程其实和 kubectl apply
是一样的,执行的仍然是“滚动更新”,只不过使用的是旧版本Pod模板,把新版本Pod数量收缩到0,同时把老版本Pod扩展到指定值。
这个V2到V1的“版本降级”的过程我同样画了一张图,它和从V1到V2的“版本升级”过程是完全一样的,不同的只是版本号的变化方向:
Kubernetes如何添加更新描述
讲到这里,Kubernetes里应用更新的功能就学得差不多了。
不过,你有没有觉得 kubectl rollout history
的版本列表好像有点太简单了呢?只有一个版本更新序号,而另一列 CHANGE-CAUSE
为什么总是显示成 <none>
呢?能不能像Git一样,每次更新也加上说明信息呢?
这当然是可以的,做法也很简单,我们只需要在Deployment的 metadata
里加上一个新的字段 annotations
。
annotations
字段的含义是“注解”“注释”,形式上和 labels
一样,都是Key-Value,也都是给API对象附加一些额外的信息,但是用途上区别很大。
annotations
添加的信息一般是给Kubernetes内部的各种对象使用的,有点像是“扩展属性”;labels
主要面对的是Kubernetes外部的用户,用来筛选、过滤对象的。
如果用一个简单的比喻来说呢, annotations
就是包装盒里的产品说明书,而 labels
是包装盒外的标签贴纸。
借助 annotations
,Kubernetes既不破坏对象的结构,也不用新增字段,就能够给API对象添加任意的附加信息,这就是面向对象设计中典型的OCP“开闭原则”,让对象更具扩展性和灵活性。
annotations
里的值可以任意写,Kubernetes会自动忽略不理解的Key-Value,但要编写更新说明就需要使用特定的字段 kubernetes.io/change-cause
。
下面来操作一下,我们创建3个版本的Nginx应用,同时添加更新说明:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
annotations:
kubernetes.io/change-cause: v1, ngx=1.21
... ...
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v2, ngx=1.22
... ...
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v3, change name
... ...
你需要注意YAML里的 metadata
部分,使用 annotations.kubernetes.io/change-cause
描述了版本更新的情况,相比 kubectl rollout history --revision
的罗列大量信息更容易理解。
依次使用 kubectl apply
创建并更新对象之后,我们再用 kubectl rollout history
来看一下更新历史:
这次显示的列表信息就好看多了,每个版本的主要变动情况列得非常清楚,和Git版本管理的感觉很像。
小结
好,今天我们一起学习了Kubernetes里的高级应用管理功能:滚动更新,它会自动缩放新旧版本的Pod数量,能够在用户无感知的情况下实现服务升级或降级,让原本复杂棘手的运维工作变得简单又轻松。
再小结一下今天的要点:
- 在Kubernetes里应用的版本不仅仅是容器镜像,而是整个Pod模板,为了便于处理使用了摘要算法,计算模板的Hash值作为版本号。
- Kubernetes更新应用采用的是滚动更新策略,减少旧版本Pod的同时增加新版本Pod,保证在更新过程中服务始终可用。
- 管理应用更新使用的命令是
kubectl rollout
,子命令有status
、history
、undo
等。 - Kubernetes会记录应用的更新历史,可以使用
history --revision
查看每个版本的详细信息,也可以在每次更新时添加注解kubernetes.io/change-cause
。
另外,在Deployment里还有其他一些字段可以对滚动更新的过程做更细致的控制,它们都在 spec.strategy.rollingUpdate
里,比如 maxSurge
、maxUnavailable
等字段,分别控制最多新增Pod数和最多不可用Pod数,一般用默认值就足够了,你如果感兴趣也可以查看Kubernetes文档进一步研究。
课下作业
最后是课下作业时间,给你留两个思考题:
- 今天学的Kubernetes的“滚动更新”,与我们常说的“灰度发布”有什么相同点和不同点?
- 直接部署旧版本的YAML也可以实现版本回退,
kubectl rollout undo
命令的好处是什么?
欢迎在留言区积极参与讨论。如果觉得今天的内容对你有帮助,也欢迎转发给身边的朋友一起讨论,我们下节课再见。
- 马以 👍(37) 💬(1)
思考题: 1: “滚动发布”是能力,“灰度发布”是功能,k8s基于“滚动发布”的能力,可以实现pod的‘水平扩展/收缩’,从而能够提供类似于“灰度发布”、“金丝雀发布”这种功能。 2:其实讨论这个问题前,我们要先了解下k8s的控制器模型,另外还要引入一个概念就是“ReplicaSet”,什么意思呢,其实Deployment并不是直接控制Pod,Pod的归属对象是“ReplicaSet”,也就是说Deployment控制的是“ReplicaSet”(版本这个概念其实我们可以等同于是ReplicaSet),然后“ReplicaSet”控制Pod的数量。我们可以通过 kubectl get rs 来看下具体内容: $ kubectl get rs NAME DESIRED CURRENT READY AGE ngx-deploy-699b5dbd6b 4 4 4 7h42m ngx-deploy-6dfbbccf84 0 0 0 9d ngx-deploy-76c65bc7db 0 0 0 9d ngx-deploy-ffd96c4fc 0 0 0 7h46m 所以这个时候,我们再来理解“版本回退”和“直接部署旧版本的 YAML”的区别就容易了,这里的版本就像是我们平时开发代码库打的tag一样,是类似于我们的快照文件一样,这个快照信息可以正确的帮我们记录当时场景的最原始信息,所以我们通过版本回退的方式能够最大限度的保证正确性(这点是k8s已经为我们保证了这一点),反之如果我们通过旧的yaml部署,就不一定能保证当前这个yaml文件有没有被改动过,这里的变数还是挺大的,所以直接通过yaml部署,极大的增加了我们部署的风险性。
2022-08-24 - 椰子 👍(6) 💬(4)
滚动更新时,新版本和旧版本共存的那一小段时间是共同对外提供服务的吗?如果这样的话是不是线上就存在不同版本的问题了
2022-08-24 - Sports 👍(5) 💬(1)
第一个问题:灰度发布应该是多个应用版本共存,按一定比例分配; 第二个问题:使用rollout版本回退,保持pod固定数量滚动更新,减轻资源压力,尽量地避免版本冲突。 目前只想到这些,希望老师再补充😁
2022-08-24 - 菲茨杰拉德 👍(1) 💬(1)
新老pod同时在线。也就意味着,新功能与旧功能同时在线吗?这样业务无法接受吧。要经过灰度或者验证后,才切换到新pod才比较安全吧。
2022-11-27 - Bachue Zhou 👍(1) 💬(1)
这个滚动更新是否真的能做到用户无感知呢?我有点怀疑。例如用户刚刚发了一个 http 请求到一个 pod,处理了一半,pod 就被关闭了,是否可能造成这个请求最终在用户这边出错了?如果 http 能够处理这种情况,那么 http/2 能吗?grpc 能吗?
2022-11-20 - 密码123456 👍(1) 💬(5)
有个问题,我看公司的项目,直接jenkens打包,k8s就能感知到。那k8s,是如何做到的呢?
2022-08-25 - SYN数字化技术 👍(0) 💬(2)
这里指的是pod更新,如果是一个pod中有多个镜像,其中部分镜像版本更新,对应的是什么情况呢?我的理解是,pod此时不动,对应版本更新的容器进行重启
2024-06-06 - William 👍(0) 💬(1)
第一次绑定8080:80 端口后面, 升级了pod, 然后访问curl 127.0.0.1 就会报错: Unable to listen on port 8080: Listeners failed to create with the following errors: [unable to create listener: Error listen tcp4 127.0.0.1:8080: bind: address already in use unable to create listener: Error listen tcp6 [::1]:8080: bind: address already 原因: 因为 kubectl 没有释放其端口绑定,可以通过以下命令手动终止 pid(示例基于尝试向前运行8080端口) lsof -i:8080 (mac电脑) 会显示pid kill -9 pid 然后再重新绑定端口就可以了
2024-01-11 - 思绪走了灬光✨ 👍(0) 💬(3)
要想实现滚动更新,是不是pod内的应用应该使用短连接?如果是长连接的应用,那么连接可能一直都不会中断,那在强制中断的时候,还是会造成有损?
2023-06-10 - 滴流乱转小胖儿 👍(0) 💬(1)
关于老师提到的“maxSurge、maxUnavailable” 可以移步:https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/deployment/#max-unavailable
2022-11-13 - dao 👍(0) 💬(1)
在实验中的 svc 可以简单实用 `type: ClusterIP` ,这样可以直接使用 ClusterIP 访问,而不需要 port-forward 端口转发了。
2022-09-23 - dao 👍(0) 💬(1)
作业: 1, 滚动更新是一个逐步使用“新”版本替换“旧”版本的发布方式;灰度发布又称金丝雀发布,在灰度期间,“新”、“旧”两个版本会同时存在,这种发布方式可以用于实现A/B测试。 2,在实验环境中,我的每个版本并不是都有 YAML 文件,有时只是做一个很小的调整接着发布,这时 undo 比较好用,真正实现版本回滚/退 :)
2022-09-23 - Sam.张朝 👍(0) 💬(2)
有机会可以讲讲灰度发布
2022-09-05 - peter 👍(0) 💬(1)
请教老师几个问题: Q1:第26讲是关于sts的,如果要清理该讲所创建的环境,需要执行多个命令删除吗? 是否可以通过执行一个删除命令就可以全部删除? 第26讲涉及到3个对象:sts、svc、pod,要清理所有创建的实例, 是否存在一个命令就可以全部删除? 比如: kubectl delete sts。 但执行该命令后发现不能删除全部实例。好像只能逐个删除。 Q2:Replica Set是个单独的对象吗?还是说它等价于yaml文件中的: spec: replicas: 2 Q3: 第27讲,创建ngx-v1部分,创建四个POD都成功了,在一个终端执行“kubectl port-forward svc/ngx-svc 8080:80 &”,执行后此终端上显示“Forwarding from [::1]:8080 -> 80”。新开一个终端,在此新终端上执行“curl 127.1:8080”,结果是:新终端上显示nginx的欢迎页,在老终端上显示“Handling connection for 8080”,但没有显示”ver、srv”等信息。 为什么? (confiMap是拷贝文中的内容,service用以前的,tpye=NodePort)
2022-08-25