4

Kubernetes中的正常关闭和零停机部署

 3 years ago
source link: https://segmentfault.com/a/1190000039974118
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.
neoserver,ios ssh client

Kubernetes中的正常关闭和零停机部署

TL; DR: 在本文中,您将学习如何在Pod启动或关闭时防止断开的连接。您还将学习如何正常关闭长时间运行的任务。

Kubernetes中的正常关闭和零停机部署

image.png

在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. 收集有关工作负载的详细信息,例如CPU和内存请求,然后
  2. 确定哪个节点最适合运行它(通过称为过滤器和谓词的过程)。

在过程结束时:

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

但是 Pod 仍然不存在

  1. 当用 kubectl apply -f 提交pod 时, 发送 YAML 配置至 Kubernetes API.
    https://learnk8s.io/a/54a28f4c41dfd3abb594848af5f71eaf.svg

image.png

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

image.png

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

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

kubelet- k8s的代理

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

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

当有 Pod 时,kubelet 会创建它。

大致如此。

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

  • 容器运行时接口(CRI) -为Pod创建容器的组件。
  • 容器网络接口(CNI) -将容器连接到群集网络并分配IP地址的组件。
  • 容器存储接口(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.png

  1. When a new Pod is assigned to its Node, the kubelet retrieves the details.

image.png

  1. The kubelet doesn't create the Pod itself. It relies on three components: the Container Runtime Interface, Container Network Interface and Container Storage Interface.
    image.png
  2. Once all three components have successfully completed, the Pod is Running in your Node and has an IP address assigned.

image.png

  1. The kubelet reports the IP address back to the control plane.
    image.png

如果 Pod 不是任何服务的一部分,那么这就是旅程的终点​​。

Pod 已创建并可以使用。

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

Pod 和服务

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

选择器,用于指定将接收流量的Pod。
本targetPort-通过舱体使用的端口接收的流量。
服务的典型YAML定义如下所示:

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - port: 80
    targetPort: 3000
  selector:
    name: app

将Service提交给集群时kubectl apply,Kubernetes会找到所有具有与选择器(name: app)相同标签的Pod,并收集其IP地址-但前提是它们已通过Readiness探针。

然后,对于每个IP地址,它将IP地址和端口连接在一起。

如果IP地址为10.0.0.3和,targetPort则3000Kubernetes将两个值连接起来并称为端点。

IP address + port = endpoint
---------------------------------
10.0.0.3   + 3000 = 10.0.0.3:3000

端点存储在etcd中另一个名为Endpoint的对象中。

Kubernetes是指:

  • IP地址+端口对(10.0.0.3:3000)是端点(在本文和Learnk8s资料中称为小写 e 端点)。
  • 端点(在本文和Learnk8s资料中被称为大写 E 端点)是端点的集合。

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

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

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

端点从Pod收集所有IP地址和端口。

但不仅仅是一次。

在以下情况下,将使用新的端点列表刷新Endpoint对象:

  • 创建一个Pod。
  • Pod已删除。
  • 在Pod上修改了标签。

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

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

很好,端点存储在控制平面中,并且端点对象已更新。

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

image.png

  1. What happens when a new Pod is deployed?
    image.png
  2. Kubernetes has to keep track of the Pod and its IP address. The Service should route traffic to the new endpoint, so the IP address and port should be propagated.

image.png

  1. What happens when another Pod is deployed?
    image.png
  2. The exact same process. A new "row" for the Pod is created in the database, and the endpoint is propagated.
    image.png
  3. What happens when a Pod is deleted, though?

image.png

  1. The Service immediately removes the endpoint, and, eventually, the Pod is removed from the database too.
    image.png
  2. Kubernetes reacts to every small change in your cluster.
    image.png

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

还有更多。

多很多!

使用Kubernetes中的端点

端点由Kubernetes中的几个组件使用。

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

因此,每次对端点(对象)进行更改时,kube-proxy都会检索IP地址和端口的新列表并编写新的iptables规则。

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

  1. Let's consider this three-node cluster with two Pods and no Services. The state of the Pods is stored in etcd.
    image.png
  2. What happens when you create a Service?
    image.png
  3. Kubernetes created an Endpoint object and collects all the endpoints (IP address and port pairs) from the Pods.
    image.png
  4. Kube-proxy daemon is subscribed to changes to Endpoints.
    image.png
  5. When an Endpoint is added, removed or updated, kube-proxy retrieves the new list of endpoints.
    image.png
  6. Kube-proxy uses the endpoints to creating iptables rules on each Node of your cluster.
    image.png

Ingress控制器使用相同的端点列表。

入口控制器是群集中将外部流量路由到群集中的那个组件。

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

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
      paths:
      - backend:
          service:
            name: my-service
            port:
              number: 80
        path: /
        pathType: Prefix

实际上,流量不会路由到服务。

取而代之的是,Ingress控制器设置了一个订阅,每次该服务的端点更改时都将得到通知。

Ingress会将流量直接路由到Pod,从而跳过服务。

可以想象,每次更改端点(对象)时,Ingress都会检索IP地址和端口的新列表,并将控制器重新配置为包括新的Pod。

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

image.png

还有更多的Kubernetes组件示例可以订阅对端点的更改。

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

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

相同的端点被Istio或Linkerd之类的服务网格所使用,云提供商也创建了type:LoadBalancer无数运营商的服务。

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

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

这次您完成了!

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

Pod存储在etcd中。
调度程序分配一个节点。它将节点写入etcd。
向kubelet通知新的和预定的Pod。
kubelet将创建容器的委托委派给容器运行时接口(CRI)。
kubelet代表将容器附加到容器网络接口(CNI)。
kubelet将容器中的安装卷委派给容器存储接口(CSI)。
容器网络接口分配IP地址。
Kubelet将IP地址报告给控制平面。
IP地址存储在etcd中。
如果您的Pod属于服务:

Kubelet等待成功的“就绪”探测。
通知所有相关的端点(对象)更改。
端点将新端点(IP地址+端口对)添加到其列表中。
通知Kube-proxy端点更改。Kube-proxy更新每个节点上的iptables规则。
通知端点变化的入口控制器。控制器将流量路由到新的IP地址。
CoreDNS会收到有关端点更改的通知。如果服务的类型为Headless,则更新DNS条目。
向云提供商通知端点更改。如果服务为type: LoadBalancer,则将新端点配置为负载平衡器池的一部分。
端点更改将通知群集中安装的所有服务网格。
订阅端点更改的任何其他操作员也会收到通知。
如此长的列表令人惊讶地是一项常见任务-创建一个Pod。

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

删除 Pods

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

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

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

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

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

由于组件可能忙于执行其他操作,因此无法保证从其内部状态中删除IP地址需要花费多长时间。

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

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

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

将通知kubelet更改并委托:

将任何卷从容器卸载到容器存储接口(CSI)。
从网络上分离容器并将IP地址释放到容器网络接口(CNI)。
将容器销毁到容器运行时接口(CRI)。
换句话说,Kubernetes遵循与创建Pod完全相同的步骤,但相反。

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

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

当您终止Pod时,将同时删除端点和发给kubelet的信号。

首次创建Pod时,Kubernetes等待kubelet报告IP地址,然后启动端点传播。

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

这可能会导致相当多的比赛条件。

如果在传播端点之前删除Pod,该怎么办?

删除端点和删除Pod会同时发生。
1 /3
删除端点和删除Pod会同时发生。

正常关机
当Pod在终结点从kube-proxy或Ingress控制器中删除之前终止时,您可能会遇到停机时间。

而且,如果您考虑一下,这是有道理的。

Kubernetes仍将流量路由到IP地址,但Pod不再存在。

Ingress控制器,kube-proxy,CoreDNS等没有足够的时间从其内部状态中删除IP地址。

理想情况下,在删除Pod之前,Kubernetes应该等待集群中的所有组件具有更新的端点列表。

但是Kubernetes不能那样工作。

Kubernetes提供了健壮的原语来分发端点(即Endpoint对象和更高级的抽象,例如Endpoint Slices)。

但是,Kubernetes不会验证订阅端点更改的组件是否是集群状态的最新信息。

那么,如何避免这种竞争情况并确保在传播端点之后删除Pod?

你应该等一下

当Pod即将被删除时,它会收到SIGTERM信号。

您的应用程序可以捕获该信号并开始关闭。

由于端点不太可能立即从Kubernetes中的所有组件中删除,因此您可以:

请稍等片刻,然后退出。
尽管使用了SIGTERM,仍然可以处理传入的流量。
最后,关闭现有的长期连接(也许是数据库连接或WebSocket)。
关闭该过程。
你应该等多久?

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

因此,您可以在最初的15秒内继续操作,因为什么都没有发生。

希望该间隔应足以将端点删除传播到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秒吗?

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

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

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

宽限期和滚动更新
正常关机适用于要删除的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准备好接收流量(换句话说,它通过“就绪”检查)后才重复每个周期。

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次)。

