jimyag's Blog

Kubernetes Native Sidecar:从普通容器到原生 Sidecar

Pod 里除了主应用容器,还经常需要一个长期运行的辅助进程:代理流量、采集日志、刷新 Secret,或者把共享 volume 中的数据转换成应用可以读取的格式。这个辅助进程通常被称为 sidecar。

在 Kubernetes 早期,sidecar 更多是一种部署约定:把辅助进程和应用都写在 spec.containers 中,让两个普通容器共同运行。这样虽然能工作,但 Kubernetes 不知道哪个容器应该先启动、哪个容器应该最后停止,也无法正确处理带 sidecar 的 Job。

Native Sidecar 是 Kubernetes 对这类模式的原生支持。它仍然写在 spec.initContainers 中,但设置容器级 restartPolicy: Always,从而获得「按 init 顺序启动、持续运行、支持探针、最后停止」的生命周期。

本文通过本地 Kubernetes 源码和 kind 实验回答几个问题:普通多容器 Pod 和 Native Sidecar 有什么区别、sidecar 什么时候算启动完成、探针如何影响初始化、为什么 Job 不会被 sidecar 卡住,以及 kubelet 如何实现停止顺序。

Kubernetes 官方 Sidecar 文档将 Native Sidecar 标记为 v1.33 稳定特性,最早在 v1.28 提供;本文实验使用 kind 的 Kubernetes v1.35.0。源码分析基于本地仓库 commit c028ba348dbaea5e8b0df94b2581e70d687a77c8

先给出本文的判断:

  1. 普通 sidecar 可以继续使用,但它只是一组并行的普通容器,Kubernetes 不会替你表达启动和停止依赖。
  2. Native Sidecar 的关键标志是:容器位于 initContainers,并设置容器级 restartPolicy: Always
  3. Native Sidecar 的 startupProbe 决定它什么时候允许 init 序列继续;readinessProbe 可以影响 Pod Ready;livenessProbe 失败会重启 sidecar。
  4. Pod 删除时,普通应用容器先停止,Native Sidecar 再按声明顺序的逆序停止。
  5. Job 的主容器完成后,kubelet 会停止 Native Sidecar,Job 可以正常进入 Complete

普通 sidecar 解决了什么问题

Sidecar 和主应用容器位于同一个 Pod 中,因此共享 Pod 的网络命名空间,也可以通过 volume 共享文件。它适合放置与主应用紧密协作、但不属于主业务逻辑的功能:

  • 日志采集器读取应用写入的 emptyDir 文件。
  • 代理容器接管应用的网络流量。
  • Secret Agent 将凭据刷新到共享目录。
  • 本地 exporter 或协议转换器为应用提供辅助接口。

例如,应用负责写入 /logs/app.log,日志采集器负责读取并发送日志。两个容器需要同时运行,但日志采集器不是应用本身;把它拆成 sidecar 可以分别升级、重启和观察。

传统写法通常如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 这是传统的普通多容器 Pod;两个容器都位于 containers 中。
apiVersion: v1 # Pod API 版本。
kind: Pod # 直接创建一个 Pod 便于观察。
metadata:
  name: ordinary-sidecar # Pod 名称。
spec:
  containers:
    - name: app # 主应用容器。
      image: busybox:1.36.1 # 应用镜像。
      command: ["sh", "-c", "while true; do echo app; sleep 5; done"] # 持续运行应用。
    - name: log-sidecar # 普通 sidecar 容器。
      image: busybox:1.36.1 # 辅助容器镜像。
      command: ["sh", "-c", "while true; do echo sidecar; sleep 5; done"] # 持续运行辅助进程。

这两个容器会被 kubelet 创建并运行,但这个写法没有表达「sidecar 必须先准备好」或「应用退出后 sidecar 才停止」。如果 sidecar 是 Job 中的日志发送器,它还可能一直运行,使 Job 无法根据主容器的退出自动完成。

普通多容器 Pod 仍然有合理的使用场景:当两个容器不需要严格的启动顺序,也不需要 Kubernetes 管理它们之间的停止依赖时,普通 containers 更直观,也能兼容不支持 Native Sidecar 的旧集群。

Native Sidecar 的 API 形态

Native Sidecar 不是一个新的顶层字段,而是复用了 initContainers,通过容器级重启策略改变语义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 容器级 restartPolicy=Always 将这个 init container 声明为 Native Sidecar。
spec:
  initContainers:
    - name: log-reader # 需要在应用旁边长期运行的辅助容器。
      image: busybox:1.36.1 # 提供 tail 等辅助工具。
      restartPolicy: Always # 关键字段:退出后重启,并在 Pod 结束时最后停止。
      command: ["sh", "-c", "exec tail -f /logs/app.log"] # 持续运行的 sidecar 进程。
  containers:
    - name: app # 业务容器。
      image: busybox:1.36.1 # 应用镜像。
      command: ["sh", "-c", "while true; do echo app >> /logs/app.log; sleep 5; done"] # 写入共享日志。

Pod 层的 restartPolicy 仍然存在,例如 Job 通常使用 Never;这里的 restartPolicy: Always 是写在 sidecar 容器自身上的,优先表达这个容器的生命周期。

Kubernetes API 类型的注释直接说明了这条语义:设置为 Always 的 init container 会持续重启,直到所有普通容器结束;它启动后不会等待退出,而是允许下一个 init container 继续执行。对应源码在 Container.RestartPolicy

为什么看起来像 init container

这个 API 形态不是把 sidecar 误放进了 initContainers,而是为了同时保留两种能力:init container 的启动顺序,以及长期运行容器的生命周期。

在 Native Sidecar 之前,通常只有两种写法:

  • 放进 initContainers:有严格的顺序保证,但必须退出成功,后面的容器才会启动。
  • 放进普通的 containers:可以和应用一起长期运行,但 Kubernetes 不知道它应该在应用之前启动;如果它用于 Job,还可能因为自己一直不退出而阻塞 Pod 完成。

Native Sidecar 把两者组合起来:它按照 initContainers 的顺序启动,启动完成后继续运行;所有普通容器完成后,kubelet 再停止它。这样可以表达「初始化配置 → 启动代理 → 数据迁移 → 启动应用」这样的顺序,也不需要让 Job 自己实现 sidecar 的退出逻辑。

设计阶段曾讨论过增加独立的 infrastructureContainers 集合,但这会引入一套新的容器集合,而且很难表达 sidecar 应该位于哪个 init container 之前或之后。因此 KEP-753 选择复用现有的 initContainers,用容器级 restartPolicy: Always 区分普通 init container 和 Native Sidecar。详细的备选方案和取舍见 KEP-753: Sidecar containers

选择 Always 也不是随意增加一个 sidecar: true 字段。sidecar 的关键行为本来就是「无论以什么退出码结束,都继续重启」,现有的 Always 已经能表达这个意图;同时,容器级重启策略也是社区长期希望支持的能力。当前 Native Sidecar 语义只接受 Always,并且这个字段写在容器上,不受 Pod 层 restartPolicy: NeverOnFailure 的限制。

这里的启动完成信号也经过了专门讨论。readiness probe 主要用于流量和 Pod Ready 判断,而且可能在运行过程中重新变为 NotReady,或者依赖其他容器提供的服务;如果用它阻塞后续容器,容易形成启动死锁。因此 kubelet 使用 Started 状态推进 init 序列:没有 startupProbe 时,sidecar 进程启动即可继续;配置了 startupProbe 时,则要等 startup probe 成功后才继续。readiness probe 留给 sidecar 启动之后的服务可用性判断。

这个特性最早在 2018 年提出,Kubernetes 1.28 进入 Alpha,1.29 进入 Beta,1.33 成为 Stable。它解决的是长期存在的 sidecar 生命周期问题,而不是为了改变普通 init container 的语义;没有设置容器级 restartPolicy 的旧 YAML 仍按原来的规则运行。

启动顺序:sidecar 先启动,但不一定马上放行

Native Sidecar 同时保留了 init container 的顺序保证和普通容器的持续运行特征:

  flowchart LR
    Sandbox["Pod sandbox\n网络和 volume"] --> Sidecar["Native Sidecar\nrestartPolicy: Always"]
    Sidecar --> Started{"started?"}
    Started -->|"startupProbe 成功\n或没有 startupProbe"| App["启动应用容器"]
    Started -->|"未成功"| Wait["等待或重启 sidecar"]
    App --> Ready["应用与 sidecar\n共同影响 Pod Ready"]

sidecar 的 started 状态有两个来源:

  • 没有配置 startupProbe 时,容器进程运行起来后即可继续。
  • 配置了 startupProbe 时,要等 startup probe 成功后,kubelet 才会把它视为已经启动,并继续 init 序列。

因此,startup probe 和 readiness probe 的职责不同:

探针对 Native Sidecar 的作用
startupProbe决定 sidecar 是否已经完成启动,失败后会阻止后续 init 或应用容器继续启动
readinessProbe决定 sidecar 是否可以提供服务,并参与 Pod Ready 判断
livenessProbesidecar 运行后失败时,kubelet 重启 sidecar

源码中的 computeInitContainerActions 会检查 restartable init container 的 startup probe;成功后才启动下一个 init container。如果 liveness probe 失败,kubelet 会杀掉并重新启动 sidecar。可以从 kuberuntime_container.go 看到这条控制流。

这和普通 init container 的区别很重要:普通 init container 只用退出码表示完成;Native Sidecar 是一个持续运行的容器,因此需要用 startupProbe 描述「已经可以继续初始化」,用 readinessProbe 描述「现在可以提供协作服务」。

一个可运行的日志 sidecar

实验文件 01-deployment.yaml 创建一个 Deployment:

  1. log-reader 作为 Native Sidecar,先创建共享日志文件并启动 tail
  2. startupProbe 检查 sidecar 写出的 sidecar.status 文件。
  3. startup probe 成功后,kubelet 启动 app 容器。
  4. app 写入 app-ready,sidecar 的 readiness probe 才成功。
  5. 两个容器都 ready 后,Pod 的 READY 才变成 2/2

YAML 中最关键的片段如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# sidecar 和应用通过同一个 emptyDir 交换日志和状态文件。
volumes:
  - name: shared-log # 共享 volume 名称。
    emptyDir: {} # 数据只在当前 Pod 生命周期内存在。
initContainers:
  - name: log-reader # 原生 sidecar。
    restartPolicy: Always # 让它持续运行,并由 kubelet 管理其顺序。
    startupProbe: # 启动成功前不推进后续初始化。
      exec:
        command: ["sh", "-c", "test -f /logs/sidecar.status"] # 检查本地准备动作完成。
    readinessProbe: # readiness 结果会进入 Pod Ready 判断。
      exec:
        command: ["sh", "-c", "grep -q app-ready /logs/app.log"] # 等应用写入启动标记。
    volumeMounts:
      - name: shared-log # 挂载共享 volume。
        mountPath: /logs # sidecar 读取日志的位置。

这个例子展示了一个常见的边界:sidecar 可以先启动,但不代表应用已经 ready。sidecar 的启动顺序解决「辅助进程是否存在」;readiness probe 解决「辅助进程现在是否能提供服务」。

Pod 删除时:应用先停,sidecar 后停

当 Pod 被删除或 Deployment 滚动更新时,kubelet 不会把 Native Sidecar 和应用容器当成没有关系的并行进程。它会先等待普通应用容器退出,再停止 sidecar;多个 sidecar 则按 initContainers 声明顺序的逆序停止。

  flowchart RL
    Delete["删除 Pod\n或滚动更新"] --> AppStop["停止普通应用容器"]
    AppStop --> Sidecar2["停止后声明的 sidecar"]
    Sidecar2 --> Sidecar1["停止先声明的 sidecar"]

这样设计对日志采集很有用:应用退出后,sidecar 仍有机会读取最后一批日志。对代理或数据同步组件也一样,sidecar 可以在主进程结束后完成必要的收尾。

Kubelet 的终止顺序代码会为普通容器和 sidecar 建立依赖关系:sidecar 等待所有普通容器退出;sidecar 之间按照反向顺序等待。实现位于 kuberuntime_termination_order.go。如果 Pod 的 terminationGracePeriodSeconds 很短,依赖链可能还没完成就到达强制终止时间,因此 sidecar 的清理逻辑仍然要能应对 SIGKILL。

terminationGracePeriodSeconds 是 Pod 级别的总预算,不会为每个容器重新计时。删除开始后,宽限期就开始倒计时;应用容器退出所花的时间、sidecar 之间等待顺序所花的时间,都消耗同一份预算。宽限期耗尽时,仍在运行的容器会被强制终止。kubectl delete --grace-period=0 --force 还可以绕过普通的优雅终止流程,生产环境需要谨慎使用。详细流程见 Kubernetes 的 Pod termination 文档

本文的 03-termination.yaml 对这两个边界分别做了验证:native-sidecar-stop-order 使用 15 秒宽限期,kind 中观察到应用退出码为 0second-sidecarfinishedAt 早于 first-sidecar,三个容器都正常完成;native-sidecar-grace-expiry 只给 5 秒宽限期,应用仍以 0 退出,但两个 sidecar 都以 137 结束。后一个结果说明 sidecar 虽然按照顺序等待,但不会获得额外的终止时间。

Job 为什么不会被 sidecar 卡住

这是 Native Sidecar 最实用的改进之一。一个日志采集 sidecar 通常需要一直运行,但 Job 的完成条件应该由主任务容器的退出码决定:

  sequenceDiagram
    participant K as kubelet
    participant S as log-sidecar
    participant J as job container
    participant C as Job controller
    K->>S: 启动 sidecar
    K->>J: 启动主任务
    J-->>K: 退出码 0
    K->>S: 停止 sidecar
    K-->>C: Pod 成功
    C-->>C: Job Complete

实验文件 02-job.yaml 中,job 容器写入两条日志后退出,log-sidecar 一直执行 tail -f。在 kind v1.35.0 中,实际观察到:主容器退出码为 0,sidecar 最终以 137 结束,这是 kubelet 在 Pod 完成时停止持续运行的 sidecar;Job 仍然进入 Complete,而不是继续处于 Active。

这里的 137 不代表 Job 主任务失败,它是 sidecar 被终止时的退出结果。排查 Job 时,应优先看主容器的终止 reason 和退出码,不要把 sidecar 在 Pod 结束阶段收到 SIGKILL 的结果当成业务任务失败。

失败行为和可观测性

Native Sidecar 失败时,要根据它处于哪个阶段来判断影响:

现象影响
sidecar 启动失败,startup probe 未成功后续 init 和应用容器不会启动,Pod 停留在初始化阶段
sidecar 已经 started,进程退出kubelet 按容器级 Always 重启 sidecar,应用通常仍然存在
sidecar readiness 失败Pod 可能变为 NotReady,但应用容器不一定会被重启
sidecar liveness 失败kubelet 重启 sidecar,应用容器通常不随之重启
Pod 删除时 sidecar 退出码非 0可能是正常终止结果,要结合 Pod 删除过程判断

不能只看 .status.initContainerStatuses 中「容器是否 Terminated」来判断 Native Sidecar 是否完成。对 Native Sidecar 来说,正常状态是持续 Running;kubelet 通过 started 字段和探针结果判断它是否已经允许后续容器启动。

资源计算仍然要特别注意

Native Sidecar 虽然写在 initContainers 中,但它是长期运行的容器。Pod 的有效资源请求需要同时考虑 init container、sidecar 和普通应用容器:初始化阶段取 init/sidecar 的最大请求,运行阶段还要考虑普通容器和 sidecar 的资源总和。

这意味着一个只负责日志采集的 sidecar 也会增加 Pod 的长期资源消耗;它不只是启动阶段的临时工具。文章中的实验没有设置 resources,适合本地 kind 观察,不代表生产配置可以省略 CPU 和内存请求。

Native Sidecar 和普通容器怎么选

可以用下面的判断:

  • 需要严格的启动顺序、探针和停止顺序:使用 Native Sidecar。
  • 两个容器只需要共同运行,不关心谁先启动:普通 containers 更简单。
  • 只需要在应用启动前执行一次动作:使用普通 init container。
  • 需要临时进入已有 Pod 调试:使用 Ephemeral Container,不要把调试工具做成长期 sidecar。

Native Sidecar 并不会自动让两个进程变成一个应用。它们仍然共享 Pod 的网络和资源边界,也仍然需要定义好通信协议、日志格式、失败后的降级方式和优雅退出逻辑。

在 kind 中验证

本文实验文件位于 k8sdev native-sidecar/。实验使用 kind v0.32.0 和 kindest/node:v1.35.0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 获取与本文一致的实验文件。
git clone https://github.com/jimyag/k8sdev.git
cd k8sdev
git checkout 1c0f6cb2e632ecfda1dd2a7e3a981aa813bb92c6

# 创建一个独立的 kind 集群,避免影响其他 kubeconfig 上下文。
kind create cluster --name k8s-advanced-lab --image kindest/node:v1.35.0 --wait 60s

# 应用 Deployment 并观察 sidecar 与应用容器。
kubectl apply -f native-sidecar/01-deployment.yaml
kubectl rollout status deployment/native-sidecar-demo --timeout=120s
kubectl get pod -l app=native-sidecar-demo

# 查看 Pod 中 init container 和应用容器的完整状态。
kubectl get pod <pod-name> -o yaml

# 查看 sidecar 日志。
kubectl logs <pod-name> -c log-reader

# 部署 Native Sidecar 的停止顺序和终止宽限期实验。
kubectl apply -f native-sidecar/03-termination.yaml

# 正常宽限期场景:删除后快速查看三个容器的终止状态和 finishedAt。
kubectl delete pod native-sidecar-stop-order --wait=false
kubectl get pod native-sidecar-stop-order -o json \
  | jq '[.status.containerStatuses[]?, .status.initContainerStatuses[]?] | map({name, reason: .state.terminated.reason, exitCode: .state.terminated.exitCode, finishedAt: .state.terminated.finishedAt})'

# 短宽限期场景:sidecar 清理时间超过 Pod 总预算,预期退出码为 137。
kubectl delete pod native-sidecar-grace-expiry --wait=false
kubectl get pod native-sidecar-grace-expiry -o json \
  | jq '[.status.containerStatuses[]?, .status.initContainerStatuses[]?] | map({name, reason: .state.terminated.reason, exitCode: .state.terminated.exitCode, finishedAt: .state.terminated.finishedAt})'

# 验证 Job 在 sidecar 持续运行的情况下仍然可以完成。
kubectl apply -f native-sidecar/02-job.yaml
kubectl wait --for=condition=complete job/native-sidecar-job --timeout=120s
kubectl get job/native-sidecar-job
kubectl logs -l app=native-sidecar-job -c log-sidecar

# 实验结束后删除集群。
kind delete cluster --name k8s-advanced-lab

本次本地验证得到的关键结果是:Deployment 中 sidecar 的 startedAt 早于应用容器,两个容器最终为 2/2 Running;Job 主容器以 0 退出后,sidecar 被停止,Job 进入 Complete

终止顺序实验的实际输出如下。正常宽限期场景中,应用先完成,后声明的 second-sidecar 再完成,先声明的 first-sidecar 最后完成;时间戳来自 kind 节点,使用 UTC:

1
2
3
app             Completed  0  2026-09-16T04:00:40Z
first-sidecar   Completed  0  2026-09-16T04:00:42Z
second-sidecar  Completed  0  2026-09-16T04:00:41Z

短宽限期场景的输出如下。应用仍然以 0 退出,但两个 sidecar 在 Pod 总宽限期耗尽后以 137 结束:

1
app/Completed/0 first-sidecar/Error/137 second-sidecar/Error/137

从源码看 Native Sidecar

实现可以分成三层:

  1. API 类型在 Container 上增加容器级 restartPolicy,并明确 Always 的 sidecar 语义。
  2. API 校验只允许 restartable init container 使用 lifecycle、liveness、readiness 和 startup probe;普通 init container 仍然拒绝这些字段。对应 validateInitContainers
  3. kubelet 在同步 Pod 时,检查 sidecar 的 startup/liveness 状态,决定继续启动、重启 sidecar,还是等待;终止 Pod 时使用单独的依赖排序保证停止顺序。

因此 Native Sidecar 不是一个只在 API 层「放宽字段」的功能,而是 API 校验、kubelet 容器状态机和 runtime 终止顺序共同实现的生命周期语义。

总结

普通多容器 Pod 只是把多个进程放到同一个逻辑主机里;Native Sidecar 则进一步声明了辅助进程的生命周期:先启动、持续运行、可探测、独立重启、最后停止。

如果 init container 表达的是「完成之前不能启动」,Native Sidecar 表达的就是「启动后仍然要陪伴应用运行」。这两个机制写在同一个 initContainers 字段里,但决定行为的是容器级 restartPolicy 和 kubelet 对 started、探针及终止依赖的处理。

#Kubernetes #Sidecar #Pod #Job