

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 文档](https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/)将 Native Sidecar 标记为 v1.33 稳定特性，最早在 v1.28 提供；本文实验使用 kind 的 Kubernetes v1.35.0。源码分析基于本地仓库 commit [`c028ba348dbaea5e8b0df94b2581e70d687a77c8`](https://github.com/kubernetes/kubernetes/tree/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`。

<!--more-->

## 普通 sidecar 解决了什么问题

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

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

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

传统写法通常如下：

```yaml
# 这是传统的普通多容器 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`，通过容器级重启策略改变语义：

```yaml
# 容器级 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`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/staging/src/k8s.io/api/core/v1/types.go#L3281-L3297)。

## 为什么看起来像 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](https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/753-sidecar-containers/README.md)。

选择 `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 的顺序保证和普通容器的持续运行特征：

```mermaid
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`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/kuberuntime/kuberuntime_container.go#L1054-L1202) 看到这条控制流。

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

## 一个可运行的日志 sidecar

实验文件 [`01-deployment.yaml`](https://github.com/jimyag/k8sdev/blob/1c0f6cb2e632ecfda1dd2a7e3a981aa813bb92c6/native-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 中最关键的片段如下：

```yaml
# 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` 声明顺序的逆序停止。

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

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

Kubelet 的终止顺序代码会为普通容器和 sidecar 建立依赖关系：sidecar 等待所有普通容器退出；sidecar 之间按照反向顺序等待。实现位于 [`kuberuntime_termination_order.go`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/kubelet/kuberuntime/kuberuntime_termination_order.go)。如果 Pod 的 `terminationGracePeriodSeconds` 很短，依赖链可能还没完成就到达强制终止时间，因此 sidecar 的清理逻辑仍然要能应对 SIGKILL。

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

本文的 [`03-termination.yaml`](https://github.com/jimyag/k8sdev/blob/1c0f6cb2e632ecfda1dd2a7e3a981aa813bb92c6/native-sidecar/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 的完成条件应该由主任务容器的退出码决定：

```mermaid
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`](https://github.com/jimyag/k8sdev/blob/1c0f6cb2e632ecfda1dd2a7e3a981aa813bb92c6/native-sidecar/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/`](https://github.com/jimyag/k8sdev/tree/1c0f6cb2e632ecfda1dd2a7e3a981aa813bb92c6/native-sidecar)。实验使用 kind v0.32.0 和 `kindest/node:v1.35.0`。

```sh
# 获取与本文一致的实验文件。
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：

```text
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` 结束：

```text
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`](https://github.com/kubernetes/kubernetes/blob/c028ba348dbaea5e8b0df94b2581e70d687a77c8/pkg/apis/core/validation/validation.go#L3923-L3977)。
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`、探针及终止依赖的处理。

