ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

K8s Deployment实战:从Pod管理到滚动更新与安全回滚

K8s Deployment实战:从Pod管理到滚动更新与安全回滚

在Kubernetes里跑应用,我见过太多人一上来就写个Pod YAML直接kubectl apply,等需要更新版本或者发布出问题的时候才傻眼——没有历史记录、没有滚动策略、Pod被删了就只能手动重建。今天这篇就专门聊清楚一件事:怎么用Deployment把Pod管起来,以及上线之后怎么安全地更新和回滚。

先说结论:Deployment是K8s里最基础的“应用管理者”,它不直接创建Pod,而是通过ReplicaSet间接管理Pod副本。你只需要告诉它“我要3个副本、镜像版本是什么、更新策略怎么走”,剩下的事它全包了。不管是新服务上线、迭代升级,还是发布事故后的紧急回滚,Deployment都有对应的命令和机制来兜底。

这篇文章适合刚入门K8s、被Pod生命周期折磨过的同学,也适合已经会用但不太清楚滚动更新内部原理的开发者。我会从部署开始讲,一步步带你把更新和回滚都实操一遍,中间穿插我实际踩过的坑。

1. 为什么部署Pod绕不开Deployment

1.1 直接跑Pod会遇到的几个麻烦

如果你只是临时调个环境,kubectl run一把梭也没问题。但要是负责一个正经业务服务,直接创建Pod就是在给自己埋雷。

最直接的问题是:Pod挂了没人管。某次我压测的时候,一个Pod直接OOMKilled,由于没有控制器管理,进程退出之后集群完全没有反应,可用副本数直接降为0,流量瞬间全打到了剩余实例上,其中一个也跟着被压垮。整个服务从报错到彻底不可用,前后不超过一分钟。后来我养成了习惯:凡是需要长期运行的业务服务,一律交给Deployment托管。

第二个问题是没有滚动的概念。假设你有5个实例,想从v1升级到v2。如果手动操作,只能一个个把Pod删掉再创建新的,删除瞬间就丢了一个副本的容量;如果赶上流量高峰,连续删两三个Pod可能直接触发熔断。Deployment可以让你配置“先起一个新的,再杀掉一个旧的”,整个过程服务不中断。

第三个问题是回滚无门。手动创建Pod的时候,Pod模板存在YAML文件里,更新之后想回退,只能翻历史文件,还得记住每个版本对应哪份YAML。而Deployment天然维护了每次变更的历史版本,一条命令就可以退回去。

1.2 Deployment到底帮你做了什么

Deployment是个控制器,它的工作机制其实不复杂:Deployment描述“最终想要什么状态”,控制器负责让“当前状态”一步步逼近“期望状态”。

Deployment创建之后,它会拉起一个ReplicaSet,再由ReplicaSet负责创建指定数量的Pod。更新应用时,Deployment会创建一个新的ReplicaSet,按你定义的策略调整新旧两个ReplicaSet的规模。回滚时,做的事情正好反过来:把旧的ReplicaSet再拉起来,把新的缩下去。

这里有个很多人误解的点:Deployment并不直接管理Pod。看kubectl get pods的时候,你会发现Pod名字是一串“Deployment名 + ReplicaSet模板哈希 + 随机ID”,中间那段哈希就是ReplicaSet的标识。理解这层关系,后面排查问题会快很多。

可以把Deployment、ReplicaSet、Pod的关系类比成三层结构:

  • Deployment:负责“发布策略”和“版本管理”,是发布操作的入口。
  • ReplicaSet:负责“保持指定数量的Pod副本”,是Deployment的执行层。
  • Pod:真正跑容器实例的最小单元,是被管对象。

某个夜里被线上告警叫起来的时候,你大概率会感激这套机制:一个kubectl rollout undo就能把服务恢复到上一个稳定版本,而不是手忙脚乱地改YAML重新发布。

2. 第一次用Deployment部署服务

2.1 写一份最小可用的Deployment YAML

先看一个标准示例。假设我们要部署一个名为myapp的Web服务,镜像叫myapp:v1.0.0,跑3个副本:

apiVersion: apps/v1 kind: Deployment metadata: name: myapp labels: app: myapp spec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp-container image: myapp:v1.0.0 ports: - containerPort: 8080

逐段拆开看,每一块都在解决具体问题:

  • apiVersion: apps/v1指定API版本。早期K8s用的是extensions/v1beta1,现在已经废弃,新项目一律用apps/v1。
  • metadata.name是这个Deployment的名字,后续所有查询、更新、回滚命令都依赖它。
  • spec.replicas期望副本数,Deployment会保证集群里始终有3个Pod在跑。
  • spec.selector.matchLabels是Pod选择器。Deployment只认带有app: myapp这个标签的Pod。这个标签必须和template.metadata.labels里的标签匹配,否则你会看到一个“Selector does not match template labels”的报错。
  • spec.template是Pod模板。里面定义容器镜像、端口、资源配额、健康检查等。每次更新应用,本质上就是修改这段template。

2.2 部署前的准备工作

直接把这套YAML apply到集群里之前,三个准备工作最好先做好:

先确认镜像能被集群拉取。如果镜像放在私有仓库,需要在Deployment里配置imagePullSecrets:

spec: template: spec: imagePullSecrets: - name: registry-secret

这里的registry-secret是提前在命名空间里创建好的Secrets对象。没有这层配置,私有仓库镜像会一直卡在ImagePullBackOff。

再确认命名空间。YAML里没写命名空间的话,命令会默认发布到default。如果你想发布到production命名空间,要么在YAML里加metadata.namespace,要么执行命令时加-n production。

最后建议给容器加上资源请求和限制。不加resources字段也能跑起来,但如果集群节点资源紧张,Pod可能被调度到性能很差的节点,或者莫名被驱逐。一个常见的底线配置:

resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi

requests是调度依据,limits是运行上限。没有这个字段,Pod可能吃满节点内存,把同节点的邻居Pod搞挂。

2.3 把YAML提交到集群并验证状态

准备完成之后,执行发布命令:

kubectl apply -f deployment.yaml

看到deployment.apps/myapp created之后,依次执行下面几条命令检查状态:

kubectl get deployment myapp kubectl get rs kubectl get pods -l app=myapp

正常情况下,kubectl get deployment输出里READY一列显示3/3,代表期望3个副本、实际可用3个副本。这个字段是排障的第一道窗户:如果一直卡在1/3或者2/3,说明有问题在阻挠Pod就绪。

还需要检查一个容易被忽略的状态:AVAILABLE列。有时候Pod虽然Running,但应用其实没起来,因为访问不到某些外部依赖,导致就绪探针没通过。如果READY一直小于REPLICAS,大概率是服务本身的问题,而不只是调度问题。

部署完成后可以通过kubectl describe deployment myapp查看事件流。最下面Event部分会列出每一步发生了什么,比如成功创建ReplicaSet、拉取了哪个镜像、Pod在哪个节点等。当发布失败、原因又看不懂的时候,这个命令是最直接的线索来源。

3. 应用更新:滚动更新背后的机制

3.1 触发更新的几种常见方式

Deployment模板里的任何字段发生改变,都会触发一次滚动更新。实际操作中,最常用的方式有三种:

第一是直接改镜像:

kubectl set image deployment/myapp myapp-container=myapp:v1.1.0

这里myapp-container是容器名,在YAMLspec.template.spec.containers[].name里定义。改完之后,K8s会立刻创建新的ReplicaSet。

第二是编辑当前Deployment:

kubectl edit deployment myapp

命令会打开一个Vim风格的编辑器,展示当前Deployment完整配置,直接改image字段保存退出即可。这种方式适合同时修改多处配置(比如多个容器的镜像、环境变量、资源限制)。

第三是修改YAML文件后重新apply。比如在本地把deployment.yaml里的镜像改成v1.1.0,然后kubectl apply -f deployment.yaml。这种方式适合配置纳入代码仓库管理的团队,每次变更走Git提交,可追溯、可评审。

注意一点:如果你在Deployment里用了一个ConfigMap,只改了ConfigMap里面的内容,Deployment不会自动更新。因为Deployment只知道自己的template变化了,而template里的内容没变,它不会去检测ConfigMap内容是否变化。这种场景要么改template触发更新,要么用工具强制重启。

3.2 调整滚动更新策略参数

Deployment默认的策略是RollingUpdate,也就是滚动更新。在这个策略下,两个参数决定了更新行为:

spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25%
  • maxSurge:更新期间允许超出期望副本数的最大Pod数。默认25%。如果期望3个副本,最多可以同时存在4个Pod(3+1),相当于最多“多起1个”。
  • maxUnavailable:更新期间允许不可用的最大Pod数。默认25%。期望3个副本时,最多可以“少1个”,保证至少2个是活的。

如果希望更新期间新旧版本同时存在的时间点更平滑,可以适当调小maxUnavailable,甚至设置为0,确保老版本始终有完整的副本数在服务。作为交换,更新速度会变慢,因为新版本Pod必须“起来一个、就绪一个”,才能继续替换下一个。

更极致的做法是把maxSurge也设为0,严格保持副本数量不变,但这样会牺牲可用性,一般不建议生产环境使用。很多发布事故,都是有人把maxUnavailable调成了100%,更新瞬间所有旧Pod全部被杀掉,服务直接空窗,那次事故后team里就定了规矩:maxUnavailable生产环境不允许超过50%。

3.3 更新过程到底发生了什么

执行kubectl set image之后,Deployment控制器会进行下面的操作:

  1. 创建新的ReplicaSet,Pod模板使用的是新镜像。
  2. 根据maxSurge允许的数量,在新ReplicaSet里创建Pod。
  3. 新Pod通过就绪探针之后,Deployment把旧ReplicaSet的Pod开始缩减。
  4. 每缩掉一个旧Pod,就再创建新ReplicaSet里的Pod,直到新副本数达到期望值。
  5. 全部完成之后,旧ReplicaSet缩到0,保留历史记录。

可以用命令观察整个过程:

kubectl rollout status deployment/myapp

如果要看每一步的细节,开两个终端,一个执行更新命令,另一个执行:

kubectl get pods -w

-w参数会实时输出Pod变化。你可以看到新Pod的状态从ContainerCreating变成Running,再变成Ready;旧Pod从Terminating到消失。这个过程就是滚动更新的全部真相。很多人以为“更新就是直接把旧Pod替换掉”,实际上新旧Pod是有一段共存时间的,这也是为什么流量不会中断。

测试更新耗时也很简单:

time kubectl rollout status deployment/myapp

如果几秒钟就结束了,说明镜像拉取非常快或者本地缓存了;如果几分钟都刷不完,大概率是卡在某一步了。

4. 应用回滚:把服务退回到安全版本

4.1 查看历史版本信息

Deployment会保留每次变更的版本记录,默认保存10个版本。先查一下历史:

kubectl rollout history deployment/myapp

输出大致是:

REVISION CHANGE-CAUSE 1 kubectl apply --filename=deployment.yaml 2 kubectl set image deployment/myapp myapp-container=myapp:v1.1.0

REVISION就是版本号,回滚时要用到。CHANGE-CAUSE默认记录的是触发变更的命令,如果想让它显示更友好的描述,可以在YAML里加metadata.annotations:

metadata: annotations: kubernetes.io/change-cause: "upgrade to v1.1.0 for new feature"

之后查看history时,显示的就是“upgrade to v1.1.0 for new feature”,发布说明一目了然。一个很实际的经验:公司里有多个负责发布的人时,这个注解能解决大量沟通问题,不然你根本不知道上一版是谁因为什么原因发布的。

如果想知道某个版本具体用了什么镜像,加一个参数:

kubectl rollout history deployment/myapp --revision=2

这会显示那个版本完整的Pod模板信息,包括镜像版本、环境变量、启动参数等。

4.2 回滚的几种操作方式

最常用的回滚命令是:

kubectl rollout undo deployment/myapp

这个命令默认回滚到上一个版本,也就是当前REVISION减1。如果你想回滚到特定版本:

kubectl rollout undo deployment/myapp --to-revision=1

注意,这里REVISION是历史记录里的序号,不是你可能以为的“版本号”。回滚动作本身也会生成一个新的REVISION。假设历史记录是1、2、3,当前在3,执行undo deployment/myapp会回滚到2,然后历史里新增一个版本4,内容跟2一致。

回滚之后,发布流程是完全一样的:依然是滚动更新的节奏,先起新Pod(内容为旧版本镜像),旧Pod逐渐缩容。

还有一个需要注意的更新场景:如果你只是修改了Deployment的metadata或者注解,没有修改Pod模板,那不会触发重新发布,也不会新增REVISION。只有template部分的变更才会产生新版本。用命令查看当前模板和某个历史版本的区别,可以这样:

kubectl diff -f deployment.yaml

这个命令对比的是本地YAML和集群里当前Deployment的差异,适合发布前预览改动。回滚前确认历史模板差异的话,就只能用--revision=2查看详细模板再人工对比了。

4.3 回滚时的注意事项

回滚虽然方便,但别以为无脑一条命令就完事了,以下几个坑是我挨个踩过的:

回滚不是零成本的。回滚本质上也是一次滚动更新,同样受maxSurge和maxUnavailable约束。如果旧版本镜像没有缓存,回滚时还是要重新拉镜像,这段时间如果节点网络不好,回滚速度会非常慢。

回滚不保证解决所有问题。如果问题是配置文件不对,而代码本身没问题,回滚应用代码可能没作用,因为ConfigMap没变。如果你发布时既改了镜像又改了ConfigMap,回滚前应该想清楚:是配置出了问题,还是代码出了问题?有些公司会把镜像和配置分离,回滚只回滚代码版本,配置单独回退。

回滚时如果目标版本下架了,会出现拉镜像失败。镜像仓库会做清理,只保留最近N个版本。如果你的历史版本对应的镜像已经被删了,回滚到那个版本就会卡在ImagePullBackOff。所以生产环境镜像仓库一定要保留足够的版本,回滚才有意义。

执行回滚后,一定要跟踪回滚状态:

kubectl rollout status deployment/myapp

回滚失败和更新失败的判断方式一样:ReplicaSet出现异常、Pod状态卡住、就绪探针不通过。别自己脑补“回滚了应该没问题”,以集群实际状态为准。

5. 常见坑位与排查实录

5.1 镜像拉取失败

现象:kubectl get pods看到Pod状态为ImagePullBackOff,描述信息显示无法拉取镜像。

第一步先看原因:

kubectl describe pod <pod-name>

下面是我遇到的几种典型原因:

  • 镜像标签拼错。v1.0写成了v1.0.0,仓库里没有这个tag,检查镜像名是否准确。
  • 私有仓库未配置凭证。报错里会带unauthorized,说明需要配置imagePullSecrets。
  • 节点上没有缓存且访问不了外网。如果集群部署在内网,可能要先用其他方式把镜像导入节点,或者配置镜像代理。

排查镜像拉取问题最快的方式是直接在节点上手动拉一次:

crictl pull myapp:v1.0.0

这个命令能帮你区分是镜像仓库的问题还是K8s配置的问题。

5.2 滚动更新卡死

现象:执行更新之后,kubectl rollout status卡在同一个状态很久,或者Pod一直是Pending。

优先检查这几个地方:

先看新ReplicaSet的状态:

kubectl get rs

看DESIRED和AVAILABLE数值。如果新RS的期望副本数一直没变满,说明maxUnavailable限制太严,或者新Pod一直没就绪。

再检查Pod的Events:

kubectl describe pod <new-pod>

Pending状态优先看资源是否够:节点CPU不足、内存不足,或者PVC没绑定。

还要看“容器启动失败,但已重试多次”这种隐藏问题。kubectl get pods可能显示CrashLoopBackOff,但deployment的状态还是卡在Progressing,要另外关注新RS是否达到了期望副本数。

滚动更新卡死的一个常见组合坑:maxUnavailable: 0+maxSurge: 1+ 只有一个节点。这种情况如果节点资源不足,新Pod永远起不来,滚动永远无法推进。既占着旧Pod的资源,又要起新Pod,两难。排查的时候先把这个组合排除掉。

5.3 回滚后配置错乱

现象:执行回滚后,应用功能不对,甚至直接启动失败。

原因大概率是:回滚只回滚了Deployment模板,但关联的ConfigMap或Secrets没有一起回滚。Deployment管的是Pod模板,但它引用的外部配置是独立对象。

