首页 / 资讯中心 / 文章详情

K8s Controller深度解析:从Pod失联到控制器选型实战

K8s Controller深度解析:从Pod失联到控制器选型实战 ★ FEATURED ARTICLE
1. 从一次Pod“失联”说起为什么需要Controller刚接触K8s那会儿我干过一件现在想起来都脸红的事。手动创建了一个Pod跑业务跑得好好的结果某天节点资源紧张Pod被驱逐了业务直接挂掉。我当时的第一反应是“谁把我的Pod删了”排查半天才发现根本没人删它是它自己“死”了之后没人管。这件事让我彻底明白了一个道理在K8s里Pod不是宠物是牲畜。你不能指望手动创建的Pod能长久稳定地活着它随时可能因为各种原因消失。而真正让K8s具备自愈能力的就是今天要聊的主角——Controller控制器。Controller这个词听起来挺唬人但它的核心逻辑其实特别朴素声明式API 控制循环。你告诉K8s“我想要3个副本一直跑着”Controller就会像个不知疲倦的监工每隔一段时间检查一下实际状态发现只有2个就赶紧补1个发现多出来1个就干掉1个。它不关心过程只关心结果。这种“面向终态”的设计是K8s区别于传统运维工具的根本所在。这篇文章我会把Controller的家族成员一个个拆开讲清楚包括Deployment、ReplicaSet、StatefulSet、DaemonSet、Job、CronJob这些你天天打交道的对象。不只是告诉你它们是什么更会讲清楚它们底层怎么工作、什么场景该用哪个、以及我在实际使用中踩过的那些坑。不管你是刚入门K8s的新手还是已经用了一段时间但总觉得“知其然不知其所以然”的老手应该都能从中找到一些有用的东西。2. Controller的底层逻辑控制循环到底在循环什么2.1 声明式API与控制循环的配合机制要理解Controller得先理解K8s的声明式API。这玩意儿跟传统的命令式操作有本质区别。命令式是你告诉系统“做什么”比如“启动一个Nginx容器”声明式是你告诉系统“我要什么”比如“我要一个Nginx容器一直运行着”。前者你发完指令就完事了容器死没死跟你没关系后者系统会持续保证这个状态容器死了它自动给你拉起来。Controller就是实现这个“持续保证”的组件。它的工作模式是一个无限循环业内叫Reconcile Loop调和循环。每一轮循环做三件事第一读取期望状态从API Server里的Spec字段读第二读取实际状态从API Server里的Status字段读或者直接查底层资源第三比较两者差异执行操作让实际状态向期望状态靠拢。这个循环不是靠事件驱动的而是定时触发 事件触发结合。定时触发就是每隔一段时间比如几秒到几分钟不等强制同步一次防止状态漂移事件触发是当被监控的资源发生变化时比如Pod被删了立即触发一次调和。两者配合既保证了实时性又保证了最终一致性。这里有个容易混淆的点Controller本身不直接操作容器运行时。它只跟API Server打交道修改的是etcd里的资源对象。真正去创建容器的是kubeletController只是“下单”kubelet才是“干活”的。理解这个分层后面排查问题的时候思路会清晰很多。2.2 期望状态与实际状态的差异从哪来实际状态和期望状态不一致的原因五花八门我大致归了几类。第一类是资源不足比如节点CPU不够了Pod调度不上去或者被驱逐了。第二类是人为干预比如有人手动kubectl delete了一个Pod或者改了某个资源的标签导致它不再被Controller管理。第三类是组件故障比如kubelet挂了节点失联了Pod状态变成Unknown。第四类是配置错误比如镜像拉不下来或者健康检查一直失败导致Pod反复重启。Controller处理这些差异的方式也不一样。对于资源不足它会不断重试调度直到有资源为止对于人为删除它会立即创建新的Pod补上对于组件故障它会等待节点恢复或者把Pod重新调度到其他节点对于配置错误它会按照重启策略反复重启容器直到你修复配置。理解这些差异来源你就能预判Controller的行为而不是被它的“反复重试”搞得一头雾水。2.3 控制器管理器多个Controller如何协同工作K8s里不是只有一个Controller而是一堆Controller同时运行它们都住在kube-controller-manager这个组件里。这个组件本质上是一个进程里面启动了多个独立的控制循环每个循环负责一种资源类型。比如Deployment Controller管DeploymentReplicaSet Controller管ReplicaSetNode Controller管节点状态Endpoint Controller管Service和Pod的映射关系。这些Controller之间通过API Server间接通信不直接互相调用。比如你创建一个DeploymentDeployment Controller会创建一个ReplicaSetReplicaSet Controller看到这个ReplicaSet后会创建对应数量的Pod。整个过程是链式的每一层只关心自己那一层的事情。这种设计的好处是解耦每个Controller可以独立升级、独立扩展坏了一个不影响其他的。坏处是排查问题的时候链路比较长你得一层一层往下查。3. 无状态应用的守护者Deployment与ReplicaSet3.1 ReplicaSet保证副本数量的最简控制器ReplicaSet是Controller家族里最基础的一个它的职责单一到令人发指保证指定数量的Pod副本一直运行。你告诉它“我要3个”它就盯着少了补多了删。它通过标签选择器Label Selector来识别哪些Pod归它管所以你在创建ReplicaSet的时候必须定义selector而且这个selector必须能匹配到它创建的Pod模板里的标签。ReplicaSet的YAML结构很直白核心就三个字段replicas副本数、selector标签选择器、templatePod模板。我刚开始用的时候犯过一个低级错误selector写的是app: nginx但template里的labels写的是app: nginx-web结果ReplicaSet创建出来的Pod它自己不认识疯狂创建新Pod最后把集群资源耗光了。这个坑的本质是selector和template.labels必须匹配不匹配的话ReplicaSet会认为“我创建的Pod都不存在”然后无限循环创建。实际使用中你几乎不会直接创建ReplicaSet而是通过Deployment来管理。ReplicaSet更像是Deployment的一个“执行单元”Deployment负责版本管理和滚动更新ReplicaSet负责维持副本数量。这种分工让版本回滚变得特别简单后面讲Deployment的时候会详细说。3.2 Deployment滚动更新与版本回滚的实现原理Deployment是日常用得最多的Controller没有之一。它在ReplicaSet之上加了一层版本管理的能力。你创建一个Deployment它会创建一个ReplicaSet你更新镜像版本它会创建一个新的ReplicaSet然后逐步把流量从旧ReplicaSet切到新ReplicaSet这个过程叫滚动更新。滚动更新的核心参数有两个maxSurge和maxUnavailable。maxSurge控制更新过程中最多可以超出期望副本数多少个Pod默认是25%maxUnavailable控制最多可以有多少个Pod不可用默认也是25%。举个例子你有4个副本maxSurge1maxUnavailable0更新过程是这样的先创建1个新Pod总数变成5等它Ready后删掉1个旧Pod总数回到4再创建1个新Pod再删1个旧Pod如此循环直到全部替换。整个过程始终保证至少有4个Pod可用。版本回滚是Deployment的另一个杀手锏。每次更新Deployment都会保留旧的ReplicaSet通过revisionHistoryLimit控制保留多少个默认10个。当你发现新版本有问题一条kubectl rollout undo命令就能回滚到上一个版本。回滚的本质是把旧ReplicaSet的副本数调上去新ReplicaSet的副本数调下来跟滚动更新的逻辑一模一样。3.3 滚动更新卡住了常见原因与排查路径滚动更新卡住是我遇到过最多的问题之一。表现是kubectl rollout status一直显示“Waiting for deployment rollout to finish”或者kubectl get pods看到新Pod一直处于Pending、CrashLoopBackOff、ImagePullBackOff状态。排查思路我总结了一个顺序第一步看新Pod的状态。如果是Pending大概率是资源不够或者调度约束不满足用kubectl describe pod看Events通常会告诉你“Insufficient cpu”或者“node(s) didnt match node selector”。如果是ImagePullBackOff检查镜像地址和拉取凭证。如果是CrashLoopBackOff看容器日志通常是应用启动失败。第二步看Deployment的Conditions。kubectl describe deployment输出里的Conditions字段会告诉你“Progressing”还是“Available”以及具体的原因。比如“ReplicaSetUpdated”表示新ReplicaSet正在创建“NewReplicaSetAvailable”表示新ReplicaSet已经可用。第三步检查readinessProbe。这是最容易被忽略的坑。如果你的应用启动需要30秒但readinessProbe的initialDelaySeconds只设了10秒那Pod会在启动过程中被判定为不健康滚动更新就会一直卡着。我一般会把initialDelaySeconds设得比应用实际启动时间多10-20秒留足余量。还有一个隐蔽的坑maxUnavailable设成了0同时maxSurge也设成了0。这两个参数不能同时为0否则滚动更新根本无法进行因为既不能多创建Pod也不能少Pod。K8s会直接报错拒绝这个配置。4. 有状态应用的专属管家StatefulSet4.1 有状态与无状态的本质区别无状态应用好处理因为每个Pod都一样随便删随便建用户请求打到哪个Pod上都行。但有状态应用不行比如数据库、消息队列、分布式存储每个实例都有自己独特的数据和身份。你删掉一个MySQL Pod再建一个新的数据没了主从关系乱了整个集群可能就崩了。StatefulSet就是为这种场景设计的。它跟Deployment最大的区别有三点稳定的网络标识、稳定的持久化存储、有序的部署和扩缩容。这三点缺一不可共同构成了有状态应用在K8s里运行的基础。4.2 稳定的网络标识Headless Service与Pod DNSStatefulSet里的每个Pod都有一个稳定的DNS名称格式是pod-name.service-name.namespace.svc.cluster.local。这个DNS名称在Pod重建后依然不变因为Pod的名字是固定的不是随机生成的。比如一个3副本的StatefulSet叫mysql那Pod名字就是mysql-0、mysql-1、mysql-2删掉mysql-1重建新Pod还是叫mysql-1DNS记录自动更新到新Pod的IP。这个能力依赖于Headless Service。普通的Service会分配一个ClusterIP做负载均衡但Headless Service的clusterIP字段设成None它不分配IP而是直接返回所有Pod的IP列表。StatefulSet必须配合Headless Service使用否则Pod之间无法通过稳定的DNS名称互相发现。这里有个细节Headless Service的selector必须匹配StatefulSet的Pod标签否则DNS解析会返回空。我见过有人创建了Headless Service但selector写错了结果Pod之间互相解析不到集群一直起不来。4.3 持久化存储volumeClaimTemplates的妙用StatefulSet通过volumeClaimTemplates为每个Pod自动创建独立的PVC。你定义一个模板StatefulSet在创建每个Pod的时候会基于这个模板生成一个名字带Pod序号的PVC比如>tolerations: - key: node-role.kubernetes.io/master operator: Exists effect: NoSchedule这个配置的意思是“我能容忍Master节点的NoSchedule污点”。不加这个DaemonSet在Master节点上的Pod会一直Pending。另一个常用的配置是nodeSelector或者nodeAffinity用来限定DaemonSet只在特定节点上运行。比如你只想在带SSD的节点上跑存储插件就可以用nodeSelector指定disktype: ssd。有个容易踩的坑DaemonSet的Pod模板里如果写了resource requests而某个节点的资源不够那个节点上的Pod就会Pending。DaemonSet不会因为一个节点资源不够就跳过它它会一直重试。所以给DaemonSet设置资源请求的时候要特别保守宁可设小一点也不要设得太大导致部分节点跑不起来。6. 一次性与定时任务Job与CronJob6.1 Job批处理任务的正确打开方式Job用来跑一次性任务比如数据迁移、批量计算、备份脚本。它跟Deployment的区别是Deployment保证Pod一直运行Job保证Pod成功退出。Job的Pod跑完就结束了不会重启除非失败。Job有几个关键参数需要理解completions需要成功完成的Pod数量默认1。parallelism同时运行的Pod数量默认1。backoffLimit失败重试次数默认6。activeDeadlineSeconds整个Job的超时时间超过这个时间Job会被标记为失败。举个例子你要处理100个文件可以设completions100parallelism10这样Job会同时跑10个Pod每个处理一个文件总共处理100个。如果某个Pod失败了Job会重试直到成功或者达到backoffLimit。这个模式特别适合工作队列场景每个Pod从队列里取一个任务处理处理完退出。6.2 CronJob定时任务的坑与避坑指南CronJob就是Job的定时版本用Cron表达式控制执行时间。格式跟Linux的crontab一样分 时 日 月 周。比如0 2 * * *表示每天凌晨2点执行。CronJob有几个特有的坑我挨个说。第一个坑是时区。K8s的CronJob默认使用UTC时间不是本地时间。你写0 2 * * *实际执行时间是UTC 2点如果你在东八区那就是北京时间上午10点。解决办法是在CronJob的spec里加timeZone: Asia/Shanghai需要K8s 1.27或者自己换算成UTC时间。第二个坑是并发策略。CronJob的concurrencyPolicy有三个值Allow允许并发、Forbid禁止并发、Replace替换。默认是Allow意思是如果上一次任务还没跑完下一次触发时间到了会再启动一个Job。如果你的任务跑得比较久可能会同时跑好几个把资源耗光。我一般会设成Forbid保证同一时间只有一个Job在跑。第三个坑是历史记录清理。CronJob会保留成功和失败的Job记录由successfulJobsHistoryLimit和failedJobsHistoryLimit控制默认分别是3和1。如果不清理时间长了会积累大量已完成的Job对象把etcd撑大。生产环境我一般设成successfulJobsHistoryLimit3failedJobsHistoryLimit1够排查问题就行。6.3 Job失败后的排查思路Job失败最常见的原因是容器退出码非0。排查第一步是kubectl logs pod-name看容器日志大部分问题日志里都有线索。如果日志没输出可能是容器启动就挂了用kubectl describe pod看Events通常是镜像问题或者命令写错了。第二个常见原因是backoffLimit耗尽。Job重试了6次都失败就会被标记为Failed。这时候你得先修复问题然后删掉旧的Job重新创建。注意Job失败后不会自动重试你得手动处理。第三个原因是activeDeadlineSeconds超时。任务跑太久超过了设定时间Job会被强制终止。这个参数要设得合理太短了任务跑不完太长了失败任务会占用资源太久。我一般会根据历史执行时间设一个1.5倍左右的余量。7. 控制器选型对照与生产环境经验7.1 一张表看清各控制器的适用边界控制器适用场景Pod身份存储启动顺序典型用例Deployment无状态应用随机无状态无序Web服务、API网关ReplicaSet副本保障被Deployment管理随机无状态无序一般不直接使用StatefulSet有状态应用固定序号独立PVC有序数据库、消息队列DaemonSet节点级守护进程每节点一个通常无状态无序日志收集、监控AgentJob一次性批处理随机通常无状态无序数据迁移、备份CronJob定时任务随机通常无状态无序定时报表、清理任务这张表我建议刚入门的时候贴在显示器旁边选型的时候对照着看能避免很多“用错控制器”的问题。比如有人用Deployment跑MySQL结果Pod重启后数据丢了这就是典型的选型错误。7.2 生产环境中的参数调优经验副本数设置Deployment的replicas不是越多越好。副本多了滚动更新慢资源占用高副本少了可用性没保障。我一般根据QPS和单Pod处理能力来算留30%左右的余量。比如单Pod能扛100 QPS业务峰值300 QPS那就设4个副本。资源请求与限制requests影响调度limits影响运行时。requests设得太小节点会超卖Pod之间抢资源设得太大资源利用率低。我的经验是requests按实际用量的70%设limits按实际用量的150%设。这样既保证调度合理又留了突发余量。健康检查livenessProbe和readinessProbe必须配但参数要调好。livenessProbe的failureThreshold不要设太小否则应用偶尔卡一下就被重启了。我一般设failureThreshold3periodSeconds10也就是连续30秒不健康才重启。readinessProbe可以设得敏感一点failureThreshold2periodSeconds5快速摘除不健康的Pod。更新策略maxSurge和maxUnavailable要根据业务容忍度来。对可用性要求极高的业务maxUnavailable0maxSurge1保证更新过程中可用Pod数不减少。对资源紧张的环境maxSurge0maxUnavailable1避免更新时资源翻倍。7.3 那些年我踩过的控制器相关的坑坑一标签选择器写错导致Pod无限创建。前面提过ReplicaSet的selector和template.labels必须匹配。我见过有人selector写app: nginxtemplate.labels写app: nginx-v2结果ReplicaSet疯狂创建Pod几分钟就把集群资源耗光了。排查的时候看Events会有大量“Created pod”的记录但Pod列表里又看不到因为创建出来的Pod不匹配selectorReplicaSet认为它们不存在。坑二StatefulSet的PVC没清理导致存储泄漏。删了StatefulSet但没删PVCPVC一直占着PV新的StatefulSet创建时如果用了同样的volumeClaimTemplates会报“PVC already exists”错误。解决办法是删StatefulSet之前先确认PVC是否需要保留不需要的话手动删掉。坑三CronJob时区问题导致任务在错误时间执行。这个坑特别隐蔽因为CronJob的YAML里写的是0 2 * * *你以为凌晨2点执行实际是UTC 2点。我建议所有CronJob都显式设置timeZone字段避免歧义。坑四DaemonSet在Master节点上Pending。忘了加tolerationsDaemonSet的Pod在Master节点上一直Pending。这个坑排查起来很简单kubectl describe pod看Events会提示“node(s) had taint”加上对应的tolerations就好了。坑五Job的backoffLimit设得太大导致资源浪费。有个任务因为配置错误一直失败backoffLimit设了100结果Job重试了100次每次创建Pod、拉镜像、启动容器、失败退出浪费了大量时间和资源。后来我把backoffLimit设成3失败3次就停人工介入排查。7.4 控制器层面的可观测性建设光会配Controller还不够生产环境得能观测到Controller的行为。我一般从三个层面做事件层面kubectl get events能看到Controller的所有操作记录比如“Scaled up replica set nginx-abc to 3”、“Created pod: nginx-abc-xyz”。这些事件默认保留1小时排查短期问题够用。长期保留需要额外部署事件收集组件。指标层面kube-controller-manager暴露了大量Prometheus指标比如workqueue_depth工作队列深度、workqueue_adds_total入队总数、reconcile_errors_total调和错误数。这些指标能反映Controller的健康状况。如果workqueue_depth持续增长说明Controller处理不过来可能需要扩容或者排查为什么调和这么慢。日志层面kube-controller-manager的日志级别可以调-v2能看到每次调和的详细信息-v4能看到API请求的细节。生产环境一般用-v2排查问题的时候临时调到-v4。日志量比较大建议配合日志收集系统使用。我个人的经验是Controller层面的问题80%都能通过kubectl describe和kubectl get events定位。剩下20%需要看kube-controller-manager的日志。所以遇到Controller行为异常先别急着翻源码把describe和events看仔细大部分问题都有线索。8. 从Controller看K8s的设计哲学用了这么久K8s我越来越觉得Controller机制是它最精妙的设计之一。它把“运维逻辑”从“基础设施”里抽离出来变成了可编程、可扩展的API对象。你想要什么行为就定义什么样的ControllerK8s负责帮你跑起来。这种可扩展性是K8s能成为事实标准的关键。另一个让我佩服的点是最终一致性的设计。Controller不追求瞬间达成目标它接受中间状态允许失败重试只要最终能收敛到期望状态就行。这种“慢就是快”的哲学让K8s在面对各种异常时表现得特别稳健。节点挂了、网络抖了、组件重启了Controller都会自动恢复不需要人工干预。当然这套机制也不是银弹。Controller的调和循环有延迟不是实时的多个Controller之间的协作链路长排查问题复杂声明式API的学习曲线陡新手容易懵。但瑕不掩瑜理解了Controller的工作原理你就掌握了K8s运维的核心能力。后面再遇到Pod异常、更新卡住、扩缩容不生效这些问题你就能从Controller的角度去思考而不是盲目地重启、删除、重建。最后分享一个我个人的习惯每次创建一个新的Controller资源之前先问自己三个问题——这个应用是有状态的还是无状态的需要稳定网络标识吗需要独立存储吗三个问题的答案基本就能确定该用哪个Controller。这个思考过程花不了几秒钟但能避免很多选型错误。
阅读完成 · 觉得有帮助?
咨询建站