ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Deployment 生产配置与灰度发布:滚动更新、探针与排障实战

Deployment 生产配置与灰度发布:滚动更新、探针与排障实战 1. 先把 Deployment 看透它是工作负载里最重要的一张表上个月我处理过一起典型的线上事故。同事往生产集群推送一个微服务的新镜像Deployment 的滚动更新跑到一半突然卡住可用副本从 4 个跌到 1 个入口网关的 502 直接涌上来。当时第一反应是怀疑代码有问题结果翻日志发现新 Pod 一直处于未就绪状态旧 Pod 又因为maxUnavailable: 0的策略不允许被删除发布就被卡死在那。那次事故给我留下一个很深的印象Kubernetes 里的工作负载类型虽然有好几种但 Deployment 是大家用得最多、也最容易被忽视细节的一种。它的参数看上去就是 replicas、strategy、探针几项真正影响线上稳定性的恰恰是这些参数灰度发布做得好不好也全看它们怎么配。这篇文章我打算站在实操角度把 Deployment 从“会用”讲到“用得稳”。不绕理论知识直接聊参数背后的逻辑、灰度发布的具体玩法以及我在生产环境里踩过的坑。不管你是刚接触 k8s 的开发者还是已经在维护集群的运维都应该能从中找到可以直接抄走的配置思路。1.1 为什么工作负载的首选项是 Deployment很多人刚开始学 k8s 时都会问一句直接用 Pod 跑容器不行吗当然行但 Pod 是一个“一次性”对象。节点宕机、资源被回收、容器被 OOM KillPod 都不会自己恢复。Deployment 的意义在于它用一种声明式的方式管理一群 Pod你告诉它“我要 4 个副本、用这个镜像”它就一直保证集群里有 4 个符合描述的 Pod 在运行。Deployment 本身并不直接管理 Pod。它管理的是 ReplicaSetReplicaSet 再负责创建和回收 Pod。这个层级关系是 k8s 里最容易被忽略但又非常重要的设计。每次你更新 Deployment 的镜像它会创建一个新的 ReplicaSet然后按策略逐步替换旧 ReplicaSet 里的 Pod。旧 ReplicaSet 的信息会被保留一段时间这就是你能回滚的底层依靠。所以当有人说“Deployment 崩了”准确的说法往往是“Deployment 管理的 ReplicaSet 无法正常创建或替换 Pod”。排查思路也要从这条链路往下走。1.2 Deployment、ReplicaSet、Pod 三者怎么协作我把这条链路用一句话概括Deployment 定目标ReplicaSet 管数量Pod 扛业务。每个 Deployment 创建时都会生成一个对应的 ReplicaSetDeployment 的每次配置变更也会触发新的 ReplicaSet。举个例子。你部署了一个replicas: 4的 Deployment版本是 v1。此时集群里有 1 个 ReplicaSet对应 4 个 Pod。过几天你更新镜像到 v2Deployment 会创建一个新的 ReplicaSet期望副本数还是 4。如果更新策略是 RollingUpdate新旧两个 ReplicaSet 会在滚动窗口内同时存在旧的逐步缩容新的逐步扩容直到新 ReplicaSet 变成 4 个 Pod。此时旧的 ReplicaSet 的期望副本数会变为 0但它的历史记录还在。这个机制解释了为什么kubectl rollout history能看到版本记录也解释了为什么不能无限制保留 ReplicaSet。每个 ReplicaSet 都对应一份 Pod 模板和一组历史配置如果 revisionHistoryLimit 设得太大etcd 里会堆大量无用数据设得太小你又可能找不到足够老的回滚点。1.3 哪些场景不适合硬套 DeploymentDeployment 最适合的是无状态服务。比如 Web API、后台任务 Worker、网关代理这些应用可以把每个副本当作等价的实例杀掉一个再拉起一个也不会影响数据一致性。但有几类工作负载不能用 Deployment有状态服务比如数据库、消息队列、缓存集群节点身份和存储卷需要固定应该用 StatefulSet它会给每个 Pod 一个稳定的网络标识和独立的存储卷。每个节点都必须跑一个的组件比如日志采集、监控 Agent用 DaemonSet保证新加入的节点自动拉起 Pod。一次性任务、定时任务用 Job 或 CronJob。把 Deployment 用到不合适的场景后面会遇到“明明起来了却连不上”“重启后数据丢了”之类的怪问题。先判断工作负载类型再谈参数配置这个顺序不能反。2. 配置 Deployment 时真正需要推敲的参数Deployment 的 YAML 看起来字段不多但如果只是在网上抄一份模板不出事是运气出事是迟早的。我整理了一份实际生产环境比较完整的配置后面逐个拆解。apiVersion: apps/v1 kind: Deployment metadata: name: app-web namespace: default spec: replicas: 4 revisionHistoryLimit: 5 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 minReadySeconds: 10 progressDeadlineSeconds: 600 selector: matchLabels: app: web template: metadata: labels: app: web version: stable spec: terminationGracePeriodSeconds: 60 containers: - name: app-web image: registry.example.com/app-web:v2.1.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 10 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi下面这份配置里的每个字段都不是随手写的都有它在防的坑。2.1 副本数与更新策略先想清楚你要的是可用性还是资源成本replicas是最直观的参数但生产环境里我很少手动调它。原因很简单业务流量有高峰有低谷手动调副本数要么太慢要么太过。比如每天晚上流量上涨你半夜爬起来改副本数这显然不现实。我更推荐把replicas交给 HPAHorizontal Pod Autoscaler管理Deployment 里只写一个基线值例如 4然后让 HPA 根据 CPU、内存或自定义指标自动扩缩。真正容易踩坑的是strategy这块。Deployment 支持两种更新策略Recreate先把旧 Pod 全部删掉再创建新 Pod。这种方式在更新窗口内服务会完全不可用一般只适合测试环境或离线任务。RollingUpdate滚动替换旧 Pod 逐个下线新 Pod 逐个上线保证服务不中断。生产环境基本都用 RollingUpdate。但它内部还有两个参数maxUnavailable和maxSurge。这两个参数决定滚动更新的速度与风险。2.2 滚动更新的 maxSurge 与 maxUnavailable 怎么配maxSurge表示滚动更新期间超出期望副本数最多能多出几个 PodmaxUnavailable表示允许有多少个副本处于不可用状态。这两个参数是滚动更新的核心。默认配置是maxSurge: 25%、maxUnavailable: 25%。如果副本数是 4那么滚动时可以多出 25% 也就是 1 个新 Pod同时允许 25% 也就是 1 个旧 Pod 不可用。这种配置的好处是更新速度快、资源占用少坏处是更新期间会有一小段时间服务容量低于 4 个副本。如果业务对可用性要求很高可以把maxUnavailable设为 0maxSurge设为 1。这样滚动期间至少保证 4 个旧副本始终在线只有新 Pod 就绪后才会下线旧 Pod。代价是发布速度变慢而且在节点资源紧张时多出来的 1 个 Pod 可能调度不上去。我自己的习惯是核心交易链路maxUnavailable: 0maxSurge: 1慢但是稳。一般内部服务maxUnavailable: 1maxSurge: 1在可用性和速度之间取平衡。批量任务或非关键服务用默认值 25% / 25%。这里有一个容易误解的地方maxSurge: 1并不代表“一次只更新一个 Pod”。它只是在更新期间允许额外多出 1 个 Pod。实际滚动节奏还受minReadySeconds和探针影响。也就是说新 Pod 必须被判定为“就绪”且稳定运行了一段时间才会继续下一轮替换。2.3 minReadySeconds 与探针决定“就绪”的门槛minReadySeconds是我很看重的参数之一。它表示 Pod 被创建出来后至少要健康运行多少秒才能算“就绪”。默认值是 0也就是只要探针通过就立刻算就绪马上可以接流量。但很多应用启动后需要加载配置、建立连接池、预热缓存探针第一次通过时并不代表服务真的能扛流量。我之前踩过一次坑服务启动 5 秒后 /healthz 接口通了但数据库连接池还没建好流量打进来瞬间超时。后来加了minReadySeconds: 10配合 readinessProbe让每个新 Pod 至少稳定运行 10 秒再上线问题就消失了。探针分三类用途完全不同readinessProbe就绪探针决定 Pod 是否进入 Ready 状态、是否加入 Service 的负载均衡后端。失败不会重启容器只会把 Pod 从 Endpoints 里摘掉。livenessProbe存活探针决定容器是否还活着。失败会触发 kubelet 重启容器。startupProbe启动探针保护启动慢的应用避免在启动阶段被 livenessProbe 误杀。配置探针最容易犯的错误是把探测路径做成一个永远返回 200 的静态接口或者只检查进程是否存活。真正有用的就绪探针应该检查依赖是否就绪比如数据库、Redis、配置中心是否能连通至少健康检查接口要能真实反映服务的可用状态。2.4 progressDeadlineSeconds 与 revisionHistoryLimitprogressDeadlineSeconds默认是 600 秒。它表示 Deployment 在指定时间内没有完成滚动更新控制器就把这个 Deployment 标记为 Progressing 状态异常并写入一条ProgressDeadlineExceeded的事件。这个参数不会真的打断滚动流程它的价值在于让你能及时发现发布卡住了。我在排查事故时经常执行kubectl describe deployment xxx最想看到的就是事件列表里有没有ProgressDeadlineExceeded。如果有基本可以断定滚动更新的某个环节坏了新 Pod 一直不就绪、副本调度失败、或者镜像拉取失败。所以我不建议把这个参数设得太大否则等于没有超时机制。revisionHistoryLimit默认是 10。它控制保留多少个历史 ReplicaSet。我见过一些人图省事把它设为 0结果发布出问题后kubectl rollout undo无版本可回退只能重新构建老镜像非常被动。正常情况下保留 5 到 10 个版本足够既能覆盖回滚需求又不会让 etcd 里堆太多无效数据。2.5 容器生命周期参数优雅停机与 preStop 钩子terminationGracePeriodSeconds指定 Pod 被删除后留给容器的优雅退出时间。默认是 30 秒。很多服务在收到 SIGTERM 后需要处理完当前请求再退出如果时间太短kubelet 会强制杀掉容器正在处理的请求直接断掉。更精细的做法是在容器里加preStop钩子。比如 Spring Cloud 应用在收到 SIGTERM 前会先从注册中心注销防止新流量再打过来然后再等待一段时间让存量请求处理完。用 preStop 钩子执行一个sleep 5或调用一个下线接口能显著减少发布时的错误请求。需要特别注意的是terminationGracePeriodSeconds是一个 Pod 级别的字段不是 Deployment 的通用字段但绝大多数人都是在 Deployment 的 Pod 模板里修改它。如果探针的periodSeconds很小、但terminationGracePeriodSeconds只有 5 秒服务处理请求稍慢就会被硬杀这种组合在线上要尽量避免。3. 不要把滚动更新当灰度发布真正的灰度链路要这么搭很多人第一次接触 Deployment 的 RollingUpdate会误以为这就是灰度发布。从表面看滚动更新确实是“一批一批”地换 Pod但它和灰度发布有本质区别。把这个概念搞混最容易导致两个后果一是发布的等级不够出问题时影响面太大二是你根本没法控制流量比例说好的“先放 10% 流量”实际没做到。3.1 滚动更新和灰度发布的本质区别滚动更新的目标是在最短时间内完成版本替换同时保证服务可用。它把新版本逐台推到全部 Pod直到集群里所有副本都是新版本。在这个过程里流量分配到新 Pod 的比例是随着替换进度自动变化的你无法精确控制也无法中途长时间停在某个比例上观察。灰度发布的目标是可控地暴露新版本。它的核心诉求是我先让一小部分真实流量打到新版本上观察一段时间确认没有异常后再逐步扩大比例最终才全量。灰度发布要求能随时暂停、能动态调整比例、能在异常时快速回退。如果用原生 Deployment 配合滚动更新你虽然也能在发布过程中暂停一眼但它本质上还是会把你推向“全部替换”。所以要真正做灰度需要在 Deployment 之外再加一层流量控制手段。3.2 用原生 Deployment 做轻量级金丝雀如果你不想引入额外组件最简单的方式是用两套 Deployment 配合一个 Service 来实现“按副本数量权重的灰度”。大致思路是稳定 Deploymentapp-web-stable标签是app: web, version: stable跑 v1.0.0副本数 3。金丝雀 Deploymentapp-web-canary标签是app: web, version: canary跑 v2.0.0副本数 1。同一个 Service 的 selector 只要app: web它就会把 3 个稳定版 Pod 和 1 个金丝雀版 Pod 都纳入后端。这样的效果是Service 后端有 4 个 Pod其中大约 25% 的流量会打到金丝雀版本上。要调整比例就加减金丝雀 Deployment 的副本数或者调整稳定版的副本数。但是这个方法有两个明显缺陷。第一流量比例不是精确的 25%而是由副本数量估算出来的节点调度不均匀时会偏差第二如果金丝雀版 Pod 启动失败、探针不过它不会出现在 Service 后端里流量自然全走稳定版你想观察的“故障表现”也观察不到。所以我一般把它定位为“应急轻量方案”适合小团队快速验证版本兼容性但不适合需要精细流量治理的核心业务。3.3 流量维度灰度Ingress 权重与 Argo Rollouts要做真正的百分比灰度必须从流量入口下手。如果你用的是 NGINX Ingress Controller可以很轻量地配置金丝雀 Ingress按权重把一部分外部流量引导到金丝雀服务。apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-canary annotations: nginx.ingress.kubernetes.io/canary: true nginx.ingress.kubernetes.io/canary-weight: 20 spec: rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: app-canary-svc port: number: 80这段配置的含义是在主 Ingress 之外再创建一个金丝雀 Ingresscanary-weight: 20表示把 20% 的流量转发给app-canary-svc。剩下的 80% 仍然走主 Ingress 里的app-stable-svc。金丝雀验证通过后把权重调成 50、80最后把稳定版 Deployment 更新到新版本再删掉金丝雀 Ingress。如果团队规模和技术成熟度允许我更推荐引入 Argo Rollouts。它是专门为 Kubernetes 做金丝雀发布和蓝绿发布设计的控制器能配合 Ingress、Service Mesh 做到自动权重调整、自动分析、自动回滚。用 Argo Rollouts 时你不再直接用 Deployment而是用Rollout这个 CRD 对象但底层管理 Pod 的方式和 Deployment 非常类似学习曲线并不陡。具体到日常使用我会把一个 Rollout 的模板写成这样apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: app-web spec: replicas: 4 strategy: canary: steps: - setWeight: 10 - pause: {duration: 1m} - setWeight: 30 - pause: {duration: 2m} - setWeight: 60 - pause: {duration: 2m} - setWeight: 100 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: app-web image: registry.example.com/app-web:v2.1.0这里的逻辑很直观先把金丝雀权重设为 10%暂停 1 分钟观察再调到 30% 停 2 分钟再 60% 停 2 分钟最后全量。如果某一步的指标分析失败Rollout 可以自动回退到上一个稳定版本。这个工具能解决的问题恰好是原生 Deployment 最难解决的“按流量百分比精细化灰度”。3.4 灰度发布里的验证、暂停与回滚不管用哪种方式做灰度发布流程里都必须有清晰的验证和回滚路径。我建议至少分成四步小流量冒烟先把新版本暴露给 1% 到 10% 的流量观察核心接口的耗时、错误率、日志异常。稳定期观察不要一上来就连续调权重。至少观察 10 到 30 分钟看容器是否出现 OOM、CPU 是否飙高、慢 SQL 是否增加。逐步放量从 30% 到 60% 再到 100%每档之间留足观察时间。配合监控大盘一旦某个指标越界立刻暂停。回滚预案如果用的是两套 Deployment直接删掉金丝雀 Deployment 就能止损如果用 Argo Rolloutskubectl argo rollouts abort app-web可以快速中止如果用原生 Deployment则依赖kubectl rollout undo deployment/app-web。很多人忽略了一个细节灰度发布时金丝雀版本的日志和监控必须单独打标签。如果你在日志平台里没法区分“这个请求是金丝雀版本处理的”那么灰度过程就相当于瞎调整异常出现了你也归因不到新版本上。所以给 Pod 的标签加上version字段不是可有可无的规范而是灰度发布的前置条件。4. 生产环境 Deployment 故障我亲历的排查链路纸上谈兵讲完了接下来聊聊实战。我在生产环境里处理过不少 Deployment 相关的故障几乎每个都能对应到某个参数或某个配置问题。这里挑几个典型的把完整的排查链路写出来希望能帮你建立自己的排查思路。4.1 探针配置不当导致的“发布假成功”有一次发布后Deployment 的状态显示Available但实际上服务时不时报错。刚开始以为是业务代码问题后来在 Service 的 Endpoints 列表里发现所有后端 Pod 都处于 Ready 状态但其中一个 Pod 的 readinessProbe 指向的是/healthz而这个接口只返回了当前进程存活状态并没有检查依赖的连接池。这其实是探针配置最常见的坑。Pod 进程活着不代表应用已经准备好接收流量。后面我把 readinessProbe 的探测路径改成一个更严格的健康检查接口它会实际访问 Redis 和数据库如果依赖半小时内不可用接口就返回 503Pod 也会被自动摘掉。修改后发布时再没出现过“明明显示成功但流量大量超时”的情况。排查这种问题标准命令是kubectl get endpoints app-web kubectl describe pod app-web-xxxx kubectl logs app-web-xxxx --previous先把 Endpoints 和 Pod 状态对比一下如果 Pod Ready 但服务异常基本可以锁定是探针“假阳性”。4.2 集群初始化阶段 API server 不健康引发的连锁问题这个故障不是发生在发布阶段而是发生在建集群阶段但它同样会阻断 Deployment 的正常使用。有一次我初始化 k8s 控制节点执行完初始化命令后输出里明确提示the api server is not healthy after 4m0.00747357s。这个报错表面上是 API Server 没健康但实际原因很多。我当时排查的顺序是先看 kubelet 状态systemctl status kubelet发现 kubelet 没有正常运行。再看容器运行时状态确认 containerd 或 docker 是否正常。检查静态 Pod 目录/etc/kubernetes/manifests里的 kube-apiserver.yaml 是否存在。查看 API Server 日志crictl logs或/var/log/pods/下的日志。最后定位到问题是 kubelet 版本与 API Server 参数不匹配导致 kube-apiserver 容器反复崩溃。修复 kubelet 配置并重启后API Server 恢复健康Deployment 控制器才能正常工作。这个故障的教训是Deployment 是在线的但它依赖一整条控制链路。API Server 不健康时你执行 kubectl create deployment 大概率会超时别急着怀疑 Deployment 本身。4.3 资源配额与驱逐导致发布反复失败还有一种常见的失败是Deployment 创建的新 Pod 一直处于 Pending 或被驱逐。表现是kubectl get pods里有一堆ContainerCreating或OOMKilled。这种情况我见过不少次基本都和resources配置有关。比如新版本镜像比旧版本内存占用更高但 Pod 的limits.memory还写在 256Mi容器启动后直接被 OOM Kill。k8s 里的 OOMKilled 状态非常明显容器会反复重启Deployment 永远无法达到 Ready。另一个情况是集群节点资源碎片化。新 Pod 调度上去时节点剩余内存不够就一直 Pending。这时候看事件里有Insufficient memory解决办法是清理节点上的无用 Pod或者给集群扩容。发布新版本前我建议大家先对比新旧镜像的资源占用曲线必要时同步调整 requests/limits。requests 设得太小节点会超卖limits 设得太小进程会被杀两个极端都会让 Deployment 发布不稳定。4.4 rollout 卡住后的标准排查节奏如果你发现 Deployment 一直没完成更新第一件事不是去删 Pod而是执行kubectl rollout status deployment/app-web --timeout30s kubectl describe deployment/app-webdescribe输出里最关键的是 Events 字段。它会直接告诉你镜像拉取失败ImagePullBackOff、探针一直失败unhealthy、配额不足FailedScheduling、还是滚动超时ProgressDeadlineExceeded。如果事件看不出来再看 ReplicaSetkubectl get rs -l appweb kubectl describe rs app-web-xxxxReplicaSet 会告诉你它期望几个副本、创建了几个、有多少不可用。顺着这条链查大多数 Deployment 卡住的问题都能在几分钟内定位到。切忌一上来就kubectl delete pod强制重启那只会让控制器再拉一个新 Pod问题依旧。5. 把 Deployment 用稳的几个习惯最后聊点长期经验。Deployment 本身不难难的是在复杂的生产环境里和它长期共处。养成几个好习惯能少踩很多坑。5.1 发布前的配置巡检清单我每次发版前都会过一遍下面这些配置检查项建议值或判断标准replicas 是否与 HPA 冲突如果用了 HPADeployment 里的值是基线不要频繁手动改maxUnavailable / maxSurge核心服务 0 / 1一般服务 1 / 1minReadySeconds至少 10启动慢的服务可以设到 30 甚至 60progressDeadlineSeconds不设太长600 到 900 比较合理revisionHistoryLimit5 到 10不要设 0readinessProbe 路径必须真实反映依赖健康状况livenessProbe 超时避免与启动时间冲突必要时加 startupProberesources.requests按监控历史数据估算不要拍脑袋terminationGracePeriodSeconds一般 30 到 60配合 preStop 使用日志与监控标签必须包含版本标签比如 version: v2.1.0这条清单看起来繁琐但能挡住绝大多数低级的发布事故。5.2 和 HPA、PDB 一起配合Deployment 不是孤立存在的。如果服务有弹性扩缩容需求HPA 会根据指标调整spec.replicas字段但它不会管滚动更新期间 Pod 是否能够被驱逐。这时候就要引入 PodDisruptionBudgetPDB。PDB 的作用是保证在节点维护、集群升级等自愿中断场景下服务仍然有足够的副本可用。比如你定义了minAvailable: 3那么在节点 drain 时控制器就不会同时驱逐超过限制数量的 Pod。滚动更新和 PDB 的配合有一个坑如果滚动更新把旧 Pod 杀掉而 PDB 要求至少 3 个副本可用那么更新过程可能会因为 PDB 约束而变慢甚至卡住。这个不一定是错反而是一种保护机制。但如果你的maxUnavailable和 PDB 都配置得特别激进两者互相扯皮发布就卡死了。所以配置 PDB 时一定要回看 Deployment 的滚动参数保证两者兼容。5.3 监控、告警与回滚预案Deployment 的健康状态是必须被监控的。我平时盯的最核心指标是kube_deployment_spec_replicas期望副本数。kube_deployment_status_replicas_available可用副本数。kube_deployment_status_replicas_unavailable不可用副本数。滚动更新状态是否有Progressing、ProgressDeadlineExceeded。告警规则至少应该包含两条可用副本数低于期望副本数超过 5 分钟触发 P2 告警出现ProgressDeadlineExceeded事件触发 P1 告警。有了告警你才能快速介入处理而不是等用户反馈才发现故障。回滚预案要提前写在发布文档里。不要等到发布失败才临时去查rollout undo的参数。我个人会把回滚命令直接写进 CI/CD 的流水线说明里每次发布都顺带把回滚版本记录好。真出问题时点一下就能回退比手忙脚乱敲命令强得多。我做基础设施维护这些年最深的体会是Deployment 的稳定不是靠某一个神奇参数而是靠参数、探针、资源、流量治理和监控整套体系一起兜底。每次在线上看到可用副本突然掉下去我都会先检查这几项基本没有失手过。架构可以花哨发布流程一定要回到这些最基础的检查项上。参数配稳了灰度发布才谈得上真正可控。
返回列表