
1. 先搞清楚Deployment在K8s里到底扮演什么角色1.1 为什么不用裸Pod还要套一层Deployment刚开始接触K8s的时候我其实一直没太搞懂一个问题明明直接创建Pod也能跑服务为什么还要套一个Deployment后来在生产环境里手动重启Pod、发布版本、回滚搞了无数次才真正理解这个工作负载抽象解决的是什么问题。简单说Deployment就是帮你把“应用实例数量维持在预期值”和“版本平滑切换”这两件事自动化掉的控制器。没有Deployment的时候你直接创建三个Pod某个Pod挂了就是挂了除非有人再补一个。有了Deployment之后它会持续对比“期望状态”和“实际状态”Pod挂了自动重建节点故障了自动调度到别的节点发布新版本时自动走滚动更新策略。这也就顺带解释了很多人常问的一个问题K8s和Docker到底有什么区别。Docker解决的是“单个容器怎么打包、怎么运行”K8s解决的是“一批容器怎么编排、怎么调度、怎么在故障场景下自愈”。Deployment就是这套编排能力里最经典的落地形态它自己本身不跑业务它只负责管理下面那一堆Pod。1.2 Deployment、ReplicaSet、Pod三层关系很多初学者看到Deployment的YAML里有副本数、选择器、Pod模板就以为Deployment直接管着Pod。实际上Deployment下面还隔着一层ReplicaSet完整链路是Deployment管理ReplicaSetReplicaSet管理Pod。每次你修改Deployment的Pod模板并执行更新Deployment会创建一个新的ReplicaSet然后按照滚动更新的参数逐渐把新ReplicaSet的Pod数量加多同时把旧ReplicaSet的Pod数量减少。整个过程不是“把三个旧Pod一起删掉再创建三个新Pod”而是“先多起来一个健康的Pod再少掉一个旧Pod”这样请求不会被突然全部中断。我见过有人直接在命令行用kubectl edit修改了某个Pod的标签结果发现它马上又被删掉了。原因很简单Pod不符合ReplicaSet的selector条件ReplicaSet认为这是一个多余Pod强制回收。K8s里最忌讳手工去动被工作负载管理的Pod所有变更应该走Deployment的YAML而不是直接改Pod。2. Deployment核心参数逐个拆解2.1 一份最小可用YAML先知道每个字段在干嘛先放一份最常见的Deployment配置后面所有参数讨论都基于它apiVersion: apps/v1 kind: Deployment metadata: name: demo-app namespace: default labels: app: demo spec: replicas: 3 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo image: registry.example.com/demo:1.0.0 ports: - containerPort: 8080 resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 20这里最容易被忽略的是selector和template.metadata.labels。selector的matchLabels表示这张Deployment要管理哪些Podtemplate里Pod的标签必须能匹配上selector否则K8s会直接报错拒绝创建。常见做法就是把app标签同时写在两处版本或功能差异用另外的标签区分比如app、version、tier。image字段确定了容器版本滚动更新触发条件就是Deployment的Pod模板发生了变化。改replicas不会触发滚动更新只有修改template里image、环境变量、标签、资源等字段才会生成新的ReplicaSet。这个区别非常重要很多人改了副本数却看到rollout history多了一堆记录其实是误改了template。2.2 副本数与资源请求要一起看不能单拍脑袋replicas的值很容易被当成一个拍脑袋数字三个实例还是五个实例完全看心情。实际排障时真正应该先看的是容器的resources.requests因为调度器是根据request去计算节点剩余容量的。假设你有三台节点每台可用CPU是2000mDeployment三个副本各请求500m总需求1500m听上去放得下。但如果集群里还有其他工作负载节点可用量已经降到每台300m那么新Pod可能一直Pending因为调度器找不到能满足500m request的节点。此时你看到的现象是rollout没有失败但新增副本始终起不来最后卡在等待中。另外如果配置了HPA自动扩缩容就不要再手工去改Deployment的replicas。HPA会持续调整replicas字段你手工改完可能不到几分钟就被HPA重新覆盖两者在“争同一个方向盘”。正确方式是用kubectl scale做临时调整或者直接调整HPA的minReplicas和maxReplicas。2.3 滚动更新参数maxSurge和maxUnavailable是发布节奏的开关Deployment的spec.strategy里有两个参数是所有发布节奏控制里最核心的两个spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25%默认值就是各25%但很多人根本没意识到它们的存在更不知道这个百分比是相对于哪个副本数算的。它们是相对于期望副本数replicas来算的。如果replicas是4maxSurge是25%意思是滚动更新期间最多允许创建超出期望副本数1个新PodmaxUnavailable是25%意思是最多允许有1个旧Pod处于不可用状态。把这两个值放在一起理解更新过程中系统会尽量保证“当前可用Pod数不低于3”同时“总Pod数不高于5”。因为maxUnavailable控制下限maxSurge控制上限。这个机制保证了发布期间服务不会因为Pod一下子全部被删掉而不可用。实际操作中maxSurge和maxUnavailable不是越大越好。有些人为了加快发布速度把maxSurge改成100%、maxUnavailable改成0%结果瞬间创建了大量新Pod把节点资源占满容器调度失败。也有些人反过来把maxUnavailable改成100%发布一触发旧Pod全删新Pod还没起来时流量直接黑洞。我比较常用的生产参数是maxSurge: 25%maxUnavailable: 0%并且加上minReadySeconds: 30让新Pod至少正常运行30秒后再继续滚动。这个组合牺牲了一点速度但换来了流量安全。2.4 探针参数readiness和liveness别搞混了Deployment里探针是决定“Pod算不算可用”和“Pod要不要被重启”的裁判。就绪探针readinessProbe决定Pod是否加入Service的端点列表存活探针livenessProbe决定Pod是否要重启。一个常见的错误是把它们配置成同一个路径和同一个严格逻辑。比如健康检查接口里对数据库做了强依赖数据库抖动一下readiness探针连续失败Pod被从Service摘除这本身是合理的。但如果同样的接口也被配置成liveness探针数据库抖动触发liveness失败Pod直接被杀掉重启重启过程中数据库还是抖就会出现CrashLoopBackOff。启动探针startupProbe是我后来才重视起来的。Java这类启动慢的应用如果只用readinessProbe它会在容器起来后立即按periodSeconds去探测冷启动阶段全失败。你可以把initialDelaySeconds调大但启动时间会随负载波动写死了反而难维护。更好的做法是用startupProbe设置initialDelaySeconds为0、failureThreshold为30、periodSeconds为5给应用最多150秒的启动窗口等它启动完成后再启用readiness和liveness。探针参数还有个容易被忽略的点periodSeconds太短会变成对应用的额外压力普通的HTTP健康检查默认10秒就够了没必要设成1秒。2.5 revisionHistoryLimit与回滚机制Deployment的spec.revisionHistoryLimit决定保留多少条历史ReplicaSet记录默认是10。保留这些记录的意义在于你可以随时回滚到此前任意一个版本。有人为了减少etcd里的对象数量把revisionHistoryLimit直接设为0确实省了一点点存储和API压力但代价是回滚功能失效。你发布了一个坏版本想用kubectl rollout undo回滚结果系统告诉你没有可用历史版本只能手工重新构造一份旧YAML。我的建议是至少保留5到10尤其频繁发布的业务历史记录太低等于放弃了K8s自带的安全网。回滚命令本身很简单kubectl rollout undo deployment/demo-app如果明确要回滚到某个历史版本可以先用kubectl rollout history deployment/demo-app查看版本号再执行kubectl rollout undo deployment/demo-app --to-revision3这里有个很容易踩的坑回滚操作也会创建一次新的ReplicaSet而不是把旧的ReplicaSet重新激活。所以回滚之后rollout history里会多出一条新记录镜像内容是旧版本但revision号是新的。不要试图去“找回”某条记录来执行一模一样的历史操作记住回滚等于一次新的发布即可。2.6 progressDeadlineSeconds发布卡住时谁来提醒你除了上述常用参数Deployment还有一个不算显眼但在排障时很有用的字段spec.progressDeadlineSeconds默认600秒。它的含义是如果Deployment在这段时间内没有完成预期的进度控制器会把condition标记为ProgressingFalse并把Reason标记为ProgressDeadlineExceeded。也就是说发布卡住10分钟后你执行kubectl describe deployment时能看到一条明确的状态说明而不是傻等。我一般会把它显式设成300秒这样发布超过5分钟没有进展就能通过kubectl get deployment看到异常状态。这个参数不会中断发布它只是一个超时提醒但对自动化发布流水线来说靠它判断要不要终止发布非常有用。很多CI/CD平台里卡住的发布任务最后都是靠这个状态触发失败回滚的。3. 灰度发布不要把滚动更新和灰度发布混为一谈3.1 滚动更新本质还是全量发布很多文章会把Deployment的滚动更新叫做灰度发布这是不严谨的。滚动更新只是分批替换Pod它最终目标是把所有流量切到新版本。对新版本来说流量占比会从一个副本逐渐变成全部副本看起来像一个灰度过程但在这个过程中所有用户的请求都有可能被分配到新版本你没有真正控制“让一部分用户先体验新功能”。灰度发布的核心是在一段时间内让指定的少量用户或指定比例的流量访问新版本观察指标正常后再逐步放大比例最终切换到新版本。这件事Deployment原生能力做不到、做不精细。生产环境里我一般把它拆成三种实现方案多Deployment配合Service、Ingress/Nginx Canary、Service Mesh按请求特征路由。3.2 最简单方案两个Deployment共用一个Service没有额外组件时也可以用最土的办法实现灰度保留旧版本Deployment再创建一个新版本Deployment两个Deployment的Pod都通过同一个Service对外暴露。关键在于Service的selector要选到两个版本的Pod通常只选择app标签不选择version标签。举例来说apiVersion: v1 kind: Service metadata: name: demo-svc spec: selector: app: demo ports: - port: 80 targetPort: 8080旧版本Deployment的Pod带app: demo、version: v1新版本Deployment的Pod带app: demo、version: v2两者都能被demo-svc选中。流量分配比例跟Pod数量大致成正比。如果旧版本有9个副本新版本有1个副本那么大约会有10%的请求落到新版本。这个方案的好处是零额外组件理解成本低坏处是权重精确度不够Service默认按照Endpoints数量轮询Pod数量比例对流量比例是个近似关系不是严格权重。发布过程中把新版本Deployment的replicas从1改成2、改成4逐步放大流量观察一段时间指标后再继续。灰度完成后直接修改旧版本Deployment的模板镜像或删除旧Deployment把流量全部切换到新版本。3.3 更精细方案Ingress Canary按权重或Header切流如果想要精确控制流量比例或者想按用户标识、请求头、Cookie规则来路由Nginx Ingress自带的Canary注解是低成本的首选。核心思路是保留旧的正式Service和Ingress同时给新版本Deployment创建一个K8s Service再创建一个标注为canary的Ingress。来看一个典型配置apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-app-canary annotations: nginx.ingress.kubernetes.io/canary: true nginx.ingress.kubernetes.io/canary-weight: 10 nginx.ingress.kubernetes.io/canary-by-header: X-Canary nginx.ingress.kubernetes.io/canary-by-header-value: enabled spec: rules: - host: demo.example.com http: paths: - path: / pathType: Prefix backend: service: name: demo-app-v2 port: number: 8080canary注解为true时这个Ingress不会独立处理请求而是作为主Ingress的流量副本参与路由。canary-weight: 10表示10%的流量会被转发到后端demo-app-v2。canary-by-header配合canary-by-header-value的意思则是请求头X-Canary的值等于enabled时忽略权重直接分流到新版本。我用这个方案做了很多A/B测试。比如内部测试团队访问时在请求里加上X-Canary: enabled能看到新功能外部用户默认只用旧版本。等新版本验证没有问题再把canary-weight逐步改成25、50、100最后完全切换。这里需要注意的是canary Ingress和后端Service可以复用同一个域名但两者不会互相干扰只要保证canary后端Service只指向新版本Deployment。3.4 结合Deployment参数的灰度发布步骤清单把Deployment参数和上面的灰度策略结合起来实际流程可以这样走第一步确认旧版本Deployment处于健康状态replicas设置为当前实际承载量比如9个副本minReadySeconds设30秒。第二步创建新版本Deploymentreplicas从1开始镜像更新到2.0.0readinessProbe的检测逻辑改成与2.0.0对应的健康检查。第三步先不接入外部流量用kubectl exec或在集群内部调用新Pod的Service验证新版本基本功能可用。第四步如果用的是多Deployment共用Service调整新版本Deployment的replicas逐步从1放大到9如果用的是Ingress Canary把canary-weight从0改成5、10、25、50。第五步灰度期间持续看两个指标新版本Pod的ready状态、监控系统里的错误率和P95延迟。只要新版本Pod出现连续重启立刻执行kubectl rollout undo deployment/demo-app-v2等待旧版本维持原有流量。第六步灰度通过后把新版本Deployment的replicas调整到和旧版本一致修改正式Ingress或者公开Service的selector指向新版本再删除旧版本Deployment和canary Ingress。这套流程把所有动作拆得很细每一步都能回退比单纯依赖原生滚动更新安全很多。灰度发布不是只有一个机制而是一整套从参数配置到流量切换再到回退的操作习惯。4. 实际发布时常见的故障与排查心得4.1 发布后Pod一直Pending先看调度器日志发布新版本后发现Pod一直Pending很多人会以为是Deployment配置写错实际上多半是资源不足或节点亲和性配置有问题。第一件事执行kubectl get pod观察Pending Pod的AGE和其他Pod是否都在同一个节点。再执行kubectl describe pod加具体Pod名字看Events尾部是否有FailedScheduling。如果提示Insufficient cpu或Insufficient memory就是requests比节点可分配容量大。解决办法不是降低limits而是先看业务实际占用再把requests调到一个真实合理的值或者临时扩一个节点。有时候Events里还有nodeSelector terms是因为Deployment里写了nodeSelector、toleration或topologySpreadConstraints。我最常遇到的是给新版本加了特定节点标签但那些节点没有足够的剩余资源。这类问题跟Deployment本身无关但排查顺序是一样的先看调度事件再看节点标签和资源水位。4.2 滚动更新卡住ReplicaSet数量一直在“打转”滚动更新执行后kubectl rollout status一直停在waiting不报错也不结束。这种情况大概率是两个原因新Pod一直起不来或者旧Pod一直删不掉。先看新ReplicaSet的Pod状态如果CrashLoopBackOfflogs里会暴露应用启动异常。如果新Pod处于Running但没有READY检查readinessProbe的路径和端口用kubectl exec进入Pod验证curl /healthz能不能通。旧Pod删不掉则要看PodDisruptionBudget或finalizer。PDB如果规则太紧比如minAvailable等于当前副本数驱逐旧Pod就会一直等待。还有一种常见情况Pod挂载了PVC且PVC正被某个节点使用节点故障后volume attach卡住Pod在删除时一直处于Terminating。这时候优先解决底层存储或节点问题而不是反复重启rollout。4.3 readiness探针太严格流量被堵但没有报错最迷惑人的故障是Deployment看起来完全正常所有Pod都是Runningrollout也显示成功但用户请求大面积超时。Service做负载均衡时只看Endpoint的ready状态。如果readinessProbe失败Pod即使Running也不会出现在Service的Endpoints列表里。你可以用kubectl get endpoints服务名确认端点数量如果比Deployment副本数少就是有Pod不健康。还有一种更隐蔽的情况readinessProbe的httpGet路径返回的是HTTP 200但内容实际是错误页。见过一个团队把健康检查接口指向了Nginx默认页Pod一直是Ready但应用已经挂了流量进来只能拿到404。健康检查一定要真正反映应用可用性不要用一个“永远返回200”的占位接口。4.4 回滚后镜像不是想象中的旧版本用kubectl rollout undo回滚后执行kubectl get pods看到的镜像确实变成了旧版本但业务行为还是新的。排查时不要只看Deployment的镜像字段还要看环境变量和启动参数是否也被改动过。回滚本质上是用旧ReplicaSet的Pod模板生成了一个新的ReplicaSet所以回滚到的只是那个ReplicaSet记录的Pod模板。如果你在新版本发布时改过配置ConfigMap、Secret回滚Deployment并不会自动把ConfigMap回滚到旧版本。Pod模板里引用的ConfigMap名字没变但ConfigMap内容已被覆盖成新版本数据应用启动时读到的还是新配置。处理办法是回滚时把ConfigMap、Secret和Deployment作为一个整体来回滚。实践上我习惯在每次发布前把配置快照存一份或者干脆把配置也打进镜像、通过镜像tag绑定配置版本这样回滚一个镜像tag就能连带配置一起回滚。4.5 常见问题速查表下面这个表是我在做技术支持时最常用到的一份快速判断逻辑故障现象优先检查常见原因Pod Pendingkubectl describe pod的Events资源不足、节点亲和标签不匹配Pod CrashLoopBackOffkubectl logs启动命令错误、环境变量缺失、探针杀死发布一直等待rollout status加describe RS新Pod未Ready、PDB阻塞驱逐流量异常但Pod都Readykubectl get endpointsreadiness探针失效、Service selector不对回滚后行为不变ConfigMap和Secret版本配置没有随Deployment一起回滚发布很快但服务中断strategy参数maxUnavailable过高或旧Pod被过早缩容这张表不解决所有问题但能帮你在报警时先锁定一个方向。K8s排查有个底层原则先看Pod再看ReplicaSet再看Deployment最后看Service。从具体现象往上层抽象逐层靠近大部分故障都能在某一层找到答案。5. 一些不写在官方文档里的实战心得Deployment参数很多文档里每个字段都有解释但真正把它们串起来的是你对业务风险的理解。maxUnavailable改成0%能保护流量代价是发布变慢maxSurge改大一点能加快发布代价是资源峰值变高。没有一套参数适合所有系统但我个人比较推荐在业务高峰期之外的时段使用maxSurge: 25%、maxUnavailable: 0%、minReadySeconds: 30、progressDeadlineSeconds: 300这个组合既保证最多只有四分之一新旧Pod数量波动又不会因为一次探针失败就打乱发布节奏。灰度发布这件事千万不要想着一步到位引Service Mesh。如果团队刚接触K8s用两个Deployment共用一个Service配合replicas调整先跑通灰度流程积累信心。流量规模大了之后再上Ingress Canary按权重切流按请求头做A/B。真正需要按域名、按用户身份做精细路由时再引入Service Mesh。工具复杂度应该跟随业务复杂度一起增长而不是一开始就把系统堆得很重。最后分享一个我每次给新版本上生产前都会做的事先在测试环境跑一遍kubectl rollout undo确认回滚命令可用、历史版本存在并检查回滚后Service的Endpoints能快速恢复。很多人只测试新版本能不能启动却忘了测试回滚这条救生通道。生产环境里新版本发布一旦出现大规模错误你大概率没有时间慢慢看YAML你能依赖的就是提前验证过的回滚路径。把回滚练成肌肉记忆比记住所有Deployment参数更有用。