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

Kubernetes Pod 核心机制与实战排障指南

Kubernetes Pod 核心机制与实战排障指南 ★ FEATURED ARTICLE
Pod 是 Kubernetes 里绕不开的第一个门槛。刚接触容器编排的人往往先从 Deployment、Service 这些概念入手但真正在集群里把业务跑起来之后才会意识到所有控制器、调度器、网络插件最终都是围绕着 Pod 在转。你不把 Pod 吃透后面排障、调优、设计高可用架构都会感觉发虚。这篇文章我会从 Pod 的本质讲起一路聊到多容器设计模式、资源配额、优雅终止和排障实录。希望能用这些年踩坑攒下来的经验帮你把 Pod 这个被很多人一笔带过的核心概念彻底搞清楚。1. Pod 到底是什么Kubernetes 里最小调度单位的来龙去脉1.1 为什么 Kubernetes 要多套一层 Pod而不是直接用容器很多人第一个问题就是Docker 容器已经能跑应用了为什么 Kubernetes 不直接调度容器非要再套一层 Pod我当年也这么想过直到自己动手部署一个有状态的中间件服务时才反应过来。假设你有一个主进程和一个负责同步配置的辅助进程两个进程必须跑在同一台机器上共享同一个网络栈而且需要一起启动、一起停止。如果用容器直调你得自己管理它们的关系还得处理端口冲突、IP 不一致这些麻烦事。Pod 就是用来解决这个问题的。它把一组关系紧密的容器打包成一个逻辑单元Kubernetes 对 Pod 做整体的调度、重启、扩缩容。Pod 是集群里最小的调度单位这句话的意思是调度器不会单独去调度某个容器只会调度整个 PodPod 内所有容器必定落在同一个节点上。打个比方Pod 就像一节火车车厢里面可以坐一个乘客也可以坐一群乘客但车厢整体被编组、调度。你不需要分别告诉车头第二节车厢的第三个人去哪只要告诉它把这节车厢拉到哪一站就行。1.2 同一个 Pod 里的容器到底共享了什么这是 Pod 设计里最核心的机制也是很多人容易忽略的细节。Pod 里的所有容器共享三样东西共享同一个 Network Namespace。这意味着它们共用同一个 IP 地址、同一组端口范围、同一个 hostname 和 hosts 文件。容器之间通过 localhost 就能互相访问不用走网络。我实测过两个容器共用一个 Pod IP端口只要不冲突直接 127.0.0.1 就能连上。共享同一个存储卷的生命周期。Pod 可以声明卷卷挂载到 Pod 里容器再各自挂载其中的子路径。数据的生命周期跟 Pod 绑定Pod 删除卷随之销毁除非你用持久卷。共享资源配额和调度约束。CPU、内存的 requests/limits 是以 Pod 为单位汇总的调度器判断节点资源够不够也是看 Pod 总量。注意同一 Pod 里的容器之间没有隔离网络和文件系统都是可见的。所以不要把互相不信任的容器塞进同一个 Pod那会放大安全风险。1.3 Pod 与容器的关系很多人其实记混了我再补一个容易被面试官或同事问住的知识点Pod 里可以不只有一个容器但一个 Pod 至少有一个主容器。你平时用 Deployment 创建业务 Pod主容器就是跑业务进程的那个其他的叫辅助容器比如 Sidecar。还有一个特例叫 Init 容器它跟普通容器的区别在于Init 容器一定在主容器启动之前按顺序跑完跑完就退出不会常驻。它适合做初始化工作比如等数据库就绪、灌基础数据、修改目录权限。这么一来Pod 的结构就清楚了一个元数据外壳Pod 零到多个 Init 容器 一个或多个业务容器。元数据里挂着标签、注解、探针、调度约束、重启策略容器都是被这个外壳管理起来的。2. 从创建到销毁Pod 生命周期里的每一个阶段2.1 五态模型与重启策略Kubernetes 里 Pod 有五个状态篇幅有限我只挑最常见的讲Pending、Running、Succeeded、Failed、Unknown。Pending 表示 Pod 已经被 API Server 接受但还没跑起来。原因可能是节点资源不足、镜像拉取中、PVC 未绑定等等。Running 至少有一个容器还在运行或者正在启动或重启。Succeeded 表示所有容器都正常退出了且退出码为 0。一次性任务 Pod 是这样的状态。Failed 表示有容器以非 0 退出码退出。Unknown 一般很少见通常是节点失联、kubelet 失联API Server 拿不到 Pod 最新状态。重启策略和 Pod 状态是配合的。restartPolicy 有三种Always、OnFailure、Never。Deployment 管的是长期运行的业务 Pod所以默认 AlwaysJob 管的是批处理 Pod一般设 OnFailure 或 Never。我踩过一次坑写了一个 CronJob处理任务的镜像脚本路径写错了容器秒退但因为 restartPolicy 是 OnFailurekubelet 一直按指数退避重启它日志刷了半小时才发现。后来我养成了习惯批处理任务一律显式写 Never让 Job 控制器来负责任务失败重试而不是让 kubelet 在单个 Pod 层面死磕。2.2 最小可用 YAML 逐段拆解刚开始写 Pod YAML 时很多东西可以直接照抄模板但抄之前得知道每个字段在干什么。一个最简单的 Pod 长这样apiVersion: v1 kind: Pod metadata: name: sample-pod labels: app: sample tier: backend spec: containers: - name: main-app image: nginx:1.25 ports: - containerPort: 80 protocol: TCP env: - name: ENV_MODE value: prod resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256MiapiVersion 和 kind 这两行决定了资源的类型和版本Pod 是 v1 核心资源几乎没有兼容性风险。metadata 里的 labels 极其重要Service、Deployment、NetworkPolicy 都靠它来筛选 Pod。spec.containers 是核心列表至少一个容器。resources 这一段很多人嫌麻烦不写但生产环境这是必须的。requests 是调度器做节点选择时看的保底需求量limits 是运行时 kubelet 强制卡住的上限。比如上面这段调度器会找一个剩余 CPU 至少 100m、内存至少 128Mi 的节点跑起来之后CPU 用到 500m 就会被限流内存用到 256Mi 就可能被 OOM Kill。提示内存没有 limit 的时候Pod 可能把一个节点的内存吃穿Node 进入 MemoryPressure其他 Pod 也会受影响。尽量每个容器都写上 limits。2.3 调度Pod 落到哪个节点不是你说了算的调度是 Kubernetes 里最容易让新手困惑的部分。默认情况下你创建 Pod调度器会根据资源余量、节点亲和性、污点容忍度等一堆条件自动决定节点。但进阶玩法是你能主动干预。nodeSelector 是最简单的指定方式给节点打上标签然后在 Pod 里写 nodeSelectorPod 只会被调度到带这些标签的节点。举个例子给 GPU 节点打上 hardwaregpu然后在 Pod 里写spec: nodeSelector: hardware: gpu如果没有任何节点带这个标签Pod 会一直 Pending。你排查时看到 Pending第一反应就去看 nodeSelector 和节点标签对不对。更灵活的节点亲和nodeAffinity支持硬性要求和软性偏好拓扑分布约束topologySpreadConstraints可以把 Pod 尽量分散到不同故障域。这些是优化高可用的重点手段但我不建议新手一上来就全用先从 nodeSelector 熟悉起来后面再逐步加深。3. 多容器 Pod 的几种设计模式以及探针配置3.1 Sidecar 模式最常用的辅助容器玩法微服务都说要轻装上阵但总有一些脏活累活不想塞进主进程里采集日志、转发流量、同步证书、监控指标。这时候 Sidecar 就派上用场了。我做过一个典型的例子主容器是一个 Java 应用日志写到本地文件Sidecar 是一个 Filebeat 容器和主容器共享同一个 emptyDir 卷读到日志后转发到日志中心。这样 Java 应用不需要关心任何日志采集逻辑它的镜像里可以完全不装采集器。Sidecar 的部署形态有两种如果你用的是 Deployment可以把 Sidecar 直接写进同一个 Pod 的 containers 列表里在较新的 Kubernetes 版本里也有 Sidecar 类型的 Init 容器可以常驻运行。老版本没有这个能力所以我建议大多数情况下直接把 Sidecar 写进 containers 列表简单可靠。实际配置里要注意一点Sidecar 容器和主容器共享卷没问题但两个容器对同一个文件的读写要小心冲突。比如日志文件轮转时Filebeat 还在读旧文件 inode谁去 rename、谁去删除必须提前商量好否则会丢日志或者重复发送。3.2 Init 容器主容器启动前的前置校验Init 容器我最常用于两类场景。一类是等依赖就绪。比如主容器依赖数据库里的表结构已经迁移完成启动太早会报错。用 Init 容器轮询数据库直到表存在了才退出主容器才被拉起来。另一类是准备配置或权限。比如往共享卷里写配置文件、修正目录属主这些工作如果放在主容器启动脚本里做会污染业务镜像放 Init 容器里就干净了。Init 容器和普通容器的 YAML 写法几乎一样只是放在 spec.initContainers 字段下spec: initContainers: - name: check-db image: busybox:1.36 command: [sh, -c, until nc -z db-service 5432; do echo waiting; sleep 2; done]需要注意的是Init 容器执行失败Pod 不会启动主容器kubelet 会按 restartPolicy 决定要不要重新执行 Init 容器。restartPolicy 为 Always 的 PodInit 容器失败后也会反复重启。我在线上就遇到过因为 Init 容器里脚本逻辑不幂等导致第二次执行时写入了重复数据的情况。所以 Init 容器里的脚本最好设计成可重复执行的。3.3 探针Kubernetes 怎么判断 Pod 到底活没活探针是进阶用户一定会碰到的机制。它有三种livenessProbe存活探针、readinessProbe就绪探针、startupProbe启动探针。它们的区别简单说livenessProbe 失败kubelet 会杀掉容器并重启readinessProbe 失败kubelet 会把 Pod 从 Service 的 Endpoints 里摘掉但不会重启startupProbe 专门用来照顾启动慢的应用在 startupProbe 成功之前其他探针不会被激活。一个常见的配置样例livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3参数里最容易被忽略的是 initialDelaySeconds 和 failureThreshold。Java 应用启动可能要三十秒initialDelaySeconds 设得太短会有一次探针失败就把容器干掉的乌龙。我就经历过一次Spring Boot 应用启动到第 8 秒时健康检查端口还没监听livenessProbe 在 initialDelaySeconds: 10 时启动第 18 秒就失败了Kubernetes 重启了容器结果重启后又要 30 秒反复循环看起来就是 CrashLoopBackOff但实际代码根本没报错。后来加了 startupProbe把启动阶段延长到 60 秒问题立刻消失。4. 资源管理requests、limits 与 QoS 等级4.1 缺失资源配额会引发什么连锁反应很多新手把 Pod 跑起来就不管资源了。直到某天晚上一个节点的内存被打满Node 进入 MemoryPressure 状态kubelet 开始按 QoS 从低到高杀 Pod 腾内存。如果你的业务 Pod 没有任何 requests/limits它属于 BestEffort 等级是第一批被杀的而且被杀毫无预兆。我有个惨痛教训一个数据分析任务的 Pod 没写内存 limits某个凌晨它突发内存飙升直接把同一节点上的核心 API Pod 挤到 OOM。那次线上事故之后我把所有新建的 Pod 都强制要求带上 resources 配置并且在团队的模板里默认填好推荐值。4.2 三个 QoS 等级是怎么划分的Kubernetes 根据 requests 和 limits 的配置方式把 Pod 分成三类Guaranteed所有容器都设置了 requests 和 limits且每个容器两者相等。Burstable至少有一个容器设置了 requests 或 limits但不符合 Guaranteed 条件。BestEffort所有容器都没有设置 requests 和 limits。宕机或资源紧张时kubelet 的驱逐顺序是先 BestEffort再 Burstable最后 Guaranteed。这不是绝对的规则但绝大多数情况下是这样。如果想给核心业务 Pod 最高的存活保障直接配成 Guaranteed 就行。写成 requests 和 limits 相等比如 cpu 都是 500m内存都是 512Mi。代价是浪费一部分弹性能力但换来的是发生节点故障时被保留的优先级。4.3 给 Pod 配资源时的经验公式我一般按下面这套经验来给 Web 服务配资源先观察压测数据。确定服务平稳运行时 CPU 均值、峰值和内存占用。requests 设成稳定值的 70%-80%留一点余量给调度limits 设成峰值的 1.2-1.5 倍。比如稳定 CPU 是 200m峰值 400m那 requests 写 150mlimits 写 500m。内存我倾向 requests 和 limits 相等因为内存不像 CPU 可以压缩回收超出 limits 直接 OOM。不过这个公式不是万能药。Java 堆外内存、JIT 缓存的波动都很大压测时一定要用接近生产的流量去测而不是随便压两下就定数据。4.4 命名空间层级的资源配额Pod 级配好之后还得防同一命名空间里别的团队乱来。用 ResourceQuota 可以限制命名空间总资源apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: 10 requests.memory: 20Gi limits.cpu: 20 limits.memory: 40Gi pods: 50这个配额一加上超出范围后创建 Pod 会直接被拒绝报错信息里会写清楚不够哪个资源。配合 LimitRange 可以对未指定 requests/limits 的 Pod 做默认填充这两个资源一起用命名空间的资源才能管住。5. 进阶实用技巧优雅终止、安全上下文与控制器选择5.1 优雅终止为什么你的服务老被判定不健康Pod 被删除时会先进入 Terminating 状态。kubelet 向容器发送 SIGTERM 信号等一个宽限期默认 30 秒到期还没退出就发 SIGKILL 强杀。很多应用不处理 SIGTERM只会被强杀这就导致明明代码里写了优雅关闭逻辑Kubernetes 却来不及等它执行完。我处理过的一个真实案例某网关服务在关闭前要把内存里的请求结果回写到下游回写大概需要 15 秒。默认 30 秒其实够但由于应用框架只处理了 SIGINT 没处理 SIGTERMKubernetes 发 SIGTERM 后应用直接终止了回写。后来在代码里补上 SIGTERM 的处理器并在 Deployment 里显式设置 terminationGracePeriodSeconds: 45问题解决。你还可以用 preStop 钩子来做更复杂的收尾。比如先通知注册中心下线再 sleep 几秒等流量切完最后退出lifecycle: preStop: exec: command: [/bin/sh, -c, curl -X POST http://localhost/offline; sleep 5]preStop 是同步阻塞的执行完或者超时后 kubelet 才发 SIGTERM所以 sleep 时间别设太长否则 Pod 会长时间留在 Terminating 状态。5.2 安全上下文别让容器以 root 裸奔安全上下文securityContext是很多项目里被忽略的配置但它非常重要。容器默认以镜像里指定的用户运行有些基础镜像默认 root一旦容器被攻破节点权限也很危险。推荐的基线配置长这样securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 30001 fsGroup: 2000 allowPrivilegeEscalation: false capabilities: drop: [ALL] seccompProfile: type: RuntimeDefaultrunAsUser 指定非 root UIDfsGroup 让挂载卷的属主自动变成这个组。allowPrivilegeEscalation 设为 false 可以防止 fork 出更高权限的进程。capabilities 直接把 Linux capabilities 全部丢掉再按需添加。这样即使容器被攻破权限也被限制在很小范围内。注意一点runAsUser 必须是镜像是里存在的用户 UID否则启动会报错。我之前随手写了一个 10001结果基础镜像里没有这个用户容器一直 CrashLoopBackOff日志提示的是权限相关错误排查了好一会儿才明白原因。5.3 Deployment、DaemonSet、StatefulSet 该怎么选很多人分不清三种控制器各自适合什么场景其实看名字就能猜个大概Deployment 管理无状态应用所有 Pod 完全一样可以随时替换、扩缩容适合 Web 服务、API、任务计算。StatefulSet 管理有状态应用每个 Pod 有稳定的身份和独立的存储适合数据库、消息队列这类需要稳定网络标识的服务。DaemonSet 保证每个节点上跑一个 Pod适合日志采集、节点监控、网络插件。选择控制器时要重点想一个问题你的 Pod 被重新调度后数据是否还在无状态应用数据无所谓重新拉起就行用 Deployment有状态应用必须绑定分布式存储用 StatefulSet 才能让新的 Pod 挂回同一个卷。我记得刚把 Redis 从 Deployment 改成 StatefulSet 的场景当时数据经常丢因为 Deployment 的 Pod 是无名的卷和数据很难对应。改成 StatefulSet 后每个 Pod 有唯一的序号和专用 PVCRedis 的数据终于跟着 Pod 身份稳定下来。6. 线上排障实录Pod 常见问题与排查套路6.1 状态异常速查表我整理了平时线上最常碰到的几种 Pod 状态异常下面的表可以直接当速查工具状态/事件典型原因第一步排查动作Pending资源不足 / nodeSelector 不匹配 / PVC 未绑定执行 describe pod看 Events 最后几行ImagePullBackOff镜像名错误 / 镜像不存在 / 拉取凭证不对确认镜像仓库地址和 imagePullSecretsCrashLoopBackOff容器启动即退出 / 探针失败 / 脚本报错看 logs再检查启动命令和探针配置RunContainerError容器运行时问题 / 权限不足 / entrypoint 缺失查看 kubelet 日志和容器运行时日志Terminating 卡住容器不响应 SIGTERM / preStop 卡住确认应用是否处理 SIGTERM必要时强制删除Unknown节点失联 / kubelet 异常检查节点状态重启 kubelet确认请求代理是否正常ImagePullBackOff 是我见过最多的新手问题。执行 kubectl describe pod 之后Events 里会给出具体原因比如 ImagePullBackOff 后跟着拉取错误的具体信息大部分情况是镜像标签写错、或者私有仓库没配 imagePullSecrets。还有一种隐蔽的镜像仓库里确实有镜像但 tag 是隐式的 latest本地节点有缓存旧镜像导致一直用的是旧的跟代码对不上。这种情况先用 docker pull 或者 crictl pull 确认镜像。6.2 调试三板斧describe、logs、exec排障就三板斧但很多人三板斧用得不够深。kubectl describe pod xxx 能看到 Pod 全貌和 Events这是第一选择。Events 里几乎会直接告诉你发生了什么节点资源不足、探针失败、拉取镜像失败、卷挂载失败等。kubectl logs 要看谁都清楚但要记住如果 Pod 里有多个容器必须加 -c 容器名否则只会看到第一个容器的日志。容器崩溃过的话用 --previous 看上一份日志很多 CrashLoopBackOff 的问题就藏在上一份里。kubectl exec -it pod名 -- sh 进入容器看现场。有时候应用日志看不出来问题就得看系统信息top、free、df、ls /proc/1/root 这些确认资源使用和进程状态。再分享一个我常用的技巧当容器挂了但你想看它崩溃前的状态可以把 command 临时改成 sleep 3600 这样的常驻命令然后 exec 进去手动执行原命令。这种方式定位启动脚本问题特别高效。6.3 一个典型案例从 CrashLoopBackOff 到修复的完整复盘去年有一个模拟项目XPod 一直 CrashLoopBackOff。我按顺序做了三件事第一步 describeEvents 显示 Liveness probe failed。第二步看 logs发现应用一直在打印数据库连接超时。第三步看数据库状态发现数据库 Pod 的 PVC 爆满了写入失败导致拒连。根本原因链是这样的数据库磁盘满 - 连接失败 - 应用启动失败进入 Ready 失败 - livenessProbe 把容器杀掉 - 重启后再启动又失败循环往复。修复过程是先扩 PVC 容量清理掉数据库占用的空间再调整应用启动时的 readinessProbe 和 livenessProbe 的超时时间避免数据库偶尔抖动就让容器被杀。这个案例提醒我CrashLoopBackOff 不一定是代码问题很可能是下游依赖故障的连锁反应。遇到 CrashLoopBackOff 先别急着改代码把依赖链从头到尾捋一遍。最后再说一点我自己的体会做了几年 Kubernetes 相关的事我最大的感受是Pod 看似简单但它把网络、调度、资源、安全这些底层逻辑都压缩在这一层里。你越早花时间把 Pod 的每个字段、每个状态、每个机制吃透后面处理 Deployment、Service Mesh、 Serverless 这些上层东西就越顺手。如果你现在正准备上手 Kubernetes我只给一个建议不要只跑通 demo 就满足尝试手动把 Pod 的 YAML 从零写一遍把探针加上把资源配额配好再人为制造几次故障去观察现象。这个过程会比看十篇教程都值。
阅读完成 · 觉得有帮助?
咨询建站