Myluzh Blog

K8S 中的优雅终止:确保服务平稳关闭

发布时间: 2024-8-2 文章作者: myluzh 分类名称: Kubernetes 朗读文章


0x01 什么是优雅停止?
优雅终止是 Kubernetes 中一个非常重要的概念,它关系到服务的稳定性和用户体验。通过合理配置和使用 Kubernetes 提供的工具,我们可以确保应用在终止时能够做到尽可能的平滑和优雅,这不仅提升了系统的可靠性,也增强了用户对服务的信任。
优雅终止指在终止应用或服务时,确保当前正在进行的操作能够正常完成,同时避免新请求的进入,使得服务能够平稳地关闭。在 Kubernetes 中,这通常涉及到 Pod 的终止流程。
点击查看原图

0x02 Pod 终止流程
1、Pod 状态变为 Terminating
Pod 被删除,API 层面上 metadata.deletionTimestamp 字段会被标记上删除时间。
2、更新转发规则
kube-proxy 发现 Pod 被删除,开始更新转发规则,将 Pod 从 service 的 endpoint 列表中摘除掉,新流量不再转发到该 Pod。
3、销毁 Pod
kubelet 发现 Pod 被删除,开始销毁 Pod。
  • 执行 PreStop Hook:如果 Pod 中有配置 preStop Hook,Kubernetes 会执行这些命令。
  • 发送 SIGTERM 信号:Kubernetes 发送 SIGTERM 信号给容器内主进程,通知其开始优雅停止。
  • 等待容器停止:Kubernetes 等待容器内主进程完全停止,如果在 terminationGracePeriodSeconds 内(默认 30 秒)未完全停止,则发送 SIGKILL 信号强制杀死。
  • 清理资源:所有容器进程终止后,清理 Pod 资源。
  • 完成 Pod 删除:通知 API Server Pod 销毁完成,完成 Pod 删除。

0x03 Pod 关闭的场景
1、优雅关机
容器在宽限期内正常关闭,执行可选的 pre-stop hook,并响应 SIGTERM 信号。一旦容器成功退出,Kubelet 从 API Server 中删除 Pod。
2、强制关机
容器无法在宽限期内关闭,Kubelet 会发送 SIGKILL 信号强制关闭进程。这可能导致数据丢失或面向用户的错误。
关闭失败可能是由于多种原因,包括:
  • 应用程序忽略 SIGTERM 信号,
  • pre-stop hook 花费的时间超过宽限期,
  • 应用程序清理资源花费的时间超过宽限期
  • 以上的组合

0x04 K8S 中的优雅终止机制
1、Termination Grace Period
Kubernetes 为每个 Pod 设置了一个终止宽限期(Termination Grace Period),默认为 30 秒。在此时间内,Pod 会尝试完成当前的工作并优雅地关闭。
2、PreStop Hook
用户可以在 Pod 的配置中定义 PreStop 生命周期钩子。当 Pod 接收到终止信号时,Kubernetes 会先执行这个钩子中的命令,给予应用清理资源的机会。
3、Readiness Probes
就绪探针(Readiness Probes)确保 Pod 在接收流量之前已经准备好。在终止过程中,Kubernetes 会停止发送流量到不再就绪的 Pod。
4、Liveness Probes
活跃探针(Liveness Probes)用于检测 Pod 是否仍然健康。如果 Pod 不再健康,Kubernetes 会根据配置的策略进行重启或终止。

0x05 如何实现优雅终止?
1、合理配置 Termination Grace Period
根据应用的实际情况,合理设置终止宽限期,给予足够的时间来完成当前任务。可以通过 terminationGracePeriodSeconds 参数进行设置。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gracefulshutdown-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: gracefulshutdown-app
  template:
    metadata:
      labels:
        app: gracefulshutdown-app
    spec:
      containers:
        - name: graceful-shutdown-test
          image: gracefulshutdown-app:latest
          ports:
            - containerPort: 8080
      terminationGracePeriodSeconds: 45
2、合理处理 SIGTERM 信号
要实现优雅终止,务必在业务代码里面处理下 SIGTERM 信号,参考 处理 SIGTERM 代码示例
例子:
如果容器启动入口使用了脚本 (如 CMD ["/start.sh"]),业务进程就成了 shell 的子进程,在 Pod 停止时业务进程可能收不到 SIGTERM 信号,因为 shell 不会自动传递信号给子进程。比如如下这种情况 使用了类似 /bin/sh -c my-app 这样的启动入口。或者使用 /entrypoint.sh 这样的脚本文件作为入口,在脚本中再启动业务进程:
cat entrypoint.sh

