Обработка 1000 асинхронных POST-запросов в секунду в Express.js
Я столкнулся с проблемой в моем боте Telegram на Node.js. Мой сервер отправляет приблизительно 1000 запросов в секунду в телеграмм сервер. Учитывая репутацию Node.js в быстрой обработке запросов, я пользуюсь асинхронной функцией для отправки запросов. И еще настроил cronjob для периодической проверки результата getConnectionCount каждые 2 секунды. В целом, эта функция возвращает N1->0, что указывает на то, что в данный момент 0 не законченных запросов. Однако в периоды особенно высокой нагрузки я заметил, что getConnectionCount возвращает необычно высокие результаты, например:
14.03 05:07:41 N95->1431
14.03 05:07:43 N81->1440
14.03 05:07:45 N99->1976
14.03 05:07:47 N96->1437
14.03 05:07:49 N100->1979
14.03 05:07:51 N84->1455
14.03 05:07:53 N101->1977
14.03 05:07:55 N101->1992
Сначала подозревал, что эти пики связаны с проблемами задержки на сервере Telegram. Однако после дальнейшего исследования я обнаружил, что отправка curl-запросов (например, sendMessage) на сервер Telegram непосредственно с того же сервера дает постоянно быстрые ответы. То есть получается в node почему то запросы висят просто так и не выполняются.
Интересно, что быстрое решение этой проблемы заключается в простом перезапуске приложения Node.js с использованием pm2 restart app. После перезапуска бот восстанавливает нормальную работу, и getConnectionCount возвращает опять N1->0 целый день. И где-то через день опять это повторяется.
Я немного озадачен тем, почему эти запросы застревают после некоторого времени, несмотря на то, что сервер способен обрабатывать ту же самую нагрузку запросов. Любые идеи или предложения о том, что может вызывать эту проблему были бы очень полезны.
Код моего приложения Node.js:
var express = require('express');
const axios = require("axios");
const Noderavel = require("@movilizame/noderavel");
const nodelaravel = require("node-laravel-queue");
const {uuid} = require("node-laravel-queue/lib/tools");
const redis = require("redis");
const {serialize} = require("php-serialize");
var app = express();
app.use(express.json({limit: '100mb'}));
app.use(express.urlencoded({limit: '100mb', extended: true, parameterLimit: 50000}));
const nocache = require('nocache');
const processId = process.pid;
app.use(nocache());
app.use(function (err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});
let connectionCount = 0;
let pendingCount = 0;
// Middleware to track connections
app.use((req, res, next) => {
connectionCount++;
res.on('finish', () => {
connectionCount--;
});
next();
});
app.get("/getConnectionCount", function (req, res) {
res.send(connectionCount.toString() + "->" + pendingCount.toString());
});
async function sendPostRequest(url, postData) {
try {
pendingCount++;
// Make the POST request using axios
const response = await axios.post(url, postData);
pendingCount--;
return response.data;
// console.log(response.data);
} catch (error) {
pendingCount--;
return error.response ? error.response.data : error.message;
}
}
app.post('/sendRequest/:bot_token', async function (req, res) {
// for (let index = 0; index < 1e7; index++);
let bot_token = req.params['bot_token'];
const postData = req.body;
let method = postData.method;
let data = postData.data;
let get_result = parseInt(postData.get_result);
let url = "https://api.telegram.org/bot" + bot_token + "/" + method;
if (get_result == 1) {
res.send(await sendPostRequest(url, data));
return;
}
sendPostRequest(url, data);
res.send("1");
});
var server = app.listen(3000, function () {
console.log(`Server Started in process ${processId}`);
console.log('Example app listening on port 3000!');
});
server.keepAliveTimeout = 65000; // Ensure all inactive connections are terminated by the ALB, by setting this a few seconds higher than the ALB idle timeout
server.headersTimeout = 66000; // Ensure the headersTimeout is set higher than the keepAliveTimeout due to this nodejs regression bug: https://github.com/nodejs/node/issues/27363
server.setTimeout(10000);
Ответы (1 шт):
Я бы вам предложил, на время отладки(а может понравится и оставить на постоянку), дополнить код сбором более детальной информации о происходящем у вас внутри.
Вот мое предложение по дополнению и оптимизации(не изменяемые функции и методы не включены)
// Текущие запросы
const requests = {
pendingCount: 0
}
// ошибки вызова
const errorRequests = {
requestErrors: 0
}
// Количество запросов(уникальный ключ)
let requestNum = 0;
app.get("/getConnectionCount", function (req, res) {
res.send(JSON.stringify({
connected: connectionCount, // Количество соединений
pending: requests.pendingCount, // Количество текущих запросов
requestCNT: requestNum, // Сколько запросов выполнено
activeRequests: requests, // Список запросов с параметрами
errorsList: errorRequests, // список ошибок вызовов
}));
});
async function sendPostRequest(url, postData, reqType, reqNum) {
try {
// Увеличили счетчик
requests.pendingCount++;
// Записываем параметры вызова
requests[reqNum] = {
start: new Date(), // Время вызова
url, // Адрес
postData, // Параметры запроса
reqType // Тип запроса(get_result == 1 ?)
}
return await axios.post(url, postData).then(res => res.data);
} catch (error) {
// Сохраняем ошибку
errorRequests[reqNum] = { ...requests[reqNum] }
errorRequests[reqNum].errorTime = new Date();
errorRequests[reqNum].errorText = error;
errorRequests.requestErrors++;
return error.response ? error.response.data : error.message;
} finally {
// В конце уменьшаем счетчик вызовов
requests.pendingCount--;
// Удаляем ключ
delete requests[reqNum];
}
}
app.post('/sendRequest/:bot_token', async function (req, res) {
requestNum++;
let url = "https://api.telegram.org/bot" + req.params['bot_token'] + "/" + req.body.method;
if (parseInt(req.body.get_result) === 1) {
res.send(await sendPostRequest(url, req.body.data, 1, requestNum));
} else {
sendPostRequest(url, req.body.data, 0, requestNum);
res.send("1");
}
});
Основные моменты:
- Добавлен объект
requests
, которых хранит в себе все параметры вызова. При завершении вызова, ключ удаляется - Если произошла ошибка, запись переносится в объект
errorRequests
с временем ошибки и ее текстом - Добавлены 2 новых параметра в функцию
sendPostRequest
, что бы было понятно, откуда вызывалось - Убраны лишние переменные, что бы сборщик мусора тратил меньше времени на свою работу
Если по результатам исследований выяснится, что есть "подвисшие запросы", то, в зависимости от версии node, можно добавить принудительное прерывание запроса axios