Node.js 服务优雅关停与数据库连接池:从 SIGTERM 到请求排空
本文目录
进程被重启不等于服务能平滑交接。PM2 reload、容器滚动更新、主机重启都会让旧进程收到终止信号;如果它立刻退出,正在写数据库的请求可能只完成一半,队列消息可能被错误确认,WebSocket 客户端也会突然断开。优雅关停的目标是停止接收新工作、在明确期限内完成安全中的工作、释放资源,超时则有界退出。它不是无限等待,也不能保证跨服务副作用自动回滚。
另一类常见故障是数据库连接池耗尽:请求排队等待连接,延迟和超时进一步增加重试,负载反而更高。池大小必须与数据库总连接预算、进程/worker 数量一起规划,不能只看单个 Prisma Client 的默认值。
1. 先画清进程与连接预算
连接池是每个数据库客户端实例的本地资源。以 3 个服务进程、每个进程 8 个连接为例,理论上就可能占用 24 个连接;再加后台 worker、迁移任务、监控、管理工具和故障期间的新旧双进程。预算可按下式估算:
应用峰值连接 = Σ(每种服务的最大副本数 × 每副本 pool.max)
可分配给应用 = PostgreSQL max_connections
- 运维/监控保留 - 管理连接保留 - 其他应用预算
按最大并发副本而非平时进程数计算。滚动发布会让新旧版本短暂并存;PM2 cluster、定时任务和独立队列 worker 也各有自己的进程和池。不要用 PostgreSQL max_connections 直接堆大池子:每个后端连接本身也消耗服务器内存和调度资源。必要时评估 PgBouncer,但确认事务/会话语义、prepared statements、迁移连接和 LISTEN/NOTIFY 等功能是否兼容。
Prisma 版本与连接器会改变池实现和默认值。Prisma ORM v6 及之前的 PostgreSQL URL 常见 connection_limit、pool_timeout、connect_timeout 参数;v7 默认 driver adapter 后,池选项来自具体 Node driver,例如 pg 的 max、connectionTimeoutMillis。升级主版本时不要照搬旧 URL 参数或默认值,先确认锁文件、adapter、实际运行配置和连接数。
2. 识别池排队,不要盲目加连接
- 同时观察应用侧连接获取等待、连接超时、查询耗时、活跃请求、数据库活动连接与锁等待;只看 SQL 慢查询会漏掉“查询还没拿到连接”的时间。
- 为连接获取、单条查询、整个 HTTP 请求和外部依赖分别设合理 timeout;它们不是同一个时钟。超时要能取消/停止后续副作用,不能只向客户端返回 504 后让昂贵查询继续无限跑。
- 检查事务是否包住了网络请求、文件转换或用户等待;长事务会占连接、延长锁持有并放大池压力。把事务缩小到必要的数据库原子操作。
- 在长生命周期 Node 服务中复用一个应用级 PrismaClient/连接池,不要每个请求创建新客户端;测试和进程结束时使用受控清理。
- 为不同工作负载设置并发上限:API、批处理和转换 worker 不应无界争抢同一个数据库预算。排队要有长度、等待时长和拒绝策略。
重试只针对可恢复错误,必须有总时限、指数退避和抖动;不能在超时后立即无限重试。写操作需要幂等键或事务约束,否则超时可能发生在提交之后,盲目重试会重复扣款/创建。
3. 定义关停状态机
关停可拆成四个阶段:
- 进入 draining:只处理一次 SIGTERM/SIGINT,切换健康状态为不再接新流量;重复信号可以缩短剩余期限,但不能重复执行复杂清理。
- 停止入口:从负载均衡摘除实例、停止 HTTP accept,并拒绝/快速返回新业务请求;已有请求继续完成。摘流量与本机监听关闭的先后应和代理、PM2/容器编排的行为配套验证。
- 排空与释放:等待在途 HTTP 请求和有界后台任务;停止领取新队列任务,正在处理的消息要完成后 ACK,或不 ACK 让租约/可见性超时后重新投递。关闭 WebSocket、数据库池、Redis 与 telemetry exporter。
- 期限到达:记录未完成工作数量和阶段耗时,按产品语义中止/重新排队;到达总 deadline 后强制终止,避免进程永远挂住。不要把不确定状态的任务误标成功。
部署平台的 kill grace period 必须长于应用排空 deadline,并预留清理时间;但不要设得远超请求最大时长。客户端、反向代理、负载均衡、PM2 与应用的 timeout 应彼此协调。若应用等待 30 秒,而平台 10 秒就 SIGKILL,代码里的 graceful handler 实际不会完成。
4. NestJS 与 Node HTTP 的实现边界
NestJS 需要显式调用 app.enableShutdownHooks() 才会为 SIGTERM/SIGINT 触发生命周期关停钩子。生命周期顺序为模块销毁、应用关闭前钩子,随后连接关闭,最后应用关闭钩子。beforeApplicationShutdown 适合有界清理;provider 的 onModuleDestroy 可停止 worker 接新任务。不同 HTTP adapter、WebSocket adapter 和 Nest 版本的关闭细节要按实际依赖验证。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(['SIGTERM', 'SIGINT']);
await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();
上面的片段强调启用钩子,不是完整信号协调器:生产应用还需健康状态切换、反向代理摘流量、HTTP 请求跟踪、队列停止/排空和总 deadline。不要同时注册多个互相竞争的 SIGTERM handler;也不要在 handler 中直接调用 process.exit(0),它会跳过异步清理。
Node server.close() 会停止接受新连接并等待活动请求结束;Node 19+ 还会在关闭时处理空闲 keep-alive 连接。closeAllConnections() 是强制关闭,适合作为期限到达后的最后手段,不是正常排空的第一步。升级前要按实际 Node 版本检查 API 行为;WebSocket 升级连接不会被普通 HTTP close 自动按业务语义排空。
5. 队列、定时任务与 WebSocket
- 队列 worker 先停止 polling/claim,避免关停期间拿到新任务;给已领取任务设置明确 lease 和 heartbeat。deadline 前完成就 ACK,来不及完成则释放/等待可见性超时重投,并保证处理幂等。
- 定时任务要使用分布式锁/唯一任务记录防止滚动期间新旧实例同时跑;shutdown 不能只清本地 timer,因为已启动的异步回调仍可能继续执行。
- WebSocket 要广播服务维护状态、拒绝新升级连接、给客户端重连退避提示,并跟踪活跃 socket。多实例需检查负载均衡粘性和共享 adapter;排空时不要向所有客户端同时触发重连风暴。
- 关停期间的日志应包含 signal、进程/版本 ID、drain 阶段、活动请求数、任务数和耗时,但不能包含 token、连接串、用户正文或完整 SQL 参数。
6. 可观测性与故障演练
至少记录池获取等待的直方图、连接超时和查询超时计数、数据库连接预算、HTTP 活跃请求、队列在途/积压数、每阶段 drain 时长、超期强退次数,以及进程重启原因。指标标签使用服务名/环境/版本等低基数字段,不用用户 ID、SQL 文本或请求 URL 任意值。
在预发布环境做可重复演练:发起一个慢但可控的请求,在请求中触发 SIGTERM,验证它能完成;同时确认新请求被摘流量、池在最后关闭、队列未误 ACK、WebSocket 可重连。再模拟依赖卡死,检查 deadline 到时会告警并按预期强退。用持续流量观察错误率和连接数,不能只验证“进程最后退出了”。
常见反例:handler 有了但平台先 SIGKILL;$disconnect() 被立即调用,活动请求仍需查库;关闭 HTTP 但没有停止定时任务;队列任务未完成却先 ACK;调大 max_connections 掩盖泄漏;每请求 new PrismaClient;升级 ORM 后沿用上一主版本连接默认值。
官方资料
- Node.js HTTP
server.close:停止接流量与空闲 keep-alive 的版本语义。 - NestJS Lifecycle Events:启用 shutdown hooks 与生命周期顺序。
- Prisma ORM Connection Pool:版本相关连接池、超时和外部 pooler。
- Prisma ORM Connection Management:长生命周期与 serverless 的客户端连接管理。
- PostgreSQL Connection Settings:
max_connections与连接资源配置。
版本快照:官方资料按 2026-10-14 核对。Prisma ORM 版本、driver adapter、Node HTTP 版本、Nest adapter 和进程管理器都会影响细节;上线前以 lockfile 和实际运行版本为准,做一次真实 SIGTERM 排空演练。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。