Пакетная обработка изображений закончилась быстро: две тысячи превью за несколько минут, очередь опустела, график задач выглядит прекрасно. Но в те же минуты обычный вход в кабинет стал заметно медленнее. Пользователь, который просто хотел ввести пароль, ждал чуть ли не дольше, чем сами картинки в очереди. Формально всё работает. По ощущениям, сервис на эти минуты немного заболел.

Первая мысль понятна: раз фоновая работа мешает основной, надо купить второй сервер. Иногда это правда. Но чаще между наблюдением «стало медленнее» и решением «нужен отдельный VPS» лежит несколько вопросов, и ответы на них стоят дешевле нового сервера. Давайте пройдём их по порядку, начав с самого простого инструмента: двух графиков на одной временной шкале.

Две кривые на одной оси

Чтобы понять, виновата ли очередь, нужно наложить друг на друга две вещи: что делали воркеры и что в это время чувствовал обычный API. Не средний график за сутки, а окно в несколько минут вокруг пакетной задачи. На оси времени должны оказаться:

  • число активных заданий в очереди и момент старта пачки;
  • загрузка CPU по ядрам, а не только общий процент;
  • занятая и доступная память, наличие обращений к swap;
  • задержка API по перцентилям: медиана и p95 или p99 для лёгких запросов вроде входа, списка или карточки;
  • время ответа Redis и базы данных, если вы можете его снять.

Почему перцентили, а не среднее? Среднее сглаживает как раз тот эффект, который заметил пользователь. Если девяносто пять запросов из ста идут как обычно, а пять упираются в занятый процессор, средняя задержка выглядит терпимо, а недовольный пользователь есть. Для лёгких запросов хвост распределения честнее.

Если кривая задержек поднимается вместе с началом пачки и опускается вместе с её окончанием, у вас сильное подозрение на связь. Если же задержка растёт без видимой связи с заданиями или раньше них, очередь, скорее всего, лишь случайный свидетель. Совпадение по времени ещё не причина, но это хорошая отправная точка для проверки.

Три разных «медленно»

Рост latency называют очередью слишком охотно. На деле за одним симптомом скрываются три разные болезни, и лечатся они по-разному.

Не хватает процессора

Обработка изображений, сжатие, генерация PDF, шифрование, разбор больших файлов — это вычисления. Node.js исполняет JavaScript в одном потоке на процесс, поэтому для вычислений картина зависит от того, где работает воркер. Если он живёт в том же процессе, что и HTTP-сервер, то долгий синхронный участок кода блокирует цикл событий, и вход в кабинет ждёт своей очереди внутри процесса. Если воркер в отдельном процессе на той же машине, он конкурирует с API уже не за цикл событий, а за ядра планировщика ОС. На VPS с одним vCPU это конкуренция в лоб, на четырёх ядрах она куда мягче.

Признак нехватки CPU: загрузка ядер близка к потолку на время пачки, а задержка лёгких запросов растёт вместе с ней. Память в этот момент может вести себя спокойно.

Не хватает памяти

Картинки в декодированном виде занимают куда больше места, чем файл на диске: счёт идёт на пиксели, а не на байты. Несколько заданий одновременно способны съесть память быстрее, чем кажется. Когда её становится мало, система начинает сбрасывать данные в swap или завершать процессы, а диск оказывается медленнее памяти на порядки. Тогда страдает всё сразу, включая API, который сам по себе ничего тяжёлого не делает.

Признак: график свободной памяти падает вместе с началом пачки, появляется активность swap, а загрузка CPU при этом может быть вполне умеренной. Это важное отличие: покупать «больше ядер» здесь бесполезно, нужен либо больший объём памяти, либо меньшая одновременность.

Ожидание внешней зависимости

Третий случай самый коварный. Задания не считают, а ждут: ответ стороннего API, загрузку в хранилище, отправку письма. Процессор простаивает, память в норме, но очередь тянется, а при нехватке соединений или лимитов страдает и обычный API, если они делят пул соединений к базе или общий исходящий канал.

Признак: CPU свободен, задания длятся долго, время ответа внешнего сервиса выросло. Отдельный VPS тут почти ничего не даст: воркер на новой машине так же будет ждать тот же сторонний сервис. Лечат это таймаутами, ограничением параллелизма к внешней стороне и повторными попытками с паузами, а не мощностью.