例如某个服务在v1.0时连接旧数据库,在v1.1时迁移到了新库,ConfigMap里的连接串也变了。发布v1.1后发现事故,回滚镜像到v1.0,但ConfigMap里的连接串还是新的,旧代码连新库可能就出各种奇怪问题。

解决思路有两种:第一,回滚前把ConfigMap一起恢复到旧版本;第二,设计发布流程时尽量让配置向后兼容,代码层能容忍配置的新增字段。

5.4 版本记录被压缩的问题

Deployment默认保留10个历史版本,超过10个之后最早的会被清理。如果发布频繁,你有可能遇到“想回滚到第3版,但第3版已经被清理”的情况。

可以用revisionHistoryLimit调整保留数量:

spec: revisionHistoryLimit: 20

注意这个字段不能保证一定保留N个版本,在某些情况下(比如更新失败过多)历史版本可能被提前清理。

如果要长期保底,可以配合外部手段,比如每次发布前把当前Deployment的YAML备份到代码仓库里,或者用GitOps工具把Deployment配置纳入版本管理。这样就算K8s内部的历史记录被清掉了,你还有Git历史可以翻。

6. 一些发布操作的常用技巧

6.1 利用Pause/Resume控制发布节奏

kubectl rollout pause deployment/myapp可以在滚动更新过程中暂停。应用这个命令后,Deployment不会再继续推进新的ReplicaSet,已经起的Pod保持现状。

这个功能在两种场景下很有用:一个是灰度验证,发布到一半,先暂停,让部分流量走新版本,观察一小段时间,确认没问题再kubectl rollout resume deployment/myapp继续。另一个是发布过程中发现问题,立刻暂停,防止问题面扩大。

6.2 把健康检查配置好,才能发挥滚动更新的价值

没有健康检查的滚动更新等于没滚。K8s判断Pod“就绪”默认只看容器有没有启动。如果你的应用启动之后要10秒才能对外服务,但容器启动只要2秒,K8s会误以为Pod就绪了,流量就会打过来,用户看到的就是一堆502。

正确做法是配置readinessProbe就绪探针:

readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5

这样子,K8s会定期请求应用的/healthz接口,只有200了才认为Pod准备好了。滚动更新时,新Pod只有通过就绪探针之后才会被放进Service的可用端点列表里,流量切换才不会断。

6.3 更新命令的等价写法

kubectl set image和kubectl edit都能改镜像,但注意容器名的坑。容器名写错会报container not found,这个错很常见。实际工作中,如果容器名记不清,用kubectl get deployment myapp -o yaml先看一眼最稳妥。

一次更新多个容器的镜像时,set image也支持逗号分隔:

kubectl set image deployment/myapp web-container=myapp:v1.1.0 api-container=myapi:v1.0.5

适合多容器Pod的场景。

收尾前的一点心得

Deployment这套滚动更新与回滚机制,核心价值就一句话:把应用发布从“手动高风险的挪动”变成了“可观测、可控制、可反悔的流程”。我见过太多线上事故是靠“紧急回滚”救回来的,也见过因为回滚不及时或者回滚不干净导致事故面扩大的。

根据我的实操经验,可以给你几个具体建议:

第一,生产环境的Deployment一定要配maxSurge和maxUnavailable的合理值,别用默认值硬扛。有流量洪峰场景时,maxUnavailable: 0更稳。

第二,发布前把变更说明写在kubernetes.io/change-cause注解里。回滚时你会感谢这个习惯。

第三,回滚前先确认要回滚到的版本镜像还存在,镜像仓库的保留策略别设太激进,至少保留最近5个版本对应的镜像。

最后分享一个小技巧:每次发布前,把当前这个Deployment完整YAML导出备份一份:

kubectl get deployment myapp -o yaml > backup/myapp-v1.1.0.yaml

发布新版本后如果发现有问题,除了rollout undo之外,还能用备份文件精确恢复到发布前的状态。这个习惯在K8s内部版本记录已经被污染(比如部署时上传过错误配置)的时候尤其有用。整体来说,Deployment的更新和回滚机制并不复杂,复杂的是你在实际使用中踩过多少坑,理解了它背后的设计和默认策略,再遇到问题就不会手忙脚乱了。

返回列表