-
50张图,掌握Kubernetes中优雅且零停机部署的实现
前言
在本文中,您将了解如何在Pod启动或关闭时防止连接异常,并将学习如何以优雅的方式关闭长时间运行的任务。

在Kubernetes中,创建和删除Pod是最常见的任务之一。
当您执行滚动更新,扩展部署,每个新发行版,每个作业和cron作业等时,都会创建Pod。
但是在节点被驱逐之后,Pods也会被删除并重新创建—例如,当您将节点标记为不可调度时。
这些Pod的生命是如此短暂,那么当Pod在响应请求的过程中却被告知关闭时会发生什么?
请求在关闭之前是否已完成?
接下来的请求又如何呢?
在讨论删除Pod时会发生什么之前,有必要讨论一下创建Pod时会发生什么。
假设您要在集群中创建以下Pod:
pod.yaml
apiVersion: v1 kind: Pod metadata: name: my-pod spec: containers: - name: web image: nginx ports: - name: web containerPort: 80
您可以使用以下方式将YAML定义提交给集群:
kubectl apply -f pod.yaml在数据库中保存集群的状态
API接收和检查Pod定义,然后将其存储在数据库etcd中。
Pod也将添加到调度程序的队列中。
调度程序:
-
检查定义
-
收集有关工作负载的详细信息,例如CPU和内存请求,然后
-
确定哪个节点最适合运行它。
在过程结束时:
-
在etcd中将Pod标记为Scheduled。
-
为Pod分配了一个节点。
-
Pod的状态存储在etcd中。
但是Pod仍然不存在。
-
当您使用
kubectl apply -f提交一个Pod时,YAML被发送到kubernetes api。

-
API将Pod保存在数据库etcd中。

-
调度程序为这个Pod分配最佳节点,并且Pod的状态更改为Pending。pod只存在于etcd中。

先前的任务发生在控制平面中,并且状态存储在数据库中。
那么谁在您的节点中创建Pod?
Kubelet — Kubernetes代理
kubelet的工作是轮询控制平面以获取更新。
您可以想象kubelet不断地向主节点询问:“我管理工作节点1,是否对我有任何新的Pod?”。
当有Pod时,kubelet会创建它。
有一点需要注意。
kubelet不会自行创建Pod。而是将工作委托给其他三个组件:
-
容器运行时接口(CRI) — 为Pod创建容器的组件。
-
容器网络接口(CNI) — 将容器连接到群集网络并分配IP地址的组件。
-
容器存储接口(CSI) — 在容器中装载卷的组件。
在大多数情况下,容器运行时接口(CRI)的工作类似于:
docker run -d <my-container-image>容器网络接口(CNI)有点有趣,因为它负责:
-
为Pod生成有效的IP地址。
-
将容器连接到网络的其余部分。
可以想象,有几种方法可以将容器连接到网络并分配有效的IP地址(您可以在IPv4或IPv6之间进行选择,也可以分配多个IP地址)。
例如,Docker创建虚拟以太网对并将其连接到网桥,而AWS—CNI将Pods直接连接到虚拟私有云(VPC)。
当容器网络接口完成其工作时,Pod已连接到网络,并分配了有效的IP地址。
还有一个问题。
Kubelet知道IP地址(因为它调用了容器网络接口),但是控制平面却不知道。
没有人告诉主节点,该Pod已分配了IP地址,并准备接收流量。
就控制平面而言,仍在创建Pod。
Kubelet的工作是收集Pod的所有详细信息(例如IP地址)并将其报告回控制平面。
您可以想象检查etcd不仅可以显示Pod的运行位置,还可以显示其IP地址。
-
Kubelet轮询控制平面以获取更新。

-
当一个新的Pod分配给它的节点时,kubelet会检索详细信息

-
Kubernetns不会自己创建pod。它依赖于三个组件:容器运行时接口、容器网络接口和容器存储接口。

-
一旦所有三个组件都成功完成,Pod就在您的节点中运行并分配了一个IP地址。

-
kubelet向控制平面报告IP地址。

如果Pod不是任何服务的一部分,那么任务将结束。
Pod已创建并可以使用。
如果Pod是服务的一部分,则还需要执行几个步骤。
Pods和Services
创建服务时,通常需要注意以下两条信息:
-
selector— 用于指定将接收流量的Pod。 -
targetPort— 通过pod使用的端口接收的流量。
服务的典型YAML定义如下所示:
service.yaml
apiVersion: v1 kind: Service metadata: name: my-service spec: ports: - port: 80 targetPort: 3000 selector: name: app
将Service提交给集群时
kubectl apply,Kubernetes会找到所有具有与selector(name: app)相同标签的Pod,并收集其IP地址 — 但前提是它们已通过Readiness探针。然后,对于每个IP地址,它将IP地址和端口连接在一起。
如果IP地址是
10.0.0.3和,targetPort是3000,Kubernetes将两个结果连接起来并称为endpoint。IP address + port = endpoint --------------------------------- 10.0.0.3 + 3000 = 10.0.0.3:3000
endpoint存储在etcd的另一个名为Endpoint的对象中。
是否有点疑惑?
Kubernetes中定义:
-
endpoint是IP地址+端口对(
10.0.0.3:3000)(在本文和Learnk8s资料中称为小写eendpoint)。 -
Endpoint是endpoint的集合(在本文和Learnk8s材料中,被称为大写
Eendpoint)。
Endpoint对象是Kubernetes中的真实对象,对于每个服务Kubernetes都会自动创建一个endpoint对象。
您可以使用以下方法进行验证:
kubectl get services,endpoints NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) service/my-service-1 ClusterIP 10.105.17.65 <none> 80/TCP service/my-service-2 ClusterIP 10.96.0.1 <none> 443/TCP NAME ENDPOINTS endpoints/my-service-1 172.17.0.6:80,172.17.0.7:80 endpoints/my-service-2 192.168.99.100:8443
Endpoint从Pod收集所有IP地址和端口。
但并不是一次性的。
在以下情况下,将使用新的endpoint列表刷新Endpoint对象:
-
创建一个Pod。
-
Pod已删除。
-
在Pod上修改了标签。
因此,您可以想象,每次创建Pod并在kubelet将其IP地址发布到主节点后,Kubernetes都会更新所有endpoint以反映更改:
kubectl get services,endpoints NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) service/my-service-1 ClusterIP 10.105.17.65 <none> 80/TCP service/my-service-2 ClusterIP 10.96.0.1 <none> 443/TCP NAME ENDPOINTS endpoints/my-service-1 172.17.0.6:80,172.17.0.7:80,172.17.0.8:80 endpoints/my-service-2 192.168.99.100:8443
很好,endpoint存储在控制平面中,并且endpoint对象已更新。
-
在此图中,集群中部署了一个Pod。Pod属于服务。如果您要检查etcd,则可以找到Pod的详细信息以及服务。

-
当部署新pod后会发生什么?

-
Kubernetes必须跟踪Pod及其IP地址。服务应该将流量路由到新的endpoint,因此应该传播IP地址和端口。

-
当部署另一个Pod时会发生什么?

-
完全相同的过程。在数据库中为Pod创建一个新的“记录”,并传递给endpoint。

-
但是,当一个Pod被删除时会发生什么呢?

-
服务会立即删除endpoint,最后,Pod也会从数据库中删除。

-
Kubernetes会对集群中的每一个小变化做出反应。

您准备好开始使用Pod了吗?
在Kubernetes中使用Endpoint
endpoint由Kubernetes中的多个组件使用。
Kube-proxy使用endpoint在节点上设置iptables规则。
因此,每当endpoint(对象)发生变化时,kube-proxy就会检索新的IP地址和端口列表,并编写新的iptables规则。
-
让我们考虑具有两个Pod且不包含Service的三节点群集。Pod的状态存储在etcd中。

-
创建服务时会发生什么?

-
Kubernetes创建了一个endpoint对象,并从pod收集所有endpoint(IP地址和端口对)。

-
Kube-proxy守护进程监听endpoint的更改。

-
当添加、删除或更新endpoint时,kube proxy检索endpoint的新列表。

-
Kube-proxy使用endpoint在集群的每个节点上创建iptables规则。

Ingress controller使用相同的endpoint列表。
Ingress controller是群集中将外部流量路由到群集中的那个组件。
设置Ingress清单时,通常将Service指定为目标:
ingress.yaml
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: my-ingress spec: rules: - http: paths: - backend: serviceName: my-service servicePort: 80 path: /
实际上,流量不会路由到服务。
取而代之的是,Ingress controller设置了一个订阅,每次该服务的endpoint更改时都将收到通知。
Ingress会将流量直接路由到Pod,从而跳过服务。
可以想象,每次更改endpoint(对象)时,Ingress都会检索IP地址和端口的新列表,并将控制器重新配置为包括新的Pod。
-
在这张图片中,有一个Ingress控制器,它带有两个副本和一个Service的Deployment。

-
如果您想通过入口将外部流量路由到Pods,您应该创建一个入口清单(一个YAML文件)。

-
一旦你运行了
kubectl apply -f ingress.yaml,入口控制器从控制平面检索文件。
-
Ingress YAML有一个serviceName属性,该属性描述它应该使用哪个服务。

-
入口控制器从服务检索Endpoint列表并跳过它。流量直接流向endpoint(pod)。

-
当一个新的Pod被创建时会发生什么?

-
您已经知道Kubernetes如何创建Pod并通告endpoint。

-
入口控制器正在订阅对endpoint的更改。因为有一个变更的通知,它检索新的Endpoint列表。

-
入口控制器将流量路由到新的Pod。

有更多的Kubernetes组件示例订阅了对endpoint的更改。
集群中的DNS组件CoreDNS是另一个示例。
如果您使用Headless类型的服务,则每次添加或删除endpoint时,CoreDNS都必须订阅对endpoint的更改并重新配置自身。
相同的endpoint被istio或Linkerd之类的服务网格所使用,云提供商也创建了
type:LoadBalancer。您必须记住,有几个组件订阅了对endpoint的更改,它们可能会在不同时间收到有关endpoint更新的通知。
够了吗,还是在创建Pod之后有什么事发生?
这次您完成了!
快速回顾一下创建Pod时发生的情况:
-
Pod存储在etcd中。
-
调度程序分配一个节点。它将节点写入etcd。
-
向kubelet通知新的和预定的Pod。
-
kubelet将创建容器的委托委派给容器运行时接口(CRI)。
-
kubelet代表将容器附加到容器网络接口(CNI)。
-
kubelet将容器中的安装卷委托给容器存储接口(CSI)。
-
容器网络接口分配IP地址。
-
Kubelet将IP地址报告给控制平面。
-
IP地址存储在etcd中。
如果您的Pod属于服务:
-
Kubelet等待成功的Readiness探针。
-
通知所有相关的endpoint(对象)更改。
-
Endpoint将新endpoint(IP地址+端口对)添加到其列表中。
-
Endpoint更改将通知Kube-proxy。Kube-proxy更新每个节点上的iptables规则。
-
通知Endpoint变化的入口控制器。控制器将流量路由到新的IP地址。
-
CoreDNS通知Endpoint更改。如果服务的类型为Headless,则更新DNS条目。
-
向云提供商通知Endpoint更改。如果服务为
type: LoadBalancer,则将新Endpoint配置为负载均衡器池的一部分。 -
Endpoint更改将通知群集中安装的所有服务网格。
-
订阅Endpoint更改的其他运营商也会收到通知。
如此长的列表令人惊讶地只是一项常见任务 — 创建Pod。
Pod正在运行。现在是时候讨论删除它时会发生什么。
删除pod
您可能已经猜到了,但是删除Pod时,必须遵循相同的步骤,但要相反。
首先,应从endpoint(对象)中删除endpoint。
这次将忽略“Readiness”探针,并立即从控制平面移除endpoint。
依次触发所有事件到kube-proxy,Ingress控制器,DNS,服务网格等。
这些组件将更新其内部状态,并停止将流量路由到IP地址。
由于组件可能正在忙于做其他事情,因此无法保证从其内部状态中删除IP地址将花费多长时间。
对于某些人来说,可能不到一秒钟。对于其他人,可能需要更多时间。
-
如果您要使用删除Pod
kubectl delete pod,则该命令将首先到达Kubernetes API
-
消息被控制平面中的特定控制器截获:Endpoint控制器。

-
Endpoint控制器向API发出命令,从端点对象中删除IP地址和端口。

-
谁侦听Endpoint更改?Kube-proxy、入口控制器、CoreDNS等会收到更改通知。

-
一些组件(如kube proxy)可能需要一些额外的时间来进一步传播更改。

同时,etcd中Pod的状态更改为Termination。
将通知kubelet更改并委托:
-
将全部容器卸载到容器存储接口(CSI)。
-
从网络上分离容器并将IP地址释放到容器网络接口(CNI)。
-
将容器销毁到容器运行时接口(CRI)。
换句话说,Kubernetes遵循与创建Pod完全相同的步骤,但相反。
-
如果您要使用删除Pod
kubectl delete pod,则该命令将首先到达Kubernetes API。
-
当kubelet轮询控制平面以获取更新时,它注意到Pod被删除了。

-
kubelet将销毁Pod委托给容器运行时接口、容器网络接口和容器存储接口。

但是,存在细微但必不可少的差异。
当您终止Pod时,将同时删除endpoint和发送到kubelet的信号。
首次创建Pod时,Kubernetes等待kubelet报告IP地址,然后开始endpoint通告。
但是,当您删除Pod时,事件将并行开始。
这可能会导致很多竞争情况。
如果在通告endpoint之前删除Pod怎么办?
-
删除endpoint和删除Pod会同时发生。

-
因此,您可以在kube-proxy更新iptables规则之前删除endpoint。

-
或者更幸运的是,只有在endpoint完全通告之后,Pod才会被删除。

正常关机(Graceful)
当Pod从kube-proxy或Ingress控制器中删除之前终止时,您可能会遇到停机时间。
而且,如果您考虑一下,这是有道理的。
Kubernetes仍将流量路由到IP地址,但Pod不再存在。
Ingress控制器,kube-proxy,CoreDNS等没有足够的时间从其内部状态中删除IP地址。
理想情况下,在删除Pod之前,Kubernetes应该等待集群中的所有组件具有更新的endpoint列表。
但是Kubernetes不能那样工作。
Kubernetes提供了健壮的机制来分布endpoint(即Endpoint对象和更高级的抽象功能,例如Endpoint Slices)。
但是,Kubernetes不会验证订阅endpoint更改的组件是否是集群状态的最新信息。
那么,如何避免这种竞争情况并确保在通告endpoint之后删除Pod?
你应该等一下!
当Pod即将被删除时,它会收到SIGTERM信号。
您的应用程序可以捕获该信号并开始关闭。
由于endpoint不太可能立即从Kubernetes中的所有组件中删除,因此您可以:
-
请稍等片刻,然后退出。
-
尽管有SIGTERM,仍然可以处理传入流量。
-
最后,关闭现有的长期连接(也许是数据库连接或WebSocket)。
-
关闭该过程。
你应该等多久?
默认情况下,Kubernetes将发送SIGTERM信号并等待30秒,然后强制终止该进程。
因此,您可以在最初的15秒内继续操作,以防万一。
希望该间隔应足以将endpoint删除通知到kube-proxy,Ingress控制器,CoreDNS等。
因此,越来越少的流量将到达您的Pod,直到停止为止。
15秒后,可以安全地关闭与数据库的连接(或任何持久连接)并终止该过程。
如果您认为需要更多时间,则可以在20或25秒时停止该过程。
但是,您应该记住,Kubernetes将在30秒后强行终止进程(除非您更改
terminationGracePeriodSecondsPod定义中的)。如果您无法更改代码以等待更长的时间怎么办?
您可以调用脚本以等待固定的时间,然后退出应用程序。
在调用SIGTERM之前,Kubernetes
preStop在Pod中公开一个钩子。您可以将
preStop钩子设置为等待15秒。让我们看一个例子:
pod.yaml
apiVersion: v1 kind: Pod metadata: name: my-pod spec: containers: - name: web image: nginx ports: - name: web containerPort: 80 lifecycle: preStop: exec: command: ["sleep", "15"]
该
preStop钩子是Pod LifeCycle钩子之一。建议延迟15秒吗?
这要视情况而定,但这可能是开始测试的明智方法。
以下是您可以选择的选项的概述:
-
您已经知道,当删除Pod时,会通知kubelet更改。

-
如果Pod有一个preStop钩子,则首先调用它。

-
当preStop完成时,kubelet向容器发送SIGTERM信号。从那时起,容器应该关闭所有长期存在的连接并准备终止。

-
默认情况下,进程有30秒的时间退出,这包括preStop钩子。如果进程还没有退出,kubelet发送SIGKILL信号并强制终止进程。

-
kubelet通知控制平面pod已成功删除。

宽限时间(Grace periods)和滚动更新
正常关机适用于要删除的Pod。
但是,如果不删除Pod,该怎么办?
即使您不这样做,Kubernetes也会始终删除Pod。
尤其是,每次部署较新版本的应用程序时,Kubernetes都会创建和删除Pod。
在部署中更改镜像时,Kubernetes会逐步推出更改。
pod.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: app spec: replicas: 3 selector: matchLabels: name: app template: metadata: labels: name: app spec: containers: - name: app # image: nginx:1.18 OLD image: nginx:1.19 ports: - containerPort: 3000
如果您有三个副本,并且一旦提交新的YAML资源Kubernetes,则:
-
用新的容器镜像创建一个Pod。
-
销毁现有的Pod。
-
等待Pod准备就绪。
并重复上述步骤,直到所有Pod都迁移到较新的版本。
Kubernetes仅在新的Pod准备好接收流量(换句话说,它通过Readiness检查)之后才重复每个周期。
Kubernetes是否在等待Pod被删除之后再移到下一个Pod?
并不会!!!
如果您有10个Pod,并且Pod需要2秒钟的准备时间和20个关闭的时间,则会发生以下情况:
-
创建第一个Pod,并终止前一个Pod。
-
Kubernetes创建一个新的Pod之后,需要2秒钟的准备时间。
-
同时,被终止的Pod会终止20秒
20秒后,所有新的Pod均已启用(10个Pod ,在2秒后就绪),并且所有之前的10个Pod都将终止(第一个Terminated Pod将要退出)。
总共,您在短时间内将Pod的数量增加了一倍(运行10次,终止10次)。

与“Readiness”探针相比,宽限时间(graceful period)越长,您同时具有“Running”(和Terminating)的Pod越多。
不好吗?
不一定,因为您要小心不要断开连接。
终止长时间运行的任务
那长期工作呢?
如果您要对大型视频进行转码,是否有其他方法可以延迟停止Pod?
假设您有一个包含三个副本的Deployment。
每个副本都分配了一个视频进行转码,该任务可能需要几个小时才能完成。
当您触发滚动更新时,Pod会在30秒内完成任务,然后将其杀死。
如何避免延迟关闭Pod?
您可以将其增加
terminationGracePeriodSeconds几个小时。但是,此时Pod的endpoint不可达。

如果公开指标以监视Pod,则检测工具将无法访问Pod。
为什么?
诸如Prometheus之类的工具依赖于Endpoints来在群集中探测Pod。
但是,一旦删除Pod,endpoint删除就会在群集中通告,甚至传播到Prometheus!
您应该考虑为每个新版本创建一个新的Deployment,而不是增加宽限时间(grace period)。
当您创建全新的deployment时,现有的deployment将保持不变。
长时间运行的作业可以照常继续处理视频。
完成后,您可以手动删除它们。
如果希望自动删除它们,则可能需要设置一个弹性伸缩,当它们用尽任务时,可以将部署扩展到零个副本。
这种Pod自动定标器的一个示例是Osiris,它是Kubernetes的通用,从零缩放的组件。
该技术有时被称为Rainbow部署,并且在每次您必须使以前的Pod运行超过宽限期的时间时很有用。
另一个很好的例子是WebSockets。
如果您正在向用户流式传输实时更新,则可能不希望在每次发布时都终止WebSocket。
如果您每天频繁发布,则可能会导致实时Feed多次中断。
为每个版本创建一个新的Deployment是一个不太明显但确是更好的选择。
现有用户可以继续流更新,而最新的Deployment服务于新用户。
当用户断开与旧Pod的连接时,您可以逐渐减少副本并退出旧的Deployment。
概要
您应该注意Pod从集群中删除,因为它们的IP地址可能仍用于路由流量。
与其立即关闭Pods,不如考虑在应用程序中等待更长的时间或设置一个
preStop钩子。仅在通告集群中的所有endpoint并将其从kube-proxy,Ingress控制器,CoreDNS等中删除后,才应删除Pod。
如果您的Pod运行诸如视频转码或使用WebSocket进行实时更新之类的长期任务,则应考虑使用Rainbow部署。
在Rainbow部署中,您为每个版本创建一个新的Deployment,并在耗尽连接(或任务)后删除上一个版本。
您可以在长时间运行的任务完成后立即手动删除较旧的Deployment。
或者,您可以自动将Deployment扩展到零副本,从而可以自动化该过程。
原文: https://learnk8s.io/graceful-shutdown
-