Как отличить причину по поведению графиков
ПричинаCPUПамятьЧто вероятно поможет
Вычислениявысокая по ядрамв пределах нормыограничить параллелизм, отдельный процесс, затем отдельный сервер
Памятьумереннаяпадает, возможен swapснизить одновременность, увеличить память, разделить по машинам
Внешняя зависимостьнизкаяв норметаймауты, лимит соединений, пауза между повторами

Реальные нагрузки часто смешивают эти причины: задание сначала скачивает файл (ожидание), потом сжимает (CPU), потом держит результат в памяти. Поэтому полезно смотреть не на задание целиком, а на его фазы.

Что на самом деле даёт вынос исполнителя

В документации NestJS про очереди сказано несколько вещей, которые здесь важны. Интеграции BullMQ и Bull хранят данные заданий в Redis. Производитель заданий и потребитель могут работать в разных процессах и даже на разных узлах. А смысл очередей в том, чтобы убрать ресурсоёмкую работу из процесса, обслуживающего HTTP, и сгладить пики. Это изложено в разделе NestJS: Queues.

Из этого следует простая и полезная вещь: переезд воркера на другую машину не требует переписывать логику заданий. Меняется то, где запускается процесс-потребитель и как он добирается до общего Redis. Но сразу же видно, что именно остаётся общим. Разберём это по частям.

Что уходит на другую машину

  • Процессорное время и память воркера. API перестаёт делить с ним ядра и оперативную память.
  • Риск того, что аварийно завершившийся воркер (например, убитый из-за нехватки памяти) утащит за собой процесс API.
  • Возможность подобрать размер сервера под нагрузку воркера, а не под нагрузку API.

Что остаётся общим

  • Брокер. Redis всё так же один, и теперь через сеть. Если он упал или перегружен, пострадают оба конца: API не сможет ставить задания, воркер не сможет их брать.
  • Данные. Если задание читает и пишет в ту же базу, нагрузка на неё никуда не делась. Вы убрали конкуренцию за CPU приложения, но не за ресурсы базы. Бывает, что узким местом была именно она, и тогда второй VPS с воркерами лишь быстрее донесёт нагрузку до той же базы.
  • Файлы. Если воркер обрабатывает изображения, лежащие на диске API-сервера, после выноса ему нужно откуда-то их брать: из объектного хранилища, по сети, через общий том. Раньше это был локальный диск, теперь появляется передача данных.
  • Семантика выполнения. Redis хранит задание, но он не превращает внешние эффекты в «ровно один раз». Если воркер отправил письмо или вызвал сторонний API и упал до отметки о завершении, задание может быть выполнено повторно. Идемпотентность внешних действий остаётся вашей заботой на любой конфигурации, на одной машине или на двух.

Во что обходится разделение

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

Новые сетевые соединения. Пока воркер и Redis жили на одном сервере, они общались через локальный интерфейс. После выноса каждое обращение к Redis проходит по сети: добавляется задержка, а на каждый воркер и каждую очередь приходятся свои соединения. Чем больше воркеров и очередей, тем больше соединений вы держите открытыми. Это не катастрофа, но значит, что Redis должен быть доступен второй машине, а значит, нужны правила файрвола, аутентификация и, возможно, шифрование канала. Открывать Redis всему интернету ради удобства нельзя; доступ стоит ограничить адресом воркера или закрытой сетью, если провайдер её предоставляет.

Соединения воркера с базой и внешними сервисами. Если задание обращается к базе, то у воркера теперь отдельный пул соединений с другого адреса. Это нужно учесть в лимите соединений самой базы: два приложения вместо одного суммарно могут исчерпать допустимое число быстрее, чем вы ожидаете.

Дрейф конфигурации. Один и тот же код развёртывается на два сервера. Версия приложения на API и на воркере должна быть совместимой: если вы поменяли формат данных задания, а воркер остался на старой версии, он будет получать то, чего не понимает. Раньше обновление было одной операцией, теперь это согласованное обновление двух мест, и порядок имеет значение.

Эксплуатация. Вам нужны два набора мониторинга, два набора логов, два места, где проверять, что процесс жив. Диагностика проблемы «задания не выполняются» теперь начинается с вопроса: что сломалось, воркер, сеть между машинами или Redis? Это вопрос, которого не было, пока всё жило в одном месте.

И наконец, формулировка, которую стоит повесить рядом с планом переезда: вторая машина не ускоряет задание, она лишь убирает конкуренцию. Если одно задание обрабатывается тридцать секунд на изолированной машине, оно будет обрабатываться примерно столько же и на отдельной, если у новой машины не больше ресурсов на ядро. Выигрыш в том, что оно перестаёт отбирать ресурсы у API.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Сначала дешёвый рычаг: ограничить параллелизм

Прежде чем покупать сервер, полезно проверить, не слишком ли агрессивно воркер сам себя запускает. Если пакетная обработка стартует по десять заданий одновременно на двух ядрах, то API неизбежно получит меньше времени. Снижение параллелизма иногда решает проблему без новых машин. Параметр конкурентности воркера задаётся в настройках интеграции; конкретное имя и способ задания зависят от используемой библиотеки и версии, их нужно сверить с документацией вашей версии.

Логика проверки такая:

  1. Запишите, какой параллелизм сейчас: сколько заданий воркер берёт одновременно.
  2. Прогоните пакетную задачу и снимите график задержек API.
  3. Уменьшите параллелизм, например, вдвое, и повторите на тех же данных.
  4. Сравните два результата: как изменились хвосты задержек API и как выросло общее время обработки пачки.

Это честный размен. Меньше параллелизма значит, что пачка обрабатывается дольше, зато API живёт спокойнее. Если очередь всё равно укладывается в нужный срок, вторая машина может быть не нужна. Если же при щадящем параллелизме пачка не успевает в отведённое окно, а при полном параллелизме мешает API, значит, одного сервера действительно мало: вы упёрлись и в бюджет ресурсов, и в требуемую пропускную способность. Вот тогда вынос обоснован.

Есть и вторая, более тонкая мера: вынести воркер в отдельный процесс на той же машине и дать ему более низкий приоритет планировщика. Это не требует новой аренды, но разделяет сбои и позволяет управлять ресурсами. Она не поможет, если проблема в нехватке памяти или общего числа ядер, но для сценария «вычисления мешают циклу событий» часто оказывается достаточной ступенью.

Пик определяется не средним временем одного задания

Здесь легко ошибиться в расчётах. Допустим, обработка одного изображения занимает в среднем две секунды процессорного времени. Кажется, что при двух ядрах система легко справляется. Но среднее время одного задания ничего не говорит о том, что происходит, когда сто пользователей загружают файлы одновременно, а пакетный импорт запускает ещё сотню.

Возьмём условный пример: два vCPU, одно задание занимает одно ядро на две секунды. Если параллелизм воркера равен четырём, то в каждый момент четыре задания претендуют на два ядра. Каждое из них получает в среднем половину ядра, поэтому длится не две секунды, а примерно четыре. Но главное другое: оба ядра заняты почти полностью, и для API остаются лишь доли времени между переключениями. Пока пачка идёт, лёгкие запросы вынуждены ждать своей очереди у планировщика.

Теперь добавим память. Если каждое задание держит в памяти, скажем, несколько сотен мегабайт в декодированном виде, то четыре параллельных задания потребуют в несколько раз больше, чем одно. Цифра условная, реальный расход вы измерите на своих файлах, но принцип неизменен: пик суммируется, а не усредняется.

Поэтому для оценки нужны не средние значения, а три величины:

  • худший случай одновременности: сколько тяжёлых заданий может оказаться в работе в одну минуту;
  • пиковая память одного задания: не средняя, а максимум на самых больших входных данных;
  • допустимый срок: за сколько времени очередь обязана опустеть, чтобы это устраивало бизнес.

Именно третья величина часто забывается. Очередь по определению сглаживает пики: задания ждут своей очереди, и это нормально. Вопрос лишь в том, какое ожидание допустимо. Миниатюра, которая появится через минуту после загрузки, обычно никого не огорчит. Квитанция, которую пользователь ждёт на экране, должна приходить быстрее. Для разных типов заданий допустимый срок разный, и это повод держать их в разных очередях с разным параллелизмом, а возможно, и на разных машинах.

Когда вторая машина действительно оправдана

Если собрать всё сказанное, решение о переносе воркеров на отдельный VPS выглядит оправданным в нескольких случаях:

  • после ограничения параллелизма пачка не укладывается в срок, а при полном параллелизме API деградирует;
  • нагрузка воркера стабильно требует много памяти, и её соседство с API угрожает аварийным завершением обоих процессов;
  • профиль нагрузки у воркера и API разный: API нужны быстрые небольшие ответы, воркеру нужны длительные вычисления, и размеры серверов под них логично подбирать по-разному;
  • вы хотите обновлять и перезапускать воркеры независимо, не затрагивая обработку HTTP-запросов.

А вот случаи, когда вынос скорее разочарует:

  • узкое место в базе данных или внешнем сервисе;
  • воркер ждёт сеть, а не считает;
  • основной бюджет времени уходит на передачу больших файлов между машинами, которой раньше не было;
  • вы не готовы поддерживать два сервера: мониторинг, обновления, безопасный доступ к Redis.

Если решение принято, выбор размера следует из замера, а не из ощущения. Линейка VPS MAATRIX включает конфигурации от одного vCPU и 2 ГБ памяти (Ba) до шестнадцати vCPU и 32 ГБ (Djed), на NVMe-накопителях и с root-доступом, а серверы можно разместить в нескольких локациях из публичного предложения: UK, US, RU и FR. Эти цифры не обещание ёмкости, а рамки выбора: сколько заданий потянет конкретный тариф, покажет ваш замер на ваших файлах. Практичная схема такая: API остаётся на уже работающем сервере, а воркерам берётся машина под их профиль. Для вычислительных заданий важнее число ядер, для работы с крупными изображениями в памяти — объём RAM. Общие соображения о размещении самого API на Node.js собраны в материале VPS для Node.js: стабильный хостинг API и веб-приложений. Если воркеры и Redis будут в разных локациях, учтите, что каждое обращение к брокеру пройдёт больше сети; разумнее держать их рядом.

Как сравнивать до и после

Самая частая ошибка при проверке переноса: сравнивать вчерашнюю нагрузку с сегодняшней. Трафик меняется, пачки отличаются размером, и вывод получается красивым, но ничего не доказывающим. Чтобы сравнение было честным, нужен одинаковый поток.

  1. Зафиксируйте сценарий. Один и тот же набор входных файлов для пакетной обработки, одинаковое число заданий, одинаковый момент старта.
  2. Воспроизведите обычный API-трафик. Лёгкие запросы вроде входа и карточки на постоянной частоте во время пачки. Подойдёт любой генератор нагрузки, главное, чтобы профиль запросов был одинаков в обоих прогонах.
  3. Снимите те же метрики. Медиана и хвост задержек API, загрузка ядер, память, время опустошения очереди.
  4. Выполните прогон на старой схеме, затем на новой. Условия, кроме расположения воркера, не меняйте.
  5. Повторите несколько раз. Один прогон может быть случайностью, особенно на разделяемых ресурсах.

Отдельно проверьте то, что вы добавили вместе с разделением. Сколько стоит дополнительный проход по сети для получения файла? Не вырос ли заметно срок ожидания задания в очереди из-за новых соединений? Не заканчиваются ли соединения к базе? Если выигрыш в API достигнут ценой того, что очередь теперь опаздывает, сделка плохая.

Полезно также включить в проверку нештатный сценарий: остановите воркер посреди пачки и посмотрите, что происходит с заданиями и с внешними эффектами. Если повторное выполнение приводит к дублям писем или платежей, проблема не в расположении сервера, а в идемпотентности, и её надо решать независимо от схемы развёртывания.

Что должно получиться в итоге

Перенос воркеров на отдельный VPS стоит считать удавшимся не по самому факту переезда, а по двум наблюдениям. Первое: во время самой тяжёлой пачки лёгкие запросы вроде входа в кабинет отвечают так же ровно, как в тишину, и хвост задержек не поднимается вместе с графиком заданий. Второе: очередь по-прежнему опустошается в заданный срок, а не просто «когда-нибудь», и новые сетевые соединения не съели выигрыш. Если оба условия выполняются на одинаковом потоке запросов, вторая машина свою работу делает. Если выполняется только одно, значит, пока рано праздновать: стоит ещё раз посмотреть на параллелизм, память и то, чего на самом деле ждут ваши задания.