【k8s】Deployment滚动更新(十六)
上篇笔记我们学习了管理有状态应用的对象 StatefulSet,再加上管理无状态应用的 Deployment 和 DaemonSet,我们就能在 Kubernetes 里部署任意形式的应用了。只是把应用发布到集群里是远远不够的,要让应用稳定可靠地运行,还需要有持续的运维工作。在【k8s】Deployment让应用永不宕机(八)里,我们学过 Deployment 的应用伸缩功能就是一种常见的运维操作
前言
上篇笔记我们学习了管理有状态应用的对象 StatefulSet,再加上管理无状态应用的 Deployment 和 DaemonSet,我们就能在 Kubernetes 里部署任意形式的应用了。
只是把应用发布到集群里是远远不够的,要让应用稳定可靠地运行,还需要有持续的运维工作。
在【k8s】Deployment让应用永不宕机(八) 里,我们学过 Deployment 的应用伸缩功能就是一种常见的运维操作,在 Kubernetes 里,使用命令 kubectl scale,我们就可以轻松调整 Deployment 下属的 Pod 数量,因为 StatefulSet 是 Deployment 的一种特例,所以它也可以使用 kubectl scale 来实现应用伸缩。
除了应用伸缩,其他的运维操作比如应用更新、版本回退等工作,该怎么做呢?这些也是我们日常运维中经常会遇到的问题。
今天就以 Deployment 为例,来讲讲 Kubernetes 在应用管理方面的高级操作:滚动更新,使用 kubectl rollout 实现用户无感知的应用升级和降级。
一、Kubernetes 应用版本更新
应用的版本更新,大家都知道是怎么回事,比如我们发布了 V1 版,过了几天加了新功能,要发布V2 版。
在 Kubernetes 里,版本更新使用的不是 API 对象,而是两个命令:kubectl apply 和 kubectl rollout,当然它们也要搭配部署应用所需要的 Deployment、DaemonSet 等 YAML 文件。
Kubernetes 里的应用都是以 Pod 的形式运行的,而 Pod 通常又会被 Deployment 等对象来管理,所以应用的版本更新实际上更新的是整个 Pod。
Pod 是由 YAML 描述文件来确定的,更准确地说,是 Deployment 等对象里的字段 template。
所以,在 Kubernetes 里应用的版本变化就是 template 里 Pod 的变化,哪怕 template 里只变动了一个字段,那也会形成一个新的版本,也算是版本变化。
Kubernetes 使用了摘要功能,用摘要算法计算 template 的 Hash 值作为版本号,虽然不太方便识别,但是很实用。
来看一个例子:
Pod 名字里的那串随机数676475ddfd就是 Pod 模板的 Hash 值,也就是 Pod 的版本号。
如果变动了 Pod YAML 描述,比如把镜像改成 nginx:stable-alpine,或者把容器名字改成 nginx-test,都会生成一个新的应用版本,kubectl apply 后就会重新创建 Pod
你可以看到,Pod 名字里的 Hash 值变成了56b6b4f4c9,这就表示 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:
labels:
app: ngx-dep
name: ngx-dep
spec:
replicas: 4
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:1.21-alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d/
name: ngx-conf-vol
把它命名为 ngx-v1.yml,然后执行命令 kubectl apply 部署这个应用:
kubectl apply -f ngx-v1.yml
我们还可以为它创建 Service 对象,利用NodePort类型进行转发:
apiVersion: v1
kind: Service
metadata:
labels:
app: ngx-dep
name: ngx-svc
spec:
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-dep

curl 127.1:30657

从 curl 命令的输出中可以看到,现在应用的版本是 1.21.6。现在,让我们编写一个新版本对象 ngx-v2.yml,把镜像升级到 nginx:1.22-alpine,其他的都不变。
因为 Kubernetes 的动作太快了,为了能够观察到应用更新的过程,我们还需要添加一个字段 minReadySeconds,让 Kubernetes 在更新过程中等待一点时间,确认 Pod 没问题才继续其余 Pod 的创建工作。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ngx-dep
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v2, ngx=1.22
spec:
minReadySeconds: 15 # 确认Pod就绪的等待时间
replicas: 4
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:1.22-alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d/
name: ngx-conf-vol
现在我们执行命令 kubectl apply 来更新应用,因为改动了镜像名,Pod 模板变了,就会触发版本更新,然后用一个新命令:kubectl rollout status,来查看应用更新的状态:
kubectl apply -f ngx-v2.yml
kubectl rollout status deployment ngx-dep

更新完成后,你再执行 kubectl get pod,就会看到 Pod 已经全部替换成了新版本“7cf8……”,用 curl 访问 Nginx,输出信息也变成了“1.22.0”:

仔细查看 kubectl rollout status 的输出信息,你可以发现,Kubernetes 不是把旧 Pod 全部销毁再一次性创建出新 Pod,而是在逐个地创建新 Pod,同时也在销毁旧 Pod,保证系统里始终有足够数量的 Pod 在运行,不会有空窗期中断服务。
新 Pod 数量增加的过程有点像是滚雪球,从零开始,越滚越大,所以这就是所谓的滚动更新(rolling update)。
使用命令 kubectl describe 可以更清楚地看到 Pod 的变化情况:
kubectl describe deploy ngx-dep

- 一开始的时候 V1 Pod(即 ngx-dep-676475ddfd)的数量是 4;
- 当“滚动更新”开始的时候,Kubernetes 创建 1 个 V2 Pod(即 ngx-dep-7cf8546bf9),并且把 V1 Pod 数量减少到 3;
- 接着再增加 V2 Pod 的数量到 2,同时 V1 Pod 的数量变成了 2 ;
- 最后 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:
kubectl rollout history deploy ngx-dep
[root@k8s-console nginx]# kubectl rollout history deploy ngx-dep
deployment.apps/ngx-dep
REVISION CHANGE-CAUSE
1 <none>
2 <none>
它会输出一个版本列表,因为我们创建 Nginx Deployment 是一个版本,更新又是一个版本,所以这里就会有两条历史记录。
但 kubectl rollout history 的列表输出的有用信息太少,可以在命令后加上参数 --revision 来查看每个版本的详细信息,包括标签、镜像名、环境变量、存储卷等等,通过这些就可以大致了解每次都变动了哪些关键字段:
kubectl rollout history deploy --revision=2
[root@k8s-console nginx]# kubectl rollout history deploy --revision=2
deployment.apps/ngx-dep with revision #2
Pod Template:
Labels: app=ngx-dep
pod-template-hash=7cf8546bf9
Containers:
nginx:
Image: nginx:1.22-alpine
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts:
/etc/nginx/conf.d/ from ngx-conf-vol (rw)
Volumes:
ngx-conf-vol:
Type: ConfigMap (a volume populated by a ConfigMap)
Name: ngx-conf
Optional: false
假设我们认为刚刚更新的 nginx:1.22-alpine 不好,想要回退到上一个版本,就可以使用命令 kubectl rollout undo,也可以加上参数 --to-revision 回退到任意一个历史版本:
[root@k8s-console nginx]# kubectl rollout history deploy ngx-dep
deployment.apps/ngx-dep
REVISION CHANGE-CAUSE
2 <none>
3 <none>
kubectl rollout undo 的操作过程其实和 kubectl apply 是一样的,执行的仍然是滚动更新,只不过使用的是旧版本 Pod 模板,把新版本 Pod 数量收缩到 0,同时把老版本 Pod 扩展到指定值。
这个 V2 到 V1 的版本降级的过程同样画了一张图,它和从 V1 到 V2 的版本升级过程是完全一样的,不同的只是版本号的变化方向:

四、Kubernetes 添加更新描述
kubectl rollout history 的版本列表好像有点太简单了呢?只有一个版本更新序号,而另一列 CHANGE-CAUSE 为什么总是显示成 呢?能不能像 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 应用,同时添加更新说明:ngx-v1
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ngx-dep
name: ngx-dep
annotations:
kubernetes.io/change-cause: v1, nginx=1.21
spec:
replicas: 4
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:1.21-alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d/
name: ngx-conf-vol
ngx-v2
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ngx-dep
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v2, ngx=1.22
spec:
minReadySeconds: 15
replicas: 4
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:1.22-alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d/
name: ngx-conf-vol
ngx-v3
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ngx-dep
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v3, change name
spec:
minReadySeconds: 15
replicas: 4
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:1.22-alpine
name: nginx-3
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d/
name: ngx-conf-vol
你需要注意 YAML 里的 metadata 部分,使用 annotations.kubernetes.io/change-cause 描述了版本更新的情况,相比 kubectl rollout history --revision 的罗列大量信息更容易理解。
依次使用 kubectl apply 创建并更新对象之后,我们再用 kubectl rollout history 来看一下更新历史:
kubectl rollout history deployment ngx-dep

这次显示的列表信息就好看多了,每个版本的主要变动情况列得非常清楚,和 Git 版本管理的感觉很像。
总结
- 在 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 文档进一步研究
更多推荐



所有评论(0)