# Node.js 优雅关闭

> Node.js 优雅关闭：在 node:http、Express 和 Fastify 中用 server.close() 处理 SIGTERM，避开常见错误，并用 shutdown-check 验证。

Source: https://shutdown.jscrate.dev/zh/docs/guides/graceful-shutdown-nodejs
Last updated: 2026-09-23

Node.js 的优雅关闭（graceful shutdown）从进程收到 `SIGTERM` 开始。服务不再接收新工作，让进行中的请求处理完，关闭共享资源，然后在平台的截止时间之前以退出码 `0` 退出。

## 没有 SIGTERM 处理函数会怎样？

在 macOS 和 Linux 上，Node.js 会按信号的默认行为直接终止。未完成的请求可能因此断开连接，代理可能把这种断开转成 `502` 或其他上游错误。

shutdown-check 会把被中断的请求报告为 [SC201](https://shutdown.jscrate.dev/zh/docs/codes/sc201)：

```text
FAIL SC201: In-flight request was interrupted: aborted
```

一旦注册了信号监听器，进程就不会再按默认行为立即退出。从这时起，就要由你的代码完成关闭流程，并让进程能够退出。

## 如何在 Node.js 中处理 SIGTERM？

可靠的信号处理函数按以下顺序执行：

1. 把实例标记为未就绪。
2. 拒绝仍然到达的新工作。
3. 停止接受新连接。
4. 等待进行中的 HTTP 请求完成。
5. 关闭数据库、队列、worker 和定时器。
6. 只有清理失败时才设置表示失败的退出码。
7. 等所有句柄（handle）都释放后，让 Node.js 自行退出。

第 3–5 步的具体顺序取决于你的应用。只要还有进行中的请求处理函数要用数据库连接池，就不要关闭它。

## node:http 示例

```js title="server.js"
const http = require("node:http");

const port = Number(process.env.PORT ?? 3000);
let draining = false;

const server = http.createServer((request, response) => {
  if (request.url === "/health") {
    response.writeHead(draining ? 503 : 200, {
      "content-type": "text/plain",
    });
    response.end(draining ? "draining" : "ok");
    return;
  }

  if (draining) {
    response.writeHead(503, {
      connection: "close",
      "retry-after": "1",
    });
    response.end("shutting down");
    return;
  }

  if (request.url === "/slow") {
    response.writeHead(200, { "content-type": "text/plain" });
    response.flushHeaders();
    setTimeout(() => response.end("work complete\n"), 2000);
    return;
  }

  response.writeHead(404).end();
});

server.listen(port, "127.0.0.1");

process.on("SIGTERM", () => {
  if (draining) return;
  draining = true;

  server.close((error) => {
    if (error) {
      console.error("HTTP shutdown failed", error);
      process.exitCode = 1;
    }
  });
});
```

有了 `draining` 这个判断，重复收到 `SIGTERM` 也不会出问题。标志改变之前进入的请求，会在原来的处理函数里继续执行。在监听器关闭之前，新请求都会收到 `503`。

`server.close()` 会停止接受新连接，并等待进行中的请求完成。服务器关闭后才会执行它的回调，但这时其他资源仍可能让事件循环保持运行。

## Express 优雅关闭

Express 底层用的是 Node.js 的 HTTP 服务器。保存 `app.listen()` 返回的对象，然后在信号处理函数里关闭它。

```js title="server.js"
const express = require("express");

const app = express();
const port = Number(process.env.PORT ?? 3000);
let draining = false;

app.get("/health", (_request, response) => {
  response.status(draining ? 503 : 200).send(draining ? "draining" : "ok");
});

app.use((_request, response, next) => {
  if (!draining) return next();
  response.set("connection", "close");
  response.set("retry-after", "1");
  response.status(503).send("shutting down");
});

app.get("/slow", async (_request, response) => {
  response.status(200);
  response.flushHeaders();
  await new Promise((resolve) => setTimeout(resolve, 2000));
  response.end("work complete\n");
});

const server = app.listen(port, "127.0.0.1");

process.on("SIGTERM", () => {
  if (draining) return;
  draining = true;

  server.close((error) => {
    if (error) {
      console.error(error);
      process.exitCode = 1;
    }
  });
});
```

排空用的中间件要放在需要拒绝新工作的业务路由之前，但要放在就绪检查（readiness）路由之后，这样就绪检查路由才能返回明确的就绪状态。

## Fastify 优雅关闭

Fastify 提供了 `fastify.close()`，它会停止接受请求，并执行已注册的 `onClose` 钩子。

```js title="server.js"
const Fastify = require("fastify");

const fastify = Fastify();
const port = Number(process.env.PORT ?? 3000);
let draining = false;

fastify.get("/health", async (_request, reply) => {
  reply.code(draining ? 503 : 200);
  return draining ? "draining" : "ok";
});

fastify.get("/slow", async (_request, reply) => {
  reply.raw.writeHead(200, { "content-type": "text/plain" });
  reply.raw.flushHeaders();
  await new Promise((resolve) => setTimeout(resolve, 2000));
  reply.raw.end("work complete\n");
});

await fastify.listen({ port, host: "127.0.0.1" });

process.on("SIGTERM", async () => {
  if (draining) return;
  draining = true;

  try {
    await fastify.close();
  } catch (error) {
    fastify.log.error(error);
    process.exitCode = 1;
  }
});
```

数据库和队列的清理，可以注册到 Fastify 钩子里，也可以等 HTTP 工作排空之后再 await 执行。

## 关闭应用资源

HTTP 服务器关闭了，Node.js 进程仍可能继续运行。等进行中的请求不再需要以下资源后，关闭它们：

- 数据库和缓存连接池
- 消息的消费者和生产者
- worker 和子进程
- 定时执行的 interval
- 文件监听器
- 遥测数据导出器
- 长期存活的客户端

依赖其他系统的清理操作要设置超时。如果没有超时，服务可能一直运行到平台发送 `SIGKILL`，shutdown-check 会把这种情况报告为 [SC300](https://shutdown.jscrate.dev/zh/docs/codes/sc300)。

## 处理 keep-alive 连接

`server.close()` 会停止接受新连接，并等待进行中的请求完成。较新的 Node.js 版本还会关闭空闲的 keep-alive 连接。如果你的技术栈自己跟踪 socket，不要在收到 `SIGTERM` 时销毁所有 socket：活跃的 socket 上承载的，正是你想保住的请求。

如果你要主动关闭空闲连接，应在监听器开始关闭之后再做，并且不要动活跃的连接。

## 谨慎添加强制退出超时

生产环境的服务通常需要一个最终超时，防止某个出错的清理步骤一直运行下去。这个超时必须长于正常的排空时间，同时短于平台强制终止进程的截止时间。

```js
const forceExit = setTimeout(() => {
  console.error("shutdown deadline exceeded");
  process.exit(1);
}, 25_000);

forceExit.unref();
```

调用过 unref 的定时器，不会让一个本已完成的进程继续存活。但强制退出仍可能打断正在进行的工作，所以只能把它当作最后的手段，并监控它何时触发。

## 常见错误

| 错误                                   | 用户看到的现象                   | shutdown-check 结果 |
| -------------------------------------- | -------------------------------- | ------------------- |
| 没有信号处理函数                       | 部署期间请求被重置               | `SC201` 或 `SC301`  |
| 在处理函数里调用 `process.exit()`      | 进行中的工作被打断               | `SC201`             |
| 请求还没完成，数据库就关闭了           | 处理函数报错或返回错误的响应     | `SC202` 或 `SC203`  |
| 服务器已关闭，但还有其他句柄没有释放   | 进程一直运行到平台的截止时间     | `SC300`             |
| 包装进程不转发信号                     | 父进程退出，子进程里的服务仍存活 | `SC302`             |
| 就绪检查一直返回 `200`                 | 新流量继续进来                   | `SC310`             |
| 仍然接受新工作                         | 晚到的请求可能被中断             | `SC311`             |
| 收到第二个信号时强制退出               | 已有的请求被中断                 | `SC114` 或 `SC201`  |

## 验证信号处理函数

使用一份检查完整关闭行为的配置：

```json title="shutdown-check.json"
{
  "command": ["node", "server.js"],
  "baseUrl": "http://127.0.0.1:3000",
  "readiness": { "path": "/health" },
  "workload": {
    "path": "/slow",
    "bodyIncludes": "work complete",
    "started": { "type": "response-headers" }
  },
  "shutdown": {
    "deadlineMs": 10000,
    "readinessWithdrawal": true,
    "newRequests": { "path": "/slow", "rejectStatuses": [503] },
    "repeatSignalAfterMs": 500
  }
}
```

运行：

```bash
npx shutdown-check test
```

通过的时间线应当依次显示：发送信号前已有进行中的工作；发送信号后撤回就绪状态、拒绝新请求；请求完成；进程干净退出。

## 相关内容

- [快速开始](https://shutdown.jscrate.dev/zh/docs/quick-start)
- [就绪检查与排空](https://shutdown.jscrate.dev/zh/docs/guides/readiness-and-draining)
- [进行中的工作与启动确认](https://shutdown.jscrate.dev/zh/docs/in-flight-work)
- [Kubernetes 与容器](https://shutdown.jscrate.dev/zh/docs/guides/kubernetes)
- [故障排查](https://shutdown.jscrate.dev/zh/docs/troubleshooting)
