为什么要做容器启动顺序控制?我们都知道 Pod 中除了 init-container 之外,是允许添加多个容器的。类似 TektonCD 中 task 和 step 的概念就分别与 pod 和 container 对应,而 step 是按照顺序执行的。此外还有服务网格的场景,sidecar 容器需要在服务容器启动之前完成配置的加载,也需要对容器的启动顺序加以控制。否则,服务容器先启动,而 sidecar 还无法提供网络上的支持。

现实

%title插图%num

 

期望

%title插图%num

 

到了这里肯定有同学会问,spec.containers[] 是一个数组,数组是有顺序的。Kubernetes 也确实是按照顺序来创建和启动容器,但是 容器启动成功,并不表示容器可以对外提供服务。

在 Kubernetes 1.18 非正式版中曾在 Lifecycle 层面提供了对 sidecar 类型容器的 支持,但是*终该功能并没有落地[2]。

那到底该怎么做?

%title插图%num

TL;DR

笔者准备了一个简单的 go 项目[3],用于模拟 sidecar 的启动及配置加载。

克隆代码后可以通过 make build 构建出镜像,假如你是用的 minikube 进行的实验,可以通过命令 make load-2-minikube 将镜像加载到 minikube 节点中。

使用 Deployment 的方式进行部署,直接用 Pod 也可以。

  1. apiVersion: apps/v1
  2. kind:Deployment
  3. metadata:
  4. creationTimestamp:null
  5. labels:
  6. app: sample
  7. name: sample
  8. spec:
  9. replicas:1
  10. selector:
  11. matchLabels:
  12. app: sample
  13. strategy:{}
  14. template:
  15. metadata:
  16. creationTimestamp:null
  17. labels:
  18. app: sample
  19. spec:
  20. containers:
  21. image: addozhang/k8s-container-sequence-sidecar:latest
  22. name: sidecar
  23. imagePullPolicy:IfNotPresent
  24. lifecycle:
  25. postStart:
  26. exec:
  27. command:
  28. -/entrypoint
  29. wait
  30. image: busybox:latest
  31. name: app
  32. imagePullPolicy:IfNotPresent
  33. command:[“/bin/sh”,”-c”]
  34. args:[“date; echo ‘app container started’; tail -f /dev/null”]

下面的截图中,演示了在 sample 命名空间中,pod 内两个容器的执行顺序。

%title插图%num

%title插图%num

Kubernetes 源码

在 kubelet 的源码 pkg/kubelet/kuberuntime/kuberuntime_manager.go 中,#SyncPod 方法用于创建 Pod,步骤比较繁琐,直接看第 7 步:创建普通容器。

  1. // SyncPod syncs the running pod into the desired pod by executing following steps:
  2. //
  3. // 1. Compute sandbox and container changes.
  4. // 2. Kill pod sandbox if necessary.
  5. // 3. Kill any containers that should not be running.
  6. // 4. Create sandbox if necessary.
  7. // 5. Create ephemeral containers.
  8. // 6. Create init containers.
  9. // 7. Create normal containers.
  10. func (m *kubeGenericRuntimeManager)SyncPod(pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff)(result kubecontainer.PodSyncResult){
  11. // Step 7: start containers in podContainerChanges.ContainersToStart.
  12. for _, idx := range podContainerChanges.ContainersToStart{
  13. start(“container”, containerStartSpec(&pod.Spec.Containers[idx]))
  14. }
  15. return
  16. }

在 #start 方法中调用了 #startContainer 方法,该方法会启动容器,并返回容器启动的结果。注意,这里的结果还 包含了容器的 Lifecycle hooks 调用。

也就是说,假如容器的 PostStart hook 没有正确的返回,kubelet 便不会去创建下一个容器。

  1. // startContainer starts a container and returns a message indicates why it is failed on error.
  2. // It starts the container through the following steps:
  3. // * pull the image
  4. // * create the container
  5. // * start the container
  6. // * run the post start lifecycle hooks (if applicable)
  7. 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){
  8. // Step 4: execute the post start hook.
  9. if container.Lifecycle!=nil&& container.Lifecycle.PostStart!=nil{
  10. kubeContainerID := kubecontainer.ContainerID{
  11. Type: m.runtimeName,
  12. ID: containerID,
  13. }
  14. msg, handlerErr := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart)
  15. if handlerErr !=nil{
  16. m.recordContainerEvent(pod, container, kubeContainerID.ID, v1.EventTypeWarning, events.FailedPostStartHook, msg)
  17. if err := m.killContainer(pod, kubeContainerID, container.Name,“FailedPostStartHook”, reasonFailedPostStartHook,nil); err !=nil{
  18. klog.ErrorS(fmt.Errorf(“%s: %v”,ErrPostStartHook, handlerErr),“Failed to kill container”,“pod”, klog.KObj(pod),
  19. “podUID”, pod.UID,“containerName”, container.Name,“containerID”, kubeContainerID.String())
  20. }
  21. return msg, fmt.Errorf(“%s: %v”,ErrPostStartHook, handlerErr)
  22. }
  23. }
  24. return“”,nil
  25. }

%title插图%num

实现方案

%title插图%num

cmd/entrypoint/wait.go#L26[4] (这里参考了 Istio 的 pilot-agent 实现 )

在 PostStart 中持续的去检查 /ready 断点,可以 hold 住当前容器的创建流程。保证 /ready 返回 200 后,kubelet 才会去创建下一个容器。

这样就达到了前面截图中演示的效果。

  1. for time.Now().Before(timeoutAt){
  2. err = checkIfReady(client, url)
  3. if err ==nil{
  4. log.Println(“sidecar is ready”)
  5. returnnil
  6. }
  7. log.Println(“sidecar is not ready”)
  8. time.Sleep(time.Duration(periodMillis)* time.Millisecond)
  9. }
  10. return fmt.Errorf(“sidecar is not ready in %d second(s)”, timeoutSeconds)