Вопрос или проблема
Короче говоря, я пытаюсь получить все электронные письма в своем почтовом ящике Gmail, который содержит 36 тысяч элементов. Я делаю это с помощью библиотеки googleapis
, чтобы получить каждое письмо (учитывая, что у меня уже есть список идентификаторов):
return this.gmailApi.users.messages.get({
userId: 'me',
id: email.id,
format: 'metadata',
metadataHeaders: ['Date', 'Subject', 'From']
});
Поскольку у меня есть тысячи писем для получения, я пытаюсь сделать как можно больше одновременно.
- Первая попытка заключалась в том, чтобы получить все письма с помощью
Promise.all
- Вторая попытка заключалась в том, чтобы сделать то же самое, но партиями по 500*
- Третья попытка сократила партию запросов до 50*
*: По-прежнему использую Promise.all
для каждой партии, итерация через for await
Во всех попытках я получил следующую ошибку:
Uncaught GaxiosError Error: request to <gmail api url> failed, reason: connect EMFILE <xxx.xxx.xx.xxx:xxx> - Local (undefined:undefined)
at _request (/gmail-api/node_modules/gaxios/build/src/gaxios.js:149:19)
at processTicksAndRejections (<node_internals>/internal/process/task_queues:95:5)
gaxios.js:149
Process exited with code 1
Теперь я знаю, что это связано с количеством дескрипторов, которые разрешает открывать моя операционная система (у меня установлено 4096, я думаю), поэтому это, вероятно, не связано с API Google. Мой вопрос:
Какой способ решения этой проблемы существует, чтобы это было эффективно и не занимало часы (как это происходит, когда я получаю письма последовательно) или чтобы не превышать максимальное количество разрешенных дескрипторов файлов? Я еще не знаю, почему запуск в небольших партиях также не сработал, но у меня есть подозрение, что существуют лучшие способы решения этой проблемы, будь то повторное использование соединений или какие-то другие методы обработки.
Кроме того, я могу подумать о нескольких обходных путях, чтобы справиться с этой ситуацией, но все они являются “плохими практиками”, и большинство из них в любом случае займет очень много времени, поэтому я был бы рад, если бы вы могли поделиться какими-либо идеями.
Некоторые (вероятно) не относящиеся к делу сведения о моем проекте:
- NodeJS v18.20
- Typescript v5.6.3
- googleapis v144.0.0
- Ubuntu 20 (на WSL2)
Ответ или решение
Как эффективно обрабатывать 30k+ HTTP-запросов в Node.js
Обработка множества HTTP-запросов, особенно таких объемных, как 36 тысяч, может представлять собой серьезную проблему для разработчиков. Поскольку вы столкнулись с ошибками, связанными с превышением лимита открытых дескрипторов, важно выработать стратегию, которая позволила бы выполнять запросы эффективно, не нарушая технические ограничения вашей системы. В этой статье мы рассмотрим несколько подходов, которые помогут оптимизировать процесс получения данных из Gmail API.
1. Понимание проблемы с EMFILE
Ошибки типа EMFILE
возникают, когда количество одновременно открытых файловых дескрипторов превышает лимит, установленный операционной системой. Это касается не только файлов, но и сетевых подключений. По умолчанию, значение может варьироваться, но в вашем случае, как вы указали, оно настроено на 4096. Это означает, что вы не сможете одновременно обрабатывать множество соединений без применения стратегий управления ими.
2. Использование "пулов" соединений
Для эффективного управления нагрузкой можно использовать библиотеки, которые управляют количеством параллельных запросов. Одной из таких библиотек является p-limit
, которая ограничивает количество одновременно выполняемых промисов. Таким образом, вы сможете обрабатывать только определенное число запросов в любой момент времени.
const pLimit = require('p-limit');
const limit = pLimit(50); // Ограничьте до 50 одновременных запросов
const results = await Promise.all(emails.map(email =>
limit(() => this.gmailApi.users.messages.get({
userId: 'me',
id: email.id,
format: 'metadata',
metadataHeaders: ['Date', 'Subject', 'From']
}))
));
3. Пакетная обработка и таймауты
Если использование ограничений по количеству параллельных запросов не решает проблему, стоит рассмотреть пакетную обработку запросов с маленькими временными интервалами между ними. Это позволит снизить нагрузку на систему и избежать ошибок EMFILE
.
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const processBatch = async (batch) => {
await Promise.all(batch.map(email =>
this.gmailApi.users.messages.get({
userId: 'me',
id: email.id,
format: 'metadata',
metadataHeaders: ['Date', 'Subject', 'From']
}))
);
await delay(100); // Задержка между пакетами
};
for (let i = 0; i < emails.length; i += 50) {
const batch = emails.slice(i, i + 50);
await processBatch(batch);
}
4. Тестирование и мониторинг
Кроме того, важно регулярно тестировать и отслеживать производительность вашего приложения даже на более низких нагрузки, чтобы лучше понять, каковы ваши лимиты. Оркестрация метрик и логов с использованием сервисов мониторинга, таких как Prometheus или Datadog, может помочь в выявлении узких мест в производительности.
5. Рекомендации по оптимизации
- Используйте Long Polling или WebSocket: Если ваша задача подразумевает частое обновление данных, рассмотрите возможность использования технологий, которые позволяют поддерживать постоянное соединение.
- Кэширование данных: Если вы повторно запрашиваете одни и те же данные, кэшируйте результаты для уменьшения количества запросов к API.
- Снижение объема данных: Используйте параметры API для получения только необходимой информации из писем.
Заключение
Обработка 30k+ HTTP-запросов в Node.js требует аккуратности и умения оптимизировать код. Ограничение параллельных запросов, пакетная обработка, внедрение системы кэширования и мониторинг помогут вам справиться с большой нагрузкой, улучшить производительность и минимизировать ошибки. Применяйте данные подходы в зависимости от особенностей вашего проекта и соблюдайте баланс между производительностью и ограничениями системы.