1. Pod控制器:先搞懂它在K8s里到底扮演什么角色
如果你用过Kubernetes,一定听过Pod控制器这个词,特别是刚接触Deployment、StatefulSet、DaemonSet的时候,很容易被这一堆名字绕晕。我最早接触K8s时也踩过这个坑——明明照着教程敲了kubectl create deployment,却说不清Deployment和ReplicaSet到底谁管谁,更别提Job、CronJob这类平时用得少的控制器了。
先把结论摆在前头:Pod控制器的核心职责,是“维持Pod副本数符合预期状态”。换句话说,你告诉它“我要3个副本”,它就保证集群里永远有3个Pod在跑,Pod挂了它拉起新的,节点宕了它去别处调度,人为删掉它也会立刻补上。这个“自我修复”的能力,就是控制器存在的最大价值。
从使用者的角度看,控制器最直观的价值有三层:
- 你不用手动
kubectl run一个个创建Pod,声明式地写好YAML,剩下的交给控制器; - 应用发布、滚动更新、回滚,这些高频操作全部由控制器接管,不用自己写脚本去协调多副本;
- Pod调度、健康检查失败后的重启、扩缩容,这些运维细节也被控制器封装好了。
这篇文章我打算用实操视角把它讲透,包括每种控制器的适用场景、工作原理、选型思路,还会附带一些我把应用搞挂之后总结出来的排错经验。不管你是刚学K8s的新手,还是已经在生产环境摸爬滚打了一阵子的运维,读完之后再去写控制器相关的YAML,心里会踏实很多。
提示:文中所用示例均基于Kubernetes 1.28+版本,较老版本(1.15以下)的Controller Manager参数和Pod安全策略有所差异,如果用的是老集群,部分细节需自行验证。
2. 控制器全家桶:Deployment、StatefulSet、DaemonSet等能干什么
每种控制器都不是凭空设计的,它背后对应着一类具体的应用场景。理解这些场景,比死记硬背API定义更重要。
2.1 Deployment:频繁发布、无状态应用的默认选择
Deployment是Kubernetes里用得最广泛的控制器,没有之一。它适合跑无状态服务,比如Web后端、API网关、定时任务的前端调度器。无状态的意思是应用本身不保存业务数据,每个副本都一样,随便杀一个都不会丢数据。
Deployment的实际工作方式,是对ReplicaSet的再封装。Deployment负责控制版本,ReplicaSet负责控制副本数。每次修改Pod模板(比如换了镜像版本),Deployment会创建一个新的ReplicaSet,然后逐步缩掉旧的ReplicaSet,这就是滚动更新的底层逻辑。
我在某公司的模拟项目X里,就用Deployment管理了一个后端的用户服务,3个副本跑在3台节点上。某次我把镜像tag从v1.0改成v1.1,执行kubectl rollout status deployment/user-service,能看到它先起一个新Pod,等它Ready了再杀掉一个旧Pod,整个过程服务没有中断。这种丝滑的体验,全靠Deployment的滚动更新策略。
Deployment的滚动更新策略中最关键的参数是maxSurge和maxUnavailable。前者表示更新时可以多出多少个Pod,后者表示最多允许多少个Pod同时不可用。举个例子,3个副本的Deployment,如果maxSurge=1、maxUnavailable=0,更新时会先新增1个Pod,等它Ready后再杀掉1个旧Pod,如此往复直到全部替换完。这样可以保证整个更新过程中始终有3个Pod可用。
注意:maxSurge和maxUnavailable在滚动更新时是配合生效的,不要只调一个。我在项目里见过有人把maxUnavailable设成2,更新3副本的服务时,瞬间只剩1个Pod在跑,流量高峰时直接把服务打到超时。
2.2 StatefulSet:有状态应用的秩序守护者
StatefulSet是给“有状态应用”准备的。所谓有状态,就是每个Pod实例都有独立的身份、独立的存储,实例之间不能随意替换。典型场景包括数据库主从节点、消息队列的broker、分布式协调服务(比如ZooKeeper、etcd)。
StatefulSet和Deployment最本质的区别在于Pod的标识和启动顺序。StatefulSet创建的Pod名称是有序的、固定的,比如mysql-0、mysql-1、mysql-2,而且序号从0开始递增。删除或重建Pod后,新Pod会复用原来的名称,并且绑定同一个PersistentVolumeClaim,数据不会丢失。
它还严格保证创建和销毁的顺序:扩容时按序号从0到N逐个创建,缩容时从N到0逐个删除。这个特性对数据库集群很重要——先启动的节点可能是主节点或种子节点,后面的节点才能以正确的身份加入集群。
用StatefulSet管理数据库时,我会主动确认每个副本是否通过pod-0.xxx-svc.headless这样的域名访问对端。因为StatefulSet通常会配一个Headless Service,为每个Pod提供稳定的网络标识。pod-0、pod-1这些Pod名可以直接作为集群通信的主机名,这在etcd这类需要成员发现的系统里是硬需求。
2.3 DaemonSet:每个节点都要有一份
DaemonSet保证集群中每个符合条件的节点上都运行一个Pod副本,节点加入集群时自动创建,节点移除时自动删除。它适合用在你需要在所有节点上部署同一个组件的时候,比如日志采集器(Filebeat、Fluentd)、监控Agent(Prometheus Node Exporter)、网络插件(Calico、Flannel)以及kube-proxy。
DaemonSet不需要关心副本数,副本数由节点数量自动决定。它也不支持滚动更新到一半暂停,但支持maxUnavailable参数控制更新时的破坏程度。生产环境中升级filebeat这类DaemonSet时,我习惯把maxUnavailable设置成1,避免多个节点的日志采集同时中断。
DaemonSet还有一个常见玩法:用nodeSelector或affinity限定它只在特定标签的节点上运行。比如我有3台机器是GPU机器,我想只在上面跑GPU监控脚本,就给这些节点打个gpu=true的标签,然后在DaemonSet的调度约束里限定nodeSelector: gpu: "true",这样其他普通节点就不会被创建多余的Pod。
2.4 Job与CronJob:跑完就退出的任务型控制器
Job和CronJob是面向“任务”的控制器。Job用来运行一次性任务,比如数据迁移、批量处理、压测脚本,任务正常结束后Pod进入Completed状态,不会被重启。CronJob则在Job的基础上加了时间调度,类似Linux的crontab,适合做周期性的备份、报表生成、日志清理这类工作。
Job的重试策略值得多说几句。backoffLimit控制失败重试次数,activeDeadlineSeconds控制整个Job的最长运行时长,restartPolicy必须设为Never或OnFailure,不能用Always——因为Pod运行完任务就是终点,一直重启没有意义。
CronJob需要特别关注startingDeadlineSeconds和concurrencyPolicy。concurrencyPolicy有三个取值:Allow允许并发执行、Forbid禁止前一个任务没跑完就启动下一个、Replace直接终止旧的并启动新的。如果任务是幂等的、耗时不确定,强烈建议设为Forbid。我有一次把数据库备份的CronJob设成Allow,结果某次备份跑慢了,下一个周期又启动了一个新的备份进程,两个进程同时写同一个文件,直接把备份数据写坏了。
2.5 其他控制器和控制器之间的关系
除了上面四种,还有几个控制器也值得认识:ReplicaSet(副本集)是Deployment的内部实现,一般不会直接使用;ReplicationController是ReplicaSet的前身,现在已经基本淘汰;HorizontalPodAutoscaler(HPA)负责自动扩缩容,它本身不创建Pod,而是通过修改Deployment或StatefulSet的副本数来实现弹性伸缩。
理清依赖关系很重要:
- Deployment 管理 ReplicaSet;
- ReplicaSet 管理 Pod;
- HPA 控制 Deployment/StatefulSet 的副本数;
- StatefulSet 直接管理 Pod,没有中间层。
用一句话总结:控制器的复杂度是一层层叠加的,Deployment层管发布,ReplicaSet层管副本数,Pod是最小调度单元。
3. 控制器如何“维持期望状态”?控制器原理与事件循环拆解
聊完了“每种控制器能干什么”,接下来要深入到“它是怎么实现的”。这部分理解透了,排错的时候会特别有用。
3.1 期望状态与当前状态:控制器的核心循环逻辑
Kubernetes整个系统都是围绕“期望状态”设计的。你提交的YAML文件声明了期望状态,比如“我要5个副本”,而集群里实际有多少个Running的Pod是当前状态,两者之间如果有差距,控制器就要努力把差距缩到零。
这个逻辑在实现上是一个无穷循环,业内叫“调谐循环”(Reconcile Loop)。控制器从API Server那里拿到期望状态和当前状态,做一次对比,然后调用API创建、删除或更新资源,让当前状态向期望状态靠拢。完成一轮之后,又继续监听资源变化,随时准备下一轮调谐。
用一个生活化的类比来理解:你设定空调温度26度是期望状态,房间实际温度30度是当前状态,空调压缩机就会持续工作(调谐动作),直到室温降到26度。如果此时有人开门让室温回到28度,压缩机重新启动,再把它拉回26度。控制器干的活,本质上就是这种“温控逻辑”。
3.2 控制器管理器:谁在跑这些调谐循环
控制器的调谐循环并不是分布式运行的,它集中在kube-controller-manager这个进程里。kube-controller-manager内置了数十种控制器,每种控制器负责一种资源。它们通过API Server监听自己关心的对象变化,然后各自执行调谐。
这个集中式设计有它的道理:把控制器集中在一个进程里,部署简单,权限和资源占用也便于统一管理。但它也意味着kube-controller-manager是集群的高危单点(高可用部署下是多个副本),如果它挂了,整个集群的资源协调能力都会降级,Pod挂了也不会被及时拉起。
Controller Manager内部采用了并发调谐机制,每个控制器可以并行处理多个工作队列里的任务。但需要注意,同一个对象的重复更新事件会被合并,如果短时间内连续更新同一个Deployment,控制器不会立刻处理每一次变更,而是等到稳定后再统一调谐。这叫“乐观并发控制”,减少API Server的压力。
3.3 控制器的“头脑”:Informer、WorkQueue与Delta
控制器要高效运行,光靠轮询API Server是不够的。它用了Informer机制,这是一个带本地缓存的监听器:
- Informer通过ListAndWatch与API Server建立长连接,先全量List一次所有资源,再通过Watch持续接收后续的增量事件;
- 事件内容包括Add、Update、Delete,Informer会把事件放到Delta FIFO队列,由Controller的EventHandler处理;
- 本地缓存让控制器在处理时不需要每次都查API Server,减少请求压力;
- 如果Watch连接断了,Informer会自动重新List,保证缓存和集群状态最终一致。
WorkQueue则是控制器的“待办事项列表”。Informer收到事件后并不会直接触发调谐,而是把对象的关键信息(比如命名空间和名称)塞进WorkQueue。控制器从队列里取出一个key,执行Reconcile,处理完再取下一条。这里有个去重逻辑:如果同一个Deployment被连续修改了10次,队列里只保留一个key,但控制器最终还是会基于最新状态完成一次调谐,这种“合并同类项”的做法在高频更新的场景下非常实用。
3.4 控制器的“手”:调谐动作如何真正执行
控制器在完成调谐之后,是通过API Server来实施具体变更的。以Deployment控制器为例,它发现期望副本数是3,而当前只有2个,就会向API Server提交创建1个ReplicaSet的请求。然后ReplicaSet控制器会创建对应的Pod。这两层控制器是协作关系:Deployment Controller只负责管理ReplicaSet,ReplicaSet Controller负责管理Pod。
这种分层设计保证了关注点分离。如果让Deployment直接创建Pod,滚动更新会变得极其复杂——因为它既要知道“哪个Pod属于哪个版本”,又要管理Pod的扩缩。有了ReplicaSet这一层,Deployment只需要创建一组新版本的ReplicaSet并调整副本数,旧版本ReplicaSet直接缩掉,中间的细节都交给下层。
另一个需要注意的点:控制器不会直接跟kubelet通信,它只通过API Server下发期望状态。Pod真正落到节点上运行,是kubelet从API Server观察到“这个节点上应该有一个Pod”,然后调用容器运行时拉镜像、起容器的。链路是“控制器 → API Server → kubelet → 容器运行时”,每一环都是解耦的。
4. 从需求到选型:生产环境中怎么挑控制器
绕了一圈原理,回到最现实的问题:我的应用该用哪个控制器?这个问题没有标准答案,但可以根据几个维度做快速判断。
4.1 按应用特征快速选型
这里有一个表,基本涵盖了大多数场景的判断依据。
| 应用特征 | 推荐控制器 | 原因 |
|---|---|---|
| 无状态、多副本、频繁发版 | Deployment | 滚动更新和回滚最成熟 |
| 每个节点都要有一个Agent | DaemonSet | 节点数决定副本数 |
| 有稳定网络标识和独立存储 | StatefulSet | Pod名称/存储固定 |
| 跑一次性批量任务 | Job | 执行完自动退出 |
| 周期性任务、批处理 | CronJob | 基于Job加时间调度 |
| 需要在集群外调度Pod | 一般不推荐(可考虑自定义控制器) | 标准控制器覆盖不足 |
判断的核心不是看“应用是不是数据库”,而是看“Pod被删除后能否用新Pod完全替代”。如果能替代,Deployment就够了;如果替代不了(比如新Pod的名字必须和旧Pod一样、存储必须复用),就必须上StatefulSet。
4.2 有状态应用的额外考量
有状态应用除了选StatefulSet,还要额外关注几个点:
- Headless Service必须提前建好,否则StatefulSet创建Pod时网络标识不稳定;
- volumeClaimTemplates会为每个Pod自动创建独立的PVC,存储类(StorageClass)要提前确认存在,且支持动态供给;
- serviceName字段必须指定,而且最好和Headless Service同名;
- 缩容操作要谨慎,StatefulSet默认不会自动清理PVC,PV和底层存储数据不会跟着删除。我曾遇到某个开发环境,反复创建删除StatefulSet,导致一堆悬空的PV和云盘,月底账单翻倍。
4.3 多控制器配合使用的场景
实际项目中,很少只用一种控制器。一个典型的业务系统往往是混合用的:入口网关用Deployment跑多副本,日志采集用DaemonSet跑在每个节点,用户数据和订单记录用StatefulSet跑数据库,定期报表生成用CronJob调度,导数据用Job跑一次性迁移。它们彼此独立,又通过Service和DNS互相协作。
这种混合架构的合理性在于,每种控制器解决的问题域不同。如果把数据库也用Deployment管理,滚动更新时Pod名字变了、IP变了,主从节点的身份对不上,集群直接崩。反过来,如果用StatefulSet管理一个纯无状态的Web服务,又平白多了很多存储和网络标识的开销,属实没必要。
5. 从零创建一个Deployment:完整实操过程与参数详解
纸上谈兵讲完了,来看点真的。这一节我会用一个完整的例子,演示从编写YAML到实现滚动更新的全过程,并且把关键参数一个一个说明白。
5.1 编写YAML:字段逐行精讲
用模拟项目X的一个用户服务来举例。先看下面这个Deployment定义:
apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: prod labels: app: user-service env: prod spec: replicas: 3 selector: matchLabels: app: user-service template: metadata: labels: app: user-service spec: containers: - name: user-service image: registry.internal.example.com/user-service:v1.0 ports: - containerPort: 8080 resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1 memory: 1Gi readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10逐个字段拆开说:
apiVersion:Deployment在apps/v1这个API组里,版本不同字段会有差异,老项目里常见的extensions/v1beta1早废弃了,新环境务必用apps/v1;metadata:Deployment本身的元数据,namespace决定了它装在哪个空间,labels是给Deployment打标签用的;spec.replicas:期望副本数,这是最核心的字段;spec.selector:Pod选择器。Deployment通过这个字段匹配它管理的Pod。这里有个坑:selector一旦创建就不能修改,改了会导致Deployment找不到自己该管的Pod。Pod的labels必须和selector完全匹配;spec.template:Pod模板,Deployment创建Pod时就是照着这个模板去创建的。template里也要有labels,而且必须包含selector里的全部键值对。
spec.template.spec.containers里的内容则是Pod里容器的定义,镜像、端口、资源限制这些都在这里。resources.requests是给调度器看的,声明应用最少需要多少资源;limits是给kubelet看的,超过之后容器会被限流或杀掉。生产环境中我倾向给所有容器都配requests和limits,没有配额限制的Pod一旦发生内存泄漏,直接拖垮节点。
5.2 创建与观察:从apply到rollout status
写好YAML后,执行:
kubectl apply -f user-service-deployment.yaml然后用下面这组命令观察状态:
kubectl get deployment user-service -n prod kubectl get rs -n prod kubectl get pods -n prod -l app=user-service kubectl rollout status deployment/user-service -n prod前两条命令用来确认Deployment和它对应的ReplicaSet已经创建,第三条命令确认3个Pod都进入Running状态,第四条命令回显部署完成情况。如果kubectl get pods的结果里有Pod一直处于Pending状态,就运行kubectl describe pod <pod-name> -n prod查看事件,看是资源不足、镜像拉取失败,还是污点容忍问题。
5.3 滚动更新实操:理解maxSurge与maxUnavailable
假设需要升级到v1.1版本,可以用kubectl set image快速触发:
kubectl set image deployment/user-service user-service=registry.internal.example.com/user-service:v1.1 -n prod然后执行kubectl rollout status deployment/user-service -n prod观察滚动过程。会看到新版本的ReplicaSet里的Pod先创建,等它通过readinessProbe探针后,旧版本ReplicaSet里的Pod才开始逐个终止。
如果在滚动过程中发现问题,可以在更新未完成时一键回滚:
kubectl rollout undo deployment/user-service -n prodrollout undo默认回滚到上一个版本。如果已经经历过多轮更新,还能加上--to-revision参数指定回到某个特定历史版本。
滚动更新策略中maxSurge和maxUnavailable的默认值是25%。用3副本举例:maxSurge=25%意味着更新时最多额外创建1个Pod,maxUnavailable=25%意味着最多允许1个Pod不可用。因此更新过程中,集群里可能短时间出现4个Pod或最少2个Pod。这个默认值在多数场景下是安全的,但低峰期发布可以把maxUnavailable调成0,通过maxSurge多创建Pod来保证服务稳定;高峰期则相反,可以减少资源浪费但多一点Pod同时重启的风险。
5.4 水平扩缩容:手动与HPA结合
手动扩容很简单:
kubectl scale deployment user-service --replicas=5 -n prod线上环境我更推荐HPA:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: user-service-hpa namespace: prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: user-service minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60这个HPA会持续监控user-service的CPU平均利用率,超过60%时逐步扩容,低于60%时缩容,但无论如何副本数不会低于3。HPA默认每15秒采集一次指标,扩容是即时的,但缩容有冷却时间(默认5分钟),防止指标抖动导致副本数频繁变化。
提示:HPA要正常工作,集群里必须部署Metrics Server,否则无法获取Pod的CPU和内存使用率。没有Metrics Server的集群里配置HPA,它会一直处于
Unknown状态。
6. 实战经验:StatefulSet、DaemonSet、Job的常见坑与排查实录
实际操作中,控制器光会创建远远不够。这一节把我踩过的坑和排查思路按控制器类型整理出来,可以当速查表用。
6.1 StatefulSet的存储与网络排查
典型的故障现象是:StatefulSet创建后Pod一直是Pending,kubectl describe pod显示0/1 nodes are available。
排查步骤:
- 先检查PVC是否创建成功:
kubectl get pvc -n <namespace>,如果PVC状态是Lost或Pending,问题出在存储层; - 检查StorageClass是否存在且支持动态供给:
kubectl get storageclass,没有的话要么手动创建PV,要么修复StorageClass; - 检查Headless Service是否创建:
kubectl get svc -n <namespace>,StatefulSet的serviceName如果指向一个不存在的Service,Pod会一直卡在Pending; - 检查节点的CPU和内存是否充足,云环境下还要检查云盘的可用区是否和节点在同一可用区,跨可用区的云盘通常无法挂载。
还有一个隐蔽的坑:StatefulSet删除后PVC不会自动删除,重新创建同名StatefulSet时,如果旧的PVC还在,新Pod会直接绑定旧PVC。数据不会丢,但如果旧PVC里残留了垃圾数据,新应用启动可能直接报错。清理时最好用kubectl delete statefulset <name> --cascade=orphan先解绑,再手动确认PVC和PV的状态,避免误删生产数据。
6.2 DaemonSet的调度与更新坑
DaemonSet最常见的坑是Pod被调度到了不想让它跑的节点上,或者无法调度到需要跑的节点上。我的排查半径一般集中在三个点:
- 节点标签:检查DaemonSet的nodeSelector和节点的labels是否匹配,不要只看节点名,要看标签键值对;
- 污点:如果节点有
NoSchedule污点,DaemonSet的Pod默认情况下也调度不了。很多云厂商在节点初始化时会打上污点,需要给DaemonSet加上对应的toleration,不然日志采集器永远起不来; - 更新方式:升级DaemonSet时如果
maxUnavailable设成0,会永远卡住,因为它的滚动更新不像Deployment那样可以额外创建Pod来腾挪。没有多余Pod的情况下,0就意味着没有可用Pod,永远无法开始更新。
另外,kubectl rollout restart daemonset <name>可以快速重启全部节点上的Pod,加载新配置。这个命令在改完ConfigMap后特别好用,但我建议重启前先做好影响面评估,因为日志采集类服务重启会造成短暂的采集空窗。
6.3 Job与CronJob的失败重试实战
Job的Pod跑完任务会进入Completed状态,这本身是正常现象,但很多人第一次看kubectl get pods里出现Completed会以为出异常了。判断Job是否成功,要看kubectl get job <name>里的COMPLETIONS列,是1/1就是成功。
Job失败后的排查路径:
- 查看Job的状态和事件,确认失败原因:
kubectl describe job <job-name>; - 查看具体Pod的日志:
kubectl logs <pod-name>,Job型Pod在任务失败退出后日志还在; - 如果Job并发度太高导致节点资源吃紧,可以调低
spec.parallelism,或者限制每个Pod的resource请求; - 如果任务本身需要重试,但
backoffLimit耗尽后还失败,需要排查代码逻辑而不是一味调大重试次数。
CronJob有些更隐蔽的问题。schedule字段用的是UTC时间,不是本地时间,如果你在UTC+8时区想每天早上8点跑备份,schedule要写成0 0 * * *(UTC的0点对应东八区8点)。另一个问题是旧Job不会被自动清理,CronJob每次执行都会产生一个Job,时间久了集群里会堆积大量已完成的Job,影响API Server性能。建议开启ttlSecondsAfterFinished字段,让完成后的Job在指定时间后自动清理。
6.4 崩溃循环与CrashLoopBackOff:控制器排错通用大招
不管你用的是哪种控制器,Pod一直处于CrashLoopBackOff都是最常见的故障。这表示Pod启动后立刻崩溃,被kubelet反复重启,但每次重启后仍然崩溃。
快速排查的四个命令:
kubectl logs <pod-name> -n <namespace> --previous kubectl describe pod <pod-name> -n <namespace> kubectl get events -n <namespace> --sort-by=.lastTimestamp kubectl get pod <pod-name> -n <namespace> -o yaml第一条命令看上一次启动时的日志(当前容器崩溃后日志可能被清空),第二条命令看Pod事件(比如镜像拉取失败、探针失败、OOMKilled),第三条命令看集群层面的近期事件,第四条命令看Pod的完整定义和状态。
导致CrashLoopBackOff的原因非常多,但按我的经验,出现频率最高的是下面五类:
- 镜像不存在或tag写错,导致拉取失败;
- 容器的启动命令或入口参数有问题,进程起来后立刻退出;
- 容器依赖的ConfigMap、Secret不存在,启动时读取配置失败;
- readinessProbe或livenessProbe配置过于严格,应用还没准备好就被探针杀掉;
- 资源limits设置过低,容器一启动就被OOM Killer杀掉。
碰到CrashLoopBackOff,不要第一时间删Pod重建,Pod是控制器管理的,删了它还会立刻拉起一个相同配置的新Pod,问题依旧。要做的是通过日志和事件定位根因,在YAML层面修复,然后重新apply。
7. 控制器调优与运维建议:让应用跑得更稳
控制器用顺了之后,进阶的问题是:怎么让控制器在生产环境里跑得更稳、更可控。这里分享几个我做运维时的调优经验。
7.1 安全地滚动更新:设置PodDisruptionBudget
如果你管理的是核心服务,强烈建议为它配置PodDisruptionBudget(简称PDB)。PDB的作用是限制自愿中断(比如节点维护、集群升级)时最多允许多少个Pod同时不可用。
apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: user-service-pdb namespace: prod spec: minAvailable: 2 selector: matchLabels: app: user-service这段配置的意思是:在任何自愿中断情况下,user-service的Pod至少要有2个处于可用状态。如果当前只有2个可用Pod,系统会拒绝执行节点排水,防止服务彻底不可用。
PDB不会阻止节点故障这类非自愿中断,但它能阻止你在维护节点时把所有Pod一锅端。生产环境里,核心Deployment和StatefulSet都建议配上PDB。
7.2 控制器的优雅升级与回滚
Deployment升级时,如果有多个副本,发布策略可以选择RollingUpdate或Recreate。Recreate是先把所有旧Pod杀掉再创建新Pod,会导致服务中断,一般不推荐。RollingUpdate支持分批次发布,配合maxSurge和maxUnavailable可以控制POD爆炸半径。
更高级一点的做法是分阶段发布。先在测试环境验证新版本,然后切到生产环境,用kubectl rollout pause deployment/user-service暂停滚动,验证一部分Pod没问题后,再kubectl rollout resume deployment/user-service继续完成剩余更新。这个操作方式在灰度发布时很实用,能让一批新版本Pod先跑一段时间观察日志和监控指标,确认没问题再放开剩余流量。
注意:暂停滚动更新不能跨太久时间,否则新旧ReplicaSet长时间并存,资源浪费倒是小事,关键是如果有新的配置变更push进去,会自动把暂停状态解除,滚动流程立即继续,可能打你个措手不及。
7.3 控制器资源的保护与限流
大规模集群里,Controller Manager既要监听海量资源,又要执行频繁调谐,API Server的压力很大。几个提升稳定性的建议:
- 控制器的调谐是并发执行的,但同一个控制器内部会有工作队列并发度限制,不要期望无限横向扩展;
- 避免创建过多的小对象。一万个几十字节的ConfigMap比一个大对象对API Server内存的冲击更明显;
- 尽量少用
kubectl edit直接改线上资源,所有变更都走GitOps流程或CI/CD管道,审计和回滚都方便; - 开启事件TTL清理机制,控制历史事件的保存时间,防止集群事件表无限膨胀。
8. 从控制器延伸到自动化:自定义控制器与Operator
用默认控制器解决了90%的问题之后,剩下10%的场景可能需要你写自己的控制器。Kubernetes控制器的设计本身就非常开放:你完全可以照着Controller Manager的模式,为自定义资源(CustomResourceDefinition)写一个调谐循环,实现自己的自动化运维逻辑。
8.1 Operator模式:把运维经验代码化
Operator是自定义控制器的一种典型形态。它把某个应用的运维知识(扩缩容、备份、故障恢复)全部写进代码里,让系统自动执行。比如你可以定义一个Database类型的自定义资源,写好Operator后,创建这个资源就会自动完成数据库集群的搭建、备份和主从切换。
Operator的运行机制和原生控制器一模一样:监听资源变化,比较期望状态和当前状态,执行动作。区别在于它的业务逻辑由你掌控,可以做得非常复杂。比如自定义的Database资源被发现主节点挂了,Operator可以自动完成故障切换并更新Service的指向。
从零写Operator的成本不低,建议先评估是否需要。如果只是想把定时备份自动化,CronJob完全够用;如果要做一套完整的“数据库集群生命周期管理”,Operator才是正确姿势。
8.2 用Kubebuilder或Operator SDK快速上手
写Operator的方式有两种:
- 手写调谐循环,代码控制所有细节,适合简单场景;
- 用Kubebuilder或Operator SDK生成项目骨架,重点写业务逻辑,推荐生产使用。
Kubebuilder生成的骨架自带Controller Manager的入口代码,你只需要实现Reconcile函数。这个函数接收一个Request参数,里面包含命名空间和资源名称,里面写业务逻辑即可:查到当前资源的状态,比对期望状态,有差异就创建或更新子资源,最终把状态写回。
我在模拟项目X里用Kubebuilder写过一个日志采集配置的Controller:每次新接入一个日志源,只要创建一个自定义资源,控制器就自动帮我在对应的DaemonSet里加上采集配置并滚动更新。整个链路跑通之后,新日志源的接入从手动改配置的半小时缩短到分钟级。
8.3 控制器的可观测性:监控、日志与调谐时延
容器化环境里,控制的稳定性离不开可观测性。Kubernetes的Controller Manager默认暴露了/metrics端点,Prometheus可以直接采集。重点监控这几个指标:
- workqueue_adds_total:工作队列累计新增任务数,可以反映资源变更的频率;
- workqueue_depth:工作队列深度,如果一直居高不下说明控制器处理不过来;
- workqueue_queue_duration_seconds:任务在队列里等待的时间,过高说明调谐速度跟不上事件产生速度;
- rest_client_requests_total:控制器访问API Server的频率,异常升高可能是Informer产生了大量重同步。
另外,调谐循环的耗时值得重点关注。如果你发现某个控制器处理一个对象要几十秒,先排查是不是业务逻辑里有耗时的API调用,或者是不是插件加载了太多不需要的Informer。调谐很快,控制器自然很轻盈。
掌握控制器的原理之后,你再看Kubernetes的很多设计就不会觉得神秘了:声明式API是期望状态的入口,控制器是期望状态的执行者,调谐循环是无处不在的修补机制。这套架构设计非常优雅,值得在业务系统设计时借鉴——把需要持续对齐的“期望状态”和当前状态的差异交给一个后台循环去处理,减少人为介入。
文章写到这差不多够用了。最后分享一个我自己的使用经验:不要试图用一个控制器解决所有问题,Deployment不是万能的,StatefulSet也没有多复杂。先把它们各自的设计意图想清楚,再对照业务场景选型,你的集群会安稳得多。