You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:22:08