Sidecar(边车)是 Kubernetes 里最古老也最容易用错的设计模式。它让一个辅助容器与主容器共享 Pod 的网络与存储,从而在不改业务代码的前提下加上代理、日志采集、配置同步等能力——Service Mesh 的整个数据面就建立在这个模式上。但「多塞一个容器」也带来了真实问题:Job 里的边车永不退出导致 Job 永远不 Complete、边车启动顺序无法保证、终止时主容器先死导致日志丢失。Kubernetes v1.29 引入的**原生边车容器(native sidecar)**正是为这些问题给出的官方答案。本文讲清模式的边界、旧方案的痛点与新机制的用法。
1. Sidecar 模式的由来与三类容器
1.1 为什么要有 Sidecar
Sidecar 的核心假设是:同一个 Pod 内的容器共享网络命名空间与可挂载的卷。这带来三个能力:
- 共享
localhost——边车可以透明劫持主容器的流量; - 共享卷——边车可以读主容器写的日志文件;
- 同生命周期调度——边车与主容器一起被调度到同一节点,一起扩缩。
Pod
├── app 容器(业务逻辑,只管监听 localhost:8080)
├── envoy 边车(接管流量,做 mTLS/重试/熔断)
└── log-agent 边车(tail 共享卷里的日志文件,推送到远端)
1.2 Pod 里的三类容器
| 类型 | 启动时机 | 结束时机 | 典型用途 |
|---|---|---|---|
| init 容器 | 主容器之前,串行执行 | 必须成功退出 | 初始化数据、等待依赖、改权限 |
| 主容器(app) | init 全部成功后 | Pod 终止时 | 业务进程 |
| 边车(经典) | 与主容器并行启动 | Pod 终止时 | 代理、日志、同步 |
关键差异:init 容器是「有明确终点的一次性任务」,经典边车是「与主容器同生共死的长期进程」。原生边车容器则是二者的折中(见第 4 节)。
2. 经典 Sidecar 用法
2.1 流量代理
最广为人知的用法。边车接管 Pod 的入站/出站流量,主容器只与 localhost 通信:
spec:
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
- name: envoy
image: envoyproxy/envoy:v1.29
ports:
- containerPort: 15001 # 出站劫持端口
- containerPort: 15006 # 入站劫持端口
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"] # 改写 iptables 需要
代价:每个 Pod 多一个 Envoy 进程,内存开销按 Pod 数线性增长;高 Pod 密度集群里,边车内存可能超过业务本身。这正是 Sidecar 模式最被诟病的地方,也是 Service Mesh 引入「Sidecarless」架构(如 Cilium、Ambient Mesh)的动机。相关取舍见 Kubernetes 服务网格 。
2.2 日志采集
边车读取共享卷中的日志文件,推送到集中式日志系统:
spec:
volumes:
- name: varlog
emptyDir: {}
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: varlog
mountPath: /var/log/app
- name: log-tailer
image: fluent/fluent-bit:3.0
volumeMounts:
- name: varlog
mountPath: /var/log/app
readOnly: true
更推荐的替代方案是 DaemonSet 采集(每个节点一个采集器,直接读节点上的 /var/log/containers/)。它的优势是资源开销与 Pod 数解耦,劣势是容器 stdout 日志才自动落盘。两种方案的取舍:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Sidecar 采集 | 与 Pod 同生命周期,可处理非 stdout 日志 | 每 Pod 一份开销 |
| DaemonSet 采集 | 一份开销覆盖全节点 | 只能拿到 stdout/stderr(落到 /var/log/containers/) |
实践建议:优先让应用把日志写到 stdout(用 DaemonSet 采集),只在必须读自定义文件时才用 Sidecar。可观测性整体设计见 Kubernetes 可观测性 。
2.3 配置同步与其他模式
| 模式 | 边车职责 | 例子 |
|---|---|---|
| Config 同步 | 拉取配置写入共享卷,主容器监听文件变化 | 从 Vault 拉密钥写入卷 |
| 适配器(Adapter) | 把主容器的输出格式标准化 | 把自定义 metrics 转成 Prometheus 格式 |
| 健康检查代理 | 对外暴露统一的健康端点 | 无 HTTP 接口的应用 |
| 数据预取 | 提前把数据拉到共享卷 | 大模型权重预热 |
# Vault Agent Injector 就是典型的「配置同步边车」自动注入
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp"
3. Init 容器
3.1 语义
Init 容器是在主容器启动前串行执行、必须成功退出的容器:
Pod 启动序列:
调度到节点 → 拉取所有镜像(含 init)
→ init-1 执行 → 成功 → init-2 执行 → 成功 → ...
→ 所有 init 成功 → 主容器与边车并行启动
任一 init 失败(非 0 退出)→ 按 restartPolicy 重试整个 Pod
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z postgres.prod.svc.cluster.local 5432; do
echo "waiting for postgres..."
sleep 2
done
- name: fix-permissions
image: busybox:1.36
command: ["sh", "-c", "chown -R 1000:1000 /data"]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
3.2 常见用途
| 用途 | 说明 |
|---|---|
| 等待依赖 | 等数据库/消息队列就绪,避免主容器启动失败 |
| 权限修复 | 以 root 改共享卷属主,主容器用非 root 运行 |
| 数据初始化 | 下载模型权重、解压初始数据到共享卷 |
| 模板渲染 | 根据环境变量生成配置文件 |
3.3 Init 与 Sidecar 的选择判据
一句话判据:「有终点」的任务用 init,「无终点」的长期进程用边车。
等待数据库就绪(会结束) → init 容器
改文件属主(会结束) → init 容器
跑一个 Envoy 代理(不结束) → 边车容器
tail 日志文件(不结束) → 边车容器
常见错误:把「启动时执行一次的初始化脚本」写成边车(因为不想用 init 容器的串行语义),结果它启动完就退出,kubelet 按 restartPolicy: Always 反复重启它,Pod 里出现大量重启计数。这类任务应该用 init 容器,或者让主容器自己在启动流程里做。
4. 原生边车容器(Native Sidecar)
4.1 经典边车的三个痛点
| 痛点 | 场景 | 后果 |
|---|---|---|
| Job 永不结束 | 边车是长期进程,Job 里主容器退出后边车还在 | Job 永远不 Complete,CronJob 堆积 |
| 启动顺序无保证 | 主容器可能比边车先启动 | 流量被漏掉、日志丢失 |
| 终止顺序无保证 | 主容器可能先于边车终止 | 边车没机会 flush 缓冲日志 |
第 1 个问题最致命:一个加了日志边车的 Job,如果没有特殊处理,会永远卡在 Running。旧方案的绕法是让边车监听一个「哨兵文件」或使用 shareProcessNamespace + 脚本自杀,极其丑陋。
4.2 新机制:init 容器 + restartPolicy: Always
Kubernetes v1.29 起(v1.33 起 GA)支持把 restartPolicy: Always 加在 init 容器上,这类容器被称为原生边车容器(native sidecar):
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:3.0
restartPolicy: Always # ← 关键:声明为原生边车
volumeMounts:
- name: varlog
mountPath: /var/log/app
- name: wait-for-db # 普通 init,仍串行执行
image: busybox:1.36
command: ["sh", "-c", "until nc -z postgres 5432; do sleep 1; done"]
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: varlog
mountPath: /var/log/app
4.3 语义保证
原生边车容器带来了三条确定性:
1) 启动顺序:带 restartPolicy: Always 的 init 容器按顺序先启动,
且必须「就绪」(startupProbe 通过或进程启动)后,后续 init 与主容器才开始。
2) 终止顺序:Pod 终止时,主容器先收到 SIGTERM 并退出,
之后原生边车才被终止 —— 边车有机会把缓冲日志 flush 出去。
3) Job 语义:Job 的完成判定只等主容器(非边车容器),
边车不会再阻止 Job 变成 Complete。
# 完整示例:边车 + 探针保证「就绪后主容器才启动」
initContainers:
- name: proxy
image: envoyproxy/envoy:v1.29
restartPolicy: Always
startupProbe:
httpGet: { path: /ready, port: 9901 }
periodSeconds: 1
failureThreshold: 30
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5; curl -X POST localhost:9901/drain"]
4.4 迁移注意事项
| 事项 | 说明 |
|---|---|
| 集群版本 | 需 v1.29+(v1.33 起默认启用);旧集群不识别 restartPolicy 字段 |
| 资源计算 | 边车容器的 requests/limits 参与 Pod 的 effective resource request 计算(取 init 容器与主容器之和的最大值) |
| 探针 | 边车可配 startupProbe/readinessProbe,livenessProbe 也可用 |
| 与旧写法共存 | 普通 init 容器(无 restartPolicy)语义不变,可混用 |
| OPA/准入 | 若集群有策略校验「init 容器不得设 restartPolicy」,需同步更新 |
一句话:原生边车 = 「init 容器的启动顺序保证」+「普通容器的长期运行语义」——它一次性解决了 Job 卡死与启停顺序两个历史难题,是新集群的默认选择。
5. 生命周期与重启策略
5.1 两级重启策略
Kubernetes 有两级重启策略,容易混淆:
spec:
restartPolicy: Always # Pod 级:Always / OnFailure / Never
containers:
- name: app
# 容器级没有 restartPolicy(原生边车例外,且只能是 Always)
| Pod restartPolicy | 行为 | 适用 |
|---|---|---|
| Always | 容器退出即重启 | 长期服务(Deployment) |
| OnFailure | 非 0 退出才重启 | Job |
| Never | 从不重启 | 一次性任务 |
注意:Deployment 的 Pod 只能是 Always;Job 只能是 OnFailure/Never;OnFailure + restartPolicy: Always 的边车组合在 Job 中需谨慎。
5.2 生命周期钩子
containers:
- name: app
lifecycle:
postStart:
exec:
command: ["sh", "-c", "echo started >> /tmp/lifecycle.log"]
preStop:
exec:
command: ["sh", "-c", "sleep 10 && nginx -s quit"]
| 钩子 | 时机 | 关键约束 |
|---|---|---|
| postStart | 容器创建后、ENTRYPOINT 并行执行 | 不保证在 ENTRYPOINT 之前完成 |
| preStop | 容器终止前、发 SIGTERM 之前 | 阻塞在删除流程中,最长受 terminationGracePeriodSeconds 限制 |
postStart 的坑:它与主进程并行,若写成「等某个文件出现」,可能主进程已经开始工作了。真正「必须先完成」的逻辑应该放 init 容器。
5.3 优雅终止的完整顺序
Pod 被删除
1. Pod 标记 Terminating,从 EndpointSlice 摘除(异步)
2. 并行执行所有容器的 preStop 钩子
3. preStop 完成后,向容器主进程发 SIGTERM
4. 等待 terminationGracePeriodSeconds(默认 30s)
5. 超时未退出 → SIGKILL
(原生边车在主容器之后才被终止)
经典坑:Endpoint 摘除与 SIGTERM 是并行的,所以在 terminationGracePeriodSeconds 很短时,容器可能在流量还没停止时就被杀掉,产生 502。解法是在 preStop 里 sleep 几秒(等 Endpoint 摘除传播完成):
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
5.4 资源与调度影响
Sidecar 的资源请求会累加到 Pod 的调度需求上:
Pod 的调度资源 = sum(所有容器的 requests)
边车 requests.cpu = 100m、memory = 64Mi
→ 每个 Pod 多占 100m/64Mi,1000 个 Pod 就是 100 核 / 64Gi
这在高密度集群里非常可观。实践建议:
- 边车尽量设小的 requests + 合理的 limits,避免因边车抬高整个 Pod 的调度门槛;
- 用
ResourceQuota与LimitRange兜住边车注入带来的配额消耗(见 资源限制与 QoS ); - 若用 Sidecar 注入器(如 Istio),注意它注入的资源请求会让原本「刚好能调度」的 Pod 变成 Pending。
6. 实践建议与反模式
6.1 反模式清单
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| 用边车做一次性初始化 | 反复重启,重启计数暴涨 | 用 init 容器 |
| Job 里加经典边车 | Job 永不 Complete | 用原生边车或去掉边车 |
| 边车依赖主容器的输出顺序 | 竞态,偶发失败 | 加就绪探针或改为 init |
| 边车设超大 requests | Pod 调度困难、成本上升 | 精确测量后设置 |
| 边车与主容器共享 PID 命名空间却互相 kill | 进程互杀 | 谨慎使用 shareProcessNamespace |
| 每个 Pod 一个日志边车 | 资源开销随 Pod 数线性增长 | 改用 DaemonSet 采集 |
6.2 共享进程命名空间
spec:
shareProcessNamespace: true # 同 Pod 内容器可见彼此进程
开启后,边车可以给主容器发信号、做进程级监控(如导出主进程的 metrics)。代价是容器间隔离性下降,且主容器的 PID 1 不再是它自己的 ENTRYPOINT。仅在明确需要时开启。
6.3 决策流程
需要一个辅助能力
├─ 是「启动前必须完成的一次性任务」吗?
│ 是 → init 容器(串行、必须成功退出)
├─ 是「与主容器同生命周期的长期进程」吗?
│ 是 → 原生边车容器(restartPolicy: Always 的 init 容器)
│ └─ 集群 < v1.29 → 经典边车 + 注意 Job/顺序问题
└─ 是「每个节点一次」的能力吗?
是 → DaemonSet(如日志采集、CNI、NodeLocal DNS)
6.4 与 Job/批处理的协同
批处理场景是边车问题的高发区:一个 Spark/ML 训练 Job 里加了日志边车,作业跑完了 Job 却不结束。除了原生边车,另一种做法是让边车自己感知主容器退出(监听共享卷里的哨兵文件),但这类 workaround 在 v1.29+ 已无必要。批处理任务的完整设计见 Kubernetes Job 与批处理 。
7. 小结
| 主题 | 关键结论 |
|---|---|
| 三类容器 | init(有终点)/ 主容器 / 边车(长期) |
| 判据 | 有终点用 init,无终点用边车 |
| 经典痛点 | Job 卡死、启停顺序、终止顺序无保证 |
| 原生边车 | restartPolicy: Always 的 init 容器,v1.29+,v1.33 GA |
| 生命周期 | postStart 与主进程并行;preStop 在 SIGTERM 之前 |
| 优雅终止 | Endpoint 摘除与 SIGTERM 并行,preStop sleep 兜底 |
| 资源 | 边车 requests 累加到 Pod 调度需求,需精确设置 |
一句话记住:Sidecar 模式的本质是「用共享命名空间换取业务无侵入」——它把横切关注点(代理、日志、配置)从业务进程里剥离出来,代价是资源开销与生命周期复杂性。选择时的唯一判据是「这个辅助进程有没有终点」:有终点用 init,无终点用边车;而在 v1.29+ 的集群上,原生边车容器应该成为长期辅助进程的默认写法,因为它把启动顺序、终止顺序与 Job 语义这三件过去靠约定维持的事,变成了 API 层面的保证。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。