在Kubernetes中实现Sidecar类型的Container

在一个Pod中,某个Container运行主要业务的同时,需要另一个Container协同 ——这是一个常见的业务场景,这个协同Container通常称为Sidecar。 主要的Container在运行时,Sidecar需要已经就绪;而当主要的Container停止后,Sidecar也需要停止。

这一功能,虽然是Pod级别的,但基本上也只有Job类业务会使用。 如果是Deployment,其实根本不需要区分Sidecar类型,大家默认都不会停,让外部统一决定生命周期即可。

目前(v1.20版本),官方仍然没有正式支持Sidecar类型的Container。 本文先介绍一个网上流传的乌龙,再介绍一种自己实现的土办法。

官方支持(乌龙)

Kubernetes自1.18版本,正式支持了Sidecar,作为一种lifestyle.type 经实测证明,这是个乌龙,实际上该功能还未正式进入主线。

一般网上给出的样例,调整成一个直接拿去可以跑的形式,大概是这样的:

---
apiVersion: batch/v1
kind: Job
metadata:
  name: hello
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: main
          image: alpine:3.12
          args:
            - sleep
            - 5
        - name: sidecar
          image: alpine:3.12
            - sleep
            - 100
          lifecycle:
            type: Sidecar

理论上,官方支持后,会以containers.lifecycle.type的方式,使用Sidecar类型。 也就是说,以上Job会自动在5秒后结束,而不需要等待100秒。

但既然功能还未进主线,相关MR也已被关闭。

误导文章如下:

其中还有一个像模像样的动态图:

Sidecar

相关PR与issue如下:

这事的情况,大概是在PR#79649提出Sidecar功能。 但是PR#1980被merge后,实现的基础消失了,lifecycle.type因其它原因被删除了。 而新提的PR#80744也是基于它的,也没被merge。 相关文章,属于提前吹捧,结果现实打脸。

Hack方案

既然官方没有,而且即使在v1.18添加,CCE也没有更新,那么只能自己准备Sidecar方案。 Sidecar类型的Container,其生命周期需要满足的条件和前面那个动图没有区别。

一个方案是,在主要的容器中,维护一个文件锁,而在其它容器中,根据这个锁的存在性决定自己的生命周期。

---
apiVersion: batch/v1
kind: Job
metadata:
  name: sidecar
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: main
          image: alpine:3.12
          imagePullPolicy: IfNotPresent
          args:
            - 'sh'
            - '-c'
            - >
              for i in 1 2 3;
              do
                date
                sleep 3s;
              done;
              rm /tmp/lifecycle/running.lock
          lifecycle:
            postStart:
              exec:
                command: ["touch", "/tmp/lifecycle/running.lock"]
            preStop:
              exec:
                command: ["rm", "/tmp/lifecycle/running.lock"]
          volumeMounts:
            - mountPath: /tmp/lifecycle/
              name: lifecycle
        - name: sidecar
          image: alpine:3.12
          imagePullPolicy: IfNotPresent
          command:
            - 'sh'
            - '-c'
            - >
              tail -f /dev/null &
              while [ -f /tmp/lifecycle/running.lock ];
              do
                date
                sleep 1s;
              done;
              jobs -p | xargs kill -9
          volumeMounts:
            - mountPath: /tmp/lifecycle/
              name: lifecycle
      volumes:
        - name: lifecycle
          emptyDir: {}
      securityContext:
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000

以上代码示例中,每一行包含/tmp/lifecycle的,都是Hack的关键。

这里通过挂载/tmp/lifecycle/目录,并确保在main中的一个入口(postStart)和两个出口(preStop与正常退出)之间, 维持/tmp/lifecycle/running.lock文件的存在。 在sidecar容器中,通过主进程轮询/tmp/lifecycle/running.lock文件,并在其消失后随之退出。

此外,监控检查也是一个可行方案。

    livenessProbe:
      exec:
        command:
          - cat
          - /tmp/lifecycle/running.lock
      initialDelaySeconds: 3
      periodSeconds: 1

但它有一些细节问题要调整,比如重试次数。 而最大的副作用是,Job总是失败的,这一点不能接受,因此放弃。

在实际使用中,可以用Python等高级语言,实现更复杂的Sidecar容器守护启动器。 除了定时检查文件锁以外,还需要监控子进程的运行状态。 以上的简单Shell样例中,子进程(其实是业务逻辑上的主进程)如果已经退出,主进程是不知道的。

结论

目前Kubernetes官方对Sidecar类型的容器,仍然缺乏原生支持。 网上流传的文案,其实都是错的。 而由于原MR的实现基础被删除,新方案还遥遥无期。

在Kubernetes正式支持Sidecar之前,仍然只能用各种Hack手段来实现其等价功能。 虽然对Sidecar容器做健康检查,可以实现类似生命周期,但是Pod总是以失败结束。 因此副作用最小的思路,还是让Sidecar容器的服务被一个特殊daemon守护,它轮询主要容器的状态,决定服务生死。 细节上,轮询无论使用文件锁还是网络通信,都是可以的。

参考


相关笔记