滚动更新和正常关机
与“就绪”探针相比,宽限期越长,您同时具有“运行”(和“终止”)的Pod越多。

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

终止长时间运行的任务
那长期工作呢?

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

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

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

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

如何避免延迟关闭Pod?

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

但是,此时Pod的端点不可达。

无法到达的豆荚
如果公开指标以监视Pod,则您的设备将无法访问Pod。

诸如Prometheus之类的工具依赖于Endpoints来将群集中的Pod抓取。

但是,一旦删除Pod,端点删除就会在群集中传播,甚至传播到Prometheus!

您应该考虑为每个新版本创建一个新的部署,而不是增加宽限期。

当您创建全新的部署时,现有的部署将保持不变。

长时间运行的作业可以照常继续处理视频。

完成后,您可以手动删除它们。

如果希望自动删除它们,则可能需要设置一个自动缩放器,当它们用尽任务时,可以将部署扩展到零个副本。

这种Pod自动定标器的一个示例是Osiris,它是Kubernetes的通用,从零缩放的组件。

该技术有时被称为Rainbow部署,并且在每次您必须使之前的Pod的运行时间超过宽限期时都非常有用。

另一个很好的例子是WebSockets。

如果您正在向用户流式传输实时更新,则可能不希望在每次发布时都终止WebSocket。

如果您白天经常出游,则可能会导致实时Feed多次中断。

为每个版本创建一个新的部署是一个不太明显但更好的选择。

现有的用户可以继续流更新,而最新的Deployment服务于新用户。

当用户断开与旧Pod的连接时,您可以逐渐减少副本数并退出过去的Deployment。

您应该注意Pod从群集中删除,因为它们的IP地址可能仍用于路由流量。

与其立即关闭Pods,不如考虑在应用程序中等待更长的时间或设置一个preStop钩子。

仅在将集群中的所有终结点传播并从kube-proxy,Ingress控制器,CoreDNS等中删除后,才应删除Pod。

如果您的Pod运行诸如视频转码或使用WebSockets进行实时更新之类的长期任务,则应考虑使用Rainbow部署。

在Rainbow部署中,您为每个发行版创建一个新的Deployment,并在耗尽连接(或任务)后删除上一个发行版。

长时间运行的任务完成后,您可以手动删除较旧的部署。

或者,您可以自动将部署扩展到零个


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK