Pod 是 Kubernetes 里绕不开的第一个门槛。刚接触容器编排的人,往往先从 Deployment、Service 这些概念入手,但真正在集群里把业务跑起来之后才会意识到:所有控制器、调度器、网络插件,最终都是围绕着 Pod 在转。你不把 Pod 吃透,后面排障、调优、设计高可用架构都会感觉发虚。
这篇文章我会从 Pod 的本质讲起,一路聊到多容器设计模式、资源配额、优雅终止和排障实录。希望能用这些年踩坑攒下来的经验,帮你把 Pod 这个被很多人一笔带过的核心概念彻底搞清楚。
1. Pod 到底是什么:Kubernetes 里最小调度单位的来龙去脉
1.1 为什么 Kubernetes 要多套一层 Pod,而不是直接用容器
很多人第一个问题就是:Docker 容器已经能跑应用了,为什么 Kubernetes 不直接调度容器,非要再套一层 Pod?
我当年也这么想过,直到自己动手部署一个有状态的中间件服务时才反应过来。假设你有一个主进程和一个负责同步配置的辅助进程,两个进程必须跑在同一台机器上,共享同一个网络栈,而且需要一起启动、一起停止。如果用容器直调,你得自己管理它们的关系,还得处理端口冲突、IP 不一致这些麻烦事。
Pod 就是用来解决这个问题的。它把一组关系紧密的容器打包成一个逻辑单元,Kubernetes 对 Pod 做整体的调度、重启、扩缩容。Pod 是集群里最小的调度单位,这句话的意思是:调度器不会单独去调度某个容器,只会调度整个 Pod,Pod 内所有容器必定落在同一个节点上。
打个比方,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,所以默认 Always;Job 管的是批处理 Pod,一般设 OnFailure 或 Never。
我踩过一次坑:写了一个 CronJob,处理任务的镜像脚本路径写错了,容器秒退,但因为 restartPolicy 是 OnFailure,kubelet 一直按指数退避重启它,日志刷了半小时才发现。后来我养成了习惯:批处理任务一律显式写 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 里写 nodeSelector,Pod 只会被调度到带这些标签的节点。举个例子,给 GPU 节点打上 hardware=gpu,然后在 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 的 Pod,Init 容器失败后也会反复重启。我在线上就遇到过因为 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 写 150m,limits 写 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 没处理 SIGTERM,Kubernetes 发 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 UID,fsGroup 让挂载卷的属主自动变成这个组。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 有唯一的序号和专用 PVC,Redis 的数据终于跟着 Pod 身份稳定下来。
6. 线上排障实录:Pod 常见问题与排查套路
6.1 状态异常速查表
我整理了平时线上最常碰到的几种 Pod 状态异常,下面的表可以直接当速查工具:
| 状态/事件 | 典型原因 | 第一步排查动作 |
|---|---|---|
| Pending | 资源不足 / nodeSelector 不匹配 / PVC 未绑定 | 执行 describe pod,看 Events 最后几行 |
| ImagePullBackOff | 镜像名错误 / 镜像不存在 / 拉取凭证不对 | 确认镜像仓库地址和 imagePullSecrets |
| CrashLoopBackOff | 容器启动即退出 / 探针失败 / 脚本报错 | 看 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 到修复的完整复盘
去年有一个模拟项目X,Pod 一直 CrashLoopBackOff。我按顺序做了三件事:
第一步 describe,Events 显示 Liveness probe failed。第二步看 logs,发现应用一直在打印数据库连接超时。第三步看数据库状态,发现数据库 Pod 的 PVC 爆满了,写入失败导致拒连。
根本原因链是这样的:数据库磁盘满 -> 连接失败 -> 应用启动失败进入 Ready 失败 -> livenessProbe 把容器杀掉 -> 重启后再启动又失败,循环往复。
修复过程是:先扩 PVC 容量,清理掉数据库占用的空间,再调整应用启动时的 readinessProbe 和 livenessProbe 的超时时间,避免数据库偶尔抖动就让容器被杀。这个案例提醒我,CrashLoopBackOff 不一定是代码问题,很可能是下游依赖故障的连锁反应。遇到 CrashLoopBackOff 先别急着改代码,把依赖链从头到尾捋一遍。
最后,再说一点我自己的体会
做了几年 Kubernetes 相关的事,我最大的感受是:Pod 看似简单,但它把网络、调度、资源、安全这些底层逻辑都压缩在这一层里。你越早花时间把 Pod 的每个字段、每个状态、每个机制吃透,后面处理 Deployment、Service Mesh、 Serverless 这些上层东西就越顺手。
如果你现在正准备上手 Kubernetes,我只给一个建议:不要只跑通 demo 就满足,尝试手动把 Pod 的 YAML 从零写一遍,把探针加上,把资源配额配好,再人为制造几次故障去观察现象。这个过程会比看十篇教程都值。