Как отключить мой экспресс-сервер изящно, когда его процесс убит?

Когда я запускаю свое приложение Express на производстве, я хочу изящно закрыть сервер, когда его процесс будет убит (т.е. отправлен SIGTERM или SIGINT).

Вот упрощенная версия моего кода:

const express = require('express');

const app = express();

app.get('/', (req, res) => res.json({ ping: true }));

const server = app.listen(3000, () => console.log('Running…'));

setInterval(() => server.getConnections(
    (err, connections) => console.log('${connections} connections currently open')
), 1000);

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

function shutDown() {
    console.log('Received kill signal, shutting down gracefully');
    server.close(() => {
        console.log('Closed out remaining connections');
        process.exit(0);
    });

    setTimeout(() => {
        console.error('Could not close connections in time, forcefully shutting down');
        process.exit(1);
    }, 10000);
}

Когда я запустил его и вызываю URL-адрес http://localhost: 3000/ в браузере, оператор журнала в функции setInterval будет печатать "1 соединение в настоящее время открыто", пока я фактически не закрою окно браузера. По-видимому, даже закрытие вкладки будет оставаться открытым.

Итак, я убил свой сервер, нажав Ctrl + C, он запустится в таймаут и распечатает "Не удалось закрыть соединения" через 10 секунд, продолжая печатать "1 соединение открыто".

Только если я закрою окно браузера, прежде чем убить процесс, я получаю сообщение "закрыть оставшиеся соединения".

Что мне здесь не хватает? Каков правильный способ изящного закрытия Express-сервера?

Ответы

Ответ 1

В случае, если кто-то заинтересован, я нашел решение самостоятельно (хотел бы услышать отзывы в комментариях).

Я добавил слушателя для открытия соединений на сервере, сохраняя ссылки на эти соединения в массиве. Когда соединения закрыты, они удаляются из массива.

Когда сервер убит, каждое соединение закрывается, вызывая его методы end. Для некоторых браузеров (например, Chrome) этого недостаточно, поэтому после таймаута я вызываю destroy для каждого соединения.

const express = require('express');

const app = express();

app.get('/', (req, res) => res.json({ ping: true }));

const server = app.listen(3000, () => console.log('Running…'));

setInterval(() => server.getConnections(
    (err, connections) => console.log('${connections} connections currently open')
), 1000);

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

let connections = [];

server.on('connection', connection => {
    connections.push(connection);
    connection.on('close', () => connections = connections.filter(curr => curr !== connection));
});

function shutDown() {
    console.log('Received kill signal, shutting down gracefully');
    server.close(() => {
        console.log('Closed out remaining connections');
        process.exit(0);
    });

    setTimeout(() => {
        console.error('Could not close connections in time, forcefully shutting down');
        process.exit(1);
    }, 10000);

    connections.forEach(curr => curr.end());
    setTimeout(() => connections.forEach(curr => curr.destroy()), 5000);
}

Ответ 2

Проблема, с которой вы столкнулись, заключается в том, что все современные браузеры используют одно соединение для нескольких запросов. Это называется связью keep-alive.

Правильный способ справиться с этим - следить за всеми новыми подключениями и запросами и отслеживать состояние каждого соединения (сейчас он бездействует или активен). Затем вы можете принудительно закрыть все незанятые соединения и не забудьте закрыть активные соединения после обработки текущего запроса.

Я реализовал модуль @moebius/http-graceful-shutdown, специально разработанный для изящного отключения приложений Express и серверов узлов в целом. К сожалению, ни Express, ни сам узел не имеют встроенной функции.

Здесь, как это можно использовать с любым приложением Express:

const express = require('express');
const GracefulShutdownManager = require('@moebius/http-graceful-shutdown').GracefulShutdownManager;


const app = express();

const server = app.listen(8080);

const shutdownManager = new GracefulShutdownManager(server);

process.on('SIGTERM', () => {
  shutdownManager.terminate(() => {
    console.log('Server is gracefully terminated');
  });
});

Не забудьте проверить модуль, на странице GitHub есть более подробная информация.

Ответ 3

Существует проект с открытым исходным кодом https://github.com/godaddy/terminus, рекомендованный создателями Express (https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html).

Основной пример использования терминалов:

const http = require('http');
const express = require('express');
const terminus = require('@godaddy/terminus');

const app = express();

app.get('/', (req, res) => {
  res.send('ok');
});

const server = http.createServer(app);

function onSignal() {
  console.log('server is starting cleanup');
  // start cleanup of resource, like databases or file descriptors
}

async function onHealthCheck() {
  // checks if the system is healthy, like the db connection is live
  // resolves, if health, rejects if not
}

terminus(server, {
  signal: 'SIGINT',
   healthChecks: {
    '/healthcheck': onHealthCheck,
  },
  onSignal
});

server.listen(3000);

terminus имеет множество опций, если вам нужны обратные вызовы жизненного цикла сервера (т.е. отменить регистрацию из реестра служб и т.д.):

const options = {
  // healtcheck options
  healthChecks: {
    '/healthcheck': healthCheck    // a promise returning function indicating service health
  },

  // cleanup options
  timeout: 1000,                   // [optional = 1000] number of milliseconds before forcefull exiting
  signal,                          // [optional = 'SIGTERM'] what signal to listen for relative to shutdown
  signals,                          // [optional = []] array of signals to listen for relative to shutdown
  beforeShutdown,                  // [optional] called before the HTTP server starts its shutdown
  onSignal,                        // [optional] cleanup function, returning a promise (used to be onSigterm)
  onShutdown,                      // [optional] called right before exiting

  // both
  logger                           // [optional] logger function to be called with errors
};

Ответ 4

Попробуйте модуль экспресс-изящного выключения NPM, изящное завершение позволит любым соединениям, включая вашу БД, закончить, а не разрешать создание новых/новых. Поскольку вы работаете с выражением, которое может быть модулем, который вы ищете, однако быстрый поиск NPM покажет весь список модулей, подходящих для серверов Http и т.д.

Ответ 5

Правильно обрабатывать сигналы ОС: https://www.npmjs.com/package/daemonix

Изящное завершение работы Express: https://www.npmjs.com/package/@stringstack/express https://www.npmjs.com/package/@stringstack/core

Эта комбинация инструментов остановит новые соединения при завершении работы, позволит завершить существующие соединения и, наконец, завершится.