如何优雅地关闭Node.Js Express应用程序

huangapple go评论74阅读模式
英文:

How to gracefully shutdown or close Node.Js Express app

问题

动机

我希望确保在 Express 服务器关闭时不会发生任何暴力(子)进程终止,并且会完成任何打开的请求。

import express, { Express, Request, Response } from 'express';

// 初始化 Express 服务器应用程序
var app = express();
var server = this.app.listen(process.env.PORT || 5000, () => { ... } );

// 用于处理购物车的 POST 端点
app.post('/purchase', async (req, res) => {
  // 从请求体中获取物品

  try {
      const { buyer, items } = req.body;
      
      // 处理物品(例如,计算总价,发起支付等)
      const totalPrice = calculateTotalPrice(items);
      // 向买家收费
      const payment = chargeBuyerForPurchase(totalPrice);
      
      // 🚧 我不希望服务器在执行每个请求的这一行之前关闭

      // 为购买的物品创建运输
      const shipping = carrierAPI.createShipping(items, buyer.address);

      res.status(200).json([totalPrice, payment, shipping]);

      // 🧘‍♂️ 现在可以安全地关闭服务器了
    } catch (error) {
      next(error);
    }

});

提出的解决方案

请参阅此链接中的文档

// ./src/app.ts
import process from 'node:process';

// 开始从标准输入读取,以防止进程退出。
process.stdin.resume();

process.on('SIGINT', () => {
  console.log('Received SIGINT. Press Control-D to exit.');
});

// FYI docker "stop" <container>, a process manager, and most hosts will send SIGTERM signal when it is shutting down.
// server.close 停止服务器接受新连接,并关闭连接到该服务器的所有未发送请求或等待响应的连接

function gracefulClose(signal) {
  console.log(`Received ${signal}`);
  server.close(() => { log('HTTP(S) server closed') });
}

process.on('SIGINT', gracefulClose);
process.on('SIGTERM', gracefulClose);

建议解决方案的上下文

这是否是一个要实现的功能?这个功能/代码是否多余?

英文:

Motivation

<!-- A clear and concise description of what the motivation for the new feature is, and what problem it is solving. -->
I want to ensure there is no brute (sub-)process termination and that any open requests are fulfilled when the Express server is closing.

import express, { Express, Request, Response } from &#39;express&#39;;

// Initialize the Express server application
var app = express();
var server = this.app.listen(process.env.PORT || 5000, () =&gt; { ... } );

// POST endpoint to process the shopping cart
app.post(&#39;/purchase&#39;, async (req, res) =&gt; {
  // Get the items from the request body

  try {
      const { buyer, items } = req.body;
      
      // Process the items (e.g., calculate total price, initiate payment, etc.)
      const totalPrice = calculateTotalPrice(items);
      // Charge the buyer for the purchase
      const payment = chargeBuyerForPurchase(totalPrice);
      
      // &#129399; I don&#39;t want the server to close before it executes this line for every request

      // Create shipping for the purchased items
      const shipping = carrierAPI.createShipping(items, buyer.address);

      res.status(200).json([totalPrice, payment, shipping]);

      // &#128129;‍♂️ This is now safe to close the server
    } catch (error) {
      next(error);
    }

});

Proposed Solution

See documentation at this link

// ./src/app.ts
import process from &#39;node:process&#39;;

// Begin reading from stdin so the process does not exit.
process.stdin.resume();

process.on(&#39;SIGINT&#39;, () =&gt; {
  console.log(&#39;Received SIGINT. Press Control-D to exit.&#39;);
});

// FYI docker &quot;stop&quot; &lt;container&gt;, a process manager, and most hosts will send SIGTERM signal when it is shutting down.
// server.close stops the server from accepting new connections and closes all connections connected to this server that are not sending a request or waiting for a response

function gracefulClose(signal) {
  console.log(`Received ${signal}`);
  server.close( () =&gt; { log(&#39;HTTP(S) server closed&#39;) } );

}

process.on(&#39;SIGINT&#39;, gracefulClose);
process.on(&#39;SIGTERM&#39;, gracefulClose);

<!-- Add any other context or screenshots about the feature request here. -->
Is it a feature to implement? Is this feature / code redundant?

答案1

得分: 1

处理 SIGTERM 以允许 Docker 优雅退出

关于 Docker,是的,您必须处理 SIGTERM 事件并进行优雅退出。如果不这样做,那么在发出退出容器的信号后,Docker 将等待一段时间(我相信是 10 秒),以等待您的容器退出,如果在规定时间内未退出,Docker 将强制终止您的容器。

一般来说,以下代码足够了:

process.on('SIGTERM', process.exit);
process.on('SIGINT', process.exit);

在同步代码中无法处理 SIGTERM 事件

在您的示例代码中,您的 post 处理程序是同步的。JavaScript 是单线程的,在同步函数完成之前无法运行您的 gracefulClose 处理程序(响应 SIGTERM 事件)。所以在这种情况下,您的代码是不必要的。如果 post 处理程序函数是异步的,那么您将需要处理不同的情况。

在异步操作期间推迟优雅退出

如果您将您的 post 处理程序修改为异步,以便您有理由不退出,并且可能在解决不退出的原因之前运行 SIGTERM 处理程序,那么是的,您必须实现一些特殊的处理。

这样的实现可能如下:

每当您创建一个构成阻止强制关闭的原因的情况时,将表示该原因的对象推入数组中。当情况不再存在时,从数组中删除原因。

当收到 SIGTERM 信号时,设置一个标志,表示您希望退出。

然后编写一个函数,检查是否可以退出。条件是您已经设置了退出标志,并且不退出的原因数组为空。如果可以退出,那么可以调用 process.exit

在设置退出标志后调用此函数,并在从数组中移除原因后调用它。

const reasonsNotToExit = [];
let isExitRequested = false;

function requestGracefulExit() {
  isExitRequested = true;
  exitIfRequired();
}

function exitIfRequired() {
  if (isExitRequested && reasonsNotToExit.length === 0) {
    process.exit();
  }
}

process.on('SIGINT', requestGracefulExit);
process.on('SIGTERM', requestGracefulExit);

示例推迟:(例如,在您的 post 处理程序的主体中。)

   // 添加不退出的原因
   let reason = {};
   reasonsNotToExit.push(reason);

   // 想象一些应该推迟优雅退出的异步操作:
   // 请注意使用 `await`
   const shipping = await carrierAPI.createShipping(items, buyer.address);
   // ... 现在想象一下,不退出的原因已解决。

   // 从数组中删除原因。
   reasonsNotToExit.splice(reasonsNotToExit.indexOf(reason), 1);
   // 当我们移除原因时,我们有责任调用这个函数:
   exitIfRequired();

注意,此示例明确使用 await 等待某些假设的异步操作,这在您的原始示例中缺少,而在这种情况下,此代码是不必要的。

注意,即使您已编写了处理优雅退出的代码,在礼貌地要求容器退出后,Docker 本质上启动了一个世界末日计时器,它可能在 10 秒后无论您是否完成清理都会强制终止您的程序。此外,即使存在这段代码,docker kill 也将不会优雅地终止您的程序。

英文:

Handle SIGTERM to permit Docker to cause a graceful exit

With respect to Docker, yes you have to handle the SIGTERM event and gracefully exit. If you do not, then after having signalled your container to exit, Docker will wait for some period of time (10 sec, I believe) for your container to exit, and if it does not do so in time, Docker will forcibly kill your container.

In general, this would be sufficient:

process.on(&#39;SIGTERM&#39;,&#160;process.exit);
process.on(&#39;SIGINT&#39;,&#160;process.exit);

You can't handle SIGTERM events during synchronous code anyway

In your example code, your post handler is synchronous. JavaScript is single threaded and cannot run your gracefulClose handler (in response to the SIGTERM event) until your synchronous function has completed. So your code is unnecessary in this case. If the post handler function were asynchronous, then you'd have to a different situation.

Deferring a graceful exit during an asynchronous operation

If you were to modify your post handler to be asynchronous, such that you could have a reason not to exit and possibly have the SIGTERM handler run before your reason not to exit is resolved, then yes, you'd have to implement something special to handle that.

Such an implementation might go like this:

Any time you create a situation that constitutes a reason to prevent forcibly closing, push an object representing that reason into an array. When the situation no longer exists, remove the reason from the array.

When you get a SIGTERM signal, set a flag saying that you want to exit.

Then write a function that checks to see if it's ok to exit. The condition will be that you have set the exit flag, and the reasons-not-to-exit array is empty. If it's ok to exit, then you may call process.exit.

Call this function after setting the exit flag, and also call it after removing a reason from the array.

const reasonsNotToExit = [];
let isExitRequested = false;

function requestGracefulExit() {
  isExitRequested = true;
  exitIfRequired();
}

function exitIfRequired() {
  if (isExitRequested &amp;&amp; resonsNotToExit.length == 0) {
    process.exit();
  }
}

process.on(&#39;SIGINT&#39;, requestGracefulExit);
process.on(&#39;SIGTERM&#39;, requestGracefulExit);

Example deferral: (for example, in the body of your post handler.)

   // Add a reason not to exit
   let reason = {};
   reasonsNotToExit.push(reason);

   // Imagine some async thing that should cause graceful exits to be deferred:
   // note use of `await`
   const shipping = await carrierAPI.createShipping(items, buyer.address);
   // ... and now imagine that the reason not to exit has been resolved.

   // Remove the reason from the array.
   reasonsNotToExit.splice(reasonsNotToExit.indexOf(reason), 1);
   // We are obligated to call this when we remove a reason:
   exitIfRequired();

Note that this example has explicitly awaited some hypothetical async operation using await, which is missing from your original example, in which this code was not necessary.

Note that even though you've coded up something to handle a graceful exit, after politely asking your container to exit, Docker essentially starts a doomsday timer, and it may forcibly terminate your program after 10 seconds anyway, whether you've finished cleaning up or not. Also docker kill will ungracefully terminate your program, even if this code is in place.

答案2

得分: 0

这是我的发现:

  1. 当发出 SIGTERM 信号时,Node.JS 服务器将立即关闭。Node.Js 应用程序/进程仍然存活。此外,即使在接收到 SIGTERM 信号之前打开连接,也不会向任何客户端发送响应。

  2. 当发出 SIGINT 信号时,Node.JS(Express)服务器和应用程序或进程将立即关闭。

  3. 当发出自定义信号 SIGUSR1SIGUSR2 时,Node.JS 服务器将立即关闭任何打开的连接。Node.Js 应用程序/进程仍然存活。Node.Js(Express)服务器也将继续存活,并且仅处理新的请求和响应。

PS: 信号是用于向程序发送的软件中断,用于指示发生了重要事件。传递信号的另一种常见方法是使用 kill 命令,其语法如下 -

$ kill -signal <pid>

我的解决方案:

我创建了一个 npm 包 super-gracefully-shutdown 来实现对 Node.Js Express 服务器的超级优雅关闭。它确保在发送超级优雅的 shutdown 消息之前,对于在此之前打开的任何连接都会发送响应。

用法 / 实现

import express from 'express';
import SGS from 'super-graceful-shutdown';

const app = express();
const router = express.Router();
const port = 80; // 设置你想要的端口号

// ...

router.get('/api', async function(req, res, next){
  try {
      // 在 3 秒内完成此请求
      setTimeout(() => { res.status(200).send(`<html> <body> <h1>${date()}</h1> </body> </html>`)  }, 3000);
    } catch (error) {
      next(error);
    }
});

const server = app.listen(port, () => console.log(`示例 Express 应用正在侦听端口 ${port}!`) );

// ℹ️ 在初始化其他路由之前,需要初始化 super-graceful-shutdown
new SGS(app, server);

// 👨‍💼‍‍ 然后,您可以初始化其他路由
app.use('/', router);

从主机发送关闭 Node.Js Express 应用程序的请求

要超级优雅地关闭您的 Node.Js Express 应用程序,请在端口 3000 上发送 TCP shutdown 消息:

echo "shutdown" | nc localhost 3000
英文:

<h3>THESE ARE MY FINDINGS:</h3>

  1. When the SIGTERM signal is emitted, the Node.JS server will be immediately closed. The Node.Js application / process will still be alive. Moreover, a response will not be sent to any client even if its connection was open before the SIGTERM signal was received.

  2. When the SIGINT signal is emitted, the Node.JS (Express) server and application or process will be immediately closed.

  3. When a custom signal is emitted, SIGUSR1 or SIGUSR2, the Node.JS server will immediately close any open connections. The Node.Js application / process will still be alive. The Node.Js (Express) server will still be alive too and fulfill only new requests and responses.

PS: Signals are software interrupts sent to a program to indicate that an important event has occurred. Another common method for delivering signals is to use the kill command, the syntax of which is as follows −

$ kill -signal &lt;pid&gt;

<h3> MY SOLUTION: </h3>
I have created an npm package to super-gracefully-shutdown your Node.Js Express server. It ensures a response is sent to every client for any connections that were open before you send a super-graceful shutdown message. <br /><br /><br />

<h2>Usage / Implementation</h2>

import express from &#39;express&#39;;
import SGS from &#39;super-graceful-shutdown&#39;;

const app = express();
const router = express.Router();

const express = require(&#39;express&#39;);
const SGS = require(&#39;super-graceful-shutdown&#39;);

const app = express();
const router = express.Router();
const port = 80; // Set your desired port number

// ...

router.get(&#39;/api&#39;, async function(req, res, next){
  try {
      // Fulfill this request in 3 seconds
      setTimeout(() =&gt; { res.status(200).send(`&lt;html&gt; &lt;body&gt; &lt;h1&gt;${date()}&lt;/h1&gt; &lt;/body&gt; &lt;/html&gt;`)  }, 3000);
    } catch (error) {
      next(error);
    }
});

const server = app.listen(port, () =&gt; console.log(`Example Express app listening on port ${port}!`) );

// ℹ️ Before you initialize other routes, you need to initialize super-graceful-shutdown
new SGS(app, server);

// &#128104;‍&#128187; Then, you can initialize other routes
app.use(&#39;/&#39;, router);

<h2> Request From Host To Shutdown Node.Js Express App</h2>

To super-gracefully-shutdown your Node.Js Express application, send the TCP shutdown message on port 3000: <br />
<br />

echo &quot;shutdown&quot; | nc localhost 3000

huangapple
  • 本文由 发表于 2023年6月19日 22:25:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76507562.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定