十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Kubernetes核心:Pod与五大控制器实战解析

Kubernetes核心:Pod与五大控制器实战解析 1. 先搞清楚Pod为什么非要套一层“壳”不能直接跑容器网上教程讲到KubernetesK8s时几乎都会给你配一张图最小的调度单位是Pod不是容器。我第一次学的时候心里是犯嘀咕的——既然Docker容器已经能把应用打包跑起来了为什么K8s还要在外层再包一层Pod多包一层不就多一层复杂度吗这个疑问一直到我第一次在真实环境里同时跑“主业务容器 日志采集容器”才彻底解开。容器本身是隔离的每个容器有自己的网络命名空间、文件系统、进程视图这种隔离对单容器应用来说是好事但对“一个应用 辅助组件”的协作模型来说就是灾难。你总不能要求业务容器把日志写到共享卷里再由另一个容器定时去读然后两者的生命周期还得分开管理。Pod解决的问题就是把这些强绑定的容器“捆”在一起共享网络栈、共享存储卷、共享生命周期像一个合租屋里的室友门牌号是同一个IP冰箱是共用的Volume开party一起开散伙一起走。1.1 共享网络和存储意味着什么当你走进一个Pod会发现里面所有容器共享同一个IP和同一个端口空间。这意味着两个容器完全可以只用localhost互相访问不用关心什么Service、DNS。最常见的场景就是日志采集器比如filebeat、fluent-bit作为sidecar容器跟在业务容器旁边直接读本地文件或者本地socket把日志转发到ES或Kafka。业务容器不需要知道日志怎么采集、怎么转发它只要把日志写进共享目录就行。共享Volume也是同样的道理。Pod里的每一个容器都可以挂载同一个PVC生产环境里最常见的组合是“主容器写日志到emptyDirsidecar容器把日志压缩上传到对象存储”。一旦Pod被重新调度两个容器会一起被杀死、一起被重建谁也不会留下孤儿进程。另外需要注意Pod里的容器是同时启动的吗答案不是。K8s会先启动initContainers初始化容器等它们全部成功退出后再启动业务容器。这个机制常被用来做“前置依赖检查”或“预置数据迁移”。如果你有一个应用需要等数据库表结构就绪才能启动把它放在initContainer里跑迁移脚本比在业务容器里sleep硬等要优雅得多。1.2 从Pod生命周期理解“临时性”Pod从创建到销毁会经历几个阶段Pending等待调度、Running已运行、Succeeded正常退出、Failed异常退出、Unknown状态不可知。很多人一开始记不住这些状态我建议你用“它是不是一次性的”这个维度去理解如果是常驻服务Pod退出后必须有人把它重新拉起来如果是任务型Pod正常跑完就Succeeded不需要再拉起来如果是定时任务每次跑完都会新建一个Pod旧的Pod保留一段时间方便看日志。Pod本身是临时且可替换的。节点宕机、资源不足被驱逐、手动删除都会导致Pod消失。如果你只学会了kubectl run手动创建Pod那你就得手动处理无数种“Pod突然没了”的情况。这显然不是规模化运维的方式。所以说控制器的出现不是锦上添花而是K8s能够自我修复的根基。2. 控制器的底层逻辑从PID反馈回路到“期望状态调和”“控制器”是个特别容易让人误会的词。搞过工控的人一听到“控制器”脑子里出现的可能是PID控制器、电机控制器、主令控制器这些实体设备搞WiFi网络的人会觉得是AC控制器而做物流SAP的人看到POD想到的是交货单上的送达确认标识。它们虽然“同名”但干的事完全不一样。K8s的控制器本质上可以类比成传统控制论里的闭环控制系统你先给定一个期望状态传感器不断测量当前状态控制器比对两者差异然后输出调节指令让系统向期望状态收敛。K8s里这个“调节”动作被叫做调谐循环reconcile loop它不是一次性执行而是“不断比对、不断纠正”的过程。2.1 一个Deployment的调谐过程拿一个最简单的Deployment举例你要跑3个Nginx副本。你告诉API Server“我要3个”这是一种期望状态。kube-controller-manager里的deployment controller看到期望是3当前实际只有0它就去创建ReplicaSetReplicaSet controller看到期望是3当前只有0就去创建3个Pod副本调度器把这些Pod调度到满足条件的节点上kubelet收到Pod绑定信息后拉起容器。整个过程像一条流水线每一环都在做“查状态、算差异、改状态”。等3个Pod都Run起来后期望值和当前值一致控制器就进入“待命”状态。但只要任何一个Pod挂了控制循环马上会再次跑起来当前值变成2期望值还是3于是重新创建一个Pod直到实际数量回到3。这就是K8s“自愈”能力的核心原理。这里有一个常见的理解盲区控制器只负责让“副本数量”符合预期它不负责保证“业务健康”。Pod即使处于Running状态也可能因为业务问题正在返回500错误。要让它更聪明你需要配合后面会提到的存活探针livenessProbe和就绪探针readinessProbe。2.2 控制器之间是有层级的K8s里的控制器不是一个大类而是一个严格的层级结构。以Deployment为例Deployment管理ReplicaSetReplicaSet管理Pod。为什么中间要多一个ReplicaSet因为滚动更新时需要同时存在新旧两批Pod如果Deployment直接管理Pod每次更新都得先销毁全部再重建全部做不到平滑过渡。ReplicaSet给“版本”加了一个维度每次你想更新Deployment会创建一个新的ReplicaSet把Pod副本数从旧的慢慢迁移到新的旧的RS保留一定数量以便回滚。这个层级还体现在kubectl get命令上。你执行kubectl get deploy看到的是Deploymentkubectl get rs看到的才是更细粒度的副本集kubectl get pod看到的是最终被托管的Pod。Debug的时候经典三步走就是describe deployment → describe rs → describe pod。层级错了你会漏掉大量线索。3. 五大控制器逐个拆解什么场景选谁K8s提供的控制器不少但这个主题下最核心的就五类Deployment、StatefulSet、DaemonSet、Job、CronJob。我在实战中见过的99%的使用场景都落在它们身上。3.1 Deployment无状态应用的默认选择先说Deployment。我个人的经验是但凡你的应用是无状态的谁都能替代谁、不需要固定身份、不需要固定存储一律用Deployment。它支持的滚动更新、一键回滚、快速扩缩容正好满足无状态服务最核心的运维诉求。一个最精简的Deployment yaml长这样apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.27-alpine ports: - containerPort: 80replicas是期望副本数selector.matchLabels是控制器用来“认领”Pod的标签必须与template里的标签一致这也是最容易写错的地方。如果要把“滚动更新参数”也纳入考虑最简单的方式是用kubectl set image或kubectl edit修改镜像版本。但你更应该了解两个关键参数maxSurge滚动更新期间最多允许多出的Pod数和maxUnavailable滚动更新期间最多允许多少Pod不可用。默认值是25%比如副本数是4更新时最多可以多1个Pod也最多允许1个Pod不可用。生产环境如果想更保守可以把maxUnavailable设为0确保每批更新前先拉起新Pod再杀掉旧Podspec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0我曾经吃过一个亏默认参数下滚动更新一次性把两个容器实例同时销毁下游服务瞬间出现连接超时。后来改成maxUnavailable: 0问题立刻消失。这类“看不见的参数”在教程里常被一笔带过但线上稳定性往往就卡在这种细节上。滚动更新期间新旧Pod会短暂共存。kubectl rollout status deployment/nginx-deploy可以看到更新是否完成kubectl rollout history deployment/nginx-deploy可以查看历史版本如果新版本有问题一条kubectl rollout undo deployment/nginx-deploy就能回到上一个稳定版本。回滚操作本身也是滚动式的不会一刀切。3.2 StatefulSet数据库、中间件的“身份识别系统”Deployment解决不了一类问题如果Pod挂了新Pod的IP变了、主机名变了、PVC也变了对有状态应用来说就是“换了一台新机器”数据可能全丢了。StatefulSet就是为了解决“状态与身份一致性”出现的。StatefulSet里每个Pod都有一个稳定的标识序号从0开始比如mysql-0、mysql-1、mysql-2。这个名称是固定的重新调度后不会变对应的PVC也不会变因为Pod和PVC通过volumeClaimTemplates绑定。网络标识方面它需要配合headless ServiceClusterIP为None的Service使用每个Pod会得到一个稳定的DNS记录形如mysql-0.mysql-service.namespace.svc.cluster.local。一个典型的StatefulSet配置apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql-headless replicas: 3 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] resources: requests: storage: 20GiStatefulSet的“有序性”常被误解为“有状态应用就自动高可用”。其实不然它保证的是启动和终止顺序0号先启动下一个才启动以及每个Pod对应独立存储。真正的主从同步、数据分片逻辑依然需要应用层来实现。比如部署MySQL集群你仍然要自己配置主从复制关系K8s只负责让“mysql-0、mysql-1、mysql-2”三个Pod稳定地存在且各自绑定独立磁盘。在扩容/缩容上要格外谨慎对StatefulSet缩容默认是从序号最大的开始删除这个顺序是安全的但缩容不会自动清理PVC这是设计如此防止误删数据。我在一次测试环境里删掉StatefulSet后PVC依然挂在集群里记录一下手动删Pod不是问题删掉StatefulSet后需要自己评估数据盘去留。3.3 DaemonSet每个节点跑一个谁来了都不少DaemonSet的语义和Deployment完全是两个方向Deployment是“数量驱动”DaemonSet是“节点驱动”。你不需要指定副本数只需要在想覆盖的节点范围内让它跑它就会保证每个符合条件的节点上都且仅有一个Pod。节点加入集群它自动补跑节点被移除Pod随之清理。实际中使用DaemonSet最多的场景也就那几类日志采集filebeat、fluentd、vector监控采集node-exporter、prometheus-node-exporter网络插件Calico的calico-node、Flannel的flanneld存储插件某些CSI Node组件。长成一个yaml是这样的apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter spec: selector: matchLabels: app: node-exporter template: metadata: labels: app: node-exporter spec: hostNetwork: true tolerations: - operator: Exists containers: - name: node-exporter image: prom/node-exporter:v1.8.1 ports: - containerPort: 9100 hostPort: 9100注意那个tolerations如果漏掉“容忍所有污点”你会发现DaemonSet在Master节点或者被打了污点的节点上怎么都不起来。我第一次部署node-exporter明明Pod被创建了却一直Pendingdescribe之后才发现是污点没容忍。要加tolerations这个字段比Deployment多了一个“要能主动去脏节点上跑”的能力。3.4 Job和CronJob一次性任务与定时任务还有一类任务不需要常驻运行审批定时跑脚本、数据清洗、批量发邮件。Job和CronJob就是干这个的。Job的核心参数是completions期望完成的Pod数量和parallelism同时并发几个Pod。一个Pod退出了Job会创建一个新Pod继续干直到成功完成的Pod数量达到completions。如果一直失败backoffLimit控制了最大重试次数超过就不再重试任务标记为失败。apiVersion: batch/v1 kind: Job metadata: name: batch-job spec: completions: 3 parallelism: 2 backoffLimit: 4 template: spec: restartPolicy: Never containers: - name: worker image: busybox command: [sh, -c, echo running job sleep 5]注意restartPolicy必须设为Never或OnFailureJob里的Pod不能设Always否则Pod失败了还会不断重启永远不会进入终态Job也永远完不成。CronJob就是“带cron表达式的Job”在spec.schedule字段写*/5 * * * *这类表达式。最容易被忽略的一个字段是concurrencyPolicy默认是Allow允许并发执行如果上一轮任务还没跑完下一轮又触发了可能会同时跑两个相同任务。做定时数据迁移时我一般会把它设成Forbid避免并发重复处理数据。想要省资源的话还可以配startingDeadlineSeconds避免Cron控制器挂了一段时间后积压的调度任务一次性全部触发。4. 控制器如何“认领”Pod标签选择器与OwnerReference的配合理解了各种控制器是什么下一步搞懂它们怎么管理Pod否则你会在排查问题时严重迷失方向。控制器的核心查找机制是标签。Deployment通过标签选择器找到自己管的ReplicaSetReplicaSet也通过标签选择器找到自己管的Pod。你在template里打什么标签selector就必须匹配上反过来你给Pod多加一个标签只要匹配关系不变它依然会被控制器托管。4.1 手动改Pod标签会发生什么这是很多新手喜欢玩但很容易翻车的操作。如果你把一个被Deployment托管的Pod标签改掉控制器会立刻发现“我的匹配范围内少了一个Pod”然后新建一个Pod把数量补回来。而被你改掉标签的那个Pod会变成“没有任何控制器认领的孤儿Pod”。它不会被删除但也不再受保护。这等于亲手把一个受管理的Pod从控制器的保护伞下面踢了出去。反过来如果你手动创建一个带同样标签的Pod控制器可能直接“收养”它——把它纳入副本计数。所以永远不要手动创建和控制器标签匹配的Pod这是基本纪律。4.2 OwnerReference为什么删了DeploymentPod也被带走了光是标签匹配还不够。每个被控制器创建的Pod都会带上一个ownerReferences字段指向创建它的控制器对象。这个字段是级联删除的关键你删Deployment的时候Deployment的ownerReferences指向ReplicaSet反过来的说法是ReplicaSet的ownerReferences指向DeploymentReplicaSet又拥有Pod于是整个链条向下级联删除。用kubectl get pod -o yaml随便看一个Pod你会看到类似这样的字段ownerReferences: - apiVersion: apps/v1 kind: ReplicaSet name: nginx-deploy-7b5dfbc9b7 uid: xxxxxx如果你不想要级联删除可以在删Deployment时加--cascadeorphan那Pod会全部留下来但失去控制器管理变成“没爹没娘”的孤儿。从此以后没有任何人保证它的存活节点重启它就起不来了。临时排查问题时可以这么干但别当作常规操作。4.3 滚动更新时的“双RS并存”状态滚动更新期间新旧两个ReplicaSet会同时存在而且各自拥有不同的Pod模板哈希。这就是为什么你有时在kubectl get pod里看到新旧两批Pod同时Running。用kubectl get rs --show-labels能直接看到哈希标签差异这也是排查滚动更新卡住时最有用的一行命令。如果新版本的Pod一直无法Ready滚动更新就会处在一个“旧的不肯退、新的又起不来”的尴尬阶段。这时候不要慌按下面这个顺序排查基本能定位kubectl rollout status deployment/xxx看当前滚动状态是否超时kubectl describe deployment/xxx看Event里面一般有镜像拉取失败、探针失败、调度失败等关键信息kubectl get rs确认新旧RS当前副本数kubectl describe pod 新pod名称看Pod级别的告警和探针情况kubectl logs 新pod名称看应用日志。一旦确认是新版本有问题kubectl rollout undo deployment/xxx火速回滚然后再慢慢排查新版本。5. 生产环境里控制器使用最容易踩的坑理论知识到位了但实战中还有几个坑值得单独拿出来讲。这些坑我在自己集群里真实遇到过踩一次就心疼一次。5.1 探针配置不当引发的滚动更新失败Deployment的滚动更新能不能顺利完成和探针强相关。readinessProbe就绪探针告诉控制器“这个Pod能不能接流量”livenessProbe存活探针告诉控制器“这个容器要不要重启”。如果这两个探针设置得太激进比如超时时间设成1秒恰好碰上应用初始化慢K8s就会反复杀掉又重建Pod滚动更新就一直无法完成。我见过最极端的情况是应用启动需要30秒但启动探针的超时时间只有5秒结果Pod永远在CrashLoopBackOff。解决办法是合理配置initialDelaySeconds让容器起来先歇一会儿再做探针和periodSeconds并且尽量把livenessProbe的阈值设得比readinessProbe宽松让“活是活着但先别接流量”成为常态。5.2 StatefulSet的优雅停机与数据风险StatefulSet虽然有稳定存储但不代表它不会丢数据。默认情况下节点出现问题时Pod可能需要很长时间才能被重新调度这就涉及PodDisruptionBudget和节点状态判定。更要命的是如果节点被强制kubectl delete nodePod还没完成优雅退出就被终止应用层来不及刷盘可能出现数据不一致。针对这个问题我的经验是两个动作一是给应用配置preStop钩子在Pod真正收到SIGTERM前多留几秒做数据刷新和反注册二是把terminationGracePeriodSeconds从默认的30秒调到更长比如60秒给数据库足够时间完成checkpoint。spec: containers: - name: mysql image: mysql:8.0 lifecycle: preStop: exec: command: [sh, -c, mysqladmin -uroot -p$MYSQL_ROOT_PASSWORD shutdown]5.3 手动扩缩容和HPA同时操作的“打架”问题自己用kubectl scale deployment xxx --replicas10和HorizontalPodAutoscalerHPA自动调副本数是两套逻辑在改同一个字段。如果你一边手动scale一边HPA也在跑两个“大脑”抢方向盘副本数会被来回拉锯最后谁赢取决于最后一次写入的时机。生产环境里我建议明确分工被HPA管理的Deployment不要手动改replicasHPA的minReplicas和maxReplicas应该覆盖业务的日常峰值和低谷。临时应对大流量优先考虑先改HPA的maxReplicas再观察Pod数量变化而不是直接kubectl scale。5.4 PodDisruptionBudget节点维护时保底的最后一道防线节点要做内核升级、硬件维修通常会用kubectl drain把节点上的Pod赶走。如果没有配PodDisruptionBudgetPDB控制器会非常“豪爽”地把所有副本从该节点全部重建到其他节点瞬间多副本同时短暂不可用。更麻烦的是如果集群资源不足重建的Pod可能一直Pending影响面直接扩大。PDB的作用是给这个驱逐过程加一道闸无论什么原因导致Pod主动驱逐PID都要求“不可用副本数不能超过N”。我一般这样配apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: nginx-pdb spec: minAvailable: 2 selector: matchLabels: app: nginx这个配置表示任意时刻至少要有2个appnginx的Pod可用。drain节点时控制器会一个个排不会一次全放。没有PDB的集群维护本质上就是在裸奔。5.5 把控制器当“永久服务管理器”用的边界最后补充一个容易忽略的边界控制器不是万能的。Deployment能拉起Pod但Pod起来了不等于业务OKStatefulSet能保证IP和名字稳定但不负责数据分片和主从选举Job能保证任务跑完但不负责把结果通知到人。K8s控制器管理的是“编排状态”真正的业务逻辑还是要靠应用自身实现。比如做数据库集群高可用你依然需要operator或者应用层的选主机制做消息队列扩容你依然要在应用层重新做分区再平衡。K8s只是把基础设施层的“扩缩容、自愈、发布”标准化了应用层的“状态协同”还得自己设计。按照我的学习路线等你把Pod和这些控制器的关系彻底理顺再回头去看Service、Ingress、Helm、Operator会感觉一通百通。后续如果你们想看我可以继续往下写一写Service的流量转发原理和排错经验那又是一个能写一整篇的大话题。
返回列表