健康检查与优雅关闭:就绪探针与信号处理
title: "健康检查与优雅关闭:就绪探针与信号处理实战指南" description: "学会为后端服务配置健康检查与优雅关闭,掌握就绪探针实现和系统信号处理,让你的应用在 Kubernetes 中安全启动、平滑终止,零中断滚动更新。"
引言:为什么需要健康检查与优雅关闭?
在生产环境中,服务的启动和停止并不是瞬间完成的。一个典型的 Web 应用可能需要数秒钟才能建立数据库连接、加载缓存或完成其他初始化工作。在这段时间内,如果请求被路由到该实例,用户将看到错误。同样,当服务需要关闭时——无论是由于版本更新、扩缩容还是维护——突然终止会导致正在处理的请求失败、数据不一致甚至资源泄露。
健康检查和优雅关闭正是为了解决这两大问题而生。健康检查确保流量只被发送到已经准备好接收请求的实例,而优雅关闭则保证服务在停止前妥善完成当前工作。本教程将聚焦于就绪探针的实现,以及如何通过信号处理构建优雅的终止逻辑,帮助你在任何容器编排环境下构建稳健的应用。
理解健康检查与就绪探针
什么是健康检查?
健康检查是一种周期性探测机制,用于判断应用实例是否处于正常状态。在微服务和容器化环境中,健康检查通常分为两类:
- 存活探针:告诉编排系统容器是否需要重启。如果探针失败,容器会被杀死并重新启动。它用于恢复陷入死锁或异常状态的进程。
- 就绪探针:告诉编排系统容器是否准备好接收流量。如果失败,该实例将从服务的负载均衡池中移除,直到探针再次成功。
就绪探针是我们实现“优雅启动”的关键。
就绪探针与存活探针的区别
| 特性 | 就绪探针 | 存活探针 |
|---|---|---|
| 目的 | 控制流量是否路由到 Pod | 决定是否重启容器 |
| 失败后果 | 从 Service 端点移除 | 杀死容器并触发重启 |
| 使用场景 | 应用启动慢、依赖未就绪 | 应用进入不可恢复的异常状态 |
| 检查时机 | 启动期间及运行时 | 运行时 |
初学者常犯的错误是将存活探针的设置过于激进,导致容器在临时抖动时被不断重启。正确做法是让就绪探针处理启动期间的延迟,让存活探针只捕获真正的死锁。
就绪探针的工作流程
- 编排系统(如 Kubernetes)按照配置的时间间隔调用探针检查。
- 探针成功时,实例被标记为“就绪”,开始接收请求。
- 探针连续失败达到给定阈值后,实例被标记为“未就绪”,从负载均衡中摘除。
- 当探针再次成功时,实例重新加入流量。
这个循环确保了高峰流量下新实例完全预热后才接替流量,滚动更新时旧实例也能在摘除后安全下线。
实现就绪探针:以 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 会处于 Running 但 READY 列为 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 的终止流程对设计优雅关闭至关重要:
- Pod 被标记为 Terminating,同时从所有 Service 的端点列表中被移除,停止接收新流量。
- 就绪探针不再被考虑,但 Pod 仍未退出。
- 如果定义了
preStop钩子,容器会执行该钩子。 - 容器主进程收到 SIGTERM 信号。
- Kubernetes 等待指定的
terminationGracePeriodSeconds(默认30秒)。 - 如果进程未退出,发送 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 中,调整 maxSurge 和 maxUnavailable 以及结合 minReadySeconds 可以更平滑地滚动:
minReadySeconds:Pod 就绪后还需等待这么久才认为可用,防止刚就绪就立刻接收全部流量。- 适当提高就绪探针的成功阈值,或者利用
minReadySeconds让新实例充分预热。
监控与告警
对健康检查和优雅关闭进行监控能及早发现问题:
- 记录就绪失败的次数和持续时间,当服务持续未就绪时发出告警。
- 监控优雅关闭时长,如果频繁因为超时而被强制 SIGKILL,说明清理逻辑耗时过长,需要优化或调整
terminationGracePeriodSeconds。 - 在日志中记录从收到 SIGTERM 到进程退出的完整生命周期,便于排查。
常见陷阱与排查
- 将存活探针配置得过紧:短暂的网络延迟或数据库连接池耗尽会导致 Pod 被意外重启。存活探针应该比就绪探针具有更宽松的参数。
- 在就绪探针中执行昂贵的检查:每次探针都重建数据库连接将导致性能问题。应使用连接池并只做轻量级验证。
- 忽略信号处理:很多应用框架默认没有实现优雅关闭,直接收到 SIGTERM 会立即终止,导致请求失败。务必显式添加监听和处理。
- 终止期设置过短:复杂应用可能需要数分钟才能完成请求排空和资源释放,默认的30秒不够用。根据实际情况增加
terminationGracePeriodSeconds。 - 优雅关闭中不改变就绪状态:即使负载均衡已经移除 Pod,仍有微秒级的竞态可能导致新请求到达。在信号处理中第一时间设置
isReady = false是防御性措施。 - 在 preStop 中使用长时间 sleep:这是掩盖应用未实现优雅关闭的临时方案,应从根本上修改应用代码。
总结
健康检查与优雅关闭是构建高可用系统的基石。就绪探针控制着流量的入口,让不健康的实例自动隔离;信号处理与优雅关闭则保证实例在离开集群时不带走未完成的请求,维持数据完整性。通过本教程的示例,你可以:
- 设计并暴露一个区分存活和就绪的健康检查 API。
- 在 Kubernetes 中配置准确的探针参数。
- 在应用内捕获 SIGTERM 信号,安全关闭服务器并清理资源。
- 理解 Pod 终止流程,并结合 preStop 钩子和探针调优实现零中断滚动更新。
在你的下一个项目中融入这些模式,你将获得一个更加稳健、运维友好的服务。现在就可以动手在你的应用中添加 /ready 端点并尝试优雅关闭的代码,感受它在真实滚动更新中的威力。