Kubernetes 在 Pod 中启动容器的方式出乎意料。在检查了源代码以确认我所看到的内容之后,我意识到我刚刚找到了 Istio Service Mesh 中一个长期存在的问题的解决方案。
我相信大多数 Kubernetes 用户都假定 Pod 的初始化容器完成后,将并行启动 Pod 的常规容器。事实并非如此。
如果检查启动容器的 Kubelet 代码,则会注意到它是按顺序执行的。该代码在一个线程中执行,并按照容器在 pod 的 spec.containers
数组中列出的顺序启动容器。
// Step 7: start containers in podContainerChanges.ContainersToStart.
for _, idx := range podContainerChanges.ContainersToStart {start("container", containerStartSpec(&pod.Spec.Containers[idx]))
}
但是,假设容器镜像已经存储在本地,则在 Kubelet 启动第一个容器之后,Kubelet 启动第二个容器所花费的时间可以忽略不计。实际上,它们都是同时启动的。
当一个容器依赖于另一个容器并要求它完全启动才能运行时,这是不理想的。Istio Proxy sidecar 容器就是一个例子。由于应用程序的传出通信是通过代理路由的,因此在启动应用程序本身之前,代理必须已启动并正在运行。
您可以在应用程序容器中添加一个 Shell 脚本,以等待代理启动,然后运行该应用程序的可执行文件。但这需要更改应用程序本身。理想情况下,我们希望在不对应用程序或其容器镜像进行任何更改的情况下将代理注入 Pod。
事实证明,这可以通过利用 Kubernetes 中的同步容器启动来完成。
首先,我们需要将代理指定为 spec.containers
中的第一个容器,但这只是解决方案的一部分,因为它只能确保首先启动代理容器,而不会等待其准备就绪。其他容器立即启动,从而导致容器之间的竞争状态。我们需要防止 Kubelet 在代理准备好之前启动其他容器。
这是启动后生命周期钩子出现的地方。事实证明,启动容器的 Kubelet 代码会阻止下一个容器的启动,直到启动后处理程序终止为止。
我们可以利用这种行为。不仅在 Istio 中,而且在必须启动 Sidecar 容器并准备就绪的每个 Pod 中,应用程序容器才能启动。
简而言之,Sidecar 启动问题的解决方案如下:
如果 sidecar 容器提供了一个等待该 sidecar 就绪的可执行文件,则可以在容器的启动后挂钩中调用该文件,以阻止 pod 中其余容器的启动。
下图应该可以帮助您直观地看到容器中发生的情况。
具体 yaml 如下:
apiVersion: v1
kind: Pod
metadata:
name: sidecar-starts-first
spec:
containers:
- name: sidecar
image: my-sidecar
lifecycle:
postStart:
exec:
command:
- /bin/wait-until-ready.sh
- name: application
image: my-application
但是,这种方法并不完美。如果在启动后挂钩中调用的命令或容器的主进程失败,则其他容器将立即启动。尽管如此,在 Kubernetes 引入对 sidecar container 的适当支持之前,这应该是一个好的解决方法。或者直到有人决定更改 Kubelet 的行为,并使其在单独的 goroutine 中启动容器。
尽管此技术可以解决容器启动顺序的问题,但是当您删除容器时,容器的容器停止顺序却无济于事。Pod 的容器实际上是并行终止的。当它关闭一个 Pod 时,Kubelet 在 goroutine 中执行容器终止代码 - 每个容器一个。与启动后挂钩不同,停止前挂钩因此并行运行。
如果仅在主应用程序容器终止后才需要停靠 sidecar,则必须以其他方式处理。
PS: 本文属于翻译,原文