
Pod 里的应用容器并不总能直接启动。有些应用需要先生成配置文件、等待依赖服务被发现，或者执行一次初始化脚本。把这些动作塞进应用镜像或入口脚本，通常会让镜像职责变得模糊，也让应用本身承担不必要的启动判断。

Kubernetes 提供了 `initContainers` 字段来表达这类「启动前必须完成的工作」。本文先说明它在 Pod 生命周期中的位置，再回答三个容易混淆的问题：失败后会发生什么、普通 init container 是否支持 liveness/readiness/startup probe，以及它和原生 sidecar 到底有什么区别。

文中的源码分析基于本地 Kubernetes 仓库的 commit [`c028ba348dbaea5e8b0df94b2581e70d687a77c8`](https://github.com/kubernetes/kubernetes/tree/c028ba348dbaea5e8b0df94b2581e70d687a77c8)，实验文件固定在 [k8sdev `0bb95c0c`](https://github.com/jimyag/k8sdev/tree/0bb95c0c853cfd267b05569325c45d1c2f5d956b/init-container)。源码和文档的具体版本可能继续变化，文章中的结论以这两个提交为准。

先把结论列出来：

1. 普通 init container 在 Pod 被调度到节点、Pod sandbox 的网络和存储准备好之后运行；多个 init container 严格按照 `spec.initContainers` 的顺序执行。
2. 只有进程以退出码 `0` 结束，kubelet 才会启动下一个 init container；全部普通 init container 成功后，应用容器才会启动。
3. 普通 init container 失败时，`restartPolicy: Always` 或 `OnFailure` 会让 kubelet 重试；`restartPolicy: Never` 不会重试，Pod 最终进入 `Failed`，应用容器不会启动。
4. 普通 init container 不支持 `lifecycle`、`livenessProbe`、`readinessProbe` 和 `startupProbe`。它的「就绪」条件不是探针，而是成功退出。
5. init 阶段会阻止 Pod 进入可服务状态，但 init container 的端口不会自动成为 Service 的后端。应用容器启动后，Pod 是否接收流量仍由应用容器的 readiness 状态决定。
6. 当前 Kubernetes 还支持把 `initContainers` 中设置了容器级 `restartPolicy: Always` 的条目作为原生 sidecar。它会持续运行并支持探针，不应再按普通 init container 理解。

<!--more-->

## init container 解决什么问题

普通容器描述的是「Pod 运行期间持续提供服务的进程」。init container 描述的是「应用启动前必须完成的一次性步骤」。两者都使用 `Container` 的大部分字段，也都可以使用独立镜像、环境变量、Secret、ConfigMap 和 volume；区别在于 kubelet 对它们的启动顺序和完成条件不同。

常见的使用场景有三类：

- 使用一个很小的工具镜像生成配置、模板或证书，再通过共享 volume 交给应用容器。
- 在应用启动前等待一个外部前置条件，例如等待 Service 的 DNS 名称出现，或等待某个初始化任务完成。
- 把只在初始化阶段需要的工具放在独立镜像里，不把 `curl`、`dig`、模板工具和调试脚本长期放进应用镜像。

它适合表达「不满足这个前置条件，应用就不应该启动」。如果条件只影响流量接收、不应该导致应用被重启，就应该使用应用容器的 readiness probe，而不是让 init container 无限等待。

## 它在 Pod 的哪个阶段触发

创建 Pod 对象时，API Server 只保存 `spec`，不会直接在 API Server 所在节点执行 init container。典型链路是：

```mermaid
flowchart LR
    API["创建 Pod 对象"] --> Schedule["Scheduler 选择节点"]
    Schedule --> Kubelet["节点 kubelet 接收 Pod"]
    Kubelet --> Sandbox["创建 Pod sandbox\n准备网络和存储"]
    Sandbox --> Init1["init-1\n按顺序运行"]
    Init1 --> Init2["init-2\n前一个成功后才运行"]
    Init2 --> App["启动应用容器"]
    App --> Ready["应用容器探针\n决定 Pod Ready"]
    Init1 -. "退出非 0" .-> Retry["按重启策略重试\n或结束 Pod"]
    Retry --> Init1
```

Kubernetes 官方文档把 init container 描述为在应用容器之前运行、并且必须运行到完成的容器。源码中，kubelet 的 runtime manager 在创建 Pod sandbox 时把第一个 init container 放入 `InitContainersToStart`；后续同步时，`computeInitContainerActions` 根据前一个 init container 的状态决定是重试当前容器，还是启动下一个容器。

这个顺序有两个直接结果：

1. **init container 还在运行时，应用容器不会进入正常启动流程。** 应用容器的状态通常会显示为 `Waiting`，原因可能是 `PodInitializing`；Pod phase 通常仍是 `Pending`。
2. **init container 是串行的。** 即使两个初始化动作互不依赖，也不能让普通 init container 并行执行。若它们确实需要并行，应合并脚本，或者重新设计成应用容器/sidecar 之间的显式协作。

多个 init container 的最小结构如下：

```yaml
# 这是 Pod 中普通 init container 的最小结构。
apiVersion: v1 # Pod API 版本。
kind: Pod # 直接创建一个 Pod 便于观察生命周期。
metadata:
  name: init-sequence-demo # Pod 名称。
spec:
  initContainers: # 按数组顺序逐个执行的初始化容器。
    - name: prepare-data # 第一个初始化步骤。
      image: busybox:1.36.1 # 只放初始化步骤需要的工具。
      command: ["sh", "-c", "echo prepare-data"] # 退出码 0 才会推进到下一个步骤。
    - name: check-data # 第二个初始化步骤。
      image: busybox:1.36.1 # 使用独立镜像检查结果。
      command: ["sh", "-c", "echo check-data"] # 第一个步骤失败时不会执行。
  containers:
    - name: app # 全部 init container 成功后才启动的应用容器。
      image: nginx:1.27-alpine # 应用镜像。
```

## init container 失败后会怎样

普通 init container 的成功标准是进程终止且退出码为 `0`。它不需要、也不能通过 readiness probe 来声明「准备好了」。如果进程退出码非 `0`，或者容器因为镜像拉取、创建或运行时错误没有正常启动，kubelet 会把初始化视为未完成。

### `Always` 和 `OnFailure`：继续重试

Pod 的 `restartPolicy` 默认是 `Always`。对于普通 init container，成功退出后不会因为 `Always` 被重新启动；`Always` 在 init 阶段实际表现为「失败时继续重试」。`OnFailure` 也会重试失败的 init container。重试之间会有 kubelet 的退避时间，因此一个持续失败的 Pod 不会无间隔地创建容器。

在重试期间，后面的 init container 和应用容器都不会启动。常见的观察结果是：

```text
NAME                          READY   STATUS                  RESTARTS   AGE
init-container-failure-retry  0/1     Init:CrashLoopBackOff   0          45s
```

这里的 `0/1` 不是「应用容器 readiness probe 失败」，而是应用容器还没有进入运行阶段。应优先查看 init container 的日志和 `initContainerStatuses`：

```sh
# 查看 init container 的状态、退出码和最近一次终止原因。
kubectl get pod init-container-failure-retry \
  -o jsonpath='{.status.initContainerStatuses[0]}{"\n"}'

# 查看当前或上一轮 init container 的日志。
kubectl logs init-container-failure-retry -c fail-on-purpose
kubectl logs init-container-failure-retry -c fail-on-purpose --previous

# 查看 kubelet 和镜像拉取相关事件。
kubectl describe pod init-container-failure-retry
```

### `Never`：Pod 进入 `Failed`

如果 Pod 使用 `restartPolicy: Never`，普通 init container 的失败不会在同一个 Pod 中重试。kubelet 会停止初始化流程，Pod phase 进入 `Failed`，应用容器不会启动。官方文档也特别强调了这个差异：init container 失败时，`Never` 会把整个 Pod 视为失败。

这时 init container 的终止状态仍然保留在 `.status.initContainerStatuses` 中，退出码、reason 和 message 是排查失败原因的首要证据。一次性初始化任务可以使用这种语义，但如果它属于 Job，通常还要结合 Job 的 `backoffLimit` 和任务级结果来设计，而不是只依赖裸 Pod。

### 重试与重新执行要求幂等

init container 可能因为非 0 退出、节点故障、Pod 被重新创建或 Pod sandbox 重建而再次执行。因此初始化脚本应当可以安全地重复运行：

- 创建目录前使用 `mkdir -p`，写文件时明确覆盖或原子替换策略。
- 数据库迁移使用版本表、事务或唯一约束，避免重复执行产生破坏性结果。
- 向外部系统注册时使用幂等 key，或者在执行前检查已有记录。
- 等待依赖时设置合理的超时和失败信息，不要让一个永远无法满足的 DNS、证书或远端 API 把 Pod 永久卡在 Pending。

如果需要给整个 Pod 的初始化设置上限，可以使用 `activeDeadlineSeconds`。它包含 init 阶段的时间，但它是 Pod 的总活动期限；Pod 已经正常运行后仍可能受到这个字段影响。对一次性任务可考虑使用 Job 的时间限制，对长期运行的 Deployment 则要谨慎设置。

## 普通 init container 受 liveness/readiness 限制吗

受限制，而且限制是 API 校验层面的，不是 kubelet 运行时的约定。

普通 init container 不支持以下字段：

| 字段 | 普通 init container | 应用容器中的作用 |
| --- | --- | --- |
| `lifecycle` | 不允许 | 启动或停止时执行钩子 |
| `livenessProbe` | 不允许 | 失败后让 kubelet 重启容器 |
| `readinessProbe` | 不允许 | 失败时从 Service 后端摘除 |
| `startupProbe` | 不允许 | 成功前暂停 liveness/readiness，并给慢启动容器更长时间 |

源码中的 `validateInitContainers` 对普通 init container 逐项拒绝这些字段。因此下面的配置不是「探针会失效」，而是提交 Pod 时就应当得到校验错误：

```yaml
# 这个片段用于说明禁止关系，不应作为可部署配置。
apiVersion: v1 # Pod API 版本。
kind: Pod # 说明校验发生在 Pod 规范上。
metadata:
  name: invalid-init-probe # 仅用于示例名称。
spec:
  initContainers:
    - name: regular-init # 未设置容器级 restartPolicy=Always，属于普通 init container。
      image: busybox:1.36.1 # 初始化镜像。
      readinessProbe: # 普通 init container 不能配置 readinessProbe。
        exec:
          command: ["sh", "-c", "exit 0"] # 这个命令不会绕过 API 校验。
  containers:
    - name: app # 至少需要一个应用容器。
      image: nginx:1.27-alpine # 应用镜像。
```

普通 init container 的完成状态由进程退出码表达。kubelet 在生成状态时，会把「已终止且退出码为 0」的普通 init container 视为完成，并据此生成 `PodInitialized=True`；只要还有 init container 未完成，应用容器就不会被正常检查和启动。

这和应用容器的 readiness 是两件事：

- **初始化阶段**：看 init container 是否成功退出，决定能否继续创建后续容器。
- **运行阶段**：看应用容器的 readiness probe 和 Pod readiness gate，决定 Pod 是否可以接收 Service 流量。
- **存活阶段**：看应用容器的 liveness probe，失败时重启该应用容器。应用容器在同一个 Pod 中被 liveness 重启，通常不会重新执行已经完成的普通 init container。

因此，[`01-shared-volume.yaml`](https://github.com/jimyag/k8sdev/blob/0bb95c0c853cfd267b05569325c45d1c2f5d956b/init-container/01-shared-volume.yaml) 里的 readiness/liveness probe 都配置在 nginx 上，而不是配置在 `generate-index` 上：init 负责生成页面并退出，nginx 负责回答「页面是否已经能服务」以及「服务进程是否仍然存活」。

### 当前版本的例外：原生 sidecar

从支持 sidecar container 的 Kubernetes 版本开始，`initContainers` 中的条目可以设置容器级 `restartPolicy: Always`。这种条目仍然写在 `initContainers` 数组里，但语义已经不是普通的一次性 init container：

- 它会在 init 序列中启动，但启动后持续运行，不等待它退出才启动后续容器。
- 如果配置了 `startupProbe`，需要先通过 startup probe，后续 init 容器才会继续。
- 它可以使用 liveness/readiness/startup probe，并在应用容器运行期间继续作为 sidecar 工作。
- Pod 终止时，sidecar 的停止顺序也与普通 init container 不同。

当前本地源码的 API 定义和校验逻辑都明确区分了这条路径：只有 `restartPolicy: Always` 的 init 条目才进入 sidecar 的探针校验分支。文章后面的实验全部是普通 init container，不依赖 sidecar 特性。

## Pod Ready、Service 流量与 init container 端口

「init container 成功」只表示 Pod 具备启动应用容器的条件，不表示应用已经可以接收流量。完整状态至少要区分三个字段：

| 状态 | 含义 | init 阶段的典型值 |
| --- | --- | --- |
| `status.phase` | Pod 的粗粒度阶段 | `Pending` |
| `status.conditions[Initialized]` | 所有 init container 是否完成 | `False` |
| `status.conditions[Ready]` | 所有应用容器和 readiness gate 是否满足 | `False` |

全部普通 init container 成功后，kubelet 才启动应用容器。应用容器没有 readiness probe 时，运行中的容器通常会被视为 ready；配置了 readiness probe 后，只有探针成功，Pod 才会进入 `Ready=True`，Service 才会把它作为可用后端。readiness 失败会摘除 Service 流量，但不会像 liveness 失败那样重启容器。

init container 的端口也不会被聚合到 Pod 的 Service 端点。它们是启动阶段的临时容器，不是 Service 的长期服务进程。需要被 Service 访问的端口，必须由应用容器提供，并由应用容器的 readiness 状态保护。

## 最常见的使用方式：共享 volume

init container 与应用容器共享 Pod 的 volume，是最容易验证也最实用的模式。下面的流程是：

1. `generate-index` 把页面写入 `emptyDir`。
2. init container 以退出码 `0` 结束。
3. kubelet 启动 nginx，并把同一个 `emptyDir` 挂载到 nginx 文档根目录。
4. nginx 的 readiness probe 请求 `/index.html`，成功后 Pod 才 ready。

实验文件使用的关键结构如下：

```yaml
# init container 与应用容器通过同一个 emptyDir 交换生成的文件。
spec:
  volumes:
    - name: web-content # Pod 级别的共享卷名称。
      emptyDir: {} # Pod 删除后卷内容一起删除。
  initContainers:
    - name: generate-index # 只运行一次并生成页面的容器。
      image: busybox:1.36.1 # 初始化工具镜像。
      command: ["sh", "-c", "printf 'ok\n' > /work/index.html"] # 成功退出。
      volumeMounts:
        - name: web-content # 引用上面的共享卷。
          mountPath: /work # 把生成文件写入共享目录。
  containers:
    - name: nginx # 长期运行的应用容器。
      image: nginx:1.27-alpine # 应用镜像。
      readinessProbe: # 应用启动后由该探针决定 Pod Ready。
        httpGet:
          path: /index.html # 检查 init 生成的文件是否能通过 nginx 提供。
          port: 80 # nginx 的 HTTP 端口。
      volumeMounts:
        - name: web-content # 仍然引用同一个共享卷。
          mountPath: /usr/share/nginx/html # nginx 的文档根目录。
```

这里的 `emptyDir` 只适合 Pod 生命周期内的数据交换。它不是持久化存储；如果初始化结果必须跨 Pod 重建保留，应使用 PVC、ConfigMap、Secret 或外部存储，并仍然考虑重复执行和并发更新。

## 等待依赖服务：可以用，但要有边界

等待依赖是官方文档给出的典型用法。init container 可以循环执行 DNS 查询，直到目标 Service 出现，然后以成功退出结束。它的好处是应用容器不需要内置一段「启动前先探测依赖」的特殊逻辑。

但等待脚本要避免三个问题：

1. **无限等待**：依赖名称写错或依赖永远不会创建时，Pod 会一直 Pending。应配合超时、重试次数或 `activeDeadlineSeconds`。
2. **把可恢复依赖当成硬前置条件**：如果应用启动后可以优雅地等待数据库恢复，就让应用容器启动，并用 readiness 控制流量；只有确实不能启动的依赖才适合放进 init container。
3. **把 DNS 可解析当成服务可用**：DNS 只证明 Service 名称存在，不证明后端有 Endpoint、端口可连接或业务已经完成迁移。需要更强的条件时，应在 init 脚本里做有超时的 TCP/HTTP/业务检查。

## 开源组件中的真实用法

前面的例子是应用自己定义 init container。很多开源组件也大量使用同一机制，但它们等待或准备的对象更具体：可能是 Pod 网络规则、节点上的 CNI 文件、存储设备状态，或者共享 volume 中的 Secret 文件。

这些组件有一个共同点：初始化动作必须发生在主进程启动之前，而且失败时宁可让 Pod 停在 init 阶段，也不能让主进程以一个不完整的环境启动。下面几个例子分别对应不同的使用方式。

### Istio：在 Envoy 启动前设置流量转发规则

在 Istio 的 sidecar 模式中，注入器会向业务 Pod 加入 `istio-init` 和 `istio-proxy`。`istio-init` 在 Pod 的网络命名空间中配置 iptables，把进出 Pod 的 TCP 流量重定向到 Envoy。这样，应用从第一个连接开始就经过 sidecar，而不会出现「应用已经启动，但早期流量绕过代理」的窗口。

这不是一个 readiness 检查。`istio-init` 的成功含义是「网络转发规则已经写入」，因此它完成后应用容器和 Envoy 才继续启动。如果配置规则失败，应用不会进入正常运行阶段，排查时要看 `istio-init` 的日志和退出状态。

这个方案需要修改网络命名空间，因此传统 sidecar 安装通常要求 `NET_ADMIN`、`NET_RAW` 能力以及较宽松的 Pod Security 设置。Istio 官方文档明确说明，使用 [Istio CNI](https://istio.io/latest/docs/setup/additional-setup/cni/) 可以把这部分节点级操作移出业务 Pod，从而避免给 `istio-init` 添加这些能力；[应用要求](https://istio.io/latest/docs/ops/deployment/application-requirements/) 也说明了这一边界。使用 Ambient 模式时，数据面的组成又不同，不能假定每个加入 Istio 的 Pod 都会有普通的 `istio-init`。

这个例子说明：init container 不只用于生成文件，也可以用来完成「应用启动前必须存在的网络环境」。

### Linkerd：用 init container 把 TCP 流量接入代理

Linkerd 的注入器在启用 Pod 注入后，会加入代理和用于启动配置的 init container。普通 init 路径中的 `linkerd-init` 使用 iptables，把 Pod 的 TCP 流量导向 `linkerd-proxy`；代理随后负责透明代理、指标采集和 mTLS 等运行期间的工作。Linkerd 的[架构文档](https://linkerd.io/docs/reference/architecture/)对这一过程有直接说明。

这里的职责分界很清楚：init container 只负责一次性的 iptables 配置，代理容器负责持续处理流量。如果只重启代理，通常不需要重新执行已经完成的普通 init；如果 Pod 被重新创建，网络命名空间也重新创建，init container 会再次执行。

需要留意版本和配置差异。Linkerd 2.20 起默认使用 Kubernetes native sidecar 部署 proxy，旧的普通 init 路径仍然存在；启用 [Linkerd CNI](https://linkerd.io/docs/features/cni/) 后，也可以由 CNI 插件完成这部分网络规则配置。因此，在集群中看到 `proxy-init`、`linkerd-init` 或 native sidecar 时，应结合 Linkerd 版本和注入结果判断，不能只凭容器名称推断它的生命周期。

### Longhorn：在驱动部署前等待 Manager 服务可用

Longhorn 的 `longhorn-driver-deployer` 是一个很典型的「等待依赖服务」案例。Longhorn 的官方 Helm 模板中包含名为 `wait-longhorn-manager` 的 init container，它循环访问 `http://longhorn-backend:9500/v1`，直到返回 HTTP `200`；只有这个 init container 成功退出，主容器才会运行 `longhorn-manager deploy-driver`。

它和应用里的 readiness probe 有一个重要区别：readiness 失败时，进程通常已经在运行，只是暂时不接收流量；Longhorn 这里则把 Manager 可用作为驱动部署器的硬前置条件，主进程在依赖满足前根本不会启动。实现可以直接查看 [Longhorn 的 Deployment 模板](https://github.com/longhorn/longhorn/blob/master/chart/templates/deployment-driver.yaml)。

这类写法的代价也很明显：如果 Service 名称、端口或 Manager 本身有问题，Pod 会长时间停在 `Init:0/1`。当前模板的等待命令是持续轮询，因此部署时应结合 Pod 事件、init container 日志和上层 Deployment 的超时策略观察，不能只看到「Pod 还在初始化」就判断为 kubelet 故障。

### Cilium：把节点准备工作从 Agent 主进程中拆出来

Cilium 的 Agent DaemonSet 还展示了另一类用法：init container 并不是为某个业务 Pod 准备环境，而是为节点上的网络组件准备文件。Cilium Helm 模板中的 `install-cni-binaries` 会执行 `/install-plugin.sh`，并把宿主机的 CNI 二进制目录挂载到容器内的 `/host/opt/cni/bin`，将所需插件安装到节点。

模板注释直接说明了这样做的目的：把 CNI 二进制安装放在 init container 中，从而避免让长期运行的 Agent 主容器持有可写的宿主机目录挂载。初始化完成后，Agent 主容器可以使用已经准备好的 CNI 文件；如果安装步骤失败，节点上的 CNI 配置就不完整，Agent 不应被当成已经正常工作。

这个过程可以在 [Cilium 的 Agent DaemonSet 模板](https://github.com/cilium/cilium/blob/main/install/kubernetes/cilium/templates/cilium-agent/daemonset.yaml) 中看到。它提醒我们，init container 的资源和权限边界不仅影响单个应用，也可能影响整个节点的网络能力。

### Rook/Ceph：按顺序激活 OSD 和准备存储目录

Rook/Ceph 中的 init container 更接近「多步存储初始化」。Rook 在生成 Ceph OSD Pod 时，可以根据配置加入多个 init container，例如生成配置、复制二进制、激活 OSD、扩展存储、更新 CephX key，以及修正 Ceph 数据目录权限。主 `ceph-osd` 进程要等这些步骤按顺序完成后才启动。

这里 init container 的价值不只是等待某个 HTTP 服务，而是把「磁盘或 PVC 已经激活」「配置和 key 已经准备好」「目录权限正确」这些前置条件显式放进 Pod 生命周期。任意一个步骤失败，OSD Pod 都会停在初始化阶段，后面的存储服务不会以不完整的状态启动。Rook 的 [OSD Pod 生成代码](https://github.com/rook/rook/blob/master/pkg/operator/ceph/cluster/osd/spec.go) 展示了这些 init container 的组合逻辑，官方[排障文档](https://rook.io/docs/rook/latest/Troubleshooting/common-issues/)也提醒需要分别查看 Pod 中各个 init container 的日志。

这个案例还说明了为什么不能只用一个很大的启动脚本替代所有 init container：把配置生成、设备激活、权限修复拆成有名字的步骤后，每一步的日志、退出状态和失败位置都可以单独观察。

### OpenBao Agent Injector：在应用启动前把 Secret 写入共享 volume

OpenBao Agent Injector 可以通过 Pod 注入机制添加一个 init container，用 Agent 身份认证并把请求的 Secret 渲染到共享 volume。应用容器只需要挂载同一个目录，在启动时读取已经生成的文件，不需要把 OpenBao 客户端、认证逻辑和模板渲染工具放进业务镜像。

这个场景特别能体现 init container 的顺序语义。OpenBao 的注入代码提供了 `PrePopulate` 选项控制是否加入预填充 init container，还提供 `InitFirst` 选项；当 Pod 已经有其他 init container，且这些步骤也需要读取 Secret 时，注入器会把 OpenBao Agent 调整到它们之前。对应实现可以查看 [OpenBao Kubernetes 注入器源码](https://github.com/openbao/openbao-k8s/blob/main/agent-inject/agent/agent.go)。

预填充 init container 只保证「Pod 启动时」Secret 已经存在。如果 Secret 需要在运行期间自动刷新，通常还要使用持续运行的 Agent sidecar，并让应用支持重新读取文件；不能把一次性 init container 当成 Secret rotation 机制。

把这些案例放在一起，可以看到 init container 的边界：

- Istio 和 Linkerd 在启动前准备网络规则。
- Longhorn 在启动驱动部署器前等待 Manager 服务。
- Cilium 在节点上安装 CNI 文件。
- Rook/Ceph 按顺序完成存储设备、配置和权限初始化。
- OpenBao 在应用启动前准备共享 volume 中的 Secret。

如果动作需要在应用运行期间持续执行，应该考虑 sidecar；如果动作只影响是否接收流量，应该考虑 readiness probe；只有「完成之前不允许主容器启动」的工作，才适合放进普通 init container。

## init container 的资源会影响调度

init container 虽然只在启动阶段运行，但它的资源请求不会被调度器忽略。对每种资源，Pod 的有效请求大致取：

```text
max(所有 init container 中该资源的最大请求,
    所有应用容器该资源请求之和)
```

因此，一个需要 2 CPU 做初始化、但应用长期只需要 200m CPU 的 Pod，在调度时仍可能按 2 CPU 的有效请求占用节点资源。资源限制也会按类似规则参与 Pod 的 QoS 和 cgroup 配置。

这不是说 init container 会一直占用 2 CPU，而是调度器必须确保节点有能力容纳它的初始化峰值。若初始化工具的资源设置过高，可能出现应用运行阶段资源很空闲、Pod 却迟迟调度不上节点的情况。

## 动手验证

本次实验文件固定在 [k8sdev `0bb95c0c`](https://github.com/jimyag/k8sdev/tree/0bb95c0c853cfd267b05569325c45d1c2f5d956b/init-container)，包含以下路径：

| 文件 | 观察目标 |
| --- | --- |
| `01-shared-volume.yaml` | init 生成页面，应用容器通过共享 `emptyDir` 提供页面 |
| `02-failure-retry.yaml` | `restartPolicy: Always` 下失败 init 的重试和退避 |
| `03-failure-never.yaml` | `restartPolicy: Never` 下 Pod 进入 `Failed` |
| `04-wait-for-service.yaml` | Service 尚未存在时，Pod 停在 init 阶段 |
| `05-dependency-service.yaml` | 创建 Service 后解除 DNS 等待 |

先准备一个能访问 Kubernetes API 的测试集群，再获取固定实验文件：

```sh
# 获取与本文一致的实验提交。
git clone https://github.com/jimyag/k8sdev.git
cd k8sdev
git checkout 0bb95c0c853cfd267b05569325c45d1c2f5d956b
cd init-container
```

### 观察成功初始化

```sh
# 应用 Pod；首次观察时应先看到 Init:0/1，再看到 Running。
kubectl apply -f 01-shared-volume.yaml
kubectl get pod init-container-demo -w

# 分别查看 init 和应用容器日志。
kubectl logs init-container-demo -c generate-index
kubectl logs init-container-demo -c nginx

# 通过应用容器验证 init 写入的页面。
kubectl port-forward pod/init-container-demo 8080:80
curl http://127.0.0.1:8080/index.html
```

预期现象是：`generate-index` 先进入 `Terminated` 且退出码为 `0`，nginx 才会创建并进入运行状态。nginx 的 readiness probe 成功后，Pod 的 `READY` 从 `0/1` 变成 `1/1`。

### 观察失败重试

```sh
# init container 每次都以退出码 1 结束，应用容器不会启动。
kubectl apply -f 02-failure-retry.yaml
kubectl get pod init-container-failure-retry -w
kubectl describe pod init-container-failure-retry
kubectl logs init-container-failure-retry -c fail-on-purpose --previous
```

重点看 `Init:CrashLoopBackOff`、事件中的重启退避和 `.status.initContainerStatuses`。不要只看 Pod 的 `RESTARTS` 摘要列，排查 init 时应直接查询 init container 的状态数组。

### 观察 `restartPolicy: Never`

```sh
# 失败的 init container 不会被同一个 Pod 重试。
kubectl apply -f 03-failure-never.yaml
kubectl get pod init-container-failure-never -w

# 查看 Pod phase 和 init container 的退出码。
kubectl get pod init-container-failure-never \
  -o jsonpath='{.status.phase}{"\n"}'
kubectl get pod init-container-failure-never \
  -o jsonpath='{.status.initContainerStatuses[0].state.terminated.exitCode}{"\n"}'
```

这里的结果应为 `Failed` 和 `42`，而 `app` 容器没有启动记录。

### 观察等待依赖

```sh
# 先创建 Pod；Service 尚不存在，init container 会反复执行 nslookup。
kubectl apply -f 04-wait-for-service.yaml
kubectl get pod init-container-wait-for-service -w

# 再创建 Service；DNS 名称出现后，init container 会成功退出。
kubectl apply -f 05-dependency-service.yaml
kubectl get pod init-container-wait-for-service -w
```

这个实验只证明 Service 名称能被 DNS 解析，不证明后端业务已经健康。真实场景中应根据依赖协议增加连接超时和业务级检查。

实验结束后，只删除这组实验对象：

```sh
# 删除本次实验创建的 Pod 和 Service。
kubectl delete -f 01-shared-volume.yaml
kubectl delete -f 02-failure-retry.yaml
kubectl delete -f 03-failure-never.yaml
kubectl delete -f 04-wait-for-service.yaml
kubectl delete -f 05-dependency-service.yaml
```

## 从 kubelet 源码看这条链路

### 1. runtime manager 只放行一个初始化步骤

[`computePodActions`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L1275-L1389) 在需要创建新的 Pod sandbox 时，如果 Pod 存在 init container，就把 `InitContainersToStart` 设置为第一个索引，然后返回。此时不会同时把普通应用容器放进 `ContainersToStart`。

当 sandbox 已经存在时，runtime manager 调用 [`computeInitContainerActions`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/kuberuntime/kuberuntime_container.go#L1054-L1280) 检查初始化进度。这个函数会从 init 列表尾部向前扫描，找到仍需要处理的最小状态集合；这样即使较早完成的容器状态已经被 runtime 垃圾回收，也不会盲目重跑全部初始化步骤。

对普通 init container，核心分支是：

- `Running`：保持当前 init，应用容器仍不进入启动流程。
- `Exited` 且退出码为 `0`：当前 init 完成，启动下一个；最后一个完成后返回「Pod 已初始化」。
- `Exited` 且退出码非 `0`：如果允许重启，把当前 init 重新加入 `InitContainersToStart`；否则设置 `KillPod=true`，初始化流程结束。
- `Unknown` 或创建状态异常：按失败或需重建处理，具体动作仍受重启策略影响。

一旦 `computeInitContainerActions` 返回未初始化，`computePodActions` 会直接返回，不再检查普通应用容器。这就是「init 没有完成时，应用容器不会被启动或按普通容器逻辑处理」的源码依据。

### 2. API 校验决定普通 init 是否能使用探针

[`validateInitContainers`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/apis/core/validation/validation.go#L3923-L3990) 先判断 init container 是否设置了容器级 `restartPolicy: Always`：

- 没有设置时，`lifecycle`、`livenessProbe`、`readinessProbe`、`startupProbe` 都会生成 `Forbidden` 错误。
- 设置为 `Always` 时，校验转入 restartable init / sidecar 分支，这些探针字段可以被校验和使用。

API 类型定义也把这条限制写在 `PodSpec.InitContainers` 的注释中：普通 init container 不允许生命周期动作和三类探针，资源请求则会参与有效 Pod 请求计算。可以对照 [`staging/src/k8s.io/api/core/v1/types.go`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/staging/src/k8s.io/api/core/v1/types.go#L4444-L4461) 和容器级 restart policy 的定义 [`types.go`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/staging/src/k8s.io/api/core/v1/types.go#L3281-L3297)。

### 3. `Initialized` 和 `Ready` 是两条状态判断

kubelet 生成 Pod 状态时，会把 init container 状态和应用容器状态分别写入 `status.initContainerStatuses`、`status.containerStatuses`。普通 init container 只有成功终止时才被视为完成；这一点在 [`prober_manager.go`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/prober/prober_manager.go#L389-L430) 的状态更新逻辑和 [`generate.go`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/status/generate.go#L171-L268) 的 condition 生成逻辑中都能看到。

`GeneratePodInitializedCondition` 判断所有 init container 是否完成；`GeneratePodReadyCondition` 则先检查所有应用容器是否 ready，再检查 `readinessGates`。因此 `Initialized=True` 不是 `Ready=True` 的替代字段：它只说明启动前置步骤完成，不代表应用已经能接收流量。

Pod phase 的判断位于 [`getPhase`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/kubelet_pods.go#L1687-L1862)：普通 init 尚未完成时会增加 pending 计数，失败且不可重启时才返回 `PodFailed`。这解释了为什么一个 init 反复失败的 Pod 常见状态是 `Pending`，而 `restartPolicy: Never` 的同类 Pod 最终是 `Failed`。

## 什么时候不应该使用 init container

以下需求通常不适合普通 init container：

- 需要和应用一起长期运行的日志代理、代理服务器、文件同步器或服务网格组件。它们是 sidecar 场景。
- 应用已经可以启动，只是暂时不应该接收流量。使用应用容器的 readiness probe。
- 需要在每次应用容器重启前都执行的动作。普通 init container 不会随着应用容器的单次重启重新执行，应使用应用自身的启动逻辑、生命周期钩子或 sidecar 协作。
- 不能安全重试、且会造成不可逆外部副作用的脚本。先补充幂等和恢复设计，再放进 init 阶段。
- 没有超时边界的远端依赖等待。否则故障会表现为大量长期 Pending 的 Pod，且应用容器日志为空。

反过来，如果动作确实是「应用启动前必须完成」，并且可以重复执行、失败可诊断，那么 init container 往往比把逻辑塞进应用入口脚本更清楚。它把启动门槛直接写进 Pod spec，状态也会通过 `initContainerStatuses`、事件和 `Initialized` condition 暴露出来。

## 总结

普通 init container 是 Pod 启动阶段的串行前置步骤：kubelet 准备 sandbox 后按顺序运行它们，成功退出才继续；失败时按 Pod 的重启策略重试或结束 Pod。它没有 liveness/readiness/startup probe，成功退出本身就是完成信号，流量接收控制仍然属于应用容器 readiness。

使用时抓住四个边界即可：

1. 把启动前的一次性准备放进 init，把长期运行的协作进程放进 sidecar。
2. 把生成结果放进共享 volume 或明确的外部对象，并让脚本幂等。
3. 用 `initContainerStatuses`、事件和退出码判断初始化问题，不要只看应用容器日志。
4. 区分 `Initialized`、`Running` 和 `Ready`；Pod 已初始化不等于应用已经可以接收流量。

进一步阅读：[Init Containers](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/)、[Configure Pod Initialization](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-initialization/)、[Pod Lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)、[Liveness, Readiness, and Startup Probes](https://kubernetes.io/docs/concepts/workloads/pods/probes/) 和 [Sidecar Containers](https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/)。

