ReplicaSet完全指南:控制器原理、YAML配置与实战排障
刚接触Kubernetes的时候很多人都会被Deployment、ReplicaSet、Pod这一串名词绕晕。ReplicaSet尤其尴尬它不像Pod那样直观也不像Deployment那样是日常操作的核心对象。不少教程讲完Deployment就顺口说一句“底层是ReplicaSet在管副本”然后就不展开了。结果就是很多人写Deployment YAML写了大半年也知道有ReplicaSet这么个东西可一旦要求手写一份ReplicaSet资源、说清楚选择器规则、解释为什么改了Pod模板却不生效一下子就卡壳了。等到了线上出现Pod被莫名重建、扩容数量对不上、删掉RS之后留下一批没有归属的Pod这种怪现象又不知道该从哪查起。这篇内容我想把ReplicaSet从头到尾讲完整它到底解决什么问题、YAML里每个字段是干什么的、控制器靠什么机制维持副本数、实战里怎么创建扩容和排障、以及它和Deployment之间到底谁听谁的。无论你是刚搭完集群正在补基础还是准备面试刷题亦或已经在生产环境被ReplicaSet相关的问题坑过、想真正弄明白原因这条主线基本都适用。1. 先从一道经典面试题切入ReplicaSet到底在管什么1.1 没有控制器的集群里Pod是没法自愈的很多人学K8s的第一个误区是以为Pod天生具备“挂了自动重建”的能力。实际上你直接kubectl run或者apply一个裸Pod它跑起来就只是跑起来如果应用崩溃、被kill、所在节点宕机这个Pod最多被标记成Error或者Terminating系统不会帮你重新拉起一个。ReplicaSet这种控制器出现的原因就是给K8s补上“自愈”这个能力。它的核心职责一句话能说清楚让某一组Pod的实际数量永远等于你在spec.replicas里指定的期望数量。实际数量少了就创建实际数量多了就删掉始终拧回到期望值。这是K8s里最典型的“声明式管理”思想你只告诉它目标不用告诉它怎么一步步达到目标。这个特性在面试里经常被包装成各种问法比如“如果某个Pod所在的Node宕机了K8s会怎么做”。回答的时候只要咬住这条主线就不会跑偏节点失联后NodeLifecycleController会等待pod-eviction-timeout默认5分钟之后将失联节点上的Pod标记为删除此时ReplicaSet发现实际数量少了才在别的节点上创建替补Pod。ReplicaSet本身不监听节点它只关心“该有多少个Pod”这件事。1.2 ReplicaSet与ReplicationController名字像能力差一代K8s最早期的副本控制器叫ReplicationControllerRC后来才演进成ReplicaSetRS。如果你去看老教程会看到RC的YAML长这样spec.selector只能支持最简单的等值匹配比如app: nginx这种keyvalue的形式想表达“只要环境是生产或者预发都可以”这类集合条件RC完全做不到。ReplicaSet在API上替代RC之后选择器能力有了本质升级。它支持两种表达matchLabels做等值匹配matchExpressions做集合匹配配合In、NotIn、Exists、DoesNotExist四种操作符表达能力比RC强出一大截。下面的表格可以帮你快速对比两者差异对比项ReplicationControllerReplicaSet所属APIcore/v1apps/v1选择器类型仅等值匹配等值匹配 集合匹配是否支持operator不支持In / NotIn / Exists / DoesNotExist实际使用方式基本被淘汰作为Deployment的底层支柱被广泛使用所以现在的面试题里问“RC和RS有什么区别”其实考的就是两点API代际和选择器能力。前者说明K8s在演进后者说明K8s在把“控制对象”的选择能力做得越来越灵活。1.3 它和Deployment不是竞争关系而是上下级还有一个高频混淆点是很多人觉得ReplicaSet和Deployment是两种并列的副本方案。实际上它们完全不在一个层次上Deployment是上层编排器ReplicaSet是Deployment用来执行“副本数量维护”的打工者。每次修改Deployment的Pod模板Deployment控制器都会创建一个新的ReplicaSet通过控制新旧RS的扩缩容节奏来实现滚动更新和回滚。换句话说你平时用Deployment管理服务Deployment管理的其实是ReplicaSetReplicaSet管理的才是Pod。所以即使你从来没手写过ReplicaSet的YAML你在生产环境里也一直在用它。这也是为什么很多人排查Deployment相关问题时最后都会落到kubectl get rs这一层——很多真相光盯Deployment是看不出来的。2. 资源定义拆解一份ReplicaSet YAML逐字段读懂2.1 先放一份能直接跑的最小配置空谈概念意义不大我直接给一份最简可用的ReplicaSet YAML。你要是想跟着实验先把这份保存成nginx-rs.yamlapiVersion: apps/v1 kind: ReplicaSet metadata: name: nginx-rs namespace: default labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80这份配置能跑但它值得拆开看的东西很多。apiVersion: apps/v1说明RS属于apps工作负载组这个API组从K8s 1.16开始进入稳定版本现在写RS基本都用它。metadata里除了名字和命名空间labels是RS自己的标签这东西不会自动传给Pod别和下面template里的labels搞混。spec.replicas默认值是1不写也不会报错但正式配置里我建议永远显式写清楚免得别人看你的YAML还要猜意图。spec.selector是控制器的“筛选条件”spec.template是“创建Pod时用的蓝图”这两块是RS的核心下面重点说。2.2 selector的两种表达matchLabels与matchExpressionsselector告诉RS控制器什么样的Pod算“我的Pod”。RS只对满足这个条件的Pod做计数多了删、少了补。两种写法各有用武之地。matchLabels最简单要求Pod的标签里必须同时包含指定的所有keyvalue对。比如上面配置里selector是app: nginx那么只有带着appnginx这个标签的Pod才会被RS纳入管理一个Pod就算同时有tier: frontend只要包含appnginx也会被算进去。matchExpressions能力更强适合表达批量筛选。比如生产环境里的Web服务你希望不管标签是appnginx还是appweb都纳入同一批副本管理可以这样写selector: matchExpressions: - key: app operator: In values: - nginx - webExists操作符只要Pod有这个key就算命中不关心value是什么DoesNotExist则相反。实际生产里matchLabels最常用因为语义一目了然matchExpressions更多出现在Operator这类自定义控制器里。2.3 模板标签和选择器为什么必须对齐这是新手写RS最常触发的报错。template里metadata.labels是“新创建Pod要贴的标签”而selector是“控制器找Pod的依据”。如果创建出来的Pod根本带不上selector要匹配的标签那这个RS创建出来的Pod就成了“自己找不见的娃”控制器会认为实际数量不够继续创建造成无限重复——所以API Server索性在创建时就强制校验。实践中的报错一般是这样的spec.template.metadata.labels: Invalid value: map[string]string{app:nginx}: selector does not match template labels规则很简单template里的一组标签必须能“喂饱”selector里所有的匹配规则。比如selector要求appnginx和tierfront两个条件那template labels里就必须同时带上这两个标签只带一个都不行。你可以多写template里不需要的标签但不能少写selector依赖的标签。2.4 别忘了status字段是控制器写出来的很多人在kubectl get rs -o yaml里看到status字段以为是YAML里写的。实际上spec是声明status是控制器观察后的实际结果。RS的status里会有replicas当前创建的Pod数、readyReplicas就绪Pod数、availableReplicas可用Pod数、observedGeneration当前观察到的代数等字段。排障时如果你发现DESIRED和CURRENT对不上要记得去看status而不是改spec。3. 控制机制原理控制器怎么做到“始终有N个Pod”3.1 控制循环期望值、实际值和差值ReplicaSet的“始终有N个Pod”不是魔法靠的是K8s里经典的控制循环control loop。这个循环跑在kube-controller-manager里对应的是ReplicaSetController这个组件。它通过informer机制监听两类对象的变更ReplicaSet本身以及所有Pod。每次Pod被创建、删除、更新或者定期resync触发控制器都会拿每一个RS的selector去列表里筛选出候选Pod然后开始算账。算账逻辑非常简单粗暴把筛选出来的Pod数量记作actual把spec.replicas记作desireddiff desired - actual。diff为正就创建Pod把缺口补齐diff为负就删掉多余的Pod。整个循环每隔一段时间也会强制重新同步一次防止Pod的某个变化事件因为网络或缓存问题漏掉。这就不难理解为什么K8s是“最终一致”的了——事件驱动意味着快周期resync兜底意味着即使丢了事件也不会永远错下去。3.2 RS只负责“数数”和“增减”其他事不归它管ReplicaSet这个名字容易让人误以为它“管”副本的一切实际上它只管数量。Pod创建出来之后调度到哪台Node是kube-scheduler的活镜像拉取、容器启动、健康探针执行全是kubelet的活。RS只关心“Pod这个对象存不存在”不关心“Pod是否真的跑得健康”。这个边界很重要。面试里常问“RS会不会自动替换NotReady的Pod”答案是不会——Pod只要还在etcd里存在RS就认为它占着副本名额NotReady是kubelet上报的状态RS根本不看。真正会触发替换的是Pod被删除删除原因可能是你手动执行的、可能是节点失联后NodeLifecycleController清理的、也可能是驱逐机制触发的。理解了这个边界排障时就不会拿着一堆NotReady的Pod去质问“RS怎么不干活”。3.3 缩容时先删谁这个算法很现实数量多了要删减时RS的Pod选择算法不是随机挑而是有一套优先级逻辑。总结下来大概是优先删还在Pending、不可调度的Pod其次删已经处于删除中或状态不健康的Pod第三优选创建时间更早的Pod如果还是分不出胜负再按UID排序保证结果稳定。这套逻辑背后的意图很直白把集群资源留给最健康、最年轻的Pod。否则缩容时留一堆卡在Pending的反而是灾难。部分版本里控制器还会尝试兼顾Pod在某节点上的分布尽量做到缩容后不把Pod集中到同一台Node上。知道这个规则对实际操作很有用。比如你想缩容一台机器上的Pod直接kubectl delete pod指定目标再让RS补一个比kubectl scale更可控但当你只是想单纯把副本数降下来且不在乎谁走时放心交给RS的算法就行它比你随机删更合理。3.4 控制器认领PodownerReferences是隐形纽带除了按标签筛选RS还通过ownerReferences和Pod建立归属关系。它每次创建的Pod都会在metadata里带上指向RS的ownerReference标明“这个Pod是我创造的”。删除RS时垃圾回收器看到Pod的owner没了会自动把Pod一并清理掉——这就是级联删除的底层原理。有意思的是RS还会“认领”孤儿Pod。如果集群里碰巧有个没人管的Pod但它的标签和RS的selector完全匹配RS会把它纳入自己名下而不是傻傻地再创建一个新的。这种机制保证了系统不会被僵尸Pod搞乱但也带来了一个隐患selector写得太宽两个RS可能盯上同一个Pod。具体怎么踩坑我在第六部分会专门讲。4. 实战示例从创建、自愈到清理跟着敲一遍4.1 提交RS观察控制器动作先把前面那份YAML保存好执行kubectl apply -f nginx-rs.yaml kubectl get rs nginx-rs -o wide kubectl get pods -l appnginx --show-labels正常情况下你会看到RS的DESIRED是3CURRENT很快就变成3然后READY可能从0慢慢变成3。Pod的名称格式是nginx-rs-xxxxx后缀是随机串。这时用kubectl describe rs nginx-rs看Events能看到控制器陆续创建的记录Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 12s replicaset-controller Created pod: nginx-rs-abc12 Normal SuccessfulCreate 12s replicaset-controller Created pod: nginx-rs-def34 Normal SuccessfulCreate 12s replicaset-controller Created pod: nginx-rs-ghi56第一条排查经验就是events里的SuccessfulCreate和SuccessfulDelete是判断RS是否在正常工作的黄金线索。4.2 故意删一个Pod看它怎么“复活”现在做个小破坏实验删掉其中一个Podkubectl delete pod nginx-rs-abc12 kubectl get pods -l appnginx不用几秒你就会看到Pod数量又变回3。新出现的Pod换了名字比如nginx-rs-xyz78。这就是自愈的现场。如果你加上watch参数能更直观地看到删除后瞬间数量掉到2随后又回到3。这一步还建议顺手做一件事再次kubectl describe rs nginx-rsEvents里会有一条SuccessfulDelete和一条新的SuccessfulCreate。记住这个节奏后面排障时如果看到只有Delete没有Create说明控制器可能认为实际数量已经够了而不是它“罢工”了。4.3 扩容缩容的现场扩容很简单kubectl scale rs nginx-rs --replicas5 kubectl get rs nginx-rsDESIRED马上变成5CURRENT随后跟上新增的Pod会按template创建。缩容同理kubectl scale rs nginx-rs --replicas2缩容时如果你盯着kubectl get pods看会发现RS挑着删如果此时有Pod还处于Pending或NotReady它大概率会被优先清除都是Ready的话老Pod更危险。4.4 修改Pod模板不会滚动更新这一条必须记牢ReplicaSet和Deployment最大的行为差异就在这Deployment改模板会触发滚动更新ReplicaSet改模板则“什么都不发生”——对存量Pod而言。实操验证一下。先看当前Pod的镜像kubectl get pods -l appnginx -o jsonpath{range .items[*]}{.metadata.name}{ - }{.spec.containers[0].image}{\n}{end}输出应该全是nginx:1.25。然后编辑RS把image改成nginx:1.26kubectl edit rs nginx-rs改完保存再查一次Pod镜像你会发现存量Pod纹丝不动还是1.25。template变化只对“接下来新建的Pod”生效。所以你永远不要指望直接改RS模板来升级应用想滚动升级请用Deployment直接改RS模板只能用于“下次重建时换镜像”这类场景。4.5 删除RS时的级联与孤儿Pod直接删除RS默认会把Pod一起清掉kubectl delete rs nginx-rs kubectl get pods -l appnginx最后一行的Pod会瞬间消失这是级联删除垃圾回收器根据ownerReferences执行清理。如果你只想删RS、保留Pod可以这样kubectl delete rs nginx-rs --cascadeorphan执行之后Pod还在但它们已经成了“没人管”的孤儿。此时如果有一个标签匹配的新RS出现这些孤儿Pod可能被直接纳入新RS管理。这个操作在带状态迁移、临时保留Pod时很有用但也容易埋雷后面会细说。5. Deployment与ReplicaSet的关系真正干活的其实是RS5.1 Deployment把RS当成“影子分身”写一份Deployment时最容易被忽略的一点是Deployment控制器会自动创建一个ReplicaSet名字格式是deployment名-pod-template-hash。比如nginx-deploy-7d8f9其中7d8f9是根据Pod模板内容哈希出来的。这个哈希同时作为标签打到Pod上Deployment的RS selector里就带着它。所以你kubectl get rs的时候会看到一堆Deployment生成出来的RS。它们不是孤立存在的而是Deployment的“影子分身”。Deployment的期望副本数会实时同步给当前生效的那个RS而RS仍然负责实际的Pod数量维持。换句话说Deployment在外层做策略RS在里层做执行。明白了这层关系你就知道为什么排查Deployment问题时总绕不开RS了。比如明明改了Deployment的replicas却不见Pod变化先去看RS的replicas有没有同步过来如果RS的replicas被某个外部动作改了Deployment控制器可能也会“纠正”它两者的状态拉扯都会体现在这两个对象的status上。5.2 滚动更新和回滚本质是RS切换滚动更新的底层逻辑是Deployment创建了一个新RS然后把新旧两个RS当作两个“版本池”。更新期间Deployment按照maxSurge和maxUnavailable的策略把新RS往上加Pod同时把旧RS往下减Pod直到旧RS的副本数变成0、新RS接管所有流量。回滚则相反Deployment把旧RS重新拉起来把新RS缩下去。所以kubectl rollout history里看到的每一条历史版本背后其实都能对应到一个具体的RS对象。很多人遇到“rollout卡住”直接查Deployment status看不懂但改成查新旧两个RS的副本数和Pod状态一眼就能看出问题要么新RS对应的Pod起不来要么旧RS缩不下去。这里有个实用技巧想强制回滚到某个副本数不为0的旧版本可以直接kubectl rollout undo deployment/xxx --to-revisionNDeployment会帮你调整对应RS的replicas而不是让你手工去scales RS。自己手工乱改RS数量和Deployment想维护的状态打架是生产事故的常见来源。5.3 什么场景才值得直接使用ReplicaSet既然Deployment这么方便为什么还要手写RS我的看法是绝大多数场景都不需要但有两个例外值得注意。一个是自定义控制器或Operator的构建场景。当你在写一个Operator想要一个简洁的“固定副本数维持器”作为底层组件时直接用RS比套Deployment少一层策略干扰行为更可控。K8s生态里不少Operator内部就是靠RS驱动一批Worker Pod的。另一个是学习和排障场景。手写RS能帮你把selector、template、控制器这三者的关系彻底吃透。很多人Deployment YAML写了一堆却不知道pod-template-hash标签是哪来的为什么不能随便改Pod标签这些底子其实都是RS层面的知识。6. 常见坑和排查思路被RS坑过的人都懂6.1 selector写得太宽把别人的Pod“抢”过来了ReplicaSet按标签筛Pod这个机制好理解但也埋着大坑。假如你的命名空间里有两个RS的selector都写了app: nginx且没有额外标签区分那么两个RS看到的Pod池子是同一批。它们会互相抢Pod的所有权、各自计算数量结果就是扩容数量怎么都不对删一个RS还可能导致另一个RS“接管”一批Pod应用行为完全失控。我曾经见过一个案例新上线的RS-B没有定义独立的唯一标签selector直接用app: web结果把老RS-A管理的Pod当成了自己的资产既不去创建自己的Pod又干扰了A的缩容。排查时你会看到Pod的ownerReferences一会儿指向A一会儿指向B非常诡异。正确的做法是手写RS时selector里务必带一个当前工作负载独有的标签比如app: nginx、tier: frontend、component: api必要时可以再拼一个routers/instance: 名称。如果你用的是Deployment它自动加的pod-template-hash就是天然的隔离手段正常情况下不要绕过它。6.2 扩不了容、起不来Pod先看Events而不是猜RS创建Pod失败时不会直接把它甩在status里。很多时候DESIRED是5CURRENT只有3READY更少你要做的第一件事是kubectl describe rs 名称然后往下翻Events。常见的失败原因和对应事件大致是下面这张表症状可能事件/原因排查方向Pod一直PendingFailedScheduling / Insufficient cpu节点资源、affinity、toleration镜像拉不出来Failed / ErrImagePull镜像仓库权限、tag是否存在创建直接被拒resource quota exceeded命名空间配额、LimitRangePod一直ContainerCreatingFailedCreatePodSandBoxCNI、网络插件、安全策略数量达不到期望值无Create事件检查selector是否匹配已有孤儿Pod这里想强调一个隐性坑如果RS要创建的Pod因为某种原因一直PendingRS不会拿着“缺额”一直疯狂创建它只会反复尝试补齐而新增的Pod也可能继续Pending。这时候你去改replicas没用得解决根本的资源或调度问题。6.3 三个容易被忽略的运维细节第一个细节是修改RS模板不会影响存量Pod。前文已经验证过了这里再从运维角度说一遍如果你真的需要给RS管理的现有Pod换镜像只能删掉重来或者干脆迁到Deployment。很多人卡在这上面好几天怀疑是不是集群坏了。第二个细节是删除RS时用了--cascadeorphan之后Pod虽然保留但这些Pod失去了owner既不会自愈也没有人保证它们数量。之后一旦有一个selector相同的RS出现它们还可能被接管导致新旧混跑。所以这个参数只建议在你有明确接管计划时使用否则就是给自己埋定时炸弹。第三个细节是HPA的target不要直接指到Deployment管理的ReplicaSet。原因前面说过Deployment滚动更新会不断切换RS如果HPA盯住的是某个具体RS更新时新RS可能没被HPA覆盖老RS又被缩到0导致副本数失控。规范的用法是让HPA指向Deployment让Deployment去协调RS。6.4 最后分享一个排查模板如果你在集群里遇到任何和副本数相关的怪事我的建议是保持一个固定排查模板。先kubectl get rs -A看全局确认哪些RS的DESIRED和CURRENT不一致再用kubectl describe rs 名称看Events接着kubectl get pods -l selector --show-labels看实际Pod的标签、状态、ownerReferences。三步走下来90%的问题都能定位到selector写错、资源不足、模板不更新、级联删除这四类原因里。我个人在实际维护集群时还有个习惯每份RS的YAML都会在template labels里多写一个version或者instance标签用来把不同批次创建的Pod区分开。这个标签平时不参与selector匹配只在线上分批次排查、灰度缩容时能帮上大忙。你也不妨试试成本几乎为零但在紧急排障时它往往能让你少用十分钟去猜Pod来历。