千呼万唤始出来的K8s Sidecar

2023年 8月 18日 53.2k 0

随着Kubernetes发布了1.28,支持了不少重磅特性,其中最令人感慨的莫过于新的Sidecar,目前是alpha版本。

之前Sidecar的称谓只是一种多容器的设计模式,在K8s看来和普通容器没什么不一样,但由于其生命周期与业务容器并不一致,对于Sidecar的生命周期管理一直是个问题,我也写过相关解决办法。

最早在15年就K8s Blog就提到了Sidecar,1.18发布前,当时很多文章宣称将支持Sidecar,但最终还是没能进入Master,三、四年多过去了,工作都换了几份,终于等来了它。

快速上手

目前Sidecar默认不开启,需要开启对应Feature Gate SidecarContainers,以minikube为例快速开启Sidecar(kind目前对1.28支持有问题):

# 目前minikube默认版本小于1.28.0,需要指定k8s版本,后续升级后可不需要
minikube start --feature-gates=SidecarContainers=true --kubernetes-version=v1.28.0

新版本的Sidecar是放置在initContainers中,指定restartPolicyAlways便开启Sidecar,其生命周期以及重启管理与普通容器也是一样的。

下面是一个带有Sidecar的Deployment示例,log Sidecar容器用来输出日志到终端,main容器模拟写入日志:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: main
          image: alpine:latest
          command: ['sh', '-c', 'while true; do echo "logging" >> /opt/logs.txt; sleep 1; done']
          volumeMounts:
            - name: data
              mountPath: /opt
      initContainers:
        - name: log # sidecar 容器
          image: alpine:latest
          restartPolicy: Always # 必须指定restartPolicy为Always才能开启sidecar
          command: ['sh', '-c', 'tail -F /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      volumes:
        - name: data
          emptyDir: {}

部署到K8s集群中,可以看到initContainers[*].restartPolicy字段,如果不开启Feature Gate是没有这个字段的:

> kubectl create -f deploy-sidecar.yaml
deployment.apps/myapp created

> kubectl get po -l app=myapp -ojsonpath='{.items[0].spec.initContainers[0].restartPolicy}'
Always

> kubectl get po  -l app=myapp  
NAME                    READY   STATUS    RESTARTS   AGE
myapp-5698fbb8d-rpjzn   2/2     Running   0          3m25s

myapp Pod中两个容器都是Ready(2/2),查看日志可以看到log Sidecar一直在输出日志。

> kubectl logs -l app=myapp -c logshipper -f
logging
logging

源码分析

相关源码在kubernetes#116429。

这次的Sidecar是通过初始化容器实现的,在Container类型中添加了额外的字段RestartPolicy,目前只支持Always策略:

// Container represents a single container that is expected to be run on the host.
type Container struct {
	// Required: This must be a DNS_LABEL.  Each container in a pod must
	// have a unique name.
	Name string
	// Required.
	Image string
	// ...
	
	// +featureGate=SidecarContainers
	// +optional
	RestartPolicy *ContainerRestartPolicy

    // ...
}

容器的重启策略有以下几种情况:

  • 普通容器不可设置,由Pod.RestartPolicy决定重启行为
  • 初始化容器默认缺省,重启行为由Pod.RestartPolicy决定,当Pod设置为Always,容器按照OnFailure策略
  • 初始化容器设置为Always,即Sidecar容器会长时间运行

通常Sidecar有特殊的启动顺序,先于业务容器启动,后于业务容器退出,比如日志收集Sidecar、服务网络Istio Envoy等。

启动顺序

由于初始化容器是有序启动的,Sidecar容器Ready后才会启动下一个,这部分复用了InitContainers的逻辑。

特别注意的是,启动Sidecar初始化容器时,并不会等会启完成(Completed),这和普通的初始化容器是不一样的。关于初始化容器启动顺序主要在kuberuntime_container.go中的computeInitContainerActions

func (m *kubeGenericRuntimeManager) computeInitContainerActions(pod *v1.Pod, podStatus *kubecontainer.PodStatus, changes *podActions) bool {
  //...
  for i := len(pod.Spec.InitContainers) - 1; i >= 0; i-- {
		container := &pod.Spec.InitContainers[i]
		status := podStatus.FindContainerStatusByName(container.Name)
		if status == nil {
			// 如果普通容器初始化了并且Sidecar没有状态,启动Sidecar
			if isPreviouslyInitialized && types.IsRestartableInitContainer(container) {
				changes.InitContainersToStart = append(changes.InitContainersToStart, i)
			}
			continue //找到最后需要处理的容器
		}

		if isPreviouslyInitialized && !types.IsRestartableInitContainer(container) {
			// 初始化过,Sidecar容器保持Running
			continue
		}

    switch status.State {
		case kubecontainer.ContainerStateCreated:
			// nothing to do but wait for it to start

		case kubecontainer.ContainerStateRunning:
      // 等待普通容器运行结束
			if !types.IsRestartableInitContainer(container) {
				break
			}

			if types.IsRestartableInitContainer(container) {
        if container.StartupProbe != nil {} // 执行探针, StartupProbe与LivenessProbe


      }
    }
  }

  // 如果没有初始化,取第一个容器开始处理 
  if !isPreviouslyInitialized {
		changes.InitContainersToStart = append(changes.InitContainersToStart, 0)
	}
}

详细启动流程如下:

  • 启动时,从第一个初始化容器开始处理
  • 获取上一个有状态的容器
    • 对于普通初始化容器,容器状态执行完成(exited 0),继续执行下一个;失败则根据Pod的重启策略进行处理
    • 对于Sidecar,状态为running不额外处理(含有探针需要执行对应探针),状态为exited进行重启
  • 重复步骤2直到处理完所有初始化容器
  • 终止顺序

    很遗憾目前Alpha版本不支持Sidecar按照特定的顺序退出,退出时将Sidecar将视作普通容器,后续Beta版本可能会支持。

    当Pod需要删除时,会调用killPodWithSyncResult,获取当前Pod的所有Running容器(包括Sidecar)进行删除

    func (m *kubeGenericRuntimeManager) killContainersWithSyncResult(ctx context.Context, pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) (syncResults []*kubecontainer.SyncResult) {
    containerResults := make(chan *kubecontainer.SyncResult, len(runningPod.Containers))
    wg := sync.WaitGroup{}

    wg.Add(len(runningPod.Containers))
    for _, container := range runningPod.Containers {
    go func(container *kubecontainer.Container) {
    defer utilruntime.HandleCrash()
    defer wg.Done()

    killContainerResult := kubecontainer.NewSyncResult(kubecontainer.KillContainer, container.Name)
    if err := m.killContainer(ctx, pod, container.ID, container.Name, "", reasonUnknown, gracePeriodOverride); err != nil {
    //...
    }
    containerResults

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论