WEBKT

彻底解决 Kubernetes Job 中 Sidecar 容器不退出的三大类方案

45 0 0 0

在 Kubernetes 运维实战中,我们经常会遇到一个尴尬的场景:一个 Job 的主任务容器(Main Container)已经运行结束并成功退出(Exit Code 0),但整个 Pod 却一直处于 Running 状态,迟迟无法进入 Completed 阶段。

究其原因,往往是 Pod 里的 Sidecar 容器(如 Istio-proxy、Cloud SQL Proxy 或日志采集插件)依然在坚守岗位。由于 K8s 的 Job 控制器只有在 Pod 内 所有 容器都退出后才会标记任务完成,这些“永不言弃”的 Sidecar 就成了任务结束的绊脚石。

本文将由浅入深,介绍几种解决该问题的硬核方案。

方案一:使用 K8s 1.29+ 原生的 Sidecar 特性(最优解)

在 Kubernetes 1.29 版本(及其后的稳定版)中,官方终于引入了原生的 Sidecar 支持。这是最优雅的解决方案,不需要任何 Hack 脚本。

通过在 initContainers 中定义容器,并将其 restartPolicy 设置为 Always,K8s 会将其视作 Sidecar 容器。这种容器具有以下特性:

  1. 它会在主容器启动前启动,并一直运行。
  2. 关键点: 当主容器(普通 containers 列表里的容器)运行结束退出后,K8s 会自动向这些 restartPolicy: Always 的初始化容器发送 SIGTERM 信号,强制其关闭。

配置示例:

apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
spec:
  template:
    spec:
      initContainers:
      - name: sidecar-proxy
        image: my-proxy:latest
        restartPolicy: Always  # 关键配置:标记为原生 Sidecar
      containers:
      - name: main-worker
        image: busybox
        command: ["sh", "-c", "echo 'Doing work...'; sleep 10"]
      restartPolicy: OnFailure

方案二:利用进程共享与信号传递(适用于旧版本)

如果你还在使用旧版本的 K8s(如 1.28 以下),无法使用原生特性,那么“共享进程命名空间”是最稳健的替代方案。

通过设置 shareProcessNamespace: true,Pod 内的所有容器将共享同一个 PID 空间。主容器可以通过脚本检测自身任务是否完成,然后直接“跨边界”杀掉 Sidecar 的进程。

核心步骤:

  1. spec 中开启 shareProcessNamespace: true
  2. 主容器在脚本最后添加逻辑,寻找 Sidecar 进程并发送信号。

配置示例:

spec:
  shareProcessNamespace: true
  containers:
  - name: main-worker
    image: alpine
    command: ["/bin/sh", "-c"]
    args:
    - |
      ./do_work.sh
      # 任务结束后,杀掉名为 sidecar-app 的进程
      pkill -SIGTERM sidecar-app
  - name: sidecar-app
    image: my-sidecar:latest

方案三:特定组件的 API 调用(针对 Istio 等)

许多流行的开源组件已经意识到了这个问题,并提供了专门的 API 接口来让 Sidecar 自毁。

Istio 为例,你可以通过向本地的 Envoy 代理发送一个 POST 请求来触发退出:

# 在主任务执行完毕后调用
curl -X POST http://localhost:15020/quitquitquit

如果是使用 Cloud SQL Proxy,较新版本也支持类似的操作。这种方案的优点是不需要共享 PID 空间,缺点是业务容器镜像里必须得有 curl 或者类似的工具。

方案四:基于文件共享的“自杀”脚本

这是一种通用的“土办法”,不需要依赖 K8s 版本,也不需要特殊的权限。

  1. 共享目录: 创建一个 emptyDir 卷,挂载到所有容器。
  2. 主容器: 任务结束后,在共享目录下创建一个文件(如 /cache/done)。
  3. Sidecar 容器: 修改启动命令,包装一个监控脚本。

Sidecar 包装脚本示例:

# sidecar-entrypoint.sh
/usr/bin/original-sidecar-bin &
SIDE_PID=$!

while true; do
  if [ -f "/cache/done" ]; then
    echo "Detection main container finished, exiting..."
    kill $SIDE_PID
    exit 0
  fi
  sleep 2
done

总结与建议

  • 新集群(1.29+): 请务必拥抱原生 restartPolicy: AlwaysinitContainers 方案,这是 K8s 社区打磨多年的标准。
  • 存量旧集群: 优先考虑 shareProcessNamespace + pkill,因为它对镜像的侵入性相对较小。
  • 如果是 Istio 环境: 直接在主任务最后一行加上 curl -X POST .../quitquitquit 是最快、成本最低的改动方式。

处理 Sidecar 退出问题的本质,是容器间生命周期同步的缺失。随着 K8s 的演进,这一问题正在从“工程 Trick”回归到“平台标准”,建议开发者在设计 Job 类应用时,优先考虑架构的前向兼容性。

码农老王 KubernetesSidecar云原生架构

评论点评