跳到主要内容

健康检查与优雅停机

问题

服务端如何实现健康检查和优雅停机?在 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。
  • NestJSapp.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();
}
}

相关链接