50 张图,掌握 Kubernetes 中优雅且零停机部署的实现

前言

在本文中,您将了解如何在Pod启动或关闭时防止连接异常,并将学习如何以优雅的方式关闭长时间运行的任务。
graceful shutdown

在 Kubernetes 中,创建和删除 Pod 是最常见的任务之一。

当您执行滚动更新,扩展部署,每个新发行版,每个作业和 cron 作业等时,都会创建 Pod。

但是在节点被驱逐之后,Pods 也会被删除并重新创建—例如,当您将节点标记为不可调度时。

这些 Pod 的生命是如此短暂,那么当 Pod 在响应请求的过程中却被告知关闭时会发生什么?

请求在关闭之前是否已完成?

接下来的请求又如何呢?

在讨论删除 Pod 时会发生什么之前,有必要讨论一下创建 Pod 时会发生什么。

假设您要在集群中创建以下 Pod:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80

您可以使用以下方式将 YAML 定义提交给集群:

1
kubectl apply -f pod.yaml

输入命令后,kubectl 便将 Pod 定义提交给 Kubernetes API。

在数据库中保存集群的状态

API 接收和检查 Pod 定义,然后将其存储在数据库 etcd 中。

Pod 也将添加到调度程序的队列中。

调度程序:

  1. 检查定义
  2. 收集有关工作负载的详细信息,例如 CPU 和内存请求,然后
  3. 确定哪个节点最适合运行它。(通过Filters 和 Predicates)。

在过程结束时:

  • 在 etcd 中将 Pod 标记为 Scheduled。
  • 为 Pod 分配了一个节点。
  • Pod 的状态存储在 etcd 中。

但是Pod仍然不存在。

  1. 当您使用 kubectl apply -f 提交一个 Pod 时,YAML 被发送到 kubernetes api。
    graceful shutdown
  2. API 将 Pod 保存在数据库 etcd g中。
    graceful shutdown
  3. 调度程序为这个 Pod 分配最佳节点,并且 Pod 的状态更改为 Pending。pod 只存在于etcd中。
    graceful shutdown

先前的任务发生在控制平面中,并且状态存储在数据库中。

那么谁在您的节点中创建 Pod?

Kubelet — Kubernetes 代理

kubelet 的工作是轮询控制平面以获取更新。

您可以想象 kubelet 不断地向主节点询问:“我管理工作节点1,是否对我有任何新的 Pod?”。

当有 Pod 时,kubelet 会创建它。

有一点需要注意。

kubelet 不会自行创建 Pod。而是将工作委托给其他三个组件:

  1. 容器运行时接口(CRI) — 为 Pod 创建容器的组件。
  2. 容器网络接口(CNI) — 将容器连接到群集网络并分配IP地址的组件。
  3. 容器存储接口(CSI) — 在容器中装载卷的组件。

在大多数情况下,容器运行时接口(CRI)的工作类似于:

1
docker run -d <my-container-image>

容器网络接口(CNI)有点有趣,因为它负责:

  1. 为 Pod 生成有效的 IP 地址。
  2. 将容器连接到网络的其余部分。

可以想象,有几种方法可以将容器连接到网络并分配有效的 IP 地址(您可以在 IPv4 或 IPv6 之间进行选择,也可以分配多个 I P地址)。

例如,Docker 创建虚拟以太网对并将其连接到网桥,而 AWS—CNI 将 Pods 直接连接到虚拟私有云(VPC)

当容器网络接口完成其工作时,Pod已连接到网络,并分配了有效的IP地址。

还有一个问题。

Kubelet 知道 IP 地址(因为它调用了容器网络接口),但是控制平面却不知道。

没有人告诉主节点,该Pod已分配了IP地址,并准备接收流量。

就控制平面而言,仍在创建 Pod。

Kubelet 的工作是收集 Pod 的所有详细信息(例如 I P地址)并将其报告回控制平面。

您可以想象检查 etcd 不仅可以显示 Pod 的运行位置,还可以显示其 IP 地址。

  1. Kubelet 轮询控制平面以获取更新。
    graceful shutdown
  2. 当一个新的 Pod 分配给它的节点时,kubelet 会检索详细信息
    graceful shutdown
  3. Kubernetns 不会自己创建 pod。它依赖于三个组件:容器运行时接口、容器网络接口和容器存储接口。
    graceful shutdown
  4. 一旦所有三个组件都成功完成,Pod 就在您的节点中运行并分配了一个 IP 地址。
    graceful shutdown
  5. kubelet 向控制平面报告 IP 地址。
    graceful shutdown

如果 Pod 不是任何服务的一部分,那么任务将结束。

Pod 已创建并可以使用。

如果 Pod 是服务的一部分,则还需要执行几个步骤。

Pods 和 Services

创建服务时,通常需要注意以下两条信息:

  1. selector — 用于指定将接收流量的 Pod。
  2. targetPort — 通过 pod 使用的端口接收的流量。

服务的典型 YAML 定义如下所示:

1
2
3
4
5
6
7
8
9
10
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 和,targetPort3000,Kubernetes 将两个结果连接起来并称为 endpoint。

1
2
3
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)。
  • Endpoint 是 endpoint 的集合。

Endpoint 对象是 Kubernetes 中的真实对象,对于每个服务 Kubernetes 都会自动创建一个 endpoint 对象。

您可以使用以下方法进行验证:

1
2
3
4
5
6
7
8
$ 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 对象:

  1. 创建一个 Pod。
  2. Pod 已删除。
  3. 在 Pod 上修改了标签。

因此,您可以想象,每次创建 Pod 并在 kubelet 将其 IP 地址发布到主节点后,Kubernetes 都会更新所有 endpoint 以反映更改:

1
2
3
4
5
6
7
8
$ 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 对象已更新。

  1. 在此图中,集群中部署了一个 Pod。Pod 属于服务。如果您要检查 etcd,则可以找到 Pod 的详细信息以及服务。
    graceful shutdown
  2. 当部署新 pod 后会发生什么?
    graceful shutdown
  3. Kubernetes 必须跟踪 Pod 及其 IP 地址。服务应该将流量路由到新的 endpoint,因此应该传播 IP 地址和端口。
    graceful shutdown
  4. 当部署另一个 Pod 时会发生什么?
    graceful shutdown
  5. 完全相同的过程。在数据库中为 Pod 创建一个新的“记录”,并传递给 endpoint。
    graceful shutdown
  6. 但是,当一个 Pod 被删除时会发生什么呢?
    graceful shutdown
  7. 服务会立即删除 endpoint,最后,Pod 也会从数据库中删除。
    graceful shutdown
  8. Kubernetes 会对集群中的每一个小变化做出反应。
    graceful shutdown

您准备好开始使用 Pod 了吗?

在 Kubernetes 中使用 Endpoint

endpoint 由 Kubernetes 中的多个组件使用。

Kube-proxy 使用 endpoint 在节点上设置 iptables 规则。

因此,每当 endpoint(对象)发生变化时,kube-proxy 就会检索新的 IP 地址和端口列表,并编写新的 iptables 规则。

  1. 让我们考虑具有两个 Pod 且不包含 Service 的三节点群集。Pod 的状态存储在 etcd 中。
    graceful shutdown
  2. 创建服务时会发生什么?
    graceful shutdown
  3. Kubernetes 创建了一个 endpoint 对象,并从 pod 收集所有 endpoint(IP 地址和端口对)。
    graceful shutdown
  4. Kube-proxy 守护进程监听 endpoint 的更改。
    graceful shutdown
  5. 当添加、删除或更新 endpoint 时,kube-proxy 检索 endpoint 的新列表。
    graceful shutdown
  6. Kube-proxy 使用 endpoint 在集群的每个节点上创建 iptables 规则。
    graceful shutdown

Ingress controller 使用相同的 endpoint 列表。

Ingress controller 是群集中将外部流量路由到群集中的那个组件。

设置 Ingress 清单时,通常将 Service 指定为目标:

1
2
3
4
5
6
7
8
9
10
11
12
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。

  1. 在这张图片中,有一个 Ingress 控制器,它带有两个副本和一个 Service 的 Deployment。
    graceful shutdown
  2. 如果您想通过入口将外部流量路由到 Pods,您应该创建一个入口清单(一个 YAML 文件)。
    graceful shutdown
  3. 一旦你运行了 kubectl apply -f ingress.yaml,入口控制器从控制平面检索文件。
    graceful shutdown
  4. Ingress YAML 有一个 serviceName 属性,该属性描述它应该使用哪个服务。
    graceful shutdown
  5. 入口控制器从服务检索 Endpoint 列表并跳过它。流量直接流向 endpoint(pod)。
    graceful shutdown
  6. 当一个新的 Pod 被创建时会发生什么?
    graceful shutdown
  7. 您已经知道 Kubernetes 如何创建 Pod 并通告 endpoint。
    graceful shutdown
  8. 入口控制器正在订阅对 endpoint 的更改。因为有一个变更的通知,它检索新的 Endpoint 列表。
    graceful shutdown
  9. 入口控制器将流量路由到新的 Pod。
    graceful shutdown

有更多的 Kubernetes 组件示例订阅了对 endpoint 的更改。

集群中的 DNS 组件 CoreDNS 是另一个示例。

如果您使用 Headless 类型的服务,则每次添加或删除 endpoint 时,CoreDNS 都必须订阅对e ndpoint 的更改并重新配置自身。

相同的 endpoint 被 istio 或 Linkerd 之类的服务网格所使用,云提供商也创建了 type:LoadBalancer

您必须记住,有几个组件订阅了对endpoint的更改,它们可能会在不同时间收到有关 endpoint 更新的通知。

够了吗,还是在创建 Pod 之后有什么事发生?

这次您完成了!

快速回顾一下创建Pod时发生的情况:

  1. Pod 存储在 etcd 中。
  2. 调度程序分配一个节点。它将节点写入 etcd。
  3. 向 kubelet 通知新的和预定的 Pod。
  4. kubelet 将创建容器的委托委派给容器运行时接口(CRI)。
  5. kubelet 代表将容器附加到容器网络接口(CNI)。
  6. kubelet 将容器中的安装卷委托给容器存储接口(CSI)。
  7. 容器网络接口分配 IP 地址。
  8. Kubelet 将 IP 地址报告给控制平面。
  9. IP 地址存储在 etcd 中。

如果您的 Pod 属于服务:

  1. Kubelet 等待成功的 Readiness 探针。
  2. 通知所有相关的 endpoint(对象)更改。
  3. Endpoint 将新 endpoint(IP 地址 + 端口对)添加到其列表中。
  4. Endpoint 更改将通知 Kube-proxy。Kube-proxy 更新每个节点上的 iptables 规则。
  5. 通知 Endpoint 变化的入口控制器。控制器将流量路由到新的 IP 地址。
  6. CoreDNS 通知 Endpoint 更改。如果服务的类型为 Headless,则更新 DNS 条目。
  7. 向云提供商通知 Endpoint 更改。如果服务为 type: LoadBalancer,则将新 Endpoint 配置为负载均衡器池的一部分。
  8. Endpoint 更改将通知群集中安装的所有服务网格。
  9. 订阅 Endpoint 更改的其他运营商也会收到通知。

如此长的列表令人惊讶地只是一项常见任务 — 创建 Pod。

Pod 正在运行。现在是时候讨论删除它时会发生什么。

删除 pod

您可能已经猜到了,但是删除 Pod 时,必须遵循相同的步骤,但要相反。

首先,应从 endpoint(对象)中删除 endpoint。

这次将忽略 “Readiness” 探针,并立即从控制平面移除 endpoint。

依次触发所有事件到 kube-proxy,Ingress 控制器,DNS,服务网格等。

这些组件将更新其内部状态,并停止将流量路由到IP地址。

由于组件可能正在忙于做其他事情,因此无法保证从其内部状态中删除IP地址将花费多长时间

对于某些人来说,可能不到一秒钟。对于其他人,可能需要更多时间。

  1. 如果您要使用删除 Pod kubectl delete pod,则该命令将首先到达 Kubernetes API
    graceful shutdown
  2. 消息被控制平面中的特定控制器截获:Endpoint 控制器。
    graceful shutdown
  3. Endpoint 控制器向 API 发出命令,从端点对象中删除 IP 地址和端口。
    graceful shutdown
  4. 谁侦听 Endpoint 更改? Kube-proxy、入口控制器、CoreDNS 等会收到更改通知。
    graceful shutdown
  5. 一些组件(如 kube-proxy )可能需要一些额外的时间来进一步传播更改。
    graceful shutdown

同时,etcd 中 Pod 的状态更改为 Termination。

将通知 kubelet 更改并委托:

  1. 将全部容器卸载到容器存储接口(CSI)。
  2. 从网络上分离容器并将IP地址释放到容器网络接口(CNI)。
  3. 将容器销毁到容器运行时接口(CRI)。

换句话说,Kubernetes遵循与创建Pod完全相同的步骤,但相反。

  1. 如果您要使用删除 Pod kubectl delete pod,则该命令将首先到达 Kubernetes API。
    graceful shutdown
  2. 当 kubelet 轮询控制平面以获取更新时,它注意到 Pod 被删除了。
    graceful shutdown
  3. kubelet 将销毁 Pod 委托给容器运行时接口、容器网络接口和容器存储接口。
    graceful shutdown

但是,存在细微但必不可少的差异。

当您终止 Pod 时,将同时删除 endpoint 和发送到 kubelet 的信号。

首次创建 Pod 时,Kubernetes 等待 kubelet 报告 IP 地址,然后开始 endpoint 通告。

但是,当您删除 Pod 时,事件将并行开始。

这可能会导致很多竞争情况。

如果在通告 endpoint 之前删除 Pod 怎么办?

  1. 删除 endpoint 和删除 Pod 会同时发生。
    graceful shutdown
  2. 因此,您可以在 kube-proxy 更新 iptables 规则之前删除 endpoint。
    graceful shutdown
  3. 或者更幸运的是,只有在 endpoint 完全通告之后,Pod 才会被删除。
    graceful shutdown

正常关机(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 中的所有组件中删除,因此您可以:

  1. 请稍等片刻,然后退出。
  2. 尽管有 SIGTERM,仍然可以处理传入流量。
  3. 最后,关闭现有的长期连接(也许是数据库连接或 WebSocket)。
  4. 关闭该过程。

你应该等多久?

默认情况下,Kubernetes 将发送 SIGTERM 信号并等待 30 秒,然后强制终止该进程。

因此,您可以在最初的15秒内继续操作,以防万一。

希望该间隔应足以将 endpoint 删除通知到 kube-proxy,Ingress 控制器,CoreDNS 等。

因此,越来越少的流量将到达您的 Pod,直到停止为止。

15 秒后,可以安全地关闭与数据库的连接(或任何持久连接)并终止该过程。

如果您认为需要更多时间,则可以在 20 或 25 秒时停止该过程。

但是,您应该记住,Kubernetes 将在 30 秒后强行终止进程(除非您更改 terminationGracePeriodSecondsPod 定义中的)。

如果您无法更改代码以等待更长的时间怎么办?

您可以调用脚本以等待固定的时间,然后退出应用程序。

在调用 SIGTERM 之前,Kubernetes preStop 在 Pod 中公开一个钩子。

您可以将 preStop 钩子设置为等待 15 秒。

让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 秒吗?

这要视情况而定,但这可能是开始测试的明智方法。

以下是您可以选择的选项的概述:

  1. 您已经知道,当删除 Pod 时,会通知 kubelet 更改。
    graceful shutdown
  2. 如果 Pod 有一个 preStop 钩子,则首先调用它。
    graceful shutdown
  3. preStop 完成时,kubelet 向容器发送 SIGTERM 信号。从那时起,容器应该关闭所有长期存在的连接并准备终止。
    graceful shutdown
  4. 默认情况下,进程有 30 秒的时间退出,这包括 preStop 钩子。如果进程还没有退出,kubelet 发送 SIGKILL 信号并强制终止进程。
    graceful shutdown
  5. kubelet 通知控制平面 pod 已成功删除。
    graceful shutdown

宽限时间(Grace periods)和滚动更新

正常关机适用于要删除的 Pod。

但是,如果不删除 Pod,该怎么办?

即使您不这样做,Kubernetes 也会始终删除 Pod。

尤其是,每次部署较新版本的应用程序时,Kubernetes 都会创建和删除 Pod。

在部署中更改镜像时,Kubernetes 会逐步推出更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 次)。
graceful shutdown

与 “Readiness” 探针相比,宽限时间(graceful period)越长,您同时具有 “Running”(和 Terminating )的 Pod 越多。

不好吗?

不一定,因为您要小心不要断开连接。

终止长时间运行的任务

那长期工作呢?

如果您要对大型视频进行转码,是否有其他方法可以延迟停止 Pod?

假设您有一个包含三个副本的 Deployment。

每个副本都分配了一个视频进行转码,该任务可能需要几个小时才能完成。

当您触发滚动更新时,Pod 会在 30 秒内完成任务,然后将其杀死。

如何避免延迟关闭 Pod?

您可以将其增加 terminationGracePeriodSeconds 几个小时。

但是,此时 Pod 的 endpoint不可达。
graceful shutdown

如果公开指标以监视 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

-------------本文结束感谢您的阅读-------------