第 4 章 Pod 与容器¶
配套实验手册:《Kubernetes 实验手册》(manual/ 目录)实验 02 「解析 Pod」(全部 10 个 Lab)。本章讲 Pod 与容器的全部核心机制——它是"在集群上跑应用"的第一章,也是后面所有章节(控制器/网络/存储/调度)的地基。
学习目标¶
学完本章,你应该能够:
- 解释"Pod 为什么是调度的最小单元",说清 Pod 内多个容器共享什么、为什么要共享
- 描述多容器 Pod 的三种协作模式(sidecar/适配器/大使)与各自场景
- 解释镜像拉取策略三种取值的行为差异,以及"为什么默认策略由镜像 tag 决定"
- 说清
command/command与 Dockerfilecommand/command的覆盖关系 - 解释 Init 容器的工作机制(顺序/失败/共享)与适用场景
- 完整讲解三种探针(startup/liveness/readiness)的职责与配合关系,以及三种探测方式的选择逻辑
- 描述容器优雅终止的完整流程(preStop → SIGTERM → grace period → SIGKILL)
- 区分
requests(调度承诺)与requests(运行时上限)两个不同机制,解释 CPU 与内存超限的不同后果 - 解释 Downward API 解决什么问题、能注入哪些元数据
- 走查一个 Pod 从提交到删除的完整生命周期
4.1 Pod 的本质:为什么是"最小调度单元"¶
4.1.1 第 2 章回顾:Pod 是"逻辑主机"¶
第 2 章讲过:Pod 是 Kubernetes 调度的最小单位,一个 Pod 包含一个或多个容器,它们共享网络命名空间(一个 IP)、共享存储卷、同生共死。现在深入"为什么这样设计"。
设计动机:有些应用场景,多个进程必须紧密耦合、同机共存:
- 主应用 + 日志采集器(sidecar):日志采集必须和主应用在同一台"机器"上才能读它的日志文件
- Web 服务 + 本地代理(如 Envoy):代理必须在主应用旁边才能拦截它的流量
- 应用 + 配置刷新器:定期从外部拉配置写入共享目录
如果把它们放成独立 Pod,就会遇到:IP 不同(没法 localhost 通信)、存储不共享、生命周期不同步(一个挂了另一个还活着)。Pod 把"这些进程必须绑在一起"的语义显式表达出来——调度器把整个 Pod 当作一个整体调度到同一节点。
4.1.2 Pod 的共享边界(到底共享什么)¶
| Pod 内共享 | 含义 |
|---|---|
| 网络命名空间 | 一个 Pod IP + 一个端口空间;容器间用 localhost 互访 |
| UTS 命名空间 | 共享主机名 |
| 存储卷 | Pod 级卷可被所有容器挂载(如共享日志目录) |
| 生命周期 | 一起调度、一起终止 |
不共享:PID 命名空间(默认不共享——容器间看不到对方的进程)、cgroups(各自独立的资源限制)。
第 2 章讲过 pause 容器:Pod 的"沙箱"持有这些共享命名空间,应用容器挂靠进来。Pod 的 IP 属于沙箱——这也是"删 Pod = 整个 Pod 消亡"的技术根源。
4.1.3 单容器 vs 多容器:三种协作模式¶
绝大多数 Pod 只有一个容器;需要多容器时,有约定俗成的三种模式:
- sidecar(边车):辅助主容器,如日志采集(filebeat)、指标暴露(prometheus-exporter)、本地文件同步。特点:跟在主应用旁边,增强而不侵入
- Adapter(适配器):把主容器的输出转换成统一格式,如把应用日志转成标准 JSON、把指标转成监控系统格式
- Ambassador(大使):代表主容器访问外部,如本地代理(连数据库的连接池代理、访问外部服务的代理)
决策逻辑:先问"能不能一个容器搞定?"——单容器最简单,运维成本最低;只有当进程必须同生命周期、共享本地资源时才拆多容器(sidecar 等模式)。
4.2 容器的配置要素¶
4.2.1 镜像与拉取策略(imagePullPolicy)¶
问题:节点上已经有镜像了,还要不要重新拉?
三种策略:
- IfNotPresent:本地有就用本地,没有才拉
- Always:每次都到仓库拉(检查 digest,有变化就更新)
- Never:只用本地镜像,绝不拉取(离线环境)
关键认知:默认策略由镜像 tag 决定:
- 镜像带具体版本(
nginx:1.27)→ 默认 IfNotPresent(版本不可变,本地有就不用重复拉) - 镜像用
:latest(或没写 tag)→ 默认 Always(latest 是"移动靶",每次启动都要确认最新)
这个默认行为的设计很巧妙:版本化镜像假设"tag 即契约"(不可变),latest 假设"始终要最新"。生产实践:永远用具体版本号 + IfNotPresent,避免
:latest的不可预测性。
4.2.2 命令与参数(command / args)¶
容器镜像里定义了默认启动命令(Dockerfile 的 ENTRYPOINT 和 ENTRYPOINT)。Kubernetes 可以覆盖它们:
| Kubernetes 字段 | 覆盖 Dockerfile 的 | 作用 |
|---|---|---|
command |
command |
替换启动程序 |
args |
args |
替换启动参数 |
覆盖规则(容易混淆,重点记忆):
- 只写
args→ 程序不变(用镜像的 ENTRYPOINT),只换参数 - 只写
command→ 程序换掉,参数用镜像默认 CMD(如果新程序不需要参数,要同时清掉 args) - 都写 → 程序和参数全换
为什么需要覆盖:同一镜像跑不同任务(如 busybox 镜像既当"睡眠容器"又当"一次性任务")、调试(进容器跑 shell)、镜像默认命令在集群环境不适用时。kubectl 命令式创建时用
--command -- <命令>(v1.36 必须带--command -- <命令>分隔符)。
4.2.3 环境变量¶
环境变量是给容器传配置的最直接方式:
- 静态值:
env: [{name: MODE, value: prod}]——写死在 Pod 定义里 - 动态来源:从 ConfigMap/Secret 引用(第 8 章)、从 Downward API 注入自身元数据(§4.5.4)
环境变量的局限:不适合传"文件型配置"(如整个配置文件);文件型用 ConfigMap 卷挂载(第 8 章)。
4.2.4 标签与注解(Pod 层面)¶
第 2 章讲过两者的区别(标签可被选择器选中、注解只是说明)。在 Pod 层面补充两点:
- Pod 的标签是"被管理的凭据":Deployment/Service 靠它选中 Pod——标签与选择器必须精确匹配,错了 Pod 就"没人管"(删了不重建、流量不进)
- 注解常用于声明"意图":如
kubernetes.io/change-cause(记录这次更新原因)、监控告警配置等——控制器/工具读取,但不参与选择
4.2.5 生产基线:SecurityContext 概览¶
问题:容器默认以 root 运行(第 1 章:容器内 root 与宿主机共享内核权限,被攻破有逃逸风险)。SecurityContext 是容器/Pod 的"安全设置区",声明降权与加固(第 12 章深入):
spec:
securityContext: # Pod 级:对 Pod 内所有容器生效
runAsNonRoot: true # 禁止以 root(UID 0)运行,否则拒绝启动
runAsUser: 1000 # 指定运行用户(UID)
containers:
- name: app
securityContext: # 容器级:只对本容器生效
readOnlyRootFilesystem: true # 根文件系统只读(防写入恶意文件)
capabilities:
drop: ["ALL"] # 丢弃全部 Linux 能力(最小能力原则)
| 字段 | 作用 | 防什么 |
|---|---|---|
runAsNonRoot + runAsNonRoot |
非 root 运行 | 容器逃逸、root 权限滥用 |
readOnlyRootFilesystem |
根文件系统只读 | 恶意文件写入(卷仍可写) |
capabilities.drop: ["ALL"] |
丢弃能力 | 危险内核能力(SYS_ADMIN 等) |
allowPrivilegeEscalation: false |
禁止提权 | 子进程提权 |
为什么在这里先提:SecurityContext 是容器配置的一部分(第 12 章会讲 PSA 如何强制它)——先建立"容器默认不安全、要声明降权"的基线认知,第 12 章"自觉 vs 强制"才立得住。
4.3 Init 容器:启动前的"先决条件执行器"¶
4.3.1 为什么需要 Init 容器¶
主容器启动前,常常有必须先完成的准备工作:
- 等待依赖就绪:数据库还没起来,主应用别急着连(如
while ! nc -z mysql 3306; do sleep 1; done) - 预置数据/权限:下载配置文件、初始化目录、设置文件权限
- 预热缓存:启动前先把缓存填充好
这些工作如果放进主容器:会和主进程争抢资源、逻辑混杂、且"启动慢导致探针误杀"(§4.4.2)。Init 容器的定位:在主容器之前按顺序完成准备,做完就退出。
4.3.2 工作机制¶
Pod 调度到节点
│
▼
Init 容器按声明顺序逐个执行:
init-1(等待数据库)──成功──► init-2(预置数据)──成功──► 主容器启动
│ 失败 │ 失败
▼ ▼
按重启策略重试(restartPolicy=Always 时整个 Pod 重启,重跑所有 init)
关键规则:
- 顺序执行:多个 Init 容器严格按声明顺序,前一个成功后下一个才启动
- 失败即重来:某个 Init 失败 → 整个 Pod 重启,所有 Init 容器从头再跑(不是从失败的继续)
- 共享卷:Init 容器与主容器共享 Pod 卷——预置的数据写在共享卷里,主容器直接读
- 独立镜像:Init 容器可以用与主容器不同的镜像(如主容器是精简运行时镜像,Init 用带工具的镜像下载数据)
- 与主容器隔离:Init 容器不参与探针检查、不占用主容器的资源声明
为什么"失败从头跑":Init 容器可能修改共享卷的中间状态——从第一个重跑保证"准备动作的确定性",避免半成品状态。
4.3.3 Init 容器 vs sidecar 容器¶
| 维度 | Init 容器 | sidecar 容器 |
|---|---|---|
| 运行时机 | 主容器之前,跑完即退出 | 与主容器并行长期运行 |
| 生命周期 | 一次性 | 与 Pod 同生共死 |
| 典型场景 | 等待依赖、预置数据 | 日志采集、代理 |
判断:"做完就撤"的用 Init,"长期伴随"的用 sidecar。
4.4 容器生命周期管理¶
4.4.1 容器的三种状态¶
kubelet 眼中的容器状态(kubectl get pod -o yaml 的 kubectl get pod -o yaml):
- Waiting:正在准备(拉镜像、创建等)
- Running:正在运行
- Terminated:已退出(正常/异常,含退出码)
退出码的意义(排障必读):0 = 正常退出;非 0 = 异常(1 通用错误、137 = SIGKILL(如内存超限被杀)、143 = SIGTERM)。看到 137 先怀疑内存超限(第 10 章排障展开)。
4.4.2 三种探针:各管一件事¶
Kubernetes 用探针(Probe)回答三个不同的问题:
readinessProbe(就绪探针)——"能用了吗?"
- 决定是否把流量发给这个 Pod(Service 后端列表是否包含它)
- 失败 → 从 Service 摘除(但 Pod 不重启)——服务"没准备好"时不让用户流量进来
- 典型场景:应用启动慢(要加载缓存、连数据库),ready 前流量不进
livenessProbe(存活探针)——"还活着吗?"
- 决定是否重启这个容器
- 失败 → kubelet 杀掉容器并按重启策略重建——自愈机制(死锁、内存泄漏、假死)
- 典型场景:进程活着但业务卡死(如死循环、goroutine 泄漏)
startupProbe(启动探针)——"开始检查了吗?"
- 决定 liveness/readiness 什么时候开始检查
- 启动阶段只查 startupProbe(成功后才开始查另外两个)——解决"慢启动应用被误杀":如果应用要 60 秒才起来,liveness 默认 10 秒就开始查,会把"还在启动"误判为"死了"
- 典型场景:JVM 启动、加载大模型、冷启动慢的应用
三者的配合关系(一个图讲清):
flowchart TD
start(["容器启动"]) --> sp{"startupProbe\n通过?"}
sp -- "否" --> wait["继续等待\n不查其他探针"]
wait --> sp
sp -- "是" --> rp{"readinessProbe\n通过?"}
rp -- "否" --> remove["从 Service 摘除\n流量不进入"]
rp -- "是" --> lp{"livenessProbe\n通过?"}
lp -- "否" --> restart["重启容器"]
restart --> sp
lp -- "是" --> running(["持续运行\n循环检查"])
running --> rp
style start fill:#E8F4FD,stroke:#4A90D9
style running fill:#E8F8E8,stroke:#5BA85B
style restart fill:#FDECEA,stroke:#D94F4F
style remove fill:#FFF3E0,stroke:#E08A3C
读图要点:startup 通过前另外两个探针都不查(慢启动保护);readiness 失败只摘流量不重启;liveness 失败才重启——三个探针的失败后果各不相同(摘除/重启/等待)。
探针的参数(速查):initialDelaySeconds(启动后等多久才第一次查)、periodSeconds(探测间隔)、timeoutSeconds(单次超时)、failureThreshold(连续几次失败才判定失败)、successThreshold(几次成功才判定成功)。
决策逻辑:三个探针不是都要配——最小化原则:只配必要的。readiness(大多数有流量的服务要配)、liveness(有死锁/泄漏风险的配)、startup(启动超过默认 10s 的必须配,否则可能被 liveness 误杀)。
4.4.3 探针的实现方式:怎么探测¶
| 方式 | 原理 | 适用 |
|---|---|---|
httpGet |
发 HTTP GET,状态码 200-399 视为成功 | HTTP 服务(最常用) |
tcpSocket |
尝试建立 TCP 连接,能连上即成功 | 非 HTTP 协议(数据库、Redis) |
exec |
容器内执行命令,exit 0 视为成功 | 无法用端口判断的场景(检查内部状态文件) |
grpc |
gRPC 健康检查协议(v1.24+) | gRPC 微服务(无需额外探针实现,走 gRPC 健康协议) |
选择逻辑:HTTP 服务用 httpGet(最贴近真实可用性);TCP 服务用 tcpSocket;都没有的用 exec;gRPC 服务用 grpc 探针。注意探测路径要选"真实反映可用性"的端点(如
/healthz,而不是只返回 200 的静态页)。gRPC 探针要求服务端实现 gRPC 健康检查协议(/healthz)。
4.4.4 生命周期钩子与优雅终止¶
postStart(启动后钩子):容器启动后立即执行的动作(如注册到注册中心、初始化脚本)。两个关键点:
- 与主进程并发执行——不是阻塞等待!主进程不会等 postStart 跑完
- 与探针无关——postStart 失败不会导致重启(只记录事件)
preStop(终止前钩子):容器被终止前执行的动作(如通知注册中心下线、排空连接、保存状态)。它是阻塞式的——必须执行完(或超时)才继续终止流程。
优雅终止的完整流程(这是"发布不中断"的关键机制):
flowchart TD
del(["删除 Pod\nkubectl delete / 滚动更新 / drain"]) --> term["① 状态 Terminating\n从 Service 摘除"]
term --> pre["② 执行 preStop 钩子\n(排空连接/反注册,阻塞)"]
pre --> sig["③ 向主进程发 SIGTERM\n(应用自己收尾)"]
sig --> grace["④ 等待宽限期\nterminationGracePeriodSeconds(默认 30s)"]
grace --> ok{"应用在期限内\n退出?"}
ok -- "是" --> done(["结束(优雅)"])
ok -- "否(超时)" --> kill["⑤ 发 SIGKILL 强杀"]
kill --> done
style del fill:#E8F4FD,stroke:#4A90D9
style pre fill:#FFF3E0,stroke:#E08A3C
style sig fill:#E8F4FD,stroke:#4A90D9
style kill fill:#FDECEA,stroke:#D94F4F
style done fill:#E8F8E8,stroke:#5BA85B
读图要点:先摘流量(不再进新请求)→ preStop 排空(处理存量)→ SIGTERM 优雅退出 → 超时才 SIGKILL 兜底——每一步都在为"不丢请求"服务。
为什么这套流程重要:生产发布/扩缩容/节点维护(drain)都走这个流程——优雅终止 = 不丢请求。应用要配合:捕获 SIGTERM 做收尾(关连接、刷数据);如果收尾需要超过 30 秒,调大 terminationGracePeriodSeconds(实验手册(实验 02) Lab 9 实测 5.8s 完整流程)。
4.4.5 重启策略(restartPolicy)¶
容器失败/退出后要不要重启,由 restartPolicy 决定:
- Always(默认):任何退出都重启——Deployment 等长期服务用
- OnFailure:只有异常退出(非 0)才重启——Job 等任务用
- Never:绝不重启——一次性任务(如批处理,跑完就完)
注意:重启策略是 Pod 级的;由控制器管理的 Pod 通常用默认 Always,Job 场景改为 OnFailure/Never。
4.5 资源模型:requests 与 limits¶
4.5.1 两个不同的机制(全书最容易混的点)¶
容器可以声明两种资源约束,它们作用在不同环节、由不同组件执行:
- requests(请求量)——调度承诺:告诉调度器"这个容器至少需要多少"——决定 Pod 落在哪个节点(scheduler 过滤依据,第 3 章讲过)
- limits(上限)——运行时限制:告诉 kubelet"最多能用多少"——决定容器能跑多快/会不会被杀(运行时执行)
CPU 与内存的本质区别(重要):
- CPU 是可压缩资源:超限 → 节流(throttling)——跑慢点,但不会死
- 内存是不可压缩资源:超限 → OOM 被杀(SIGKILL,退出码 137)——没有"慢一点"选项
这就是为什么"内存 limit 必须设、CPU limit 可选":不设 CPU limit 只是不节流(可接受);不设内存 limit,一个泄漏的容器可以把整台节点拖垮(影响同节点的其他 Pod)——生产上内存 limits 是底线。
4.5.2 不设置会怎样¶
- 只有 requests 没 limits:调度有保障,但运行时不受限(可超用节点资源)
- 只有 limits 没 requests:limits 隐式等于 requests(Kubernetes 自动补)——注意这个隐含行为
- 都没有:调度"任何节点都行"(可能挤爆别人),运行时无上限(风险)
4.5.3 requests/limits 与 QoS 等级¶
Kubernetes 根据资源声明把 Pod 分成三个服务质量等级(QoS),决定节点资源紧张时谁先被杀:
- Guaranteed(保证):requests = limits(都设且相等)——优先级最高,最后被杀
- Burstable(可突发):requests < limits,或只设 requests——有最低保障,可超用,中间
- BestEffort(尽力而为):什么都没设——最容易被杀
生产实践:核心服务配 Guaranteed(requests=limits);一般服务 Burstable;BestEffort 只给测试任务。
4.5.4 Downward API:Pod 怎么"认识自己"¶
问题:容器内部怎么知道自己的 Pod 名、命名空间、IP、所在节点?
Downward API:把 Pod 自身的元数据注入容器(环境变量或文件)——注意它注入的是"自身信息",不是外部配置(外部配置用 ConfigMap,第 8 章):
- 环境变量方式:Pod 名、命名空间、节点名、Pod IP(部分字段限制:label/annotation 不能进 env)
- 文件方式(volume):label/annotation 等全部字段(挂载成文件,支持热更新)
典型用途:应用上报日志时带"我是哪个 Pod"、监控系统标记来源、按 Pod 标签决定行为。
4.6 走查:一个 Pod 的完整生命周期¶
把本章所有机制串起来,走查一个 Deployment 管的 Pod 从生到死:
① 提交:kubectl apply deployment.yaml(期望:1 副本,带 readiness+liveness 探针)
│
▼
② 调度:scheduler 按 requests 过滤打分,选中 node2
│
▼
③ 创建:kubelet 建 pause 沙箱 → 拉镜像 → 创建容器(Waiting:Pulling)
│
▼
④ 启动:容器 Running;postStart 钩子并发执行(注册/初始化)
│
▼
⑤ 探针:startup 成功后 → readiness 成功 → 加入 Service 后端,开始接流量
│ (liveness 持续检查:失败 → 重启容器,自愈)
│
▼
⑥ 更新/删除(滚动更新、扩容缩容、节点维护 drain):
│ 从 Service 摘除 → preStop 钩子(排空)→ SIGTERM → 优雅退出
│ 超时 → SIGKILL
▼
⑦ 回收:Pod 对象删除;如果内存超限,中途可能已因 OOM 被 SIGKILL(退出码 137)
对照第 2 章控制循环:这个 Pod 的生死由 ReplicaSet 控制器监视——它死了,控制器立刻补一个新的(自愈)。
4.7 实验演练指引¶
本章机制对应实验手册:
- 实验 02 「解析 Pod」(7 个 Lab):极简创建 → 多容器 → Init 容器 → 拉取策略 → 环境变量 → command/args → 标签注解
- 实验 02 Lab 8(探针):readiness 摘流量、liveness 重启,亲眼验证探针行为
- 实验 02 Lab 9(钩子与优雅终止):preStop 生效、实测完整终止流程 5.8s
- 实验 02 Lab 10(资源限制):requests/limits 生效、超限节流与 OOM
教学建议:先理解 4.1-4.5 的机制,再按实验手册逐个验证——每个 Lab 的"观察点"都对应本节一个机制。
本章小结¶
- Pod 是调度最小单元:多容器共享网络/IP/存储/生命周期;三种协作模式(sidecar/适配器/大使)
- 容器配置:镜像策略默认由 tag 决定(版本化→IfNotPresent、latest→Always);command/args 覆盖 ENTRYPOINT/CMD;env 传简单配置,文件型走 ConfigMap
- Init 容器:主容器前顺序执行、失败从头重跑、共享卷预置数据——"做完就撤"
- 探针三兄弟:readiness(摘流量)/ liveness(重启)/ startup(慢启动保护);httpGet/tcpSocket/exec 三种探测方式按协议选
- 优雅终止:摘流量 → preStop → SIGTERM → grace period → SIGKILL——发布不丢请求的关键
- 资源模型:requests 管调度、limits 管运行;CPU 可压缩(节流)、内存不可压缩(OOM 137);QoS 三档决定被杀顺序;内存 limits 是生产底线
- Downward API:注入 Pod 自身元数据(env/文件两种方式)
- 生命周期走查:提交→调度→创建→探针→接流量→优雅终止→回收,环环相扣
衔接:第 5 章讲"管 Pod 的人"——Deployment 等控制器如何基于本章的 Pod 机制实现滚动更新、扩缩容与自愈。
思考题¶
- 为什么"内存超限"比"CPU 超限"危险?分别会发生什么(提示:可压缩 vs 不可压缩)?
- 一个应用启动需要 90 秒,直接配 liveness 会发生什么?startup 探针怎么解决?
- preStop 钩子里
sleep 5的常见用途是什么?如果应用收尾需要 60 秒,要改什么配置? - Init 容器与 sidecar 容器都"在主容器旁做事",它们的本质区别是什么?
- 为什么"只设 requests 不设 limits"(内存)在生产上是危险的?
- 镜像带
:latest时默认拉取策略是 Always——这带来什么风险?怎么规避?
CKA 考点标注(对应域 2:工作负载与调度 15%): - 必考配置:探针三件套(readiness/liveness/startup)与参数、资源 requests/limits、restartPolicy、imagePullPolicy - 必考命令:
kubectl run --command --(v1.36 分隔符)、kubectl run --command --、kubectl run --command --(看探针/事件) - 必考机制:优雅终止流程(preStop/SIGTERM/SIGKILL)、OOM 退出码 137、Init 容器顺序执行 - 域 5 排障(30%)大量题目围绕本章:CrashLoopBackOff(探针失败/退出码)、ImagePullBackOff(拉取策略/镜像名)