Node.js中通过HTTP请求注销Consul时优雅关机失败
Node.js优雅关机时Consul注销请求未完成进程就退出的问题
在Node.js应用中实现优雅关机功能,期望收到SIGINT/SIGTERM终止信号时,依次完成从Consul注销、关闭Redis、MySQL等资源的操作。但执行Consul注销的PUT请求PUT /v1/agent/service/deregister/<serviceId>时,进程会在请求完成前直接退出。
应用核心代码(index.ts)
import http from "http"; import config from "@/config"; import consul from "@/consul"; import logger from "@/logger"; import { closeMongoDB } from "@/db/setup/mongo"; import { closeMySQL } from "@/db/setup/mysql"; import { clearRedisConn } from "@/db/setup/redis"; import _ from "lodash"; logger.info("Current environment: %s", config.NODE_ENV); const shutdownTasks = [ consul.deregisterServiceViaHTTP, clearRedisConn, closeMySQL, closeMongoDB, closeServer, ]; let isShuttingDown = false; async function gracefullyShutdown() { if (isShuttingDown) return; isShuttingDown = true; const shutdownTimeout = setTimeout(() => { console.warn("Graceful shutdown timed out. Forcing exit."); process.exit(1); }, 10000); logger.info("⌚ Stopping..."); let errors = 0; for (const task of shutdownTasks) { try { await task(); } catch (e: any) { errors += 1; logger.warn("Failed to execute task. Error: %s", e); } finally { logger.debug("Finished: %s", task.name || "Unnamed Task"); } } logger.info("😈 All clear. Errors: %s", errors); clearTimeout(shutdownTimeout); process.exit(errors > 0 ? 1 : 0); } ["SIGTERM", "SIGINT"].forEach((signal) => { process.on(signal, async () => { if (isShuttingDown) return; logger.info("Received signal: %s", signal); try { await gracefullyShutdown(); } catch (err) { console.error("Graceful shutdown failed:", err); process.exit(1); } }); }); process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); }); process.on("uncaughtException", (err) => { console.error("Uncaught Exception:", err); }); process.on("exit", (code) => { console.log(`Process exit event with code: ${code}`); }); process.on("beforeExit", () => { console.log(">>> beforeExit hook triggered"); }); let server: http.Server; async function closeServer() { if (_.isUndefined(server)) return; await new Promise<void>((resolve, reject) => { server.closeIdleConnections(); server.close((err) => { if (err) return reject(err); logger.info("[express] Stopped server"); resolve(); }); }); } (async () => { const { default: app } = await import("@/app"); server = http.createServer(app); const port = Number.parseInt(config.PORT); const addr = "0.0.0.0"; logger.info("[express] Starting HTTP server..."); server.listen(port, addr, () => { logger.info("[express] Server is up on %s:%s 👌", "0.0.0.0", port); consul.registerServiceViaHTTP(); }); server.keepAliveTimeout = 60000; server.headersTimeout = 65000; })();
Consul注销相关代码
async function deregisterServiceViaHTTP() { const url = `${consulBaseURL}/v1/agent/service/deregister/${encodeURIComponent(serviceId)}`; try { logger.info("[consul] deregistering service: %s", serviceId); console.log("---Every time it stops here---"); const r = await fetch(url, { method: "PUT", signal: AbortSignal.timeout(3000), headers: { Connection: "close" }, }); console.log("---This line won't appear---"); } catch (err: any) { console.log("---This line won't appear---"); throw err; } console.log("---This line won't appear---"); }
按下CTRL-C后的输出
^C2025-05-24 13:41:46.036 INFO: Received signal: SIGINT 2025-05-24 13:41:46.036 INFO: ⌚ Stopping... 2025-05-24 13:41:46.036 INFO: [consul] deregistering service: (...) ---Every time it stops here--- Process exit event with code: 130
已尝试的无效方法
- 用curl验证Consul API可用
- 移除
process.exit(0)测试是否提前终止应用 - 将
process.on替换为process.once,无变化 - 尝试使用axios和fetch,设置超时并禁用keepAlive
注:若移除Consul注销任务,其余关机任务可正常执行:
^C2025-05-24 13:58:53.832 INFO: Received signal: SIGINT 2025-05-24 13:58:53.832 INFO: ⌚ Stopping... 2025-05-24 13:58:53.832 INFO: [redis] Disconnected from Redis 2025-05-24 13:58:53.832 DEBUG: Finished: clearRedisConn 2025-05-24 13:58:53.832 INFO: [mysql] no need to close MySQL pool. 2025-05-24 13:58:53.832 DEBUG: Finished: closeMySQL 2025-05-24 13:58:53.832 INFO: [mongo] no need to close 2025-05-24 13:58:53.832 DEBUG: Finished: closeMongoDB 2025-05-24 13:58:53.833 INFO: [express] Stopped server
更新 - 2025/5/25 12:34 +08:00
按照建议更新代码后问题仍未解决:
修改后的Consul代码
async function deregisterServiceViaHTTP() { const logtag = "[consul-http]"; const url = `${consulBaseURL}/v1/agent/service/deregister/${encodeURIComponent(serviceId)}`; logger.info("%s deregistering service...", logtag); console.log("---Every time it stops here---"); // 也尝试过`return await fetch(...)`,无效 const r = await fetch(url, { method: "PUT", signal: AbortSignal.timeout(3000), headers: { Connection: "close" }, }); logger.info("%s consul API result: %s %s", logtag, r.status, r.statusText) return r; }
修改后的index.ts信号处理代码
let isShuttingDown = false; ["SIGTERM", "SIGINT"].forEach((signal) => process.on(signal, () => { logger.info("Received: %s", signal); if (isShuttingDown) return; isShuttingDown = true; logger.info("⌚ Stopping..."); let errors = 0; Promise.allSettled(shutdownTasks.map((task) => task())) .then((results) => { console.log("------this line never appear----"); for (const [idx, result] of results.entries()) { console.log("------this line never appear----"); logger.info("task #%s result: %s", idx, result); if (result.status === "rejected") { errors += 1; logger.warn("Failed to execute task. Error: %s", result.reason); } } logger.info("😈 All clear. Errors: %s", errors); process.exit(errors > 0 ? 1 : 0); }) .catch((err: any) => { logger.error("Caught error during settling the tasks: %s", err); }); console.log("----end of the signal callback----") }), );
当前更新后的输出
^C2025-05-25 13:27:18.728 INFO: Received: SIGINT 2025-05-25 13:27:18.728 INFO: ⌚ Stopping... 2025-05-25 13:27:18.728 INFO: [consul-http] deregistering service... ---Every time it stops here--- 2025-05-25 13:27:18.731 INFO: [redis] Disconnected from Redis 2025-05-25 13:27:18.731 INFO: [mysql] no need to close MySQL pool. 2025-05-25 13:27:18.731 INFO: [mongo] no need to close ----end of the signal callback---- 2025-05-25 13:27:18.731 INFO: [express] Stopped server Process exit event with code: 130
内容的提问来源于stack exchange,提问作者fakeKmz
相关产品推荐
相关产品推荐

