Kubernetes 基础体系 · 第 15/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Init、Sidecar 与 Ephemeral Container:启动顺序、共享和调试边界
在 Kubernetes 中,init container、sidecar container 和 ephemeral container 都是 Pod 内的容器,但它们解决的问题不同:
- Init container:在应用容器启动前执行初始化任务,通常必须运行完成后才能进入下一步。
- Sidecar container:与应用容器并行运行,为应用提供代理、日志采集、配置同步、证书刷新等辅助能力。
- Ephemeral container:Pod 运行期间临时加入的调试容器,用于检查正在运行的容器和 Pod 环境。
三者最容易混淆的地方,不是 YAML 写法,而是以下三个边界:
- 启动顺序边界:哪些容器必须完成,哪些容器只需要启动,哪些容器根本没有启动顺序保证。
- 共享边界:容器是否共享网络、进程、文件系统和卷。
- 生命周期边界:容器失败时谁会重启,Pod 何时算失败,任务何时算完成,调试容器能否被删除。
Pod 是这些容器的共同边界
Pod 不是多个相互独立容器的简单集合。Pod 首先由 kubelet 创建一个运行时沙箱,通常包括:
- 为 Pod 创建网络命名空间;
- 创建 Pod IP;
- 准备 Pod 级别的卷;
- 启动 init container;
- 启动普通应用容器和 sidecar;
- 在运行期间根据重启策略处理容器退出;
- 删除 Pod 时按生命周期规则终止容器。
在容器运行时中,Pod 通常还会有一个基础的 pause 或 sandbox 容器。它不承载用户业务,但帮助运行时持有 Pod 的共享网络命名空间。这个实现细节由容器运行时负责,不应把 pause 容器当作应用容器使用。
可以把 Pod 内容器关系抽象为:
flowchart TD
A[Pod 对象] --> B[Pod Sandbox]
B --> C[共享网络命名空间]
A --> D[Init Containers]
A --> E[应用容器]
A --> F[Sidecar Containers]
A --> G[Ephemeral Containers]
D --> H[顺序初始化]
E --> I[业务进程]
F --> J[辅助进程]
G --> K[临时调试进程]
C --> E
C --> F
C --> G
这里的“共享”不是“所有东西都共享”。Pod 共享的是一组明确的命名空间和可选卷;每个容器仍然拥有自己的镜像根文件系统、进程树边界和容器级配置。
Init container:按列表顺序完成初始化
Init container 的定义
Init container 通过 spec.initContainers 声明。它有三个核心语义:
- 多个 init container 按
initContainers列表顺序执行; - 当前 init container 必须成功完成,kubelet 才会启动下一个;
- 所有 init container 成功完成后,Pod 才会进入应用容器启动阶段。
假设有三个 init container:
spec:
initContainers:
- name: init-a
image: alpine:3.20
command: ["sh", "-c", "echo A; sleep 2"]
- name: init-b
image: alpine:3.20
command: ["sh", "-c", "echo B; sleep 2"]
- name: init-c
image: alpine:3.20
command: ["sh", "-c", "echo C; sleep 2"]
其执行约束可以表示为:
其中 finish(x) 表示容器以成功退出状态完成,而不是仅仅“进程已经启动”。
因此,可能的事件顺序是:
创建 Pod Sandbox
└─ 启动 init-a
└─ init-a 退出 0
└─ 启动 init-b
└─ init-b 退出 0
└─ 启动 init-c
└─ init-c 退出 0
└─ 启动应用容器
Init container 的失败路径
如果 init-b 退出非零,kubelet 不会启动 init-c,也不会启动普通应用容器:
init-a 成功
init-b 失败
init-b 被重启
init-b 再次失败
...
Pod 通常会显示类似状态:
STATUS: Init:CrashLoopBackOff
这不是应用容器的 CrashLoopBackOff。诊断时应先查看 init container:
kubectl describe pod <pod-name>
kubectl logs <pod-name> -c init-b
kubectl logs <pod-name> -c init-b --previous
--previous 用于查看上一次已经退出的容器实例日志。如果 init container 当前正在反复重启,当前日志和上一次日志可能分别包含不同阶段的信息。
Init container 的典型用途
Init container 适合那些具有明确“前置完成条件”的工作:
- 下载配置或证书到共享卷;
- 等待某个依赖服务可用;
- 执行数据库迁移;
- 创建目录、生成初始文件;
- 检查配置是否满足启动条件。
例如,使用共享卷生成配置:
apiVersion: v1
kind: Pod
metadata:
name: init-example
spec:
volumes:
- name: generated-config
emptyDir: {}
initContainers:
- name: generate-config
image: alpine:3.20
command:
- sh
- -c
- |
set -eu
printf 'mode=production\n' > /config/app.conf
volumeMounts:
- name: generated-config
mountPath: /config
containers:
- name: app
image: alpine:3.20
command:
- sh
- -c
- |
cat /config/app.conf
sleep 3600
volumeMounts:
- name: generated-config
mountPath: /config
创建并验证:
kubectl apply -f init-example.yaml
kubectl logs init-example -c generate-config
kubectl logs init-example -c app
预期应用日志包含:
mode=production
这里成立的原因不是两个容器共享根文件系统,而是它们都挂载了同一个 Pod 卷 generated-config。如果删除 volumeMounts,init container 写入的 /config/app.conf 位于它自己的容器文件系统中,应用容器无法直接看到。
Init container 不是“只运行一次的永久保证”
Init container 的“初始化完成”是相对于当前 Pod 生命周期的。
当 Pod 被删除并由控制器重新创建时,新 Pod 会重新执行 init container。Deployment 滚动更新、Job 重试、节点故障后的重新调度,都可能产生新的 Pod,从而再次执行初始化逻辑。
因此,init container 中的操作应考虑幂等性。例如:
if [ ! -f /data/initialized ]; then
initialize_database
touch /data/initialized
fi
不过,文件标记并不能自动解决所有并发问题。若多个 Pod 同时初始化同一个外部数据库,仅依赖本地文件通常不够,需要数据库锁、迁移工具的并发控制或外部协调机制。
还要区分两种情况:
- 容器在同一个 Pod 内重启:init container 是否重新执行,取决于 Pod 仍处于初始化阶段以及 kubelet 的处理过程;
- Pod 被重新创建:一定会按新 Pod 的 init container 顺序重新执行。
控制器通常不会“修复同一个 Pod 的 init 顺序”,而是通过创建新 Pod 恢复期望副本数。
Sidecar:概念模式与原生 API
Sidecar 的本质
Sidecar 是一种部署模式:把辅助进程放入与主应用相同的 Pod,使它们共享 Pod 的部分环境。
常见例子包括:
- 代理进程与业务进程共享网络命名空间;
- 日志采集器从共享卷读取业务日志;
- 配置同步器更新共享卷中的配置;
- 证书刷新器定期更新证书;
- 服务网格数据面代理拦截业务流量。
Sidecar 不是一个独立的 Kubernetes 顶级资源。它可以通过不同方式实现:
- 传统方式:把 sidecar 放入
spec.containers; - 原生 sidecar:把容器放入
spec.initContainers,并设置restartPolicy: Always; - 某些平台或控制器自动注入 sidecar。
“sidecar”描述的是角色;“原生 sidecar container”描述的是 Kubernetes 提供的生命周期语义。
传统 sidecar:能并行运行,但不能天然保证先启动
传统 sidecar 通常写成两个普通容器:
spec:
containers:
- name: app
image: example/app:1.0
- name: proxy
image: example/proxy:1.0
这两个容器都位于 spec.containers。Kubernetes 会尝试启动它们,但 Pod API 不提供“列表中的第一个普通容器必须先于第二个普通容器启动”的通用保证。
因此,以下推断是不成立的:
app 写在 proxy 前面
=> app 一定先启动
也不成立:
proxy 写在 app 前面
=> proxy 一定已经可以接受请求
即使 kubelet 在一次实际运行中看起来总是先启动某个容器,也属于实现观察,不是应依赖的 API 契约。
如何处理传统 sidecar 的依赖关系
如果应用必须等代理真正可用,可以让应用自身等待代理:
until wget -q -O- http://127.0.0.1:15000/healthz; do
sleep 1
done
exec /app/server
或者使用应用的 startupProbe、启动脚本和代理健康检查组合。这里的关键是:等待逻辑必须由应用或控制逻辑表达出来,不能靠 containers 列表顺序表达。
传统 sidecar 还存在一个终止问题。普通容器通常作为并行工作单元运行;当主应用退出时,sidecar 不一定会自动退出。对 Job 来说,这可能导致:
主应用成功退出
sidecar 仍然运行
Pod 没有满足完成条件
Job 一直不结束
如果 sidecar 只是普通容器,就需要额外的退出协调,例如:
- sidecar 监听共享文件并在应用完成后退出;
- 应用通过 HTTP、Unix socket 或信号通知 sidecar;
- 使用控制器或注入器提供专门的终止协调;
- 改用原生 sidecar 生命周期语义。
原生 sidecar:位于 initContainers,但不会完成退出
Kubernetes 原生 sidecar 使用如下形式:
apiVersion: v1
kind: Pod
metadata:
name: native-sidecar-example
spec:
initContainers:
- name: log-agent
image: busybox:1.36
restartPolicy: Always
command:
- sh
- -c
- |
while true; do
date
sleep 5
done
containers:
- name: app
image: alpine:3.20
command:
- sh
- -c
- |
echo "application started"
sleep 20
这里的 log-agent 仍然写在 initContainers 下,但它不是传统 init container,因为它声明了:
restartPolicy: Always
这种容器被 Kubernetes 识别为原生 sidecar。它具有两个重要特征:
- 它在应用容器之前启动;
- 它不会通过“退出成功”来完成初始化,而是作为长期运行的辅助容器继续存在。
可以把启动过程表示为:
创建 Pod Sandbox
└─ 启动原生 sidecar
└─ sidecar 进入运行状态
└─ 启动后续 init container
└─ 所有普通 init container 成功
└─ 启动应用容器
如果原生 sidecar 配置了 startupProbe,kubelet 会等待该启动探针成功后,再继续启动后续 init container 或应用容器。这样可以表达:
sidecar 进程已启动
并且 sidecar 已完成启动
=> 才允许应用启动
但应注意,“进程已启动”和“服务已可用”不是同一条件。没有 startupProbe 时,sidecar 进程刚进入运行状态,并不等于它已经完成监听端口、加载配置或建立上游连接。
一个带启动探针的示例:
apiVersion: v1
kind: Pod
metadata:
name: native-sidecar-probe
spec:
initContainers:
- name: proxy
image: nginx:1.27
restartPolicy: Always
startupProbe:
httpGet:
path: /
port: 80
periodSeconds: 2
failureThreshold: 30
containers:
- name: app
image: alpine:3.20
command: ["sh", "-c", "echo app-started; sleep 3600"]
这个配置表达的是:先启动 proxy,并等待其启动探针成功,再启动 app。探针失败时,应用容器不会因为自己的问题而被启动阻塞;阻塞点在 sidecar 启动阶段。
原生 sidecar 的终止顺序
Pod 终止时,原生 sidecar 具有专门的生命周期处理。应用容器结束后,原生 sidecar 会按与启动相反的顺序终止。多个原生 sidecar 的终止顺序与其声明顺序相关。
这比普通 sidecar 更适合 Job 或批处理 Pod,因为 sidecar 不必依赖业务进程通过共享文件自行协调退出。
但这不意味着任意程序都能立即优雅退出。容器内的进程仍必须正确处理终止信号;如果超过 Pod 的终止宽限期,kubelet 仍可能发送强制终止信号。
原生 sidecar 的版本边界
原生 sidecar 的 API 形态是:
initContainers:
- name: sidecar
restartPolicy: Always
它经历过 Kubernetes 的逐步引入和稳定化过程。使用生产集群时,应以目标集群的官方版本文档和 API discovery 为准,不能只根据客户端版本判断支持情况。
可检查集群版本:
kubectl version
kubectl get --raw /version
还应确认 API server 是否接受该字段:
kubectl explain pod.spec.initContainers.restartPolicy
如果目标集群版本较旧,可能出现以下问题:
- API server 拒绝未知字段;
- admission webhook 不理解原生 sidecar 语义;
- 注入器生成的 YAML 在不同版本表现不同;
- Job 完成和 sidecar 终止行为与当前集群不同。
不能把“传统 sidecar 放在 containers 中”与“原生 sidecar 放在 initContainers 中并设置 restartPolicy: Always”混为一谈。前者依赖应用或平台自行协调,后者依赖 Kubernetes 的原生生命周期规则。
三种容器的启动约束对比
设:
- 表示普通 init container;
- 表示原生 sidecar;
- 表示普通应用容器;
- 表示运行期间加入的 ephemeral container。
普通 init container 的约束是:
并且:
原生 sidecar 的约束通常是:
如果配置了启动探针,则更精确地表示为:
传统普通 sidecar 与应用容器之间没有类似的声明式启动顺序约束:
这里的 parallel 表示它们都属于普通容器启动阶段,不能用声明顺序推导先后关系。
Ephemeral container 则没有 Pod 创建时的启动位置:
它是在 Pod 已经存在后通过 API 动态加入的,因此不参与 init 阶段,也不能用来为应用提供启动前置条件。
Pod 中到底共享什么
共享网络命名空间
Pod 内普通容器、sidecar 和 ephemeral container 通常共享 Pod 网络命名空间。因此:
- 它们共享 Pod IP;
- 监听端口属于同一个网络命名空间;
- 容器之间可以使用
127.0.0.1通信; - Pod 内两个容器不能同时监听同一个地址和端口。
例如,sidecar 监听 127.0.0.1:8080,应用可以通过以下地址访问:
http://127.0.0.1:8080
这个地址不是 sidecar 容器自己的 loopback,而是 Pod 共享网络命名空间中的 loopback。
因此下面的配置可能冲突:
containers:
- name: app
ports:
- containerPort: 8080
- name: sidecar
ports:
- containerPort: 8080
containerPort 本身主要是元数据,不会直接创建监听;真正造成冲突的是两个进程都试图在同一个 Pod 网络命名空间中绑定 :8080。
共享卷,但不共享根文件系统
每个容器有自己的镜像根文件系统:
/app 容器的 /
/sidecar 容器的 /
它们不会自动看到对方在根文件系统中的文件。要共享文件,必须使用 Pod 卷,例如:
emptyDirconfigMapsecretprojected- PVC
- 其他受支持的卷类型
emptyDir 的生命周期与 Pod 绑定。Pod 被删除后,emptyDir 中的数据也会消失;容器在同一 Pod 内重启通常不会清空它。
共享 IPC、hostname 和进程视图
Pod 级别的命名空间共享不是所有维度都相同:
- 网络命名空间:Pod 内容器通常共享;
- IPC 命名空间:Pod 内容器通常共享;
- UTS/hostname:Pod 通常共享 Pod hostname;
- PID 命名空间:默认不一定共享,受
shareProcessNamespace影响; - 文件系统:默认不共享,必须通过卷显式共享。
如果设置:
spec:
shareProcessNamespace: true
Pod 内容器可以看到同一个进程命名空间中的进程。此时一个调试容器可以使用 ps 查看应用进程,并在满足权限条件时进行诊断。
apiVersion: v1
kind: Pod
metadata:
name: process-sharing-example
spec:
shareProcessNamespace: true
containers:
- name: app
image: alpine:3.20
command: ["sh", "-c", "sleep 3600"]
- name: inspector
image: alpine:3.20
command: ["sh", "-c", "sleep 3600"]
验证:
kubectl exec -it process-sharing-example -c inspector -- ps
未设置 shareProcessNamespace: true 时,容器通常只能看到自己的进程和运行时暴露的有限信息。不能仅因为两个容器属于同一个 Pod,就断定它们可以互相 kill、读取 /proc/<pid> 或使用 strace。
Sidecar 的共享模型决定了它能做什么
代理 sidecar
代理 sidecar 与应用共享网络命名空间,可以通过 localhost 通信:
应用 -> 127.0.0.1:15001 -> sidecar 代理 -> 上游服务
但如果应用没有按照代理约定发送流量,sidecar 不会因为“同一个 Pod”就自动获得业务语义。流量劫持还可能依赖 iptables、eBPF、CNI 或注入器配置。
日志 sidecar
日志 sidecar 通常通过共享卷读取应用写入的文件:
应用容器
└─ /var/log/app.log
│
└─ emptyDir: logs
│
└─ sidecar 容器读取 /logs/app.log
这要求:
- 应用确实写入该卷;
- 两个容器的路径挂载一致;
- 日志轮转、权限和文件锁策略兼容;
- sidecar 的退出不会阻塞 Pod 完成。
如果应用只把日志写到标准输出,sidecar 不会自动读取另一个容器的 stdout。此时应使用节点日志采集或明确的共享日志方案,而不是假设容器 stdout 会出现在共享文件系统中。
Ephemeral container:运行期间加入的调试容器
它不是 init container,也不是永久 sidecar
Ephemeral container 通过 Pod 的 ephemeralcontainers 子资源添加。它的设计目标是调试,而不是承载正常业务路径。
它具有以下边界:
- 可以在运行中的 Pod 中动态添加;
- 不参与 Pod 创建时的 init 顺序;
- 不应被用于正式业务服务;
- 不支持普通容器的全部字段;
- 通常不会像普通容器那样按 Pod 重启策略自动恢复;
- 一旦加入 Pod,通常不能像删除 Deployment Pod 那样单独从 Pod 规范中移除。
常用命令:
kubectl debug -it pod/<pod-name> \
--image=busybox:1.36 \
--target=<target-container-name> \
-- sh
例如:
kubectl debug -it pod/web-abc123 \
--image=nicolaka/netshoot:latest \
--target=web \
-- bash
参数含义:
-it:为交互式终端分配 stdin 和 TTY;--image:调试容器镜像;--target=web:请求调试容器加入目标容器相关的进程命名空间;--后面的内容:在调试容器中执行的命令。
--target 不等于共享所有内容
--target 主要用于指定目标容器,帮助运行时把调试容器放入适合检查目标进程的环境。它不意味着:
- 调试容器自动拥有目标容器的根文件系统;
- 调试容器自动获得目标容器的所有 Linux capability;
- 调试容器可以绕过 Pod 或容器的安全边界;
- 调试容器一定能看到目标进程。
目标进程命名空间功能依赖容器运行时实现。若运行时不支持,或者安全策略阻止加入,--target 可能不能达到预期效果。若需要 Pod 内所有容器共享进程视图,应在 Pod 创建时设置:
shareProcessNamespace: true
但这个字段无法事后安全地随意修改来改变一个已有 Pod 的进程命名空间布局。调试前没有设置时,通常只能依赖运行时的目标容器支持,或者使用复制 Pod 的方式进行离线检查。
查看调试容器
kubectl get pod <pod-name> -o jsonpath='{.spec.ephemeralContainers[*].name}'
kubectl describe pod <pod-name>
kubectl logs <pod-name> -c <ephemeral-container-name>
调试容器也可能因为镜像拉取失败、权限不足、节点资源不足而无法启动:
kubectl describe pod <pod-name>
重点查看:
Events中的镜像拉取错误;FailedCreatePodSandBox;CreateContainerError;PermissionDenied;OOMKilled;- 调试容器自身的状态和退出码。
Ephemeral container 的加入操作通常需要相应的 RBAC 权限,例如对 pods/ephemeralcontainers 子资源执行更新。即使用户拥有查看 Pod 的权限,也不一定拥有注入调试容器的权限。
调试容器的镜像和安全风险
生产应用镜像经常采用 distroless 或极简基础镜像,没有 shell、ps、curl、DNS 工具和调试符号。这正是 ephemeral container 有价值的原因:
kubectl debug -it pod/<pod-name> \
--image=nicolaka/netshoot:latest \
--target=<container-name> \
-- bash
但调试镜像不能随意使用:
- 镜像必须来自集群允许的镜像仓库;
- 镜像内容需要经过安全审计;
- 镜像中的工具可能读取网络凭据、环境变量或挂载卷;
- 过宽的
privileged、host namespace 或 capability 会扩大事故影响面; - 调试完成后,包含敏感信息的命令输出和日志应按安全要求处理。
尤其要避免把“为了调试方便”误写成长期 Pod 配置,例如:
securityContext:
privileged: true
特权调试容器可以显著扩大对节点和其他工作负载的访问能力。它不是普通排障命令的默认要求,应由集群安全策略、值班流程和授权机制共同约束。
Ephemeral container 不能替代修复应用
调试容器适合回答运行时问题:
Pod 能否解析 DNS?
Pod 能否连接某个 Service?
目标进程是否存在?
共享卷中是否有文件?
环境变量和挂载内容是否符合预期?
例如检查网络:
kubectl debug -it pod/<pod-name> \
--image=nicolaka/netshoot:latest \
--target=<app> \
-- bash
# 在调试容器中执行
ip addr
ip route
getent hosts kubernetes.default.svc
curl -v http://127.0.0.1:<port>/healthz
但它不能修复以下持久问题:
- Deployment 模板中的错误环境变量;
- 镜像中缺少依赖;
- Service selector 配错;
- Probe 路径或端口错误;
- 容器镜像架构不匹配;
- Pod 安全上下文与应用需求不兼容。
在调试容器中临时修改文件,只会改变当前 Pod 的运行状态;Pod 被重建后这些修改通常消失。正确修复应回到镜像、配置、控制器模板或外部依赖。
“复制 Pod 调试”与“加入原 Pod 调试”的区别
如果原 Pod 不能注入 ephemeral container,或者不希望修改线上 Pod,可以使用:
kubectl debug pod/<pod-name> \
--copy-to=<debug-pod-name> \
--share-processes \
--container=<container-name> \
--image=<debug-image> \
-- sh
这会创建一个新的调试 Pod,而不是向原 Pod 添加容器。两者的证据含义不同:
| 方式 | 观察对象 | 优点 | 主要限制 |
|---|---|---|---|
| 注入 ephemeral container | 原 Pod | 网络、卷和运行现场更接近真实状态 | 受 RBAC、运行时和安全策略限制 |
--copy-to 复制 Pod |
新 Pod | 可更换镜像、调整调试参数 | 不一定保留原 Pod 的全部运行时状态 |
普通 kubectl exec |
已有容器 | 影响最小 | 应用镜像可能没有工具 |
复制 Pod 可能改变 Pod 名称、IP、节点调度结果、Service 关联和控制器归属,因此不能把复制 Pod 的网络结果直接等同于原 Pod 的网络结果。
完整示例:普通 init、原生 sidecar 和应用
下面的 Pod 同时展示三种启动角色:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
shareProcessNamespace: true
volumes:
- name: shared
emptyDir: {}
initContainers:
- name: prepare
image: alpine:3.20
command:
- sh
- -c
- |
set -eu
echo "prepared" > /shared/state
volumeMounts:
- name: shared
mountPath: /shared
- name: helper
image: alpine:3.20
restartPolicy: Always
command:
- sh
- -c
- |
while true; do
echo "helper is alive"
sleep 5
done
volumeMounts:
- name: shared
mountPath: /shared
containers:
- name: app
image: alpine:3.20
command:
- sh
- -c
- |
cat /shared/state
echo "app is running"
sleep 3600
volumeMounts:
- name: shared
mountPath: /shared
执行:
kubectl apply -f lifecycle-demo.yaml
kubectl get pod lifecycle-demo -w
预期过程:
- 创建 Pod sandbox;
prepare启动并退出 0;helper启动并保持运行;app启动;helper和app并行运行;helper的输出可以通过容器名查看。
kubectl logs lifecycle-demo -c helper
kubectl logs lifecycle-demo -c app
app 能读到 prepared,因为 prepare、helper 和 app 都挂载了同一个 emptyDir 卷,而不是因为容器共享根文件系统。
查看进程:
kubectl exec lifecycle-demo -c app -- ps
kubectl exec lifecycle-demo -c helper -- ps
由于设置了 shareProcessNamespace: true,两个容器可以看到同一个 Pod 进程视图。若不设置该字段,这个观察结果通常会不同。
向正在运行的 Pod 添加调试容器:
kubectl debug -it pod/lifecycle-demo \
--image=busybox:1.36 \
--target=app \
-- sh
进入后可以尝试:
ps
cat /proc/1/cmdline
cat /shared/state
其中:
ps是否能看到应用进程,取决于进程命名空间和安全限制;/shared/state能否读取,取决于调试容器是否能访问该卷以及卷挂载配置;/proc/1/cmdline的含义取决于调试容器所在的 PID 命名空间,不能无条件认为它是应用进程。
Pod 状态如何反映三类容器
Init 阶段
普通 init container 尚未完成时,Pod 可能显示:
Init:0/2
Init:1/2
Init:CrashLoopBackOff
这些状态表示应用容器还没有正常进入运行阶段。此时查看应用容器日志往往没有意义,因为应用容器可能尚未创建。
应先执行:
kubectl describe pod <pod-name>
kubectl get pod <pod-name> -o jsonpath='{.status.initContainerStatuses}'
应用或普通 sidecar 阶段
当应用容器或普通 sidecar 反复退出时,可能看到:
CrashLoopBackOff
需要按容器分别查看:
kubectl logs <pod-name> -c app
kubectl logs <pod-name> -c sidecar
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses}'
不要只看 Pod 总体状态。一个 Pod 可以同时存在:
app: Running
sidecar: CrashLoopBackOff
这表示 Pod 的网络和卷可能仍然存在,但辅助能力已经不可用。
Ephemeral container 阶段
Ephemeral container 的状态出现在:
kubectl get pod <pod-name> -o jsonpath='{.status.ephemeralContainerStatuses}'
它的失败不一定让业务容器重启,也不一定改变控制器对应用副本数的判断。调试容器失败通常应单独诊断,而不是把它当成应用故障。
探针与启动顺序不是同一机制
探针主要描述容器运行后的健康状态:
startupProbe:应用是否完成启动;readinessProbe:是否可以接收流量;livenessProbe:是否需要被重启。
它们不能简单替代 init container。
例如,数据库迁移必须在应用进程启动前完成时,应该使用 init container。若只是应用启动较慢,可以使用 startupProbe:
containers:
- name: app
image: example/app:1.0
startupProbe:
httpGet:
path: /startup
port: 8080
periodSeconds: 5
failureThreshold: 60
startupProbe 失败时,kubelet不会立即按 livenessProbe 重启容器,而是等待启动探针成功或达到失败阈值。它表达的是“这个应用进程已启动但尚未准备好”,不是“另一个容器必须先完成”。
对于原生 sidecar,startupProbe 可以表达 sidecar 的启动完成条件;对于普通 sidecar,探针可以影响该 sidecar 自身状态,但不能自动建立应用容器的启动依赖。
常见错误与反例
反例一:用 init container 运行长期服务
initContainers:
- name: proxy
image: example/proxy:1.0
command: ["proxy", "run"]
如果该进程长期运行且不退出,普通 init container 永远无法完成,应用容器也不会启动。
修正方式有两种:
- 如果它是一次性初始化程序,让它最终成功退出;
- 如果它是长期辅助进程,使用原生 sidecar或普通 sidecar。
反例二:把普通 sidecar 写在应用前面来保证顺序
containers:
- name: proxy
- name: app
这不能构成 Kubernetes API 级别的启动顺序保证。应用仍可能先于代理完成初始化。
修正方式是:
- 使用原生 sidecar;
- 对原生 sidecar 配置合适的
startupProbe; - 或让应用显式等待代理就绪。
反例三:假设 Pod 内容器共享文件系统
# init container
echo token > /tmp/token
# app container
cat /tmp/token
这通常失败,因为两个 /tmp 属于不同容器的根文件系统层。
修正方式:
volumes:
- name: shared
emptyDir: {}
并让两个容器分别挂载该卷。
反例四:用 ephemeral container 修改 Deployment 配置
在调试容器中执行:
export LOG_LEVEL=debug
只会影响当前调试容器的环境,不能改变应用容器已经启动时的环境变量,也不能改变 Deployment 模板。调试容器适合观察和验证,不是控制器配置修改入口。
反例五:Job 中使用没有退出机制的普通 sidecar
spec:
restartPolicy: Never
containers:
- name: worker
image: example/worker:1.0
- name: log-sidecar
image: example/log-agent:1.0
worker 成功退出后,log-sidecar 仍可能运行,导致 Job 无法完成。应考虑原生 sidecar,或设计明确的 sidecar 退出协议。
排障时的顺序
Pod 卡在 Init
kubectl get pod <pod-name>
kubectl describe pod <pod-name>
kubectl get pod <pod-name> \
-o jsonpath='{range .status.initContainerStatuses[*]}{.name}{"\t"}{.state}{"\n"}{end}'
kubectl logs <pod-name> -c <init-container>
kubectl logs <pod-name> -c <init-container> --previous
重点判断:
- 镜像是否拉取成功;
- 命令是否一直等待外部依赖;
- 卷是否正确挂载;
- 权限是否允许写入目标目录;
- 资源是否不足;
- 是否错误地把长期进程放成普通 init container。
应用启动但 sidecar 不工作
kubectl get pod <pod-name> \
-o jsonpath='{.status.containerStatuses[*]}'
kubectl logs <pod-name> -c <sidecar>
kubectl exec <pod-name> -c app -- sh
kubectl exec <pod-name> -c app -- wget -qO- http://127.0.0.1:<sidecar-port>
检查共享网络和共享卷是否真的存在,不要只查看 YAML 中是否写了容器名。
需要检查线上进程
优先使用:
kubectl debug -it pod/<pod-name> \
--image=<approved-debug-image> \
--target=<container-name> \
-- sh
如果看不到目标进程,检查:
kubectl get pod <pod-name> -o jsonpath='{.spec.shareProcessNamespace}'
kubectl describe pod <pod-name>
同时确认运行时、RBAC 和安全策略是否允许目标命名空间加入。不能为了让 ps 生效就直接启用特权模式。
生产取舍
Init container 的取舍
Init container 把前置依赖显式化,失败后应用不会误启动;代价是外部依赖故障会阻塞整个 Pod。数据库迁移尤其需要幂等、超时和并发控制,否则可能让整个 Deployment 卡在初始化阶段。
原生 sidecar 的取舍
原生 sidecar 提供了更清晰的启动和终止语义,适合长期辅助进程和 Job 场景;代价是需要确认集群版本、运行时、注入器和准入策略的一致性。不能只在开发集群启用,然后把未验证的 YAML 直接部署到旧版本生产集群。
普通 sidecar 的取舍
普通 sidecar 兼容性较好、写法直观,但启动和终止协调通常需要应用、脚本或平台注入器自行实现。它适合辅助进程之间没有严格前置依赖,或者已有成熟生命周期协调机制的场景。
Ephemeral container 的取舍
Ephemeral container 能在不重建 Pod 的情况下接近现场调试,尤其适合网络、进程和卷问题;但它会改变 Pod 的实际运行形态,可能增加资源消耗和安全风险。调试完成后,应记录注入的容器、命令、镜像和观察结果,并避免将临时修改误认为永久修复。
最后的边界判断
遇到一个容器需求时,可以按以下因果关系判断:
必须在应用启动前执行,并且成功完成
=> 普通 init container
需要在应用启动前启动,并在应用运行期间持续存在
=> 原生 sidecar,或经过明确协调的普通 sidecar
只是希望在 Pod 已运行后临时检查现场
=> ephemeral container
需要和应用共享网络
=> 放入同一个 Pod,并使用 Pod 的网络命名空间
需要共享文件
=> 显式挂载同一个 Pod 卷
需要查看彼此进程
=> 创建 Pod 时考虑 shareProcessNamespace,
或确认运行时支持 target container 调试
需要保证辅助服务先于应用可用
=> 使用原生 sidecar 的启动语义和 startupProbe,
或让应用显式等待依赖
真正可靠的设计,不是记住三个字段的位置,而是先明确容器之间的约束:谁必须完成、谁只需启动、谁可以晚到、谁负责退出,以及哪些状态通过网络、进程命名空间或卷传递。只有这些约束被正确表达,Kubernetes 的生命周期控制和调试工具才会产生与预期一致的结果。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Pod 生命周期:Phase、Condition、RestartPolicy、终止和 Eviction
- 下一篇:Deployment 与 ReplicaSet:滚动更新、Revision、暂停、回滚和失败
- 延伸:Kubernetes Pod 完整模型:共享 Namespace、容器、Sandbox 和生命周期
- 延伸:Kubernetes 工作负载排障:Pending、CrashLoop、ImagePull、OOM 和 Probe
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论