#!/bin/bash
 /webserver
问题:
如果容器启动入口使用了脚本(如 CMD ["/start.sh"]),业务进程可能收不到 SIGTERM 信号。
解决方法:
  • 使用 exec 启动:在 shell 中使用 exec 启动二进制命令,使其代替当前 shell 进程。
    #! /bin/bash
    exec /bin/yourapp # 脚本中执行二进制 
  • 多进程场景:使用 trap 传递信号:通过 trap 捕获 SIGTERM 信号,并传递给业务进程。
    #! /bin/bash
    
    /bin/app1 & pid1="$!" # 启动第一个业务进程并记录 pid
    echo"app1 started with pid $pid1"
    /bin/app2 & pid2="$!" # 启动第二个业务进程并记录 pid
    echo"app2 started with pid $pid2"
    
    handle_sigterm() {
      echo "[INFO] Received SIGTERM"
      kill -SIGTERM $pid1 $pid2 # 传递 SIGTERM 给业务进程
      wait $pid1 $pid2 # 等待所有业务进程完全终止
    }
    
    trap handle_sigterm SIGTERM # 捕获 SIGTERM 信号并回调 handle_sigterm 函数
    wait # 等待回调执行完,主进程再退出
  • 使用 init 系统:使用像 dumb-init 或 tini 这样的 init 系统来管理子进程,确保信号能够正确传递。
    # 这是以 dumb-init 为例制作镜像的 Dockerfile 示例
    FROM ubuntu:22.04
    RUN apt-get update && apt-get install -y dumb-init
    ADD start.sh /
    ADD app1 /bin/app1
    ADD app2 /bin/app2
    ENTRYPOINT ["dumb-init", "--"]
    CMD ["/start.sh"]
    # 这是以 tini 为例制作镜像的 Dockerfile 示例
    FROM ubuntu:22.04
    ENV TINI_VERSION v0.19.0
    ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
    COPY entrypoint.sh /entrypoint.sh
    RUN chmod +x /tini /entrypoint.sh
    ENTRYPOINT ["/tini", "--"]
    CMD [ "/start.sh" ]
    
3、编写 PreStop 脚本
确保在 Pod 终止前,所有清理工作都能得到执行。
# 当使用 pre-stop 事件的容器终止时,nginx -s quit在 kubelet 向SIGTERM主进程发送信号之前,在容器中执行命令。
apiVersion: v1
kind: Pod
metadata:
  name: prestop-demo
spec:
  containers:
    - image: nginx
      name: nginx-container
      ports:
        - containerPort: 80
      lifecycle:
        preStop:
          exec:
            command:
              - sh
              - -c
              - echo "Stopping container now..." > /proc/1/fd/1 && nginx -s stop 
在某些极端情况下,Pod 被删除的一小段时间内,仍然可能有新连接被转发过来,这种情况下,我们也可以利用 preStop 先 sleep 一小下,等待 kube-proxy 完成规则同步再开始停止容器内进程。
        lifecycle:
          preStop:
            exec:
              command:
              - sleep
              - 5s
4、使用 Liveness 和 Readiness Probes
配置探针来监控应用状态,确保在终止过程中,应用不会接收到新的流量。
5、利用 Deployment 的滚动更新策略
使用 Deployment 资源的滚动更新策略,逐步替换旧版本的 Pod,确保服务的平滑过渡。
6、配置保守的更新策略
保持足够多的可用副本数量,减缓发版速度,给新副本留预热时间。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  strategy:
    type: RollingUpdate
    rollingUpdate:
      # 单个串行升级,等新副本 ready 后才开始销毁旧副本
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          startupProbe:
            httpGet:
              path: /
              port: 80
            successThreshold: 5 # 新副本启动时,连续探测成功多次后才交给 readinessProbe 探测
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /
              port: 80
            successThreshold: 1 # 运行过程中探测 1 次成功就认为 ready,可在抖动导致异常后快速恢复服务
            periodSeconds: 5

标签: k8s kubernetes 优雅停止 PreStop Readiness SIGTERM Liveness

发表评论