Kubernetes 基础体系 · 第 15/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Init、Sidecar 与 Ephemeral Container:启动顺序、共享和调试边界

在 Kubernetes 中,init containersidecar containerephemeral container 都是 Pod 内的容器,但它们解决的问题不同:

  • Init container:在应用容器启动前执行初始化任务,通常必须运行完成后才能进入下一步。
  • Sidecar container:与应用容器并行运行,为应用提供代理、日志采集、配置同步、证书刷新等辅助能力。
  • Ephemeral container:Pod 运行期间临时加入的调试容器,用于检查正在运行的容器和 Pod 环境。

三者最容易混淆的地方,不是 YAML 写法,而是以下三个边界:

  1. 启动顺序边界:哪些容器必须完成,哪些容器只需要启动,哪些容器根本没有启动顺序保证。
  2. 共享边界:容器是否共享网络、进程、文件系统和卷。
  3. 生命周期边界:容器失败时谁会重启,Pod 何时算失败,任务何时算完成,调试容器能否被删除。

Pod 是这些容器的共同边界

Pod 不是多个相互独立容器的简单集合。Pod 首先由 kubelet 创建一个运行时沙箱,通常包括:

  1. 为 Pod 创建网络命名空间;
  2. 创建 Pod IP;
  3. 准备 Pod 级别的卷;
  4. 启动 init container;
  5. 启动普通应用容器和 sidecar;
  6. 在运行期间根据重启策略处理容器退出;
  7. 删除 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 声明。它有三个核心语义:

  1. 多个 init container 按 initContainers 列表顺序执行;
  2. 当前 init container 必须成功完成,kubelet 才会启动下一个;
  3. 所有 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(inita)start(initb)finish(init_a) \rightarrow start(init_b)

finish(initb)start(initc)finish(init_b) \rightarrow start(init_c)

finish(initc)start(app)finish(init_c) \rightarrow start(app)

其中 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 顶级资源。它可以通过不同方式实现:

  1. 传统方式:把 sidecar 放入 spec.containers
  2. 原生 sidecar:把容器放入 spec.initContainers,并设置 restartPolicy: Always
  3. 某些平台或控制器自动注入 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。它具有两个重要特征:

  1. 它在应用容器之前启动;
  2. 它不会通过“退出成功”来完成初始化,而是作为长期运行的辅助容器继续存在。

可以把启动过程表示为:

创建 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 的原生生命周期规则。


三种容器的启动约束对比

设:

  • I1,I2,,InI_1, I_2, \ldots, I_n 表示普通 init container;
  • SS 表示原生 sidecar;
  • A1,A2,,AmA_1, A_2, \ldots, A_m 表示普通应用容器;
  • EE 表示运行期间加入的 ephemeral container。

普通 init container 的约束是:

success(Ik)start(Ik+1)success(I_k) \Rightarrow start(I_{k+1})

并且:

success(In)start(Ai)success(I_n) \Rightarrow start(A_i)

原生 sidecar 的约束通常是:

started(S)start(Ik+1) 或 start(Ai)started(S) \Rightarrow start(I_{k+1}) \text{ 或 } start(A_i)

如果配置了启动探针,则更精确地表示为:

startupProbeSuccess(S)start(Ik+1) 或 start(Ai)startupProbeSuccess(S) \Rightarrow start(I_{k+1}) \text{ 或 } start(A_i)

传统普通 sidecar 与应用容器之间没有类似的声明式启动顺序约束:

start(Sordinary)start(Ai)start(S_{ordinary}) \parallel start(A_i)

这里的 parallel 表示它们都属于普通容器启动阶段,不能用声明顺序推导先后关系。

Ephemeral container 则没有 Pod 创建时的启动位置:

runningPod+debugRequeststart(E)runningPod + debugRequest \Rightarrow start(E)

它是在 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 卷,例如:

  • emptyDir
  • configMap
  • secret
  • projected
  • 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

这要求:

  1. 应用确实写入该卷;
  2. 两个容器的路径挂载一致;
  3. 日志轮转、权限和文件锁策略兼容;
  4. 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、pscurl、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

预期过程:

  1. 创建 Pod sandbox;
  2. prepare 启动并退出 0;
  3. helper 启动并保持运行;
  4. app 启动;
  5. helperapp 并行运行;
  6. helper 的输出可以通过容器名查看。
kubectl logs lifecycle-demo -c helper
kubectl logs lifecycle-demo -c app

app 能读到 prepared,因为 preparehelperapp 都挂载了同一个 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

重点判断:

  1. 镜像是否拉取成功;
  2. 命令是否一直等待外部依赖;
  3. 卷是否正确挂载;
  4. 权限是否允许写入目标目录;
  5. 资源是否不足;
  6. 是否错误地把长期进程放成普通 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、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。