前置回顾
第 01 篇的核心认知:
- Pod 是 K8s 最小调度单元,是一组共享 Namespace 的容器集合
- 所有组件通过 watch API Server 协作:Scheduler 选节点 → kubelet 拉镜像/创建容器
本篇深入 Pod 内部,逐一拆解 Pause 容器的结构,容器间的共享与隔离机制,以及生产环境必须掌握的探针和资源管理.
Pause 容器:Pod 的"骨架"
第 01 篇 Pod 创建全流程的第 5 步提到:runc 会先启动一个 Pause 容器,再启动用户容器.现在来详细看这个特殊容器到底是什么.
每个 Pod 里都有一个你看不到的特殊容器--Pause 容器(Pause Container).它是 Pod 里第一个启动,最后一个退出的容器.
|
Pause 容器只有约 700KB 大小(一个极简的 C 程序),做的事情就只有三件:
| 职责 | 说明 |
|---|---|
| 持有 Network Namespace | 用户容器启动时不创建新网络 NS,而是加入 Pause 已有的 NS.Pod 内所有容器共享同一个 IP,同一个 localhost. |
| 持有 IPC Namespace | 容器间可通过 System V 信号量或 POSIX 消息队列通信(极少使用). |
| 收割僵尸进程 | 作为 PID 1,用 wait() 回收孤儿进程.没有它的话,用户容器里产生的僵尸进程永远无法被清理. |
|
共享机制:Pod 内容器能看到什么
回顾阶段一第 02 篇--Linux Namespace 是隔离的基本单元.Pod 的共享机制就是有选择地共享或隔离这些 Namespace.
共享的 Namespace
| 共享的 Namespace | 效果 | 典型场景 |
|---|---|---|
| Network NS | 同一个 IP,同一组端口,localhost 互通 | nginx 通过 localhost:24224 把日志发给 fluentd |
| IPC NS | 共享信号量和 POSIX 消息队列 | 极少数遗留场景,现代微服务基本不用 |
PID NS(可选,shareProcessNamespace: true) |
容器间互相看到对方的进程 | 调试用(看 Sidecar 进程是否存活) |
| Volume | 共享文件系统路径(通过 emptyDir 等方式挂载) | nginx 写 access.log → fluentd 读取并转发 |
|
独立的 Namespace
| 独立的 Namespace | 原因 |
|---|---|
| Mount NS | 每个容器有独立的 rootfs(OverlayFS),不共享才能各自跑不同镜像 |
| PID NS(默认) | 每个容器只能看到自己的进程,安全隔离 |
| Cgroup | 每个容器的 CPU/内存限制独立计算 |
| User NS | 安全考虑,各自 UID/GID 映射 |
Mount NS 不共享是关键设计
如果 Pod 内所有容器共享 Mount NS,那就必须用同一个镜像了.保持 Mount NS 独立,nginx 用 alpine 镜像,fluentd 用 debian 镜像,互不干扰--这正是 Pod 设计的精妙之处:网络共享,文件系统独立.
Pod 生命周期
五个 Phase
Pod 的 status.phase 是宏观状态,整个生命周期只增不减:
|
| Phase | 含义 | 时机 |
|---|---|---|
| Pending | Pod 已创建但尚未运行 | 已写入 etcd,等待 Scheduler 调度,或正在拉镜像,启动 Init Container |
| Running | 至少一个容器在运行 | 所有容器已创建,至少一个处于运行中(含启动或重启中) |
| Succeeded | 所有容器正常终止 | 所有容器 exit 0,且 restartPolicy=Never(典型:Job) |
| Failed | 某容器异常终止 | 某容器 exit ≠ 0 且 restartPolicy=Never,或容器反复崩溃达到回退上限 |
| Unknown | 无法获取 Pod 状态 | 节点失联(kubelet 停止汇报心跳),常见于节点宕机或网络分区 |
容器状态 ≠ Pod 状态
Pod 的 phase 是汇总状态,每个容器有自己独立的 containerStatus.这是排查时最容易踩的坑:kubectl get pod 显示 Running,不代表所有容器都健康.
|
提示
- 只要有一个容器在Running, Pod 的 phase 就是 Running
- 用
kubectl describe pod才能看到每个容器的真实状态
重启策略(restartPolicy)
restartPolicy 作用于 Pod 内所有容器(K8s 1.28 前),由 kubelet 在容器退出时执行:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| Always(默认) | 只要退出就重启,无论 exit code | 长期运行的服务(Deployment,StatefulSet,DaemonSet) |
| OnFailure | 仅非零退出码才重启 | Job(任务执行完就结束,失败才重试) |
| Never | 退出后绝不重启 | 一次性任务,Init Container |
kubelet 实现了一个指数退避(exponential backoff) 机制:容器重启间隔从 10 秒开始,每次翻倍(20s → 40s → 80s...),上限 5 分钟.防止崩溃容器无限重启吃光节点资源.
探针(Probes)
为什么需要探针
容器进程在跑 ≠ 应用已经就绪.
一个 Java 服务启动后可能要花 60 秒加载 Spring 上下文;一个 Web 服务可能因为死锁卡住,进程不退出但再也无法响应请求.
从操作系统的视角看,这些容器都是"活着的"--进程存在,PID 正常--但从业务视角看它们早就废了.
探针就是 kubelet 定期对容器做的健康检查.
kubelet 按你在 YAML 里配置的规则,每隔几秒对容器执行一次检测(发 HTTP 请求 / 执行命令 / 尝试 TCP 连接),根据结果决定是否重启容器或摘除流量.
没有探针的话,K8s 只能靠"进程是否退出"来判断容器是否正常--这远远不够.
探针 = 医院的定期巡房.
护士(kubelet)每隔几分钟来看一次病人(容器):能说话吗?(liveness)能下床活动吗?(readiness)刚做完手术醒了吗?(startup).不同的检查结果对应不同的处置方案--而不是只看"心跳还有没有".
三种探针
这是 Pod 配置里最容易设错的部分:
| 探针 | 作用 | 失败后果 | 典型检查什么 |
|---|---|---|---|
| startupProbe | 应用是否启动完成 | 重启容器 | 数据库连接就绪,缓存预热完成 |
| livenessProbe | 应用是否还活着 | 重启容器 | 死锁,内存泄漏导致僵死 |
| readinessProbe | 应用能否接流量 | 从 Service Endpoints 移除(不杀容器) | 线程池满,依赖服务不可达 |
三种探测方式
每种探针都支持同样的三种检查方法:
| 方式 | 怎么用 | 适用场景 |
|---|---|---|
| exec | 在容器内执行命令,返回 0 = 成功 | 没有 HTTP 端口的应用(如数据库,消息队列);检查本地文件 |
| httpGet | 发起 HTTP GET,2xx-3xx = 成功 | Web 应用(最常用),可带自定义 Header |
| tcpSocket | TCP 三次握手成功 = 成功 | 非 HTTP 协议的 TCP 服务(如 gRPC,MySQL,Redis) |
startupProbe:慢启动的守护神
这是最容易被忽略但最重要的探针.它的唯一职责是:给慢启动应用争取时间,防止被 liveness 误杀.
|
没有 startupProbe 的经典翻车场景
Java 应用启动需要 90 秒(加载 Spring 上下文,建立连接池).
如果给 livenessProbe 设了 initialDelaySeconds: 30, failureThreshold: 3(最多 60 秒容忍窗口).启动 60 秒后 liveness 判定失败 → 杀掉容器 → 重新启动 → 又 60 秒被杀 → 无限重启循环.
你以为是代码 bug,其实是探针配错了.startupProbe 就是解决这个问题的.
|
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
periodSeconds |
10 | 探测间隔 |
failureThreshold |
3 | 连续失败多少次算失败;对于 startupProbe,这个值应该设大一些(如 30),给足启动时间,而不是调大 periodSeconds |
successThreshold |
1 | 连续成功多少次算成功(startup/liveness 必须是 1) |
timeoutSeconds |
1 | 单次探测的超时时间 |
initialDelaySeconds |
0 | 容器启动后等多久再开始探(有 startupProbe 时通常设为 0) |
startupProbe 失败 ≠ 立即重启容器
每次探测失败只是记一次数,然后按 periodSeconds 继续探下一次.只有连续失败次数达到 failureThreshold,才判定启动失败,杀掉容器重来--整个 startupProbe 窗口也随之重置.
这就是为什么 failureThreshold 值得设大(如 30):它不是"重试 30 次",而是"容忍 30 次失败后才判死刑".
设成 30 × 10s = 5 分钟的窗口,远比把 periodSeconds 设成 100s 要合理--因为 periodSeconds 不仅控制重试间隔,更决定了应用就绪后多快能被发现(最多浪费一个 interval).
livenessProbe:死了就重启
liveness 的语义是:"进程在跑,但已经无法提供服务"--比如死锁,无限循环,内存泄漏导致 GC 停顿.进程没退出(所以 restartPolicy 不触发),但无法正常响应请求.
liveness 探测发现后杀掉容器,让 kubelet 重建.
|
liveness 不要做重操作
/healthz 端点里不要查数据库连接状态,不要调下游服务健康检查.这些属于 readiness 的范畴.liveness 挂了会杀容器--因为瞬时 DB 抖动杀掉应用进程是极其糟糕的设计.
readinessProbe:活着但不下线
readiness 的语义是:"暂时无法服务"--应用还活着,但当前不适合接收请求.失败时 kubelet 不杀容器,只是通知 Service Controller 把 Pod 从 Endpoints 列表里摘掉.恢复后自动加回来.
|
| 场景 | liveness 结果 | readiness 结果 | 后果 |
|---|---|---|---|
| 正常运行 | ✅ 成功 | ✅ 成功 | 接收流量 |
| DB 连接池耗尽 | ✅ 成功(/healthz 还能回 200) | ❌ 失败 | 从 Service 摘除,不接新请求;等连接池恢复后自动加回 |
| 死锁 / 僵死 | ❌ 失败 | ❌ 失败 | 杀掉容器,重建 |
| 启动中(预热) | ❓ 由 startupProbe 覆盖 | ❌ 失败 | 不接流量直到就绪 |
常见配置错误
| 错误 | 为什么有问题 | 正确做法 |
|---|---|---|
| liveness 和 readiness 用同一个 endpoint | 数据库抖动 → readiness 摘除(正确)且 liveness 杀进程(错误) | liveness 用 /healthz(极简),readiness 用 /ready(可查依赖) |
| liveness 配得太敏感 | periodSeconds: 1, failureThreshold: 2 → 任何 GC pause 都可能触发误杀 |
periodSeconds: 10, failureThreshold: 3(给 30 秒窗口) |
| 慢启动无 startupProbe | 应用还在初始化就被 liveness 杀掉,陷入重启循环 | 加 startupProbe,failureThreshold 设足够大 |
| liveness 查外部依赖 | 依赖抖动 → 误杀应用 → 雪崩(大量 Pod 同时被重启) | 外部依赖检查只放在 readiness 里 |
Init Container:启动前的准备工作
Init Container 是在用户容器启动之前按顺序执行的一组容器.它们的核心特性:顺序执行,必须成功退出,可以有不同镜像.
|
关键特性一览:
| 特性 | 说明 |
|---|---|
| 顺序执行 | 1 → 2 → 3,前面失败后面不启动,整个 Pod 卡在 Pending 状态 |
| 必须退出 | Init Container 是任务型容器,完成就退出(exit 0);不持续运行 |
| 独立镜像 | Init Container 可以用 busybox,curl,python 等工具镜像,即使主容器用的是 distroless(无 shell)镜像 |
| 共享 Volume | 通过 Volume 把 Init Container 的产出(下载的配置,生成的证书等)传递给主容器 |
| 独立资源限制 | 每个 Init Container 有独立的 resources.requests/limits |
|
Init Container = 舞台搭建工,Main Container = 演员.
搭建工先把灯光,道具(DB 迁移,配置下载)准备好,全搞定了才退场.然后演员上台--他不需要知道灯光怎么搭的,只需要舞台是就绪的.搭建工和演员用不同的工具(不同镜像),互不干扰.
Sidecar 模式(K8s 1.28+)
传统上 Pod 内所有容器的 restartPolicy 是一样的.K8s 1.28 引入了容器级别的 restartPolicy,让 Sidecar 类型容器拥有独立于主容器的生命周期管理.
|
|
Sidecar 生命周期:
|
常见 Sidecar 场景:
| 场景 | 工具 | 做什么 |
|---|---|---|
| 服务网格 | Istio/Envoy proxy | 每 Pod 注入一个流量代理,接管进出流量,实现 mTLS,限流,路由 |
| 日志收集 | fluentd / filebeat | 读取应用日志文件并转发到日志中心 |
| 指标暴露 | Prometheus exporter | 暴露 /metrics 端点供 Prometheus 采集 |
| 配置热更新 | confd / consul-template | 监听配置中心变更,动态渲染配置文件并 reload 主容器 |
资源管理:requests 与 limits
每个容器都可以声明自己需要多少资源和最多能用多少资源.这两个值直接影响 Scheduler 的调度决策和 kubelet 的驱逐行为.
| 字段 | 含义 | 调度影响 | 运行时影响 |
|---|---|---|---|
| requests | "调度时保证给我的量" | Scheduler 只把 Pod 放到剩余资源 ≥ requests 的节点上 | 决定 QoS 等级 |
| limits | "你最多用这么多,不准超" | 不影响调度 | CPU 超限 → 被 throttle(限速);内存超限 → OOMKilled(杀容器) |
|
CPU 和内存超限行为不同
- CPU 是可压缩资源: 超了 limits 只会被 throttle(降速),容器不会被杀
- 内存是不可压缩资源: 超了 limits 就是 OOMKilled,容器直接被内核杀掉重启
limits.memory一定得配,不然一个内存泄漏的容器可能吃光整台机器的内存
QoS 等级与驱逐优先级
K8s 根据 requests 和 limits 的配置自动给 Pod 分配 QoS(Quality of Service)等级.当节点资源紧张时,kubelet 按这个优先级驱逐 Pod:
| QoS 等级 | 条件 | 被驱逐优先级 | 典型场景 |
|---|---|---|---|
| Guaranteed | 所有容器的 requests == limits(且都设了) | 最后被驱逐 | 核心数据库,支付服务 |
| Burstable | 至少一个容器设了 requests 或 limits,但不满足 Guaranteed 条件 | 中等 | 普通 Web 应用 |
| BestEffort | 没有任何容器设 requests 或 limits | 最先被驱逐 | 测试/开发环境,批处理任务 |
|
Pod 终止流程
当 Pod 被删除时(无论是手动 kubectl delete,滚动更新缩容,还是节点驱逐),K8s 执行一个优雅终止流程:
|
应用必须处理 SIGTERM 才能优雅关闭
很多应用(尤其是简单脚本)不处理 SIGTERM,导致 kubelet 等到宽限期结束直接 SIGKILL: 正在处理的请求丢失,导致数据不一致.
这需要你在代码里用 signal.Notify(Go),process.on('SIGTERM', ...)(Node.js)或 atexit + signal(Python)等机制来捕获并做清理.
可以自定义宽限期:
|
小结
本篇要点回顾
| 要点 | 一句话概括 |
|---|---|
| Pause 容器 | Pod 的"骨架":持有共享 Namespace,收割僵尸进程,最先启动最后退出 |
| 共享机制 | 共享 Network/IPC NS 和 Volume;Mount/PID/User/Cgroup NS 各自独立 |
| 生命周期五阶段 | Pending → Running → Succeeded/Failed(终态),不会自动回到 Running |
| startupProbe | 给慢启动应用争取时间;成功后才启动 liveness + readiness;防止误杀 |
| livenessProbe | 检测应用是否"活着但废了"(死锁等);失败 → 杀容器重启 |
| readinessProbe | 检测应用是否"能接流量";失败 → 从 Service 摘除(不杀容器) |
| Init Container | 按顺序执行的任务型容器;不同镜像,不同工具;产出通过 Volume 传给主容器 |
| Sidecar | 与主容器平级的辅助容器;K8s 1.28+ 原生支持独立生命周期和重启策略 |
| requests vs limits | requests 影响调度("保证给我的量"),limits 限制运行时("最多用这么多");内存超限 = OOMKill |
| QoS 等级 | Guaranteed(requests=limits)> Burstable > BestEffort;资源紧张时按此优先级驱逐 |
| SIGTERM vs SIGKILL | 优雅关闭 = SIGTERM + 等待宽限期 + 应用清理;超时 = SIGKILL 强杀 |
动手练习
- 在一个 Pod 里跑两个容器(nginx + busybox),用
kubectl exec分别进入两个容器,执行ip addr和curl localhost,验证它们共享同一个 IP 和 localhost. - 写一个 Deployment,给容器配置 startupProbe + livenessProbe + readinessProbe 三探针,故意让 readiness 失败(比如检查一个不存在的文件),观察
kubectl get endpoints的变化. - 配一个带 Init Container 的 Pod:Init Container 往
emptyDir写入index.html,主容器用 nginx 挂载并 serve 该文件.验证 Init Container 的产出能传递给主容器. - 给一个容器设
resources.limits.memory: "50Mi",用stress-ng --vm 1 --vm-bytes 100M模拟 OOM,观察 Pod 的 OOMKilled 状态和 restartCount 增长. - 思考题:如果 readinessProbe 和 livenessProbe 用同一个 HTTP endpoint 会有什么风险?画一个流量摘除 vs 容器重启的决策矩阵.