3. PreStop 钩子:优雅关闭的核心机制与配置方法

在 Kubernetes 中,Pod 的终止并非瞬间完成。当用户执行 kubectl delete pod 或 Deployment 触发滚动更新时,Pod 会进入 Terminating 状态。此时,Kubelet 会并行执行两个关键动作:运行 PreStop 钩子 和等待 SIGTERM 信号。PreStop 钩子是实现优雅关闭的第一道防线,它允许容器在收到终止信号前执行自定义的清理逻辑。

3.1 PreStop 钩子的执行时序

理解 PreStop 钩子在 Pod 生命周期中的位置至关重要。下图展示了从 Pod 进入 Terminating 状态到最终被删除的完整流程:

PreStop 钩子在 Pod 终止流程中的执行时序 1. Pod 进入 Terminating 状态 2. 执行 PreStop 钩子 (阻塞等待完成) 3. 发送 SIGTERM (主进程处理) 等待 Grace Period 超时 → 强制 SIGKILL 正常退出 → Pod 删除 关键时序说明: • PreStop 钩子与 SIGTERM 是串行关系:PreStop 执行完毕后才发送 SIGTERM。 • PreStop 钩子的执行时间计入 terminationGracePeriodSeconds(默认 30s)。 • 如果 PreStop 钩子超时,Kubelet 会跳过 SIGTERM 直接发送 SIGKILL。 • 建议 PreStop 钩子总耗时 ≤ terminationGracePeriodSeconds 的 60%,为 SIGTERM 处理预留时间。
最佳实践:PreStop 钩子应设计为幂等且可重入。如果钩子脚本在第一次执行时失败,Kubelet 不会重试,但 Pod 仍会继续终止流程。建议在钩子内部实现重试逻辑和超时控制。

3.2 PreStop 钩子的两种配置方式

Kubernetes 支持两种 PreStop 钩子实现:Exec(执行命令)和 HTTP GET(发送 HTTP 请求)。在生产环境中,Exec 方式更为常见,因为它可以执行任意脚本或调用自定义工具。

配置方式 适用场景 配置示例 注意事项
Exec 需要执行复杂清理逻辑(如排空连接、通知注册中心、等待任务完成)
lifecycle:
  preStop:
    exec:
      command:
      - /bin/sh
      - -c
      - "sleep 10 && /usr/local/bin/drain-connections.sh"
命令在容器内执行;需确保容器内有该命令;超时由 terminationGracePeriodSeconds 控制
HTTP GET 容器内运行 Web 服务,需要触发内部 API 进行清理
lifecycle:
  preStop:
    httpGet:
      path: /shutdown
      port: 8080
      scheme: HTTP
仅适用于 HTTP 服务;无法传递复杂参数;响应码 2xx/3xx 视为成功

3.3 优雅关闭的典型配置模板

以下是一个生产级别的 Deployment 配置片段,展示了如何将 PreStop 钩子与 terminationGracePeriodSeconds 配合使用:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        image: my-app:1.0.0
        ports:
        - containerPort: 8080
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - |
                # 1. 从服务注册中心摘除本实例
                /usr/local/bin/deregister-from-consul.sh
                
                # 2. 等待 5 秒让负载均衡器更新
                sleep 5
                
                # 3. 优雅关闭 HTTP 服务(发送 SIGTERM 给应用进程)
                kill -TERM 1
        terminationGracePeriodSeconds: 30
      dnsPolicy: ClusterFirst
配置要点解析:
  • terminationGracePeriodSeconds: 30 — 整个终止流程的总超时时间,包括 PreStop 钩子执行时间和 SIGTERM 处理时间。
  • PreStop 钩子中的 sleep 5 — 为负载均衡器/网关提供缓冲时间,确保流量不再路由到本 Pod。
  • kill -TERM 1 — 向主进程(PID 1)发送 SIGTERM,触发应用内部的优雅关闭逻辑。
  • 如果 PreStop 钩子执行时间超过 30 秒,Kubelet 会直接发送 SIGKILL,导致强制终止。

3.4 常见陷阱与避坑指南

在实际生产环境中,PreStop 钩子的配置存在多个容易忽视的陷阱:

陷阱 现象 根因 解决方案
PreStop 钩子阻塞时间过长 Pod 长时间处于 Terminating 状态,滚动更新卡住 钩子脚本中存在无限等待或未设置超时 在脚本内部实现超时机制,使用 timeout 命令包裹
PreStop 钩子与 SIGTERM 处理逻辑重叠 应用收到 SIGTERM 时仍在执行 PreStop 清理,导致状态不一致 PreStop 钩子中调用了与 SIGTERM 处理函数相同的资源 PreStop 只做外部清理(如注册中心摘除),内部清理交给 SIGTERM 处理
PreStop 钩子依赖外部服务 外部服务不可用时,钩子执行失败,Pod 仍被删除 钩子脚本未处理网络超时或服务不可用异常 添加重试逻辑和 fallback 策略,确保即使外部服务不可用也能正常退出
terminationGracePeriodSeconds 设置过短 PreStop 钩子未执行完毕就被 SIGKILL 总超时时间小于 PreStop 钩子预期执行时间 根据钩子脚本的最大执行时间 + 10~15 秒余量设置该参数
重要警告:不要在 PreStop 钩子中执行 sleep 超过 terminationGracePeriodSeconds 的 50%。例如,如果 terminationGracePeriodSeconds 为 30 秒,PreStop 钩子中的 sleep 不应超过 15 秒。否则,SIGTERM 可能无法被发送,应用将直接被 SIGKILL 杀死,导致连接丢失或数据损坏。

3.5 网关场景下的 PreStop 钩子优化

在网关升级场景中,PreStop 钩子的设计需要额外考虑流量调度延迟。下图展示了网关场景下 PreStop 钩子的优化流程:

网关场景下 PreStop 钩子优化流程 1. 从网关摘除 调用 API 移除本 Pod 2. 等待流量耗尽 sleep 5~10s 3. 确认无活跃连接 检查连接池/指标 退出 网关场景 PreStop 钩子脚本示例: #!/bin/sh # 1. 从网关摘除(假设使用 Nginx Plus API) curl -X POST http://nginx-plus:8080/api/3/upstreams/my-upstream/peers/$(hostname)/drain # 2. 等待网关更新路由表(根据网关类型调整等待时间) sleep 8 # 3. 检查活跃连接数,如果仍有连接则继续等待(最多等待 15s) for i in $(seq 1 15); do [ $(curl -s localhost:8080/metrics/active_connections) -eq 0 ] && break; sleep 1; done
网关场景优化要点:
  • 摘除操作必须幂等:如果 Pod 已经摘除,再次调用摘除 API 不应报错。
  • 等待时间应可配置:不同网关(Nginx、Envoy、HAProxy)的路由更新延迟不同,建议通过环境变量传入等待时间。
  • 连接检查应使用本地指标:避免在 PreStop 钩子中依赖外部监控系统,优先使用容器内的本地指标端点。
  • 设置最大等待上限:即使仍有活跃连接,也应在 terminationGracePeriodSeconds 的 80% 时间点强制退出。

3.6 验证 PreStop 钩子的正确性

配置完成后,建议通过以下步骤验证 PreStop 钩子是否按预期工作:

  1. 手动触发 Pod 删除:kubectl delete pod <pod-name> --grace-period=30
  2. 观察 Pod 事件:kubectl describe pod <pod-name> 查看 PreStop 钩子的执行状态。
  3. 检查应用日志:确认 PreStop 钩子脚本的日志输出,验证摘除和等待逻辑是否执行。
  4. 监控网关指标:在 Pod 删除期间,观察网关是否将流量正确转移到其他 Pod。
  5. 压力测试:在持续流量下执行滚动更新,验证是否有请求失败或连接重置。
验证命令速查:
# 查看 Pod 终止事件
kubectl get events --field-selector involvedObject.name=<pod-name>

# 查看 Pod 日志(在 PreStop 钩子执行期间)
kubectl logs <pod-name> --tail=50

# 模拟滚动更新
kubectl set image deployment/my-app app=my-app:2.0.0 --record

# 观察 Pod 终止状态
kubectl get pods -w | grep Terminating