4

Kubernetes中Sidecar生命周期管理

 1 year ago
source link: https://qingwave.github.io/k8s-sideccar-lifecycle/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Sep 21, 2020 · cloud

Kubernetes中Sidecar生命周期管理



在多个容器的 Pod 中,通常业务容器需要依赖 sidecar。启动时 sidecar 需要先启动,退出时 sidecar 需要在业务容器退出后再退出。k8s 目前对于 sidecar 的生命周期比较有争议,见issuesidecarcontainers

Kubernetes Pod 内有两种容器: 初始化容器(init container)和应用容器(app container)。

其中初始化容器的执行先于应用容器,按顺序启动,执行成功启动下一个:

    if container := podContainerChanges.NextInitContainerToStart; container != nil {
        // Start the next init container.
        if err := start("init container", containerStartSpec(container)); err != nil {
            return
        }

        // Successfully started the container; clear the entry in the failure
        klog.V(4).Infof("Completed init container %q for pod %q", container.Name, format.Pod(pod))
    }

而对于应用容器,无法保证容器 ready 顺序,启动代码如下:

    // Step 7: start containers in podContainerChanges.ContainersToStart.
    for _, idx := range podContainerChanges.ContainersToStart {
        // start函数向docker发请求启动容器,这里没有检测函数返回而且不确定ENTRYPOINT是否成功
        start("container", containerStartSpec(&pod.Spec.Containers[idx]))
    }

在删除时,同样无法保证删除顺序,代码如下

    for _, container := range runningPod.Containers {
        go func(container *kubecontainer.Container) {
            killContainerResult := kubecontainer.NewSyncResult(kubecontainer.KillContainer, container.Name)
            // 每一个容器起goroutine执行删除
            if err := m.killContainer(pod, container.ID, container.Name, "", gracePeriodOverride); err != nil {
               ...
            }
            containerResults <- killContainerResult
        }(container)
    }

k8s 原生方式,对于 pod 中一个容器依赖另一个容器,目前需要业务进程判断依赖服务是否启动或者 sleep 10s,这种方式可以工作,但不太优雅。需要业务更改启动脚本。

那么,有没有其他的解决办法?

在启动时,start 函数调用 startContainer 来创建容器,主要代码如下:

func (m *kubeGenericRuntimeManager) startContainer(podSandboxID string, podSandboxConfig *runtimeapi.PodSandboxConfig, spec *startSpec, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP string, podIPs []string) (string, error) {
    container := spec.container

    // Step 1: 拉镜像.
    imageRef, msg, err := m.imagePuller.EnsureImageExists(pod, container, pullSecrets, podSandboxConfig)
    if err != nil {
        ...
     }

    // Step 2: 调用cri创建容器
    // For a new container, the RestartCount should be 0
    containerID, err := m.runtimeService.CreateContainer(podSandboxID, containerConfig, podSandboxConfig)
   ...

    // Step 3: 启动容器
    err = m.runtimeService.StartContainer(containerID)

    // Step 4: 执行 post start hook.
    if container.Lifecycle != nil && container.Lifecycle.PostStart != nil {
        kubeContainerID := kubecontainer.ContainerID{
            Type: m.runtimeName,
            ID:   containerID,
        }
        // 调用Run来执行hook
        msg, handlerErr := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart)
        ...
    }

    return "", nil
}

步骤如下:

  1. 执行 hook

一个 Pod 中容器的启动是有顺序的,排在前面容器的先启动。同时第一个容器执行完 ENTRYPOINT 和 PostStart 之后(异步执行,无法确定顺序),k8s 才会创建第二个容器(这样的话就可以保证第一个容器创建多长时间后再启动第二个容器)

如果我们 PostStart 阶段去检测容器是否 ready,那么只有在 ready 后才去执行下一个容器。

配置如下,sidecar 模拟需要依赖的容器,main 为业务容器

apiVersion: v1
kind: Pod
metadata:
  name: test-start
spec:
  containers:
    - name: sidecar
      image: busybox
      command: ['/bin/sh', '-c', 'sleep 3600']
      lifecycle:
        postStart:
          exec:
            command: ['/bin/sh', '-c', 'sleep 20']
    - name: main
      image: busybox
      command: ['/bin/sh', '-c', 'sleep 3600']

得到结果如下,可以看到 sidecar 启动 21s 后才开始启动 main 容器,满足需求

Events:
  Type    Reason     Age   From                                          Message
  ----    ------     ----  ----                                          -------
  Normal  Scheduled  54s   default-scheduler                             Successfully assigned default/test-start to tj1-staging-k8s-slave95-202008.kscn
  Normal  Pulling    53s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Pulling image "busybox"
  Normal  Pulled     44s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Successfully pulled image "busybox"
  Normal  Created    44s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Created container sidecar
  Normal  Started    44s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Started container sidecar
  Normal  Pulling    23s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Pulling image "busybox"
  Normal  Pulled     19s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Successfully pulled image "busybox"
  Normal  Created    18s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Created container main
  Normal  Started    18s   kubelet, tj1-staging-k8s-slave95-202008.kscn  Started container main

此方案可能存在的缺点:

  1. 如果 sidecar 启动失败或者 hook 失败,其他容器会立即启动

容器启动顺序比较好解决,退出顺序则是按照相反的顺序,业务容器先退出,之后 sidecar 再退出。

目前,在 kubelet 删除 pod 步骤如下;

  1. 遍历容器,每个容器起一个 goroutine 删除
  2. 删除时,先执行 pre stop hook,得到 gracePeriod=DeletionGracePeriodSeconds-period(stophook)
  3. 再调用 cri 删除接口 m.runtimeService.StopContainer(containerID.ID, gracePeriod)

如果在 sidecar 的 pre stop hook 检测业务容器状态,那么可以延迟退出。

业务容器 main 退出时,创建文件;sidecar 通过 post-stop 检测到文件后,执行退出

apiVersion: v1
kind: Pod
metadata:
  name: test-stop
spec:
  containers:
    - name: sidecar
      image: busybox
      command:
        - '/bin/sh'
        - '-c'
        - |
          trap "touch /lifecycle/sidecar-terminated" 15
          until [ -f "/lifecycle/sidecar-terminated" ];do
            date
            sleep 1
          done
          sleep 5
          cat /lifecycle/main-terminated
          t=$(date)
          echo "sidecar exit at $t"
      lifecycle:
        preStop:
          exec:
            command:
              - '/bin/sh'
              - '-c'
              - |
                until [ -f "/lifecycle/main-terminated" ];do
                  sleep 1
                done
                t=$(date)
                echo "main exit at $t" > /lifecycle/main-terminated
      volumeMounts:
        - name: lifecycle
          mountPath: /lifecycle
    - name: main
      image: busybox
      command:
        - '/bin/sh'
        - '-c'
        - |
          trap "touch /lifecycle/main-terminated" 15
          until [ -f "/lifecycle/main-terminated" ];do
            date
            sleep 1
          done
      volumeMounts:
        - name: lifecycle
          mountPath: /lifecycle
  volumes:
    - name: lifecycle
      emptyDir: {}

在日志中看到,main 容器先结束,sidecar 检测到 main-terminated 文件后,执行完 post-stop-hook,sidecar 主进程开始退出

$ kubectl  logs -f test-stop main
...
Tue Sep  8 03:14:20 UTC 2020
Tue Sep  8 03:14:21 UTC 2020
Tue Sep  8 03:14:22 UTC 2020

$ kubectl  logs -f test-stop sidecar
Tue Sep  8 03:14:22 UTC 2020
Tue Sep  8 03:14:23 UTC 2020
# post stop hook 检测到main容器退出,记录日志
main exit at Tue Sep  8 03:14:23 UTC 2020
# sidecar主进程退出
sidecar exit at Tue Sep  8 03:14:29 UTC 2020

通过测试,使用 postStopHook 可以达到 sidecar 延迟退出的目的,但这种方式也有一些缺点

  1. 配置复杂,多个 sidecar 都需要配置 postStop 监听业务容器状态
  2. 业务容器需要有可观察性(提供特定形式的健康检测)
  3. poststop 执行异常,会等到最大优雅退出时间(默认 30s)后才终止

目前对于 sidecar 生命周期的支持方案对比如下:

方案启动顺序退出顺序job sidecar是否需要用户修改代码是否需要修改 k8s 代码缺点备注
用户控制支持不支持不支持需要不需要需要用户更改启动脚本;退出支持难度大,需要同时修改业务容器与 sidecar 启动脚本;大部分情况不支持启动时需要检测 sidecar 服务状态
Lifecycle Hooks支持支持不支持不需要不需要配置 hook 复杂度高;在 hook 执行异常情况下不能确保顺序
富容器支持部分支持部分支持不需要需要(更改镜像或启动命令)所有功能集成在一个容器中,对于外部 sidecar 如 istio envoy 等,不可控;
修改源码支持支持支持不需要需要需要满足各种情况,实现难度较大社区有计划支持

在 k8s 提供此类功能前,目前没有完善的方案。Lifecycle Hooks 不需要更改用户启动代码以及 k8s 相关代码,相对于其他方式不失为一种解决思路。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK