使用preStop Hook实现ALB Ingress后端Pod滚动升级时平滑下线Pod

本文介绍如何通过配置preStop Hook,实现ALB Ingress后端Pod的平滑下线。使Pod在被ALB Ingress Controller完全从后端服务组中移除后才正式下线,以此来保障在Kubernetes容器进行滚动更新期间,服务的流量无缝切换,避免出现中断。

前提条件

原理解析

Pod生命周期

Pod中的容器包括两种类型:

  • 初始化容器:运行服务的主容器前置依赖的容器,完成一些初始化操作。

  • 主容器(工作容器):就是运行服务进程的容器。

Pod启动过程如下:

image
  1. 运行初始化容器:在主容器启动之前,要运行的容器,为主容器执行预置操作。

  2. 运行主容器:

    1. 容器启动后执行postStart Hook函数。

    2. 容器执行存活性探测(liveness probe)、就绪性检测(readiness probe)。

    3. 在容器退出前执行preStop Hook函数。

Pod终止流程和网络规则更新流程

在删除Pod时K8s会同时进行Pod终止和网络规则更新两个主要的流程。Pod终止流程和网络更新流程分别如下:

Pod终止流程

  1. kube-apiserver收到Pod删除请求,将Pod标记为Terminating状态。

  2. 如果Pod定义了preStop Hook,将执行preStop Hook。

  3. K8s集群向容器发送SIGTERM信号。

  4. 等待容器停止,或等待Pod删除宽限期超时。

    说明

    Pod中容器删除宽限期terminationGracePeriodSeconds默认为30秒。

  5. 超时Pod删除宽限期后容器仍未终止,K8s发送SIGKILL信号给容器。

  6. Pod被完全删除。

网络规则更新流程

  1. kube-apiserver收到Pod删除请求,将Pod标记为Terminating状态。

  2. Endpoint Controller从Endpoint对象中删除Pod的IP。

  3. ALB Ingress Controller进行Server节点调谐,将Service Endpoints从后端服务器组中移除,不再将流量路由到被删除的Pod。

使用preStop Hook来实现Pod平滑下线

当Pod滚动升级过程中,如果旧的Pod没有平滑退出,导致应用无法正常处理业务请求,会出现如下错误状态码,影响应用的可用性。

状态码

问题原因

404

当旧Pod在未处理完正常请求情况下被删除,并且该请求不是幂等的时,会出现404的情况。

502

当K8s集群已经将旧Pod删除,容器收到终止信号(SIGTERM)并迅速终止,ALB Ingress Controller仍在调谐流程中,导致Pod未及时从后端服务器组中移除,流量依然被错误地路由到已终止的旧Pod上。

为了避免以上的情况发生,您可以采取如下方案:

在kube-apiserver接收到Pod删除请求时,通过在Deployment配置内添加preStop Hook,可以为容器设置一个"Sleep"暂停期。这个暂停让容器有时间在收到终止信号(SIGTERM)之前完成网络规则的更新,并等待ALB Ingress Controller完成Server调谐事件以及确保Pod已从后端服务器组中移除。这一步骤对于保障Pod的平稳下线至关重要,避免在滚动更新或服务重启期间造成流量中断。

同时,K8s为容器设定了在接收到终止信号(SIGTERM)后能持续运行的最大宽限期是30秒,当程序关闭时间与preStop Hook指定的操作时间之和超过了30秒时,意味着默认宽限期不足以让容器完成所有的关闭步骤。kubelet会等待2秒后直接发送立即终止(SIGKILL)信号给容器,导致容器强制退出。

说明

如果程序的关闭时间和在Deployment配置的preStop Hook之和超过30秒,应将terminationGracePeriodSeconds重新设置,调整为大于30秒,确保容器优雅退出。

步骤一:并配置Pod平滑退出preStop Hook

  1. 创建tea-service.yaml,配置Pod平滑退出preStop Hook。

    执行和部署以下命令与模板。

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: tea
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: tea
      template:
        metadata:
          labels:
            app: tea
        spec:
          containers:
          - name: tea
            image: registry.cn-hangzhou.aliyuncs.com/acs-sample/nginxdemos:latest
            ports:
            - containerPort: 80
            lifecycle:  # 设置preStop Hook函数,使kube-apiserver等待10秒后再向Pod发送SIGTERM信号。
              preStop:  # preStop钩子设置。
                exec:  # 通过执行命令的方式来实现preStop操作。
                  command:  # 定义要执行的命令。
                  - /bin/sh
                  - -c
                  - "sleep 10"  # 执行sleep操作,暂停10秒。
          terminationGracePeriodSeconds: 45  # 设置Pod删除宽限期。
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: tea-svc
    spec:
      ports:
      - port: 80
        targetPort: 80
        protocol: TCP
      selector:
        app: tea
      type: ClusterIP
  2. 执行以下命令,部署Deployment和Service。

    kubectl apply -f tea-service.yaml

    预期输出:

    deployment "tea" created
    service "tea-svc" created
  3. 执行以下命令和内容,部署到tea-ingress.yaml文件中。

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    metadata:
      name: tea-ingress
    spec:
      ingressClassName: alb
      rules:
       - host: example.com
         http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: tea-svc
                port:
                  number: 80
  4. 执行以下命令,部署Ingress。

    kubectl apply -f tea-ingress.yaml
  5. 执行以下命令,获取ALB Ingress的ADDRESS

    kubectl get ingress

    预期输出:

    NAME                    CLASS   HOSTS                     ADDRESS                                              PORTS   AGE
    tea-ingress             alb     example.com          alb-110zvs5nhsvfv*****.cn-chengdu.alb.aliyuncs.com   80      7m5s

步骤二:结果验证

  1. 启动测试脚本。

    1. 使用以下内容,创建test.sh测试脚本文件。此脚本用于测试应用的可用性,按每秒一次的频率请求Nginx服务并查看状态码。

      #!/bin/bash
      HOST="example.com"
      DNS="alb-110zvs5nhsvfv*****.cn-chengdu.alb.aliyuncs.com"   # 填写ALB Ingress的ADDRESS值。
      while true; do
        RESPONSE=$(curl -H Host:$HOST  -s -o /dev/null -w "%{http_code}" -m 1  http://$DNS/)
        TIMESTAMP=$(date +%Y-%m-%d_%H:%M:%S)
        echo "$TIMESTAMP - $RESPONSE" 
        sleep 1
      done
    2. 执行以下命令运行测试脚本test.sh

      bash test.sh
  2. 打开一个新的终端窗口执行以下命令,重新部署应用,触发Pod滚动升级。

    kubectl rollout restart deploy tea 
    说明

    同时打开运行脚本的终端窗口和验证滚动升级的窗口可以更直观的看到应用升级的过程中服务是否可以正常访问。

  3. 验证Pod滚动升级。

    1. 初始阶段应用副本数为3。

    2. 在重新部署应用时,新Pod创建阶段由于新Pod没有就绪,旧Pod仍在提供服务,会同时运行新旧两个Pod。

    3. 新Pod就绪且成功挂载至ALB后端服务器组后,会终止旧Pod。

      image

    4. 所有旧Pod完成preStop Hook函数或超出terminationGracePeriodSeconds超时时间后,kubectl会向Container发送信号终止旧的Pod,此时滚动更新完成。

      image

  4. 可以看到左边窗口脚本执行的结果状态码全部为200,更新过程没有中断。