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

前言

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

img

在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

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

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

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

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

调度程序:

  1. 检查定义

  2. 收集有关工作负载的详细信息,例如CPU和内存请求,然后

  3. 确定哪个节点最适合运行它。

在过程结束时:

  • 在etcd中将Pod标记为Scheduled

  • 为Pod分配了一个节点。

  • Pod的状态存储在etcd中。

但是Pod仍然不存在。

  1. 当您使用kubectl apply -f提交一个Pod时,YAML被发送到kubernetes api。

image-20201130100654026

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

image-20201130100718353

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

image-20201130100736831

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

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

Kubelet — Kubernetes代理

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

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

当有Pod时,kubelet会创建它。

有一点需要注意。

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

  1. 容器运行时接口(CRI) — 为Pod创建容器的组件。

  2. 容器网络接口(CNI) — 将容器连接到群集网络并分配IP地址的组件。

  3. 容器存储接口(CSI) — 在容器中装载卷的组件。

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

docker run -d <my-container-image>

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

  1. 为Pod生成有效的IP地址。

  2. 将容器连接到网络的其余部分。

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

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

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

还有一个问题。

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

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

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

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

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

  1. Kubelet轮询控制平面以获取更新。

    image-20201130101322978

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

    image-20201130101453973

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

    image-20201130101558956

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

    image-20201130101635036

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

    image-20201130101659246

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

Pod已创建并可以使用。

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

Pods和Services

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

  1. selector — 用于指定将接收流量的Pod。

  2. 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和,targetPort3000,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对象:

  1. 创建一个Pod。

  2. Pod已删除。

  3. 在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对象已更新。

  1. 在此图中,集群中部署了一个Pod。Pod属于服务。如果您要检查etcd,则可以找到Pod的详细信息以及服务。

    image-20201130102609847

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

    image-20201130102654401

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

    image-20201130120358647

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

    image-20201130120315478

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

    image-20201130120651341

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

    image-20201130120520548

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

    image-20201130120753662

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

    image-20201130120839521

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

在Kubernetes中使用Endpoint

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

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

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

  1. 让我们考虑具有两个Pod且不包含Service的三节点群集。Pod的状态存储在etcd中。

    image-20201130151734209

  2. 创建服务时会发生什么?

    image-20201130152239276

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

    image-20201130152518479

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

    image-20201130152726507

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

    image-20201130155158708

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

    image-20201130155249997

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。

  1. 在这张图片中,有一个Ingress控制器,它带有两个副本和一个Service的Deployment。

    image-20201130160151211

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

    image-20201130160243611

  3. 一旦你运行了kubectl apply -f ingress.yaml,入口控制器从控制平面检索文件。

    image-20201130160611413

  4. Ingress YAML有一个serviceName属性,该属性描述它应该使用哪个服务。

    image-20201130160717516

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

    image-20201130160936005

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

    image-20201130161047499

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

    image-20201130161143388

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

    image-20201130161641120

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

    image-20201130162449233

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

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

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

相同的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

    image-20201130164758284

  2. 消息被控制平面中的特定控制器截获:Endpoint控制器。

    image-20201130165149640

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

    image-20201130165234680

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

    image-20201130165321966

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

    image-20201130165402333

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

将通知kubelet更改并委托:

  1. 将全部容器卸载到容器存储接口(CSI)。

  2. 从网络上分离容器并将IP地址释放到容器网络接口(CNI)。

  3. 将容器销毁到容器运行时接口(CRI)。

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

  1. 如果您要使用删除Pod kubectl delete pod,则该命令将首先到达Kubernetes API。

    image-20201130165827829

  2. 当kubelet轮询控制平面以获取更新时,它注意到Pod被删除了。

    image-20201130165924907

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

    image-20201130170005283

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

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

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

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

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

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

  1. 删除endpoint和删除Pod会同时发生。

    image-20201130170311714

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

    image-20201130170408155

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

    image-20201130170505624

正常关机(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之前,KubernetespreStop在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秒吗?

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

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

  1. 您已经知道,当删除Pod时,会通知kubelet更改。

    image-20201130172342245

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

    image-20201130172430200

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

    image-20201130172512823

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

    image-20201130172558753

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

    image-20201130172622775

宽限时间(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个关闭的时间,则会发生以下情况:

  1. 创建第一个Pod,并终止前一个Pod。

  2. Kubernetes创建一个新的Pod之后,需要2秒钟的准备时间。

  3. 同时,被终止的Pod会终止20秒

20秒后,所有新的Pod均已启用(10个Pod ,在2秒后就绪),并且所有之前的10个Pod都将终止(第一个Terminated Pod将要退出)。

总共,您在短时间内将Pod的数量增加了一倍(运行10次,终止10次)。

image-20201130173248925

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

不好吗?

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

终止长时间运行的任务

那长期工作呢?

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

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

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

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

如何避免延迟关闭Pod?

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

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

image-20201130173957230

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

译者: 祝祥