ARTICLE DETAIL

资讯详情

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

K8s副本数深度解析:从ReplicaSet到HPA与滚动更新

K8s副本数深度解析:从ReplicaSet到HPA与滚动更新 先说个反直觉的现象一个replicas: 3的 Deployment在滚动更新期间集群里最多可能出现 4 个 Pod。也就是说你在 k8s 里设置的副本数和实际跑着的 Pod 数量并不是简单的一对一关系。很多人觉得 k8s 设置副本数就是把 YAML 里的 replicas 改个数字这话没错但只看到这一层后面滚动更新、缩容、跨可用区调度时一定会踩坑。这篇内容我打算把“副本数”这个看上去人畜无害的字段拆开聊透它背后的控制器逻辑是什么生产环境里到底有哪些改法为什么多副本不等于高可用以及我在真实集群里踩过的那些和副本数相关的坑。无论你是刚接触 K8s 的运维新人还是已经被线上事故教育过的“资深玩家”都应该能从里面找到点值回票价的东西。1. 副本数到底在管什么先搞懂Deployment背后那一串对象1.1 从yaml里的replicas字段说起几乎所有人第一次接触副本数都是从 Deployment 开始的。一个最小化的 YAML 长这样apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25-alpine这里replicas: 3的含义不是“创建 3 个 Pod 就完事”而是“整个集群里必须时刻保持有 3 个符合标签选择器的 Pod 处于 Running 状态”。这个差别非常关键。我举个例子你把其中一个 Podkubectl delete pod删掉过几秒钟再看Deployment 会自动帮你拉起一个新的 Pod。你手动杀掉一个它补一个再杀再补。因为这个字段描述的是一种“期望状态”控制循环会持续将实际状态向期望状态收敛。这也就是 Kubernetes 和单机 Docker 之间最本质的分水岭——你用docker run起三个容器挂了就是挂了没人管你你用 Deployment 声明replicas: 3挂在哪个节点、什么时候补、怎么滚动替换全部有控制器兜底。1.2 ReplicaSet是怎么“数”Pod的Deployment 本身并不直接操作 Pod它下面还有一层 ReplicaSet。你用kubectl get rs就能看到kubectl get rs -n default kubectl describe rs nginx-deploy-7d8f8f6f8ReplicaSet 负责“数 Pod”这件事。它怎么数靠的是标签选择器label selector通过spec.selector.matchLabels里的标签去匹配当前集群里的 Pod然后统计匹配数量和期望副本数做比较多了就干掉少了就创建。这里有个很容易被忽略的坑ReplicaSet 不是按“Pod 名字白名单”来数的它只看标签。如果你手动创建了一个带app: nginx标签的“野生 Pod”ReplicaSet 会把这个 Pod 也当成自己的副本直接把实际数量撑到 4 个。反过来如果你改了 Deployment 的 Pod 标签导致新模板的 Pod 不再匹配旧 ReplicaSet 的选择器那旧 ReplicaSet 就会一直试图把旧 Pod 维持住新模板则创建一个新 ReplicaSet两个控制器就“打”起来了。这就是为什么 Deployment 的 selector 一旦确定官方就不建议你去改它。再说一层每次 Deployment 的 Pod 模板发生变化比如镜像版本升级都会生成一个新的 ReplicaSet。旧 ReplicaSet 的副本数会被滚动到 0但它不会立刻消失会保留一段时间用来支持回滚操作。这个“旧副本保留”机制在你查看集群资源时经常会看到一堆 0/0 的 ReplicaSet那就是历史的“化石”。1.3 什么时候副本数不该用Deployment管很多人把 Deployment 当作 K8s 里的万能工作负载什么东西都往里塞这是个很不好的习惯。副本数这个字段在不同控制器下的语义完全不同。工作负载副本数语义典型场景Deployment无状态副本任意替换Web 服务、API、定时批量任务StatefulSet有状态副本带稳定网络标识和存储数据库、消息队列、ZooKeeperDaemonSet每个节点一个不支持随意设置副本数日志采集、监控 Agent、网络插件Job / CronJob完成指定次数任务副本数由 completions 控制一次性数据处理、定时备份如果你硬要用 Deployment 去跑一个需要稳定存储的 MySQL 实例把replicas设成 3会是什么结果三个 Pod 共享一个 PVC同时往同一个数据目录写数据轻则锁文件冲突重则直接数据损坏。StatefulSet 的意义就在于给每个副本一个固定的序号和独立的存储卷缩容时也是按照序号的逆序从最大的那个开始删不会乱删。所以先确认自己跑的是无状态服务还是有状态服务再谈副本数是多少。拿 Deployment 的replicas解决一切问题是新手阶段最容易犯的认知性错误。2. 改副本数的五种姿势从改YAML到HPA自动伸缩副本数从来不是改完就不动的。线上流量洪峰来了要扩容压测结束要缩容节点要维护要迁移这些场景都要修改副本数。我从最稳的方案到最“野”的方案把生产环境里能用到的五种改法过一遍。2.1 最稳的方式改YAML再apply不管集群有没有上 GitOps我都建议把 Deployment 的 YAML 当成唯一的配置源改副本数直接改 YAML 文件然后执行kubectl apply -f deployment.yaml这种方式的好处有几个第一改动可以被 Git 记录出问题能回溯第二kubectl apply是声明式操作它会准确计算出当前集群状态和目标 YAML 之间的差异只修改有变更的字段而不是把整个 Deployment 推倒重来第三配合审计系统你永远知道副本数是被谁、在什么时候改的。有一个细节要特别注意不要直接用kubectl get deployment xxx -o yaml导出的 YAML 再 apply 回去。为什么因为线上导出的 YAML 里包含大量系统生成字段比如status、creationTimestamp、resourceVersion等直接 apply 会产生一堆莫名其妙的字段冲突。正确做法是维护一份干净的、只包含业务关键字段的 YAML 清单线上有漂移就用kubectl diff -f deployment.yaml先对比一下差异确认没问题再 apply。优化一下操作流程我建议的规范步骤是kubectl diff -f deployment.yaml kubectl apply -f deployment.yaml kubectl rollout status deployment/nginx-deploy --timeout120s最后这条rollout status很重要它会等 Deployment 的所有新副本都进入 Ready 状态才返回如果卡住了你能立刻看到具体阻塞在哪。2.2 应急场景kubectl scale与kubectl edit线上流量突然翻了三倍不可能再走一遍 Git 提交流程再等 CI/CD这时候用kubectl scale是最快的kubectl scale deployment/nginx-deploy --replicas5 kubectl scale deployment/nginx-deploy --replicas5 -n prodscale 命令的本质也是修改 Deployment 的replicas字段但它是点对点的极简操作不需要完整文件。生产环境里我见过不少团队在扩容时直接用kubectl scale等流量过去之后再手动缩回来这在紧急情况下完全可行。kubectl edit deployment/nginx-deploy则是另一种思路它会打开默认编辑器一般是 vi等你保存退出后自动触发更新。这个命令适合需要同时改多个字段的场景比如既要改副本数又要改资源限制。但它在生产环境有个隐患——所有人都可以直接 edit没有审批、没有审计留痕多人同时操作时还可能互相覆盖。我的建议是小规模测试集群随便用生产集群尽量留给apply和scale。这里必须提醒一句如果 Deployment 已经被 HPA 托管你用kubectl scale手动改副本数是没有意义的。HPA 会在下一个同步周期按照当前指标把副本数再调回去你改完甚至在事件日志里能看到 “the deployment size is managed by horizontalpodautoscaler” 之类的提示。先确认有没有 HPA 再决定手动操作方案。2.3 自动化场景HPA让副本数自己动副本数的最高境界是“不需要人改”交给 HPAHorizontalPodAutoscaler自动伸缩。比如一个服务设置了 CPU 使用率超过 60% 自动扩容平时就维持最小副本数apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx-deploy-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-deploy minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60HPA 的原理是定期从 Metrics Server 拉取 Pod 的监控指标计算实际利用率与目标利用率的比值然后调整副本数。注意HPA 不是“指标一超过就立刻扩容”它内部有冷却窗口和容差机制默认每 15 秒同步一次指标但要等待指标稳定一段时间才会真正执行扩缩容具体周期受 API Server 上 HPA 控制器参数影响。这个设计是有意为之的避免业务抖动导致副本数在 3 和 10 之间疯狂横跳。用命令行也能快速创建 HPAkubectl autoscale deployment nginx-deploy --min3 --max10 --cpu-percent60但这段命令生成的策略比较粗糙生产环境我还是建议用上面的 YAML 声明可以精确控制多个指标、设置缩容稳定窗口甚至通过behavior字段限制每次扩容/缩容的比例防止流量突刺时副本数一次性飙升导致后端被压垮。2.4 命令行的隐藏款kubectl patch如果只想在脚本或 CI 里改一个字段又不想拉整个 YAMLkubectl patch是个很干净的选项kubectl patch deployment nginx-deploy -p {spec:{replicas:4}}这种 JSON Patch 写法在某些自动化场景里很实用比如运维平台要批量调整多个 Deployment 的副本数用循环执行 patch 比逐个人工 apply 高效得多。但 patch 的可读性和审计性都偏弱我不建议刚接触 K8s 的人把它当主力方案。还有一个老运维常用来“重启 Pod”的骚操作kubectl scale deployment/nginx-deploy --replicas0 kubectl scale deployment/nginx-deploy --replicas3把副本数降到 0 再拉回来所有 Pod 都会重新创建这在紧急处理“Pod 状态正常但内存异常飙高”之类的问题时很管用。前提是你确定服务可以接受全部断连而且缩到 0 的那一瞬间如果 Service 后面没有其他可用副本流量会直接 5xx。所以这个方法只适合救急不适合常规操作。五种方式的适用场景对比如下方式典型场景优点风险apply YAML日常变更、GitOps声明式、可审计文件管理不规范容易漂移kubectl scale流量突增/突减秒级生效容易被 HPA 覆盖kubectl edit临时多字段修改所见即所得无审计易冲突kubectl patch脚本批量调整精准、轻量可读性差HPA常态化弹性伸缩免人工、智能指标配置不当会误判3. 副本数不是你以为的那个数滚动更新里的双重数字陷阱很多人在学习阶段只记住“replicas 是期望 Pod 数”一旦到了滚动更新期间会发现实际 Pod 数量完全不是那么回事。这个数字陷阱值得单独开一节讲清楚。3.1 maxSurge和maxUnavailable如何改变实际Pod数量Deployment 默认更新策略是 RollingUpdate滚动更新时有两个字段决定“副本数的弹性边界”maxSurge和maxUnavailable。默认值都是 25%计算方式是按照replicas的百分比向上取整。拿replicas: 3举例25% 是 0.75向上取整为 1。这意味着滚动更新期间最多允许超出期望副本数 1 个 Pod也就是集群中最多同时有 4 个同版本或新旧版本混合的 Pod最多允许比期望副本数少 1 个可用 Pod也就是更新期间最少可以只有 2 个 Pod 在正常服务。如果你在给一个有 SLA 的线上服务做版本升级这个默认值是很危险的——更新窗口期内可能短暂只有 2 个 Pod 在承接流量而正常情况下是 3 个。为了不让实例数跌破底线生产环境我通常会显式声明spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0maxUnavailable: 0意味着滚动过程中绝对不允许可用 Pod 少于期望值新 Pod 先起来、确认 Ready旧 Pod 才被销毁。代价是更新速度会变慢因为每一次只能替换一个副本maxSurge: 1整个滚动过程是“一个萝卜一个坑”地挪。如果你的集群需要快速完成发布可以把maxSurge调大比如设为 2 或 50%让新 Pod 一次性多起来几个再开始杀旧 Pod。这里最容易翻车的地方是百分比向上取整。replicas: 1时maxSurge默认的 25% 向上取整后是 1所以单副本 Deployement 也具备滚动更新能力——旧 Pod 还没删新 Pod 先创建。replicas: 5时25% 是 1.25取整为 2。不同副本数下实际弹性空间不一样别按整数百分比心算会算错。3.2 缩容时优雅终止Pod不是被“杀掉”而是被“请走”副本数从 3 缩到 1 的时候被选中删除的那两个 Pod 会经历一套“优雅终止”流程而不是直接一个 SIGKILL 结束进程。完整流程大致是这样Pod 进入 Terminating 状态同时从 Service 的 EndpointSlice 中移除不再接收新流量kubelet 对容器发送 SIGTERM 信号如果没有配置 preStop 钩子容器进程自行处理退出kubelet 等待容器进程退出等待时间上限是terminationGracePeriodSeconds默认 30 秒超时后仍然未退出才发送 SIGKILL 强制杀死。这中间最容易出问题的点是“从 EndpointSlice 移除”和“发送 SIGTERM”并不是严格先后关系两者几乎是并行发生的。也就是说Pod 已经开始优雅退出但网络中可能还有正在建立的连接会被转发到这个将死的 Pod 上。如果你没有给容器留出足够的时间去处理存量请求就会看到缩容瞬间出现一批 502 或 504。我处理过类似生产事故最后在容器配置里加了 preStop 钩子让进程先睡几秒钟再退出lifecycle: preStop: exec: command: [/bin/sh, -c, sleep 10]这 10 秒足够让 kubelet 完成 EndpointSlice 的更新把新连接全部导到其他存活 Pod 上再让旧 Pod 优雅退出。如果你的应用处理一个请求就要好几秒那 preStop 的 sleep 时间也要相应拉长最好再结合应用的退出钩子去排空连接池。至于terminationGracePeriodSeconds默认 30 秒通常够用但如果你知道业务在峰值时有些长事务会超过 30 秒就要提前调大。很讽刺的是有时候把它调小了也能“解决”问题——比如 Pod 一直 Terminating 卡住往往就是进程对 SIGTERM 没有反应死等 grace period这时候适当调低这个值反而能让回收更快。3.3 为什么replicas设了3实际只有2个Running我在不少团队里见过这种困惑YAML 里明明写replicas: 3但kubectl get pod一看只有 2 个 Running。先别急着怀疑人品按下面这个清单一步步排查先用一条组合命令看整体状态kubectl get pod -o wide kubectl get rs -o wide kubectl get events --sort-by.lastTimestamp | tail -30常见原因基本逃不出这五类资源不足Pod 处于 Pending。调度器找不到一个节点能同时满足 CPU、内存、亲和性等条件事件里会写 “0/3 nodes are available: 1 Insufficient cpu, 2 Insufficient memory”。健康检查失败Pod 在 CrashLoopBackOff 里反复重启。镜像启动慢或者 liveness probe 阈值设得太小都会被误杀。节点异常Pod 被驱逐后一直没找到地方重建。有 Pod 卡在 Terminating占了副本名额。前面说的优雅终止问题如果进程始终不退出旧的删不掉新的也补不上看起来就是副本数一直少一个。手动创建了带相同标签的 Pod把副本计数搞乱了。这个前面提过ReplicaSet 只认标签不认人。遇到卡在 Terminating 的情况如果确认节点正常、PDB 也允许删除可以用kubectl delete pod pod-name --force --grace-period0强制清理但这是个非常手段优先还是找到进程不退出的根因。4. 多副本不等于高可用把Pod放在不同的“篮子”里这一节我要泼一盆冷水就算你把副本数设成 50如果这 50 个 Pod 全部跑在同一台服务器上那台服务器宕机时你的服务一样全挂。多副本只有建立在“副本分散”的前提下才叫高可用。4.1 副本都堆在同一个节点上等于没有副本K8s 的默认调度器确实会倾向于把多个副本调度到不同节点但这只是基于最小化资源碎片和负载均衡的“尽力而为”不是硬性保证。尤其当集群节点数量很少或者有的节点标签特殊、有的节点有污点Pod 很容易全部堆到少数几个节点上。我见过一个比较典型的例子一个 3 节点的测试集群其中 1 个节点被打上了污点只给专用服务用剩下 2 个节点扛着全部业务副本。某天其中一个节点重启等于集群一半的服务实例同时消失。如果你平时只看“副本数有没有满足”不看“Pod 分布在哪些节点”这种风险就一直潜伏着。要快速查看副本分布可以用kubectl get pod -o wide -l appnginx输出里 NODE 那一列如果所有 Pod 都集中在同一个节点名上那就说明你的副本确实是“一个篮子里的鸡蛋”。4.2 topologySpreadConstraints与反亲和性让K8s把Pod铺开要让副本真正分散开业界有标准做法拓扑分布约束topologySpreadConstraints。这个机制允许你告诉调度器Pod 必须尽量均匀分布在不同的拓扑域里。拓扑域可以是节点kubernetes.io/hostname也可以是可用区topology.kubernetes.io/zone。比如跨可用区均匀分布的配置spec: replicas: 3 topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: nginxmaxSkew: 1表示不同可用区之间的 Pod 数量差值最多为 1。比如集群有 3 个可用区每个区都节点充足那么 3 个副本会被调度成每个区 1 个正好打散。whenUnsatisfiable: DoNotSchedule是硬约束如果集群里可用区数量不够、无法满足均匀分布Pod 干脆不调度宁可 Pending 也不集中。这里有个极易踩的坑labelSelector必须写而且不能写错。如果把 Deployment 的 selector 标签和这里不匹配拓扑分布约束等于没配Pod 该怎么堆还怎么堆。如果集群没有多可用区只有多个节点可以改用topologyKey: kubernetes.io/hostname做节点级打散。另一种做法是 Pod 反亲和affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: [nginx] topologyKey: kubernetes.io/hostnamepreferredDuringSchedulingIgnoredDuringExecution是软约束“尽量不打到同一个节点上但如果实在没节点可用也可以放一起”。生产环境我更倾向于硬约束加软约束组合核心业务用拓扑分布约束保底普通业务用反亲和性尽量打散。4.3 PodDisruptionBudget给“自愿中断”设个底线节点要做系统升级、要缩容运维人员会执行kubectl drain节点上的 Pod 会被驱逐。这种“有意为之”的中断在 K8s 里叫自愿中断。为了防止驱逐过程中可用副本数降到业务无法接受的程度就需要 PodDisruptionBudgetPDB。apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: nginx-pdb spec: minAvailable: 2 selector: matchLabels: app: nginx含义很清楚在任何属于自愿中断的操作中保证app: nginx的可用 Pod 不少于 2 个。如果当前只有 2 个可用 Podkubectl drain就会在驱逐第 3 个 Pod 时卡住因为驱逐掉一个可用 Pod 就会低于 2 的底线。PDB 不保护节点宕机这种非自愿中断因为那种场景下控制器已经来不及阻止节点掉线了副本数能否恢复完全取决于新副本能否在其他节点上被调度起来。注意 PDB 的minAvailable和 Deployment 的replicas是两套独立配置它们不会自动对齐。如果replicas: 2却设minAvailable: 2维护节点时 drain 会被 PDB 卡到天荒地老因为驱散任何一个 Pod 都会跌破底线。要么设minAvailable: 1要么把副本数提高到 3。这类配置冲突我在多个集群里见过不是罕见问题。5. 资源配额、缩容风暴与监控我在生产环境里踩过的副本数相关的坑前面讲的是原理和操作最后这部分完全来自我的实战经历希望对你有直接的参考价值。5.1 副本数先看资源配额再看需求我在接手一个集群时习惯先跑一条命令kubectl describe node重点看每个节点的Allocatable可分配资源。假设一个节点可分配 4 核 CPU、8G 内存Deployment 里每个 Pod 的requests是 500m CPU 和 512Mi 内存那理论这个节点最多放 8 个这样的 Pod再算上 kubelet、CNI 插件、日志采集组件等系统占用实际安全上限大概在 6 个左右。很多人把replicas从 3 改成 30然后发现新 Pod 全都是 Pending。原因非常简单集群算力根本塞不下 30 个副本。这不是 K8s 的限制而是物理资源的天花板。集群级的容器配额也要心里有数kubectl get resourcequota -n prod -o yaml如果某个 Namespace 配了 ResourceQuota比如 CPU limit 总量 10 核那你在这个命名空间里无论怎么调副本数超过配额之后 Pod 一样建不出来事件里会明确写 “exceeded quota”。5.2 缩容风暴从100到0会发生什么有一类事故特别经典晚间低峰期运维想省资源一条kubectl scale deployment/xxx --replicas0把服务缩没了或者想优雅缩容但从 100 缩到 20Pod 大规模并发退出。如果应用没有优雅退出机制在几十个副本同时被 Terminating 的那一瞬间到达 Service 的请求会大量失败——因为 EndpointSlice 更新有延迟而 Pod 里的连接已经被强制断开了。我处理这种问题的思路是这样先确保应用侧有优雅退出逻辑配合 preStop 和合理的 terminationGracePeriodSeconds大批量缩容时不要一次从 100 缩到 20而是中间停一下先缩到 60观察服务错误率再缩到 40、20分批走如果服务后面挂着注册中心比如 Nacos 或 Eureka缩容前先摘除节点让上游不再往这些将要关停的实例上发流量。这些在教科书里都不会写但线上环境里是最经常让人睡不踏实的地方。5.3 从压测得来的副本数计算模板副本数设多少合适没有万能答案但有一个能被大家复用的估算模板。你先通过压测拿到单副本的性能数据。假设用 wrk 或者 ab 对服务打压力在 CPU 使用率 70% 附近测得单副本 QPS 为 200。再统计业务高峰期的峰值 QPS假设是 1000。那副本数的粗略计算就是目标利用率 0.7 预估副本数 峰值QPS / (单副本QPS 目标利用率) 1000 / (200 0.7) 1000 / 140 ≈ 7.14向上取整是老副本数取 8。再考虑突发流量加上 20%~50% 冗余最终副本数大概落在 10~12。如果集群覆盖 3 个可用区务必让这个数字能被 3 基本整除比如 9 或 12方便拓扑分布约束把副本均匀铺开。如果压测数据不充分就把 HPA 的 maxReplicas 放宽让控制器自动找到临界点不要盲猜一个固定值端在那里。5.4 改完副本数之后的验证习惯最后聊聊我个人的一个收尾习惯。无论在哪个环境改完副本数我都会用一条命令确认最终状态kubectl get deployment nginx-deploy -o jsonpath{.spec.replicas} {.status.readyReplicas} {.status.availableReplicas}理想输出是3 3 3也就是期望副本数、Ready 副本数、Available 副本数三个数完全一致。如果不一致再去查 ReplicaSet 和事件不要直接下班。这个习惯看起来非常简单但它帮我拦住过不少问题。有一次改完副本数后看到3 2 3凭空多出一个 Ready 却没 Available 的 Pod一查发现是镜像里没有配置就绪探针Pod 进程起来了但服务端口还没监听好。如果只看spec.replicas还以为一切正常呢。副本数这种基础字段恰恰是 K8s 里最值得认真对待的东西之一。它看似只是一个数字背后却连着控制器逻辑、调度策略、滚动更新、优雅终止和容量规划。把这个数字玩明白你离一个合格的 K8s 运维人员就又近了一步。
返回列表