健康检查与优雅关闭:就绪探针与信号处理

FreeGuideOnline 最新 2026-06-16

title: "健康检查与优雅关闭:就绪探针与信号处理实战指南" description: "学会为后端服务配置健康检查与优雅关闭,掌握就绪探针实现和系统信号处理,让你的应用在 Kubernetes 中安全启动、平滑终止,零中断滚动更新。"

引言:为什么需要健康检查与优雅关闭?

在生产环境中,服务的启动和停止并不是瞬间完成的。一个典型的 Web 应用可能需要数秒钟才能建立数据库连接、加载缓存或完成其他初始化工作。在这段时间内,如果请求被路由到该实例,用户将看到错误。同样,当服务需要关闭时——无论是由于版本更新、扩缩容还是维护——突然终止会导致正在处理的请求失败、数据不一致甚至资源泄露。

健康检查优雅关闭正是为了解决这两大问题而生。健康检查确保流量只被发送到已经准备好接收请求的实例,而优雅关闭则保证服务在停止前妥善完成当前工作。本教程将聚焦于就绪探针的实现,以及如何通过信号处理构建优雅的终止逻辑,帮助你在任何容器编排环境下构建稳健的应用。

理解健康检查与就绪探针

什么是健康检查?

健康检查是一种周期性探测机制,用于判断应用实例是否处于正常状态。在微服务和容器化环境中,健康检查通常分为两类:

  • 存活探针:告诉编排系统容器是否需要重启。如果探针失败,容器会被杀死并重新启动。它用于恢复陷入死锁或异常状态的进程。
  • 就绪探针:告诉编排系统容器是否准备好接收流量。如果失败,该实例将从服务的负载均衡池中移除,直到探针再次成功。

就绪探针是我们实现“优雅启动”的关键。

就绪探针与存活探针的区别

特性 就绪探针 存活探针
目的 控制流量是否路由到 Pod 决定是否重启容器
失败后果 从 Service 端点移除 杀死容器并触发重启
使用场景 应用启动慢、依赖未就绪 应用进入不可恢复的异常状态
检查时机 启动期间及运行时 运行时

初学者常犯的错误是将存活探针的设置过于激进,导致容器在临时抖动时被不断重启。正确做法是让就绪探针处理启动期间的延迟,让存活探针只捕获真正的死锁。

就绪探针的工作流程

  1. 编排系统(如 Kubernetes)按照配置的时间间隔调用探针检查。
  2. 探针成功时,实例被标记为“就绪”,开始接收请求。
  3. 探针连续失败达到给定阈值后,实例被标记为“未就绪”,从负载均衡中摘除。
  4. 当探针再次成功时,实例重新加入流量。

这个循环确保了高峰流量下新实例完全预热后才接替流量,滚动更新时旧实例也能在摘除后安全下线。

实现就绪探针:以 Web 应用为例

设计健康检查端点

一个良好的健康检查端点应该能够验证关键依赖是否可用,但又不应该过于耗时。常见设计:

  • 简单健康检查:仅返回 HTTP 200,表示进程存活。适用于存活探针,但不足以用作就绪探针。
  • 就绪检查:检查数据库连接、消息队列连接、缓存状态等。只有在所有关键依赖就绪时才返回 200,否则返回 503。

例如,一个 RESTful 接口可以定义:

GET /healthz    -> 存活检查,总是返回 200(若进程正常)
GET /ready      -> 就绪检查,检查依赖后返回 200 或 503

在代码中暴露健康检查 API

以下分别给出 Node.js (Express) 和 Python (Flask) 的示例。

Node.js + Express

const express = require('express');
const app = express();

let isReady = false; // 标志位,由初始化逻辑控制

// 存活性探针
app.get('/healthz', (req, res) => {
  res.status(200).send('OK');
});

// 就绪探针
app.get('/ready', (req, res) => {
  if (isReady) {
    res.status(200).send('Ready');
  } else {
    res.status(503).send('Not Ready');
  }
});

// 模拟启动时的异步初始化
async function init() {
  // 模拟数据库连接等耗时操作
  await new Promise(resolve => setTimeout(resolve, 5000));
  console.log('应用初始化完成');
  isReady = true;
}

const server = app.listen(3000, () => {
  console.log('服务器启动,端口 3000');
  init();
});

Python + Flask

from flask import Flask
import threading
import time

app = Flask(__name__)
is_ready = False

@app.route('/healthz')
def healthz():
    return 'OK', 200

@app.route('/ready')
def ready():
    if is_ready:
        return 'Ready', 200
    else:
        return 'Not Ready', 503

def initialize():
    global is_ready
    # 模拟初始化过程
    time.sleep(5)
    print('初始化完成')
    is_ready = True

if __name__ == '__main__':
    threading.Thread(target=initialize).start()
    app.run(host='0.0.0.0', port=5000)

在这两个示例中,应用启动后 /ready 端点会返回 503,直到后台初始化任务将 isReady 置为 true

在 Kubernetes 中配置就绪探针

将上述应用打包成容器镜像并部署到 Kubernetes 时,可在 Pod 规范中定义 readinessProbe

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: my-app:latest
    ports:
    - containerPort: 3000
    readinessProbe:
      httpGet:
        path: /ready
        port: 3000
      initialDelaySeconds: 5   # 等待容器启动后5秒才开始检查
      periodSeconds: 5          # 每5秒检查一次
      failureThreshold: 3       # 连续失败3次才标记为未就绪
      successThreshold: 1       # 成功1次即标记为就绪
  • initialDelaySeconds:给应用一些启动时间,避免探针过早运行导致重启循环。
  • periodSeconds:探测频率。
  • failureThreshold:容忍连续失败的次数,防止瞬间抖动导致误判。
  • successThreshold:对于就绪探针,一般设置为 1 即可。

测试就绪探针

部署后,使用 kubectl get pods 查看状态,刚启动的 Pod 会处于 RunningREADY 列为 0/1。随着初始化完成,/ready 返回 200,探针成功后 READY 变为 1/1

你可以模拟未就绪状态:修改代码故意让 is_ready 保持 false,观察 Pod 永远不会变为就绪,也不会接收来自 Service 的流量。

优雅关闭与信号处理

优雅关闭的概念

优雅关闭是指服务在收到终止请求时,不再接受新的请求,同时等待正在处理的请求完成,最后释放资源并退出。这可以避免:

  • 客户端收到连接拒绝错误。
  • 正在进行的事务被中断导致数据不一致。
  • 日志或临时文件清理不完整。

在容器环境中,优雅关闭通常由操作系统信号触发,并要求应用正确响应。

操作系统信号:SIGTERM 与 SIGKILL

  • SIGTERM:终止信号,可以被应用捕获并用来触发优雅关闭。Kubernetes 在删除 Pod 时,会先向容器主进程发送 SIGTERM,给予应用时间进行清理。
  • SIGKILL:强制杀死进程,无法被捕获。如果应用在 terminationGracePeriodSeconds(默认30秒)内没有退出,Kubernetes 会发送 SIGKILL 强制终止。

因此,应用必须监听 SIGTERM 并在规定时间内完成退出。

如何捕获信号并执行优雅关闭

以 Node.js 为例,扩展前面的服务器以实现优雅关闭:

const server = app.listen(3000, () => {
  console.log('服务器启动');
  init();
});

// 优雅关闭函数
function gracefulShutdown(signal) {
  console.log(`收到 ${signal} 信号,开始优雅关闭...`);
  // 将就绪状态置为 false,让负载均衡停止转发新请求
  isReady = false;
  
  // 关闭 HTTP 服务器,停止接收新连接,但不中断现有连接
  server.close(() => {
    console.log('HTTP 服务器已关闭,没有新请求接入');
    // 执行清理操作:关闭数据库连接、清除缓存等
    // db.disconnect();
    console.log('清理完成,进程退出');
    process.exit(0);
  });

  // 防止长时间等待,设置超时强制退出
  setTimeout(() => {
    console.error('优雅关闭超时,强制退出');
    process.exit(1);
  }, 25000); // 应小于 terminationGracePeriodSeconds
}

// 监听 SIGTERM 和 SIGINT
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Python (Flask) 中的对应实现:

import signal
import sys
from flask import Flask

app = Flask(__name__)
is_ready = False

@app.route('/ready')
def ready():
    return ('Ready', 200) if is_ready else ('Not Ready', 503)

def graceful_shutdown(signum, frame):
    global is_ready
    print('收到终止信号,开始优雅关闭...')
    is_ready = False
    # Flask 开发服务器没有 server.close,在生产中应使用 gunicorn 或 uWSGI
    # 这里演示设置标志位让就绪探针立即返回 503
    # 实际应等待现有请求完成后退出
    sys.exit(0)

signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

确保在关闭前完成正在处理的请求

在 Node.js 中,server.close() 只停止接收新连接,已建立的连接会继续处理直到完成。你需要确保所有长连接(如 WebSocket)也被妥善处理。一个更完善的模式是使用连接计数器:

let activeConnections = 0;
server.on('connection', (conn) => {
  activeConnections++;
  conn.on('close', () => activeConnections--);
});

function gracefulShutdown() {
  isReady = false;
  server.close(() => {
    if (activeConnections === 0) {
      process.exit(0);
    }
  });
  // 如果一段时间后仍有活跃连接,强制退出
  setTimeout(() => {
    process.exit(1);
  }, 20000);
}

在 Kubernetes 中的 Pod 终止流程

理解 Kubernetes 的终止流程对设计优雅关闭至关重要:

  1. Pod 被标记为 Terminating,同时从所有 Service 的端点列表中被移除,停止接收新流量。
  2. 就绪探针不再被考虑,但 Pod 仍未退出。
  3. 如果定义了 preStop 钩子,容器会执行该钩子。
  4. 容器主进程收到 SIGTERM 信号。
  5. Kubernetes 等待指定的 terminationGracePeriodSeconds(默认30秒)。
  6. 如果进程未退出,发送 SIGKILL 强制终止。

值得注意的是,步骤1和步骤2几乎是同时发生的,这意味着在容器收到 SIGTERM 之前,网络规则已经更新,不再向该 Pod 发送新请求。但总有极短暂的时间窗口可能导致请求到达,因此应用侧仍需在收到信号后立即将就绪状态置为 false 并排空现有请求。

进阶技巧与最佳实践

就绪探针的高级配置

  • 初始延迟:根据应用平均启动时间设置,没有统一标准,需要监控实际启动耗时。
  • 探测超时:确保 timeoutSeconds 小于 periodSeconds,防止探测堆积。
  • 使用命令探针:当 HTTP 端点不能准确反映就绪状态时,可改用 exec 命令直接检查内部状态文件或进程。
readinessProbe:
  exec:
    command:
    - cat
    - /tmp/healthy
  initialDelaySeconds: 10
  periodSeconds: 5

使用 preStop 钩子辅助优雅关闭

如果应用本身不能正确处理信号,或者需要在收到 SIGTERM 前提前执行命令,可以使用 preStop 钩子:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 15"]  # 等待15秒让负载均衡移除完成

配合就绪探针的快速失败(例如将 failureThreshold 设置得很小),可以确保在 Pod 终止的早期就将其从负载均衡摘除,再用 preStop 等待残余请求清空。

就绪探针与滚动更新策略的结合

在 Deployment 中,调整 maxSurgemaxUnavailable 以及结合 minReadySeconds 可以更平滑地滚动:

  • minReadySeconds:Pod 就绪后还需等待这么久才认为可用,防止刚就绪就立刻接收全部流量。
  • 适当提高就绪探针的成功阈值,或者利用 minReadySeconds 让新实例充分预热。

监控与告警

对健康检查和优雅关闭进行监控能及早发现问题:

  • 记录就绪失败的次数和持续时间,当服务持续未就绪时发出告警。
  • 监控优雅关闭时长,如果频繁因为超时而被强制 SIGKILL,说明清理逻辑耗时过长,需要优化或调整 terminationGracePeriodSeconds
  • 在日志中记录从收到 SIGTERM 到进程退出的完整生命周期,便于排查。

常见陷阱与排查

  1. 将存活探针配置得过紧:短暂的网络延迟或数据库连接池耗尽会导致 Pod 被意外重启。存活探针应该比就绪探针具有更宽松的参数。
  2. 在就绪探针中执行昂贵的检查:每次探针都重建数据库连接将导致性能问题。应使用连接池并只做轻量级验证。
  3. 忽略信号处理:很多应用框架默认没有实现优雅关闭,直接收到 SIGTERM 会立即终止,导致请求失败。务必显式添加监听和处理。
  4. 终止期设置过短:复杂应用可能需要数分钟才能完成请求排空和资源释放,默认的30秒不够用。根据实际情况增加 terminationGracePeriodSeconds
  5. 优雅关闭中不改变就绪状态:即使负载均衡已经移除 Pod,仍有微秒级的竞态可能导致新请求到达。在信号处理中第一时间设置 isReady = false 是防御性措施。
  6. 在 preStop 中使用长时间 sleep:这是掩盖应用未实现优雅关闭的临时方案,应从根本上修改应用代码。

总结

健康检查与优雅关闭是构建高可用系统的基石。就绪探针控制着流量的入口,让不健康的实例自动隔离;信号处理优雅关闭则保证实例在离开集群时不带走未完成的请求,维持数据完整性。通过本教程的示例,你可以:

  • 设计并暴露一个区分存活和就绪的健康检查 API。
  • 在 Kubernetes 中配置准确的探针参数。
  • 在应用内捕获 SIGTERM 信号,安全关闭服务器并清理资源。
  • 理解 Pod 终止流程,并结合 preStop 钩子和探针调优实现零中断滚动更新。

在你的下一个项目中融入这些模式,你将获得一个更加稳健、运维友好的服务。现在就可以动手在你的应用中添加 /ready 端点并尝试优雅关闭的代码,感受它在真实滚动更新中的威力。