Раздел 8 · Frameworks и Libs

Очереди и фоновые задачи

middle~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Очереди и фоновые задачи

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

Урок про то, как устроены очереди, что такое retry, backoff и dead-letter, и почему «доставить ровно один раз» это иллюзия, с которой надо уметь жить.

1. Зачем очередь

Очередь, это буфер между тем, кто создаёт работу (producer), и тем, кто её выполняет (worker). Producer кладёт job и сразу отвечает пользователю, worker разбирает очередь в своём темпе. Что это даёт:

  • Отвязка по времени: HTTP-запрос завершается мгновенно, тяжёлая работа идёт потом. Пользователь не ждёт отправку письма.
  • Сглаживание пиков: всплеск нагрузки оседает в очереди, а не роняет сервис. Воркеры разгребают её с той скоростью, на которую способны.
  • Надёжность: если worker упал, job не потерян, он вернётся в очередь и повторится. Это главное отличие от «запустил и забыл» через setTimeout.
  • Масштабирование: воркеров можно поднять сколько угодно, они разбирают одну очередь параллельно. Мост на масштабирование Node.

Это та же event-driven модель, что в разделе 9 (backend) и 24 (микросервисы), только в пределах одного приложения или пары сервисов.

2. Чем класть: Redis, BullMQ, Kafka

Под очередью лежит хранилище. Три типичных выбора:

  • BullMQ поверх Redis: де-факто стандарт для Node. Готовые повторы, backoff, отложенный запуск, cron, приоритеты. Это дефолт для большинства фоновых задач.
  • Redis Streams: низкоуровневый лог сообщений в Redis с consumer-группами. Берут, когда нужен сам поток событий и контроль над подтверждением, а не готовая обвязка job.
  • Kafka: распределённый лог для больших объёмов и межсервисных событий. Тяжелее Redis, окупается на потоках в сотни тысяч сообщений и на интеграции многих сервисов. Глубже, в разделе 24 (микросервисы, Kafka).

Грубое правило: фоновые задачи внутри приложения, BullMQ на Redis; поток событий между многими сервисами с большим объёмом, Kafka. Дальше в уроке работаем с BullMQ, потому что модель повторов и DLQ на нём виднее всего.

3. Producer и worker

Разделение простое: один код кладёт job, другой их разбирает. На Fastify worker, это обычный сервис в плагине (как репозиторий в уроке 05).

import { Queue, Worker } from 'bullmq';

// producer: внутри обработчика роута
const emails = new Queue('emails', { connection });
await emails.add('welcome', { userId }, { attempts: 3, backoff: { type: 'exponential', delay: 1000 } });

// worker: отдельный процесс или плагин
new Worker(
  'emails',
  async (job) => {
    await sendWelcomeEmail(job.data.userId);
  },
  { connection },
);

На Nest, тот же BullMQ, но через модуль и декораторы (мы упоминали это в уроке 08):

// producer
@Injectable()
class EmailsService {
  constructor(@InjectQueue('emails') private readonly queue: Queue) {}
  enqueueWelcome(userId: string) {
    return this.queue.add('welcome', { userId }, { attempts: 3, backoff: { type: 'exponential', delay: 1000 } });
  }
}

// worker
@Processor('emails')
class EmailsProcessor extends WorkerHost {
  async process(job: Job) {
    await this.mailer.sendWelcome(job.data.userId);
  }
}

Модель одна: producer кладёт, worker разбирает. Меняется только обвязка фреймворка. Cron и повторяющиеся задачи задаются опцией repeat: { pattern: '0 * * * *' }, это встроенный планировщик BullMQ, отдельный демон не нужен.

4. Retry, backoff и dead-letter

Главная ценность очереди, в том, что временный сбой не теряет работу. Внешний сервис ответил 503, worker бросил ошибку, job не пропал: BullMQ вернёт его и повторит. Три ключевых понятия:

  • attempts: сколько всего попыток разрешено. После исчерпания job считается проваленным.
  • backoff: пауза перед повтором. Exponential удваивает её с каждой попыткой, чтобы не добивать упавший сервис частыми повторами.
  • dead-letter queue: куда уходит job, исчерпавший все попытки. Не теряется, а ложится с историей ошибок, чтобы разобрать руками.

Покрути виджет: задай, на какой попытке обработчик начнёт отдавать успех (или поставь «никогда»), и посмотри путь job по очереди.

Job: attempts, backoff, DLQ
  1. жми «прогнать job»

Видно две развязки. Если успех приходит в пределах бюджета, промежуточные падения гасит backoff и наружу ничего не протекает. Если бюджет исчерпан, job не пропадает молча, а уезжает в DLQ, и это сигнал, что нужен человек. В BullMQ DLQ обычно делают через событие failed и отдельную очередь, куда складывают окончательно проваленные задачи.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.