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。
先给出本文的判断:
- 普通 sidecar 可以继续使用,但它只是一组并行的普通容器,Kubernetes 不会替你表达启动和停止依赖。
- Native Sidecar 的关键标志是:容器位于
initContainers,并设置容器级restartPolicy: Always。 - Native Sidecar 的
startupProbe决定它什么时候允许 init 序列继续;readinessProbe可以影响 Pod Ready;livenessProbe失败会重启 sidecar。 - Pod 删除时,普通应用容器先停止,Native Sidecar 再按声明顺序的逆序停止。
- Job 的主容器完成后,kubelet 会停止 Native Sidecar,Job 可以正常进入
Complete。
普通 sidecar 解决了什么问题
Sidecar 和主应用容器位于同一个 Pod 中,因此共享 Pod 的网络命名空间,也可以通过 volume 共享文件。它适合放置与主应用紧密协作、但不属于主业务逻辑的功能:
- 日志采集器读取应用写入的
emptyDir文件。 - 代理容器接管应用的网络流量。
- Secret Agent 将凭据刷新到共享目录。
- 本地 exporter 或协议转换器为应用提供辅助接口。
例如,应用负责写入 /logs/app.log,日志采集器负责读取并发送日志。两个容器需要同时运行,但日志采集器不是应用本身;把它拆成 sidecar 可以分别升级、重启和观察。
传统写法通常如下:
| |
这两个容器会被 kubelet 创建并运行,但这个写法没有表达「sidecar 必须先准备好」或「应用退出后 sidecar 才停止」。如果 sidecar 是 Job 中的日志发送器,它还可能一直运行,使 Job 无法根据主容器的退出自动完成。
普通多容器 Pod 仍然有合理的使用场景:当两个容器不需要严格的启动顺序,也不需要 Kubernetes 管理它们之间的停止依赖时,普通 containers 更直观,也能兼容不支持 Native Sidecar 的旧集群。
Native Sidecar 的 API 形态
Native Sidecar 不是一个新的顶层字段,而是复用了 initContainers,通过容器级重启策略改变语义:
| |
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: Never 或 OnFailure 的限制。
这里的启动完成信号也经过了专门讨论。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 判断 |
livenessProbe | sidecar 运行后失败时,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:
log-reader作为 Native Sidecar,先创建共享日志文件并启动tail。startupProbe检查 sidecar 写出的sidecar.status文件。- startup probe 成功后,kubelet 启动
app容器。 app写入app-ready,sidecar 的 readiness probe 才成功。- 两个容器都 ready 后,Pod 的
READY才变成2/2。
YAML 中最关键的片段如下:
| |
这个例子展示了一个常见的边界: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 中观察到应用退出码为 0,second-sidecar 的 finishedAt 早于 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。
| |
本次本地验证得到的关键结果是:Deployment 中 sidecar 的 startedAt 早于应用容器,两个容器最终为 2/2 Running;Job 主容器以 0 退出后,sidecar 被停止,Job 进入 Complete。
终止顺序实验的实际输出如下。正常宽限期场景中,应用先完成,后声明的 second-sidecar 再完成,先声明的 first-sidecar 最后完成;时间戳来自 kind 节点,使用 UTC:
| |
短宽限期场景的输出如下。应用仍然以 0 退出,但两个 sidecar 在 Pod 总宽限期耗尽后以 137 结束:
| |
从源码看 Native Sidecar
实现可以分成三层:
- API 类型在
Container上增加容器级restartPolicy,并明确Always的 sidecar 语义。 - API 校验只允许 restartable init container 使用 lifecycle、liveness、readiness 和 startup probe;普通 init container 仍然拒绝这些字段。对应
validateInitContainers。 - kubelet 在同步 Pod 时,检查 sidecar 的 startup/liveness 状态,决定继续启动、重启 sidecar,还是等待;终止 Pod 时使用单独的依赖排序保证停止顺序。
因此 Native Sidecar 不是一个只在 API 层「放宽字段」的功能,而是 API 校验、kubelet 容器状态机和 runtime 终止顺序共同实现的生命周期语义。
总结
普通多容器 Pod 只是把多个进程放到同一个逻辑主机里;Native Sidecar 则进一步声明了辅助进程的生命周期:先启动、持续运行、可探测、独立重启、最后停止。
如果 init container 表达的是「完成之前不能启动」,Native Sidecar 表达的就是「启动后仍然要陪伴应用运行」。这两个机制写在同一个 initContainers 字段里,但决定行为的是容器级 restartPolicy 和 kubelet 对 started、探针及终止依赖的处理。