Kubernetes中Sidecar生命周期管理
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.
Sep 21, 2020 · cloud
Kubernetes中Sidecar生命周期管理
在多个容器的 Pod 中,通常业务容器需要依赖 sidecar。启动时 sidecar 需要先启动,退出时 sidecar 需要在业务容器退出后再退出。k8s 目前对于 sidecar 的生命周期比较有争议,见issue、sidecarcontainers。
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
}
步骤如下:
- 执行 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
此方案可能存在的缺点:
- 如果 sidecar 启动失败或者 hook 失败,其他容器会立即启动
容器启动顺序比较好解决,退出顺序则是按照相反的顺序,业务容器先退出,之后 sidecar 再退出。
目前,在 kubelet 删除 pod 步骤如下;
- 遍历容器,每个容器起一个 goroutine 删除
- 删除时,先执行 pre stop hook,得到 gracePeriod=DeletionGracePeriodSeconds-period(stophook)
- 再调用 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 延迟退出的目的,但这种方式也有一些缺点
- 配置复杂,多个 sidecar 都需要配置 postStop 监听业务容器状态
- 业务容器需要有可观察性(提供特定形式的健康检测)
- poststop 执行异常,会等到最大优雅退出时间(默认 30s)后才终止
目前对于 sidecar 生命周期的支持方案对比如下:
方案 | 启动顺序 | 退出顺序 | job sidecar | 是否需要用户修改代码 | 是否需要修改 k8s 代码 | 缺点 | 备注 |
---|---|---|---|---|---|---|---|
用户控制 | 支持 | 不支持 | 不支持 | 需要 | 不需要 | 需要用户更改启动脚本;退出支持难度大,需要同时修改业务容器与 sidecar 启动脚本;大部分情况不支持 | 启动时需要检测 sidecar 服务状态 |
Lifecycle Hooks | 支持 | 支持 | 不支持 | 不需要 | 不需要 | 配置 hook 复杂度高;在 hook 执行异常情况下不能确保顺序 | |
富容器 | 支持 | 部分支持 | 部分支持 | 不需要 | 需要(更改镜像或启动命令) | 所有功能集成在一个容器中,对于外部 sidecar 如 istio envoy 等,不可控; | |
修改源码 | 支持 | 支持 | 支持 | 不需要 | 需要 | 需要满足各种情况,实现难度较大 | 社区有计划支持 |
在 k8s 提供此类功能前,目前没有完善的方案。Lifecycle Hooks 不需要更改用户启动代码以及 k8s 相关代码,相对于其他方式不失为一种解决思路。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK