健康检查与优雅停机
问题
服务端如何实现健康检查和优雅停机?在 K8s 环境下有什么特别考虑?
面试速答版
liveness 和 readiness 探针有什么区别? 两个探针解决两个不同问题:
- liveness(存活):判断进程是否还活着,失败 → K8s 重启 Pod。检查应用本身是否还能响应。
- readiness(就绪):判断能否接收流量,失败 → K8s 从 Service 摘除(不重启)。检查依赖(DB、Redis)是否都连上了。
- 典型场景:应用启动中(readiness 失败但 liveness 正常)、数据库暂时不可用(不需要重启,等恢复就好)。
- startupProbe:启动慢的应用专用,启动期间暂停其他探针,避免被误重启。
- 接口实现:
/health简单返 200,/ready真实检查db.queryRaw('SELECT 1')和redis.ping()。
优雅停机怎么实现?为什么直接 kill 不行? 直接 kill 会导致进行中请求被中断、事务未提交、用户看到 502:
- 监听 SIGTERM/SIGINT:K8s 删 Pod 时先发 SIGTERM,给应用清理时间。
- 五步走:①标记
isShuttingDown=true,readiness 返 503 ②server.close()停止接收新连接 ③等进行中请求完成 ④关闭 DB/Redis 连接 ⑤process.exit(0)。 - 超时兜底:
setTimeout(forceExit, 30000),防止有连接卡住永不退出。 - K8s 配置:
terminationGracePeriodSeconds: 60给应用足够时间,超过就强制 SIGKILL。 - NestJS:
app.enableShutdownHooks()后OnModuleDestroy会自动调用。
答案
健康检查
health-check.ts
import express from 'express';
const app = express();
// 存活检查(liveness):是否需要重启
app.get('/health', (_req, res) => {
res.status(200).json({ status: 'ok' });
});
// 就绪检查(readiness):是否能接收流量
app.get('/ready', async (_req, res) => {
try {
// 检查数据库连接
await db.$queryRaw`SELECT 1`;
// 检查 Redis 连接
await redis.ping();
res.status(200).json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: String(error) });
}
});
K8s 探针配置
deployment.yaml
spec:
containers:
- name: api
livenessProbe: # 失败 → 重启 Pod
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe: # 失败 → 从 Service 摘除
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
startupProbe: # 启动完成前不执行其他探针
httpGet:
path: /health
port: 3000
failureThreshold: 30
periodSeconds: 2
优雅停机
graceful-shutdown.ts
const server = app.listen(3000);
let isShuttingDown = false;
async function gracefulShutdown(signal: string) {
console.log(`Received ${signal}, starting graceful shutdown...`);
isShuttingDown = true;
// 1. 停止接受新连接
server.close(async () => {
console.log('HTTP server closed');
// 2. 等待进行中的请求完成
// 3. 关闭数据库连接
await db.$disconnect();
// 4. 关闭 Redis 连接
await redis.quit();
// 5. 清理其他资源
console.log('All resources cleaned up');
process.exit(0);
});
// 超时强制退出
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
}
// 健康检查中间件:停机中返回 503
app.use((req, res, next) => {
if (isShuttingDown) {
return res.status(503).json({ error: 'Server is shutting down' });
}
next();
});
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
常见面试问题
Q1: liveness 和 readiness 的区别?
答案:
- liveness:应用是否存活。失败 → K8s 重启 Pod
- readiness:应用是否就绪。失败 → K8s 不发流量到该 Pod
典型场景:应用启动中(readiness 失败但 liveness 正常),或依赖的数据库暂时不可用。
Q2: 为什么需要优雅停机?
答案:
直接 kill 进程会导致:
- 进行中的请求被中断(用户看到 502)
- 数据库事务未提交
- 消息队列中的任务丢失
优雅停机确保已接收的请求处理完毕、资源正确释放。
Q3: NestJS 中如何实现优雅停机?
答案:
// main.ts
app.enableShutdownHooks(); // 启用生命周期钩子
// service.ts
@Injectable()
class AppService implements OnModuleDestroy {
async onModuleDestroy() {
await this.cleanup();
}
}
相关链接
- 容器与编排基础 - K8s 部署
- Node.js 部署与运维 - PM2 优雅停机