Sidecar 模式与原生边车容器:init 容器、生命周期与重启策略

系统梳理 Kubernetes 的 Sidecar 模式:代理、日志采集、配置同步等经典用法,init 容器与边车的语义差异,v1.29 原生边车容器(restartPolicy: Always 的 init 容器)如何解决 Job 卡死与启停顺序问题,容器生命周期钩子与优雅终止顺序,以及资源共享、调度与反模式规避。

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
边车设超大 requestsPod 调度困难、成本上升精确测量后设置
边车与主容器共享 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 层面的保证。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「云原生」更多文章

  1. 证书管理与自动轮换:cert-manager 与 kubelet 证书轮换
  2. CoreDNS 与服务发现:插件链、NDOTS 与 Headless Service
  3. etcd 运维与性能调优:备份恢复、碎片整理与配额