ARTICLE DETAIL

资讯详情

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

Kubernetes Pod核心解读:调度原理、生命周期与故障排查

Kubernetes Pod核心解读:调度原理、生命周期与故障排查 Pod这个词在Kubernetes里几乎天天见。网上教程翻来覆去就一句话Pod是最小的调度单元。但真正上手写YAML、部署应用、排查故障的时候你会发现这六个字背后的东西多得很。我搞Kubernetes这些年从集群初始化看到[init] Using Kubernetes version: v1.26.0开始到大规模支撑业务应用把Pod当成一个完整系统来理解才算是真正入了门。这篇内容适合刚入门的同学快速建立一个正确认知也适合已经写过几个Deployment、但总被Pod各种诡异状况卡住的人查漏补缺。咱们不讲虚的就从一个最实际的问题开始Pod到底是什么它到底解决了什么问题。1. Pod到底是什么从一次创建说起1.1 Container和Pod差在哪里很多人第一反应是Pod不就是套了一层壳的容器吗这个理解方向对但没说透。先记住一句话Pod是Kubernetes里真正被调度、被分配IP、被创建销毁的最小对象而容器只是Pod内部的一个运行单元。我给你打个比方。容器像是一个人有手有脚会干活。但问题是一个人干活的时候必须有一个工作环境要有工位、要有网线接口、要有公用的工具柜。Pod就是这个工位。Kubernetes不直接调度人它调度的是工位。Kubernetes把工位分给某台机器节点然后把人放进工位里干活。工位上的网线接口、工具柜所有人共用。具体到技术层面同一个Pod里的所有容器共享三样东西网络命名空间也就是同一个IP和端口空间、IPC命名空间、以及Pod级别的存储卷。这意味着你从外部访问一个Pod只需要访问这个Pod的IP而不用管里面跑了一个容器还是五个容器。Pod里的容器之间用localhost就能通信端口不能冲突挂载的卷互相可见。1.2 为什么Kubernetes不直接调度Container问这个问题的人通常都踩过容器编排的坑。如果Kubernetes直接调度单个容器你会遇到几个现实问题。第一个问题一组进程必须在一起跑。比如日志采集器Filebeat、Fluentd和业务容器它们天生是强绑定关系。业务容器写日志文件日志采集器读日志文件转发出去。如果两者被调度到不同节点采集器就看不到业务容器的日志了。把这两个容器放进同一个Pod共享存储卷和网络问题直接消失。第二个问题进程的健康状态和生命周期不是一一对应的。有的应用是主进程带辅助进程的模式辅助进程挂了不影响主进程但整个应用的功能可能已经不完整。Kubernetes以Pod为单位做健康检查和重启比单看一个容器更符合真实应用形态。第三个问题资源分配和IP管理的粒度。如果每个容器都有自己的IPKubernetes就要管理海量IPService做负载均衡的时候也得维护成倍的后端。以Pod为单位分配IP网络模型一下子就清爽了。1.3 Pod里的多容器怎么协作既然一个Pod可以放多个容器那实际生产里最常见的组合方式是什么我用得最多的是三种模式。第一种是Sidecar模式。主容器跑业务逻辑旁边挂一个辅助容器做日志采集、流量转发或者监控上报。比如在Pod里同时跑Nginx和FilebeatNginx写访问日志到共享卷Filebeat负责发到日志平台。主容器挂了辅助容器跟着重建因为它们在同一个Pod里。第二种是Adapter模式。主容器输出的日志格式不是平台想要的辅助容器在中间做格式转换再转发。类似一个适配器把A格式翻译成B格式。第三种是Ambassador模式。辅助容器充当主容器访问外部服务的代理。比如主容器连数据库本地的localhost:3306其实是旁边的代理容器在监听由代理去连接真正的数据库实例这样切换数据库地址时不用改主容器的配置。这里有一个很多人忽略的重点Pod里的多个容器是同时启动、同时终止的但它们之间没有依赖顺序管理。如果你想控制启动顺序得用Init容器这个后面细讲。2. 创建Pod的几种方式别一上来就写YAML2.1 kubectl run调试用的快车道最快创建一个Pod的方式是直接敲命令。我刚学的时候特别喜欢这招因为不用写文件、不用记一堆字段。kubectl run nginx-pod --imagenginx:1.25 --port80 --restartNever这个命令创建一个名为nginx-pod的Pod跑nginx镜像容器端口80--restartNever表示不自动重启。注意一个坑新版kubectl里直接create/run一个Pod默认会套上Deployment的壳所以要加--restartNever才会生成裸Pod。命令创建更多是拿来调试的比如我临时想拉一个镜像测试网络或者想看某个配置在容器里对不对直接run一个很快。但命令创建的Pod有个毛病它没有自我修复能力。你手动创建了一个Pod它跑在节点A上节点A宕机了这个Pod不会在其他节点重建。它就这么没了。所以只要是想长期运行的应用都不能用这种方式。2.2 认真写一份Pod的YAML命令适合快进快出正经部署还得靠YAML。我给你看一份我平时最常用的最小可用配置先把它跑通再谈扩展。apiVersion: v1 kind: Pod metadata: name: web-pod labels: app: web env: prod spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 env: - name: TZ value: Asia/Shanghai resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi创建命令是kubectl apply -f web-pod.yaml。这里我特别强调一下metadata里的labels新手最容易偷懒不写。labels是Kubernetes做关联的核心——Service靠selector选Pod、Deployment靠labels匹配Pod、监控告警也靠labels区分环境。一个没有labels的Pod就像一个人没有工号系统根本没法识别它。哪怕只有一个Pod我也建议把app和env这种基础标签写全。再强调一下resources这一段。开发环境里经常有人不写资源限制我的建议是所有环境都要写哪怕你只写requests不写limits。不写requests会导致调度器的调度结果完全不可控Pod可能会被塞到一台机器上把其他服务挤垮。这个后面单独讲。2.3 Pod与ConfigMap配合部署热词里出现了pod configmap deploy这组合太典型了。实际部署中配置和镜像分离是基本要求。有一次我要把同一套服务部署到三个环境镜像完全一样只有数据库地址、日志级别和缓存连接串不同。这种情况不要改镜像用ConfigMap。先创建ConfigMapapiVersion: v1 kind: ConfigMap metadata: name: app-config data: app.properties: | db.hostmysql.internal db.port3306 log.levelinfo nginx.conf: | server { listen 80; location / { proxy_pass http://backend:8080; } }ConfigMap里可以存简单的键值对也可以像这样把一个完整配置文件当成一个key。然后用两种方式把它挂进Pod。第一种是环境变量适合简单键值第二种是挂载成文件适合完整配置文件spec: containers: - name: app image: myapp:1.0.0 envFrom: - configMapRef: name: app-config volumeMounts: - name: config-volume mountPath: /etc/nginx volumes: - name: config-volume configMap: name: app-config注意这里有一个我踩过好几次的坑ConfigMap更新后已经存在的Pod里挂载的文件不会自动更新。Kubernetes采用的是kubelet周期性同步方式默认有几十秒到几分钟的延迟而且更新到文件里也需要时间。如果想快速生效最可靠的方法就是滚动重建Pod。别在生产环境里等它自动同步等不起。还有一点ConfigMap的大小有上限默认是1MiB。超过这个量别硬塞ConfigMap要么拆开要么换其他存储方案。3. Pod的生命周期从Pending到Running再到终止3.1 五个阶段的含义与判断Pod从提交到销毁会经历几个状态kubectl get pod里的STATUS列就是这些状态。我先把它们列出来状态含义常见出现场景Pending已被接受但还没准备好镜像拉取中、等待调度、存储卷等不到Running至少一个容器在运行正常状态Succeeded所有容器正常退出Job跑完Failed至少一个容器非正常退出容器报错退出Unknown节点失联状态拿不到节点宕机或网络分区我最担心的是Pending和Unknown。Unknown几乎都是节点层面出了问题比如节点宕机、kubelet失联这时候Pod在另外一个节点上可能已经重建也可能还在等。Pending则要看具体卡在哪是调度不上去还是镜像拉不下来执行kubectl describe pod就能看到事件这个放在故障排查那节展开讲。3.2 Init容器正式启动前的准备工作前面说过Pod里多个普通容器之间没有启动顺序控制。那如果B容器必须在A容器起来之后才能跑怎么办答案是Init容器。Init容器是串行执行的Pod启动时先跑第一个Init容器成功退出后再跑第二个全部成功后才开始启动普通容器。任何一个Init容器失败Pod就会不断重启这个Init容器直到成功或者超过restartPolicy的限制。最常用的场景是等待依赖就绪。比如你的应用启动时需要连数据库而数据库可能还没ready那就写一个Init容器去探测数据库端口spec: initContainers: - name: wait-for-db image: busybox:1.36 command: - sh - -c - | until nc -z mysql.internal 3306; do echo waiting for mysql... sleep 2 done containers: - name: app image: myapp:1.0.0这个方案简单粗暴但也非常好用。注意Init容器要尽量小比如busybox这种几MB的镜像因为Init容器跑完就退出拉镜像也要时间镜像越大启动越慢。3.3 探针怎么判断Pod真的活着很多人问容器还在跑Kubernetes怎么知道应用到底有没有问题答案是探针。有三种探针livenessProbe存活探针、readinessProbe就绪探针、startupProbe启动探针。我做了个表方便你对照探针类型失败后的行为典型用途livenessProbe重启容器检测死锁、内存泄漏进程活着但实际卡死readinessProbe从Service后端摘除检测是否准备好接收流量startupProbe和liveness联动给慢启动应用留足启动时间一个容易混的点readiness探针失败不会重启容器只是不让流量进来。等探针恢复流量自动恢复。liveness失败会重启容器但如果你的应用启动要两三分钟liveness探针一开始就失败容器就会陷入不断重启的循环。解决办法是加一个startupProbe或者把initialDelaySeconds调大。配置探针时我最常用的写法是HTTP探测livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3这几个参数里initialDelaySeconds和failureThreshold是最常踩坑的。initialDelaySeconds太短应用还没监听端口就探测直接失败failureThreshold太小网络抖一下探针就判死。我的习惯是探针路径单独做一个轻量的健康检查接口别把整个压测流量都打进来别把依赖数据库的检查写进健康接口里否则数据库一抖动所有Pod都被判定不健康。4. 资源限制与调度Pod跑去哪谁来定4.1 requests与limits的差别资源这块如果你只记住一个概念那就是requests和limits的区别。requests是我至少需要多少limits是我最多能用多少。以CPU为例requests为100m表示这个Pod至少需要0.1个CPU核。调度器找节点的时候会把节点上所有Pod的requests加起来找出还能放下这个Pod的节点。注意limits只是运行时限制不参与调度计算。也就是说一个节点可能所有Pod的requests加起来只有2核但limits加起来有8核比物理CPU还多。这没问题因为不是每个Pod都会打满limits。但反过来就有问题节点上所有Pod的requests之和超过了节点容量调度器就不会再往这个节点放Pod了即使节点的实际使用率很低。这就是为什么有人在节点上看到load不高Pod却一直Pending。内存的limits比CPU的limits严格得多。CPU是可压缩资源打满顶多慢一点内存一旦超过limits容器直接被OOM杀掉。所以内存的limits不能乱写写小了应用会无缘无故被杀。4.2 QoS等级怎么分Kubernetes会根据Pod的requests和limits配置给Pod分一个QoS等级分三档QoS等级判断条件容器被杀的概率Guaranteedrequests等于limits每个容器都设置最低Burstable至少一个容器有requests但不是所有容器都等于limits中等BestEffort没有任何requests和limits节点内存不够时第一个被杀节点内存不足时kubelet是按QoS等级杀Pod的先杀BestEffort再杀Burstable最后才动Guaranteed。同等级内再按内存使用率排序用得多的先杀。这个机制看起来公平但生产环境里一个常见的坑是关键业务Pod忘了写limits降级成Burstable旁边一个非关键Pod写了完整的requests和limits变成Guaranteed。内存一紧张kubelet先把关键业务杀了。我自己的原则是凡是生产环境的核心Podrequests和limits必须写而且尽量写成一样保证Guaranteed级别。非核心Pod允许Burstable但起码得有requests。4.3 调度器选节点的逻辑默认调度器选节点分两步过滤和打分。过滤阶段把不符合条件的节点剔除比如资源不够、端口占用了、磁盘不满足打分阶段给剩下节点排序资源越富余分越高。如果你想让某个Pod必须落在特定节点上最简单的方式是nodeSelectorspec: nodeSelector: disktype: ssd给节点打标签然后Pod用nodeSelector匹配。这个机制简单可靠适合大多数场景。进阶的还有nodeAffinity、podAffinity能做更细的软性偏好和Pod间亲和部署。说实话小规模集群里nodeSelector完全够用别一上来就搞复杂的亲和性规则维护成本太高。有一类我强烈建议用Pod亲和性的场景两个服务之间网络流量极大比如应用和本地的Redis缓存。把它们调度到同一节点走本地回环或同机通信延迟能降一个量级。用podAffinity可以实现spec: affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: redis topologyKey: kubernetes.io/hostnamepreferredDuringSchedulingIgnoredDuringExecution表示软偏好调度器尽力而为实在放不了一起也没关系不影响Pod创建。5. Pod常见故障排查手册5.1 ImagePullBackOff这是新手遇到最多的错误。Pod创建后一直ImagePullBackOff多数是镜像拉不下来。先看完整错误kubectl describe pod xxxEvents里会有具体原因常见的有镜像名字写错、tag不存在、私有仓库需要认证但没配imagePullSecrets、镜像仓库地址网络不通。如果是在本地测试环境配了私有镜像仓库记得先验证节点上能否直接拉取crictl pull registry.example.com/app:v1.0.0节点上能拉Kubernetes里不能拉那问题基本出在imagePullSecrets或ServiceAccount配置上。再提一个容易忽略的点ImagePullBackOff是分次数指数退避的每次重试间隔变长别看到Pod在重启就觉得它卡住了先看Events。5.2 CrashLoopBackOff容器起来又挂、挂又起来这叫CrashLoopBackOff。排查思路是看日志kubectl logs pod-name --previous最后一个容器退出的日志最有用。没有日志的话用kubectl describe看容器的exit code退出码能提供不少线索137是被杀通常OOM143是被SIGTERM终止通常是优雅退出失败。CrashLoopBackOff还有一种隐藏情况容器其实没退出但liveness探针一直失败容器被kubelet杀掉重启。这时候日志里可能什么报错都没有顺畅运行几分钟就被杀一次。遇到这种情况先看kubectl describe里的Liveness事件出现了说明探针配置有误或者应用真的卡死了。5.3 Pod卡在PendingPod一直Pending问题几乎都出在调度或依赖准备阶段。常用的排查顺序是这样的先kubectl get events --sort-by.lastTimestamp | grep pod看有没有调度失败事件最常见的报错是0/3 nodes are available后面会跟具体原因比如Insufficient cpu、Insufficient memory、node(s) had taint。内存不够就加节点或减副本数有taint就Node亲和或容忍如果调度事件正常但还Pending可能是存储卷没就绪比如用了StorageClass但PV创建不出来还有可能是ImagePull一直没完成Pod在启动阶段等着拉镜像我给新手一个直接建议看到Pending第一件事永远是kubectl describe pod而不是kubectl get pod。get只看得到状态describe里能看到当前卡在哪一步。一张describe输出看下来80%的Pending原因都写在Events里了。5.4 改了ConfigMapPod却没生效这个坑我前面提了一次这里展开说。有人改了ConfigMap后用kubectl get configmap确认已经改成功再看Pod里文件还是旧的就开始怀疑Kubernetes出bug了。其实不是。kubelet默认通过定期轮询默认大概一分钟检测ConfigMap变更然后再把文件更新到容器的挂载点。而且注意如果ConfigMap被多个Pod挂载更新是逐个Pod进行的不是同时生效。更关键的是有些应用只在启动时读一次配置文件文件内容虽然更新了但应用不重新加载等于白改。所以我的做法是ConfigMap改动比较大的时候直接滚动重启关联的Deployment最干净kubectl rollout restart deployment/myapp这个命令会让Deployment创建一个新的ReplicaSet分批重建Pod新Pod启动时会挂载最新的ConfigMap。整套操作对业务无感知强烈推荐。6. 生产环境里Pod的进阶玩法6.1 优雅终止别让请求断在半路Pod被删除时默认行为是先收到SIGTERM信号等待一定宽限期然后强制SIGKILL。但这个默认流程有两个隐患。第一个隐患是应用对SIGTERM处理不当。Java应用、Nginx这些都有各自的优雅退出机制但如果应用不监听SIGTERM直接默认退出正在处理的请求就断了。解决思路是在容器里加一段优雅退出逻辑或者在启动脚本里trap信号。第二个隐患是Service还没把流量摘掉Pod就开始停了。一个新Pod删掉后kube-proxy的更新有个时间差在摘除期间打到Pod上的流量Pod已经不在了请求直接失败。这个问题一般用preStop钩子处理spec: containers: - name: app lifecycle: preStop: exec: command: - sh - -c - sleep 5preStop在收到SIGTERM之前执行先等5秒让Service和kube-proxy把流量摘干净再真正退出应用。这个数字你可以根据自己集群的规模调集群节点多时摘流量慢几秒到十几秒都可能。再配合terminationGracePeriodSeconds调大宽限期应用才有充足时间完成收尾工作。6.2 PDB节点维护时保住可用性日常运维里给节点打补丁、内核升级都是常规操作。节点要重启上面的Pod就得迁走。如果是Deployment管理的Pod重建很快通常没问题。但当你故意把节点全部排空的时候如果一次迁走太多副本服务可能就不可用了。这时候需要PodDisruptionBudgetPDB。它不像Deployment控制副本数它管的是主动驱逐的时候最多允许多少个Pod不可用。举个例子你有10个副本PDB设置minAvailable: 7那每次主动驱逐最多只允许3个不可用剩下的7个确保在线。apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: web-pdb spec: minAvailable: 7 selector: matchLabels: app: web注意PDB只拦截主动驱逐比如kubectl drain不拦截节点宕机这种不可控事件。所以它不是高可用方案本身而是防止运维操作的时候雪上加霜。我们团队上线运维流程后所有核心服务都配了PDB几次节点维护都平稳度过没再出现跨副本一起下线的情况。6.3 常见反模式与心得最后说几个我在生产环境里见过的反模式希望你别踩。第一个反模式把Pod当宠物养。直接创建裸PodSSH进去改配置、装软件。Pod的宿命就是随时可以销毁重建任何改动都应该通过镜像和YAML沉淀下来。前脚改完后脚Pod一重建全没了。第二个反模式一个Pod塞太多容器。虽然Pod支持多容器但容器越多日志排查越痛苦资源统计越复杂启动失败的组合可能性越大。我一般原则是强耦合的两个容器才放一个Pod没有共享网络和存储卷需求的服务坚决拆成独立Pod。第三个反模式忽略Pod的优雅退出直接把terminationGracePeriodSeconds调到0。有人觉得这样删除快但代价是用户请求被粗暴中断数据库连接没来得及释放。彻底删掉一个Pod的体验和你直接拔电源差不多。第四个反模式secret明文写在Pod的YAML里。Secret和ConfigMap一样有文件形态能挂载能引用。但记住Secret的内容只是base64编码不是加密别把真密钥放进去后还提交到Git仓库。生产环境密钥建议接入外部密钥管理系统通过CSI驱动同步到Pod。把Pod用明白是在Kubernetes里少踩坑的关键说了这么多Pod其实就是一个模型它定义了Kubernetes这个调度系统眼里的应用单元。理解它的网络共享、存储共享、生命周期、资源约束后面管Deployment、管Service、管集群稳定性都顺了。我个人的体会是真正常踩的坑往往不是概念不理解而是配置细节不到位——资源限制没写、探针超时设太短、ConfigMap改了不重建、删除Pod不处理优雅退出。这些经验都是拿线上故障换来的写出来给你做个参考希望你能少走几次弯路。最后再分享一个习惯每次看到Pod状态异常先问自己一句这个Pod从创建到现在Events里说了什么养成看事件的肌肉记忆排查效率能翻倍。
返回列表