【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-tes
t,都会生成一个新的应用版本,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)