jimyag's Blog

Kubernetes Init Container:启动阶段、失败重试与探针边界

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

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

文中的源码分析基于本地 Kubernetes 仓库的 commit c028ba348dbaea5e8b0df94b2581e70d687a77c8,实验文件固定在 k8sdev 0bb95c0c。源码和文档的具体版本可能继续变化,文章中的结论以这两个提交为准。

先把结论列出来:

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

init container 解决什么问题

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

常见的使用场景有三类:

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

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

它在 Pod 的哪个阶段触发

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

  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 的最小结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 这是 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 会把初始化视为未完成。

AlwaysOnFailure:继续重试

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 查看 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 时就应当得到校验错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 这个片段用于说明禁止关系,不应作为可部署配置。
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 里的 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.phasePod 的粗粒度阶段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。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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-initistio-proxyistio-init 在 Pod 的网络命名空间中配置 iptables,把进出 Pod 的 TCP 流量重定向到 Envoy。这样,应用从第一个连接开始就经过 sidecar,而不会出现「应用已经启动,但早期流量绕过代理」的窗口。

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

这个方案需要修改网络命名空间,因此传统 sidecar 安装通常要求 NET_ADMINNET_RAW 能力以及较宽松的 Pod Security 设置。Istio 官方文档明确说明,使用 Istio CNI 可以把这部分节点级操作移出业务 Pod,从而避免给 istio-init 添加这些能力;应用要求 也说明了这一边界。使用 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 的架构文档对这一过程有直接说明。

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

需要留意版本和配置差异。Linkerd 2.20 起默认使用 Kubernetes native sidecar 部署 proxy,旧的普通 init 路径仍然存在;启用 Linkerd CNI 后,也可以由 CNI 插件完成这部分网络规则配置。因此,在集群中看到 proxy-initlinkerd-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 模板

这类写法的代价也很明显:如果 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 模板 中看到。它提醒我们,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 生成代码 展示了这些 init container 的组合逻辑,官方排障文档也提醒需要分别查看 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 注入器源码

预填充 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 的有效请求大致取:

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

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

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

动手验证

本次实验文件固定在 k8sdev 0bb95c0c,包含以下路径:

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

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

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

观察成功初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 应用 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 的 READY0/1 变成 1/1

观察失败重试

1
2
3
4
5
# 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

1
2
3
4
5
6
7
8
9
# 失败的 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"}'

这里的结果应为 Failed42,而 app 容器没有启动记录。

观察等待依赖

1
2
3
4
5
6
7
# 先创建 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 解析,不证明后端业务已经健康。真实场景中应根据依赖协议增加连接超时和业务级检查。

实验结束后,只删除这组实验对象:

1
2
3
4
5
6
# 删除本次实验创建的 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 在需要创建新的 Pod sandbox 时,如果 Pod 存在 init container,就把 InitContainersToStart 设置为第一个索引,然后返回。此时不会同时把普通应用容器放进 ContainersToStart

当 sandbox 已经存在时,runtime manager 调用 computeInitContainerActions 检查初始化进度。这个函数会从 init 列表尾部向前扫描,找到仍需要处理的最小状态集合;这样即使较早完成的容器状态已经被 runtime 垃圾回收,也不会盲目重跑全部初始化步骤。

对普通 init container,核心分支是:

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

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

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

validateInitContainers 先判断 init container 是否设置了容器级 restartPolicy: Always

  • 没有设置时,lifecyclelivenessProbereadinessProbestartupProbe 都会生成 Forbidden 错误。
  • 设置为 Always 时,校验转入 restartable init / sidecar 分支,这些探针字段可以被校验和使用。

API 类型定义也把这条限制写在 PodSpec.InitContainers 的注释中:普通 init container 不允许生命周期动作和三类探针,资源请求则会参与有效 Pod 请求计算。可以对照 staging/src/k8s.io/api/core/v1/types.go 和容器级 restart policy 的定义 types.go

3. InitializedReady 是两条状态判断

kubelet 生成 Pod 状态时,会把 init container 状态和应用容器状态分别写入 status.initContainerStatusesstatus.containerStatuses。普通 init container 只有成功终止时才被视为完成;这一点在 prober_manager.go 的状态更新逻辑和 generate.go 的 condition 生成逻辑中都能看到。

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

Pod phase 的判断位于 getPhase:普通 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. 区分 InitializedRunningReady;Pod 已初始化不等于应用已经可以接收流量。

进一步阅读:Init ContainersConfigure Pod InitializationPod LifecycleLiveness, Readiness, and Startup ProbesSidecar Containers

#Kubernetes #Pod #Init Container #Kubelet