乐趣区

关于云计算:Kubernetes-上如何控制容器的启动顺序

去年写过一篇博客:管制 Pod 内容器的启动程序,剖析了 TektonCD 的容器启动管制的原理。

为什么要做容器启动顺序控制?咱们都晓得 Pod 中除了 init-container 之外,是容许增加多个容器的。相似 TektonCD 中 taskstep 的概念就别离与 podcontainer 对应,而 step 是依照程序执行的。此外还有服务网格的场景,sidecar 容器须要在服务容器启动之前实现配置的加载,也须要对容器的启动程序加以控制。否则,服务容器先启动,而 sidecar 还无奈提供网络上的反对。

事实

冀望

到了这里必定有同学会问,spec.containers[] 是一个数组,数组是有程序的。Kubernetes 也的确是依照程序来创立和启动容器,然而 容器启动胜利,并不示意容器能够对外提供服务

在 Kubernetes 1.18 非正式版中曾在 Lifecycle 层面提供了对 sidecar 类型容器的 反对,然而最终该性能并没有落地。

那到底该怎么做?

TL;DR

笔者筹备了一个简略的 go 我的项目,用于模仿 sidecar 的启动及配置加载。

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

应用 Deployment 的形式进行部署,间接用 Pod 也能够。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: sample
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: sample
    spec:
      containers:
      - image: addozhang/k8s-container-sequence-sidecar:latest
        name: sidecar
        imagePullPolicy: IfNotPresent
        lifecycle:
          postStart:
            exec:
              command:
                - /entrypoint
                - wait
      - image: busybox:latest
        name: app
        imagePullPolicy: IfNotPresent
        command: ["/bin/sh","-c"]
        args: ["date; echo'app container started'; tail -f /dev/null"]

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

Kubernetes 源码

在 kubelet 的源码 pkg/kubelet/kuberuntime/kuberuntime_manager.go 中,#SyncPod 办法用于创立 Pod,步骤比拟繁琐,间接看第 7 步:创立一般容器。

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

    return
}

#start 办法中调用了 #startContainer 办法,该办法会启动容器,并返回容器启动的后果。留神,这里的后果还 蕴含了容器的 Lifecycle hooks 调用

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

// startContainer starts a container and returns a message indicates why it is failed on error.
// It starts the container through the following steps:
// * pull the image
// * create the container
// * start the container
// * run the post start lifecycle hooks (if applicable)
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) {
     
     ...
     
    // Step 4: execute the post start hook.
    if container.Lifecycle != nil && container.Lifecycle.PostStart != nil {
        kubeContainerID := kubecontainer.ContainerID{
            Type: m.runtimeName,
            ID:   containerID,
        }
        msg, handlerErr := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart)
        if handlerErr != nil {m.recordContainerEvent(pod, container, kubeContainerID.ID, v1.EventTypeWarning, events.FailedPostStartHook, msg)
            if err := m.killContainer(pod, kubeContainerID, container.Name, "FailedPostStartHook", reasonFailedPostStartHook, nil); err != nil {klog.ErrorS(fmt.Errorf("%s: %v", ErrPostStartHook, handlerErr), "Failed to kill container", "pod", klog.KObj(pod),
                    "podUID", pod.UID, "containerName", container.Name, "containerID", kubeContainerID.String())
            }
            return msg, fmt.Errorf("%s: %v", ErrPostStartHook, handlerErr)
        }
    }

    return "", nil
}

实现计划

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

PostStart 中继续的去查看 /ready 断点,能够 hold 住以后容器的创立流程。保障 /ready 返回 200 后,kubelet 才会去创立下一个容器。

这样就达到了后面截图中演示的成果。

for time.Now().Before(timeoutAt) {err = checkIfReady(client, url)
    if err == nil {log.Println("sidecar is ready")
        return nil
    }
    log.Println("sidecar is not ready")
    time.Sleep(time.Duration(periodMillis) * time.Millisecond)
}
return fmt.Errorf("sidecar is not ready in %d second(s)", timeoutSeconds)

参考

  • Sidecar container lifecycle changes in Kubernetes 1.18
  • Delaying application start until sidecar is ready

    文章对立公布在公众号 云原生指北

退出移动版