Очереди и фоновые воркеры
Делать медленную работу позже и надёжно, не заставляя пользователей ждать.
За 30 секунд
Очередь — это список дел для вашего бэкенда. Веб-запрос добавляет задачу (отправить это письмо, синхронизировать этот заказ) и сразу возвращает ответ; отдельные процессы-воркеры забирают задачи и выполняют их, повторяя при ошибках. Очереди делают приложения быстрее для пользователей и устойчивее, когда внешние сервисы тормозят или недоступны.
Почему я это использую
- Я использую очереди, потому что письма, SMS, колбэки платежей, синхронизация с ERP и отчёты никогда не должны блокировать запрос клиента.
- Я использую повторные попытки с backoff, потому что внешние API падают постоянно, а большинство сбоев временные.
- Я пропускаю вебхуки в обе стороны через очереди, чтобы падение партнёра никогда не утянуло за собой мой API.
- Я делаю задачи идемпотентными, чтобы повторная попытка никогда не списала деньги дважды и не отправила одну и ту же SMS два раза.
Как это работает
01Зачем нужны очереди: продюсеры, консьюмеры и брокеры
Представьте планку для чеков на кухне ресторана. Официант (продюсер) прикалывает чек с заказом и возвращается к гостям; повара (консьюмеры) берут чеки, когда освобождаются. Если одновременно пришло десять заказов, чеки ждут на планке, а официант не застывает на месте. В программах эта планка — брокер: Redis, таблица в базе данных, RabbitMQ или SQS. Ваше веб-приложение — продюсер, процессы-воркеры — консьюмеры. Преимущества: быстрые ответы, сглаживание пиков трафика, изоляция сбоев медленных сервисов и масштабирование воркеров независимо от веб-серверов. Цена: работа становится асинхронной, и приходится думать о повторах, дубликатах, порядке и мониторинге.
- Задача (job): небольшое сообщение с типом и данными, в идеале только id, а не объекты целиком.
- Имена очередей разделяют приоритеты: critical, default, emails, reports.
- Конкурентность: сколько задач воркер выполняет одновременно.
02Повторы, backoff и dead-letter-очереди
Задачи падают: платёжный API отвечает по таймауту, в базе случается дедлок, сторонний сервис возвращает 503. Повторяйте, но аккуратно. Экспоненциальный backoff каждый раз ждёт дольше (например, 10 секунд, 1 минуту, 5 минут), давая другой стороне время восстановиться; jitter разносит повторы, чтобы они не ударили все разом. Ограничивайте число попыток. Различайте ошибки: таймаут стоит повторить, а ошибка валидации («клиент не существует») будет повторяться вечно, поэтому сразу помечайте задачу как проваленную. После последней попытки переносите задачу в dead-letter-очередь (в Laravel — таблица failed_jobs, в BullMQ — набор failed). Кто-то должен туда смотреть: настройте алерты на количество сбоев и сделайте способ просматривать и перезапускать задачи после исправления причины.
class SyncOrderToErp implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public int $timeout = 60;
public function __construct(public int $orderId) {}
public function backoff(): array
{
return [10, 60, 300, 900];
}
public function handle(ErpClient $erp): void
{
$order = Order::findOrFail($this->orderId);
$erp->upsertOrder($order); // idempotent by order id on the ERP side
}
}
03Идемпотентные задачи: безопасно выполнить дважды
Большинство очередей гарантируют доставку at-least-once: задача может выполниться дважды — например, если воркер упал после того, как сделал работу, но до подтверждения. Поэтому каждая задача должна быть идемпотентной, то есть повторный запуск не даёт дополнительного эффекта. Приёмы: использовать естественный уникальный ключ (создать счёт для заказа 123, только если его ещё нет), хранить отметку об обработке с уникальным ограничением, использовать upsert вместо insert и передавать платёжным провайдерам idempotency key. Проверяйте состояние перед действием («уже отправлено?»). Доставка exactly-once между системами — во многом миф; эффект exactly-once достигается идемпотентными обработчиками. И избегайте классической гонки: диспатчьте задачи только после коммита транзакции в базе, иначе воркер может взять задачу для строки, которой ещё не существует.
04BullMQ в Node
BullMQ — стандартная очередь на Redis для Node. Queue добавляет задачи, Worker обрабатывает их с заданной конкурентностью, а опции управляют числом попыток, backoff, задержками, приоритетом и очисткой. Задайте jobId для дедупликации: добавление задачи с уже существующим id игнорируется. Повторяющаяся работа использует job schedulers (upsertJobScheduler) с cron-паттерном или интервалом; в BullMQ v6 старую опцию repeat убрали в пользу планировщиков. Запускайте воркеры отдельными от API процессами, чтобы тяжёлые задачи никогда не замедляли запросы, и задавайте каждой очереди свою конкурентность. Redis должен быть настроен с maxmemory-policy noeviction, иначе он может молча удалять задачи.
import { Queue, Worker } from 'bullmq';
const connection = { host: '127.0.0.1', port: 6379 };
export const webhooks = new Queue('webhooks', { connection });
await webhooks.add('deliver', { eventId }, {
jobId: 'evt-' + eventId, // dedupe
attempts: 8,
backoff: { type: 'exponential', delay: 5_000 },
removeOnComplete: 1000,
removeOnFail: 5000,
});
new Worker('webhooks', async (job) => deliverWebhook(job.data.eventId), {
connection,
concurrency: 10,
});
05Расписание и запуск воркеров в продакшене
Работа по расписанию (ночные отчёты, истечение подписок, ежечасные синхронизации) запускается по таймеру. Классический cron выполняет команду в фиксированное время; планировщику Laravel нужна одна запись в cron, а остальное описывается в коде; планировщики BullMQ хранят расписания в Redis. Делайте задачи по расписанию безопасными при наложении или явно запрещайте наложение и запускайте их только на одном сервере. Воркеры — это долгоживущие процессы, поэтому их должен поддерживать живыми менеджер процессов: supervisor или systemd для воркеров Laravel, pm2 или systemd для Node, или по контейнеру на каждый тип воркера в Docker. Перезапускайте воркеры на каждом деплое, чтобы они загрузили новый код, и обеспечьте graceful shutdown, чтобы выполняющаяся задача успела завершиться.
; /etc/supervisor/conf.d/app-worker.conf
[program:app-worker]
command=php /var/www/app/artisan queue:work redis --queue=critical,default --sleep=3 --tries=3 --max-time=3600
process_name=%(program_name)s_%(process_num)02d
numprocs=2
user=app
autostart=true
autorestart=true
stopwaitsecs=3600
stdout_logfile=/var/log/app-worker.log
06Входящие и исходящие вебхуки
Входящие вебхуки (от платёжных провайдеров, ERP, служб доставки): проверьте подпись, сохраните сырое событие, быстро ответьте 200 и обработайте его в задаче. Провайдеры повторяют отправку, поэтому дедуплицируйте по их id события. Когда речь о деньгах, никогда не доверяйте одному лишь payload: заново запросите статус платежа через API провайдера. Исходящие вебхуки (вашим партнёрам): записывайте событие в таблицу outbox в той же транзакции, что и бизнес-изменение, пусть воркер его доставит, подписывайте тело через HMAC-SHA256 с временной меткой, чтобы получатели могли его проверить и отклонить повторы, повторяйте с backoff в течение суток или дольше и показывайте партнёру логи доставки. Этот паттерн outbox гарантирует, что вы никогда не уведомите о том, что было откачено, и не потеряете событие, которое было закоммичено.
Когда применять
- Письма, SMS, push-уведомления и всё, что вызывает сторонние API.
- Тяжёлая работа: экспорты, импорты, обработка изображений, генерация отчётов.
- Синхронизация с ERP, бухгалтерскими или фискальными системами, которые могут тормозить или быть офлайн.
- Приём и отправка вебхуков, а также рассылка (fan-out) доменных событий.
Когда не стоит
- Когда результат нужен пользователю прямо сейчас, чтобы продолжить (проверить купон, показать цену).
- Крошечные быстрые операции, где накладные расходы и сложность очереди ничего не дают.
- Как замена транзакции базы данных, когда шаги должны либо все выполниться, либо все откатиться.
Альтернативы
- Очереди Laravel + Horizon
Встроенные очереди с драйверами Redis, database или SQS; Horizon добавляет дашборд.
- RabbitMQ
Полноценный брокер сообщений с маршрутизацией и подтверждениями; хорош для множества сервисов.
- Amazon SQS
Полностью управляемая очередь; без серверов, оплата за запросы.
- Kafka
Лог событий для высоконагруженного стриминга и повторного воспроизведения; больше, чем нужно большинству продуктов.
Лучшие практики
- Передавайте в задачи id, а не объекты целиком, и загружайте свежие данные в обработчике.
- Делайте каждую задачу идемпотентной; исходите из того, что она выполнится больше одного раза.
- Повторяйте с экспоненциальным backoff и лимитом; при постоянных ошибках падайте сразу.
- Диспатчьте задачи после коммита транзакции в базе.
- Разделяйте очереди по приоритету и запускайте воркеры под менеджером процессов.
- Настройте алерты на проваленные задачи и глубину очереди; сделайте инструмент для перезапуска.
- Подписывайте исходящие вебхуки и проверяйте входящие.
Частые ошибки
- Неидемпотентные задачи, которые при повторе дважды списывают деньги или дважды отправляют сообщения.
- Бесконечные повторы без backoff, которые добивают и без того упавший сервис.
- Месяцами никто не смотрит в таблицу проваленных задач.
- Забытый перезапуск воркеров при деплое.
- Redis с политикой вытеснения, которая удаляет задачи из очереди при нехватке памяти.
Глубже
07Как задача «резервируется»: visibility timeout, блокировки и зависшие задачи
Когда воркер берёт задачу, брокер её не удаляет. Он скрывает её на время и ждёт подтверждения. Если подтверждение не пришло вовремя, он считает, что воркер умер, и отдаёт задачу кому-то другому. В SQS это visibility timeout. В Laravel это retry_after в config/queue.php: если задача всё ещё выполняется спустя столько секунд, другой воркер запускает её заново, поэтому timeout задачи всегда должен быть меньше retry_after, иначе долгие задачи выполняются дважды параллельно. BullMQ использует блокировку, которую воркер постоянно продлевает во время обработки; если блокировка истекла, например потому что тяжёлый по CPU код заблокировал event loop, задача помечается как stalled и возвращается в ожидание, а после слишком большого числа таких случаев падает. Все три подхода дают доставку at-least-once, и именно поэтому идемпотентность обязательна.
// config/queue.php
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 960, // longer than the slowest job's timeout
'block_for' => 5,
'after_commit' => true, // push only after the DB transaction commits
],
// Slow export job: public $timeout = 900; run it on a worker
// started with --timeout=900, on its own queue.
08Управление конкурентностью: уникальность, перекрытие и rate limits
Повторы и параллельные воркеры создают новые проблемы: две синхронизации одного клиента соревнуются друг с другом, или всплеск задач упирается в rate limit партнёра. Laravel даёт строительные блоки. ShouldBeUnique игнорирует новую отправку, пока идентичная задача (по uniqueId) ещё ожидает. Job middleware WithoutOverlapping разрешает одновременно выполняться только одной задаче на ключ и возвращает остальные в очередь. RateLimited применяет к задачам именованный rate limiter, а ThrottlesExceptions отступает после повторяющихся ошибок, работая как circuit breaker для нестабильного API; сочетайте его с retryUntil, чтобы повторы ограничивало время, а не число попыток. Блокировкам уникальности и перекрытия нужно кэш-хранилище с атомарными блокировками, например Redis или база данных. В BullMQ опция deduplication игнорирует повторные задачи с тем же id, а limiter воркера ограничивает число задач за окно времени.
class SyncCustomerToCrm implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public function __construct(public int $customerId) {}
public function uniqueId(): string
{
return (string) $this->customerId;
}
public function middleware(): array
{
return [
new WithoutOverlapping('crm-'.$this->customerId),
new ThrottlesExceptions(10, 5 * 60), // 10 errors, then pause 5 min
];
}
public function retryUntil(): DateTime { return now()->addHours(6); }
}
09Workflows: цепочки, батчи и родительско-дочерние потоки
Реальная работа часто состоит из нескольких задач с зависимостями. Цепочка запускает задачи одну за другой и останавливается, если одна упала: сгенерировать счёт, затем отправить его, затем уведомить бухгалтера. Батч запускает много задач параллельно и вызывает колбэк, когда все они завершились, что подходит для импортов и экспортов, разбитых на тысячи маленьких задач; Laravel отслеживает прогресс в таблице job_batches, а задачи в батче используют трейт Batchable. В BullMQ FlowProducer создаёт деревья родитель–потомок, где родитель выполняется только после завершения всех потомков и может читать их результаты. Проектируйте каждый шаг идемпотентным и маленьким, чтобы повтор одного шага не переделывал весь workflow. Когда поздний шаг падает после того, как ранние уже изменили внешние системы, нужна явная компенсация, например отмена резервирования, потому что очередь не может откатить платёж.
$batch = Bus::batch(
$orderIds->map(fn (int $id) => new ExportOrderRow($exportId, $id))->all()
)
->name('orders-export-'.$exportId)
->allowFailures()
->then(fn (Batch $batch) => BuildExportFile::dispatch($exportId))
->catch(fn (Batch $batch, Throwable $e) => report($e))
->finally(fn (Batch $batch) => CleanupExportChunks::dispatch($exportId))
->onQueue('reports')
->dispatch();
// Progress for the UI: Bus::findBatch($batch->id)->progress()
10Ёмкость и наблюдаемость: глубина, возраст и закон Литтла
Сколько воркеров вам нужно? Закон Литтла даёт первый ответ: среднее число задач в обработке равно скорости поступления, умноженной на среднее время обработки. Если в секунду приходит 30 задач и каждая занимает 200 мс, нужно минимум 6 параллельных слотов только чтобы успевать, и больше — для всплесков и повторов. Одна лишь глубина очереди вводит в заблуждение: 10,000 крошечных задач могут разойтись за секунды, а 50 медленных могут отставать на часы. Самая полезная единичная метрика — возраст самой старой ожидающей задачи в каждой очереди с алертом относительно цели, например «письма в течение 60 секунд». Отслеживайте также пропускную способность, долю ошибок и перцентили времени обработки. Horizon умеет автоматически балансировать воркеры между очередями, а php artisan queue:monitor может генерировать событие, когда очередь превышает порог. В мультитенантных системах следите за справедливостью: импорт одного тенанта не должен задерживать чеки всех остальных.
- Автомасштабируйте воркеры по возрасту самой старой задачи, а не по CPU.
- Разделяйте очереди по целевой задержке: critical, default, bulk.
- Ограничивайте конкурентность на тенант для тяжёлых типов задач.
Реальные ситуации
Webhook оплаты выполнил один заказ дважды
Проблема
С клиента списали деньги один раз, но он получил две посылки. Логи показывают, что платёжный провайдер отправил один и тот же webhook «payment succeeded» дважды с разницей в несколько секунд, и оба были обработаны.
Как решить
Провайдеры доставляют webhooks по принципу at-least-once, так что дубликаты — норма. Сохраняйте каждое входящее событие с event id провайдера под уникальным индексом до постановки в очередь; повторная вставка падает, и вы возвращаете 200 без обработки. В задаче блокируйте строку заказа или используйте условное обновление, например установить статус paid там, где статус pending, и продолжайте, только если строка изменилась. Сделайте идемпотентной и отгрузку: одна посылка на заказ, что обеспечивается уникальным ограничением. Перед действием заново запросите статус платежа через API провайдера. И наконец, добавьте тест, который запускает один и тот же webhook дважды и проверяет, что посылка одна.
ERP лежала шесть часов
Проблема
Бухгалтерская система за синхронизацией с ERP была недоступна целый день. Тысячи задач синхронизации упали, повторились и оказались в таблице failed jobs, а команде пришлось исправлять данные вручную.
Как решить
Проектируйте под сбои, а не реагируйте на них. Используйте ThrottlesExceptions или circuit breaker, чтобы после повторяющихся ошибок задачи вставали на паузу, а не сжигали попытки, и ограничивайте повторы временем через retryUntil, например 24 часами, а не маленьким числом попыток. Пусть backoff растёт с ограничением сверху. Сделайте задачи идемпотентными upsert с ключом по id заказа, чтобы повторное воспроизведение было безопасным. Добавьте health check, который ставит очередь синхронизации на паузу, когда ERP недоступна, и оповещает ответственного. После восстановления повторите задачи через php artisan queue:retry для затронутой очереди и проверьте итоги отчётом сверки, сравнивающим обе системы.
Большой экспорт задерживает письма сброса пароля
Проблема
Когда админ запускает большой экспорт товаров, клиенты на двадцать минут перестают получать письма о сбросе пароля и подтверждения заказов.
Как решить
Всё работает на одной очереди default с одним пулом воркеров, поэтому тысячи частей экспорта стоят впереди писем. Разделите очереди по целевой задержке: critical для писем аутентификации и чеков, default для обычной работы, reports для экспортов и импортов. Запустите выделенные воркеры для critical, чтобы она никогда не голодала, и ограничьте конкурентность очереди reports, чтобы она не могла исчерпать и базу данных. В Horizon задайте supervisors для каждой группы очередей со своим числом процессов. Добавьте алерт на возраст самой старой задачи в очереди critical, например выше 60 секунд, чтобы следующую регрессию поймать быстро.
Шпаргалка
- Повторить все упавшие задачи Laravel
php artisan queue:retry all- Повторить упавшие задачи одной очереди
php artisan queue:retry --queue=emails- Список упавших задач
php artisan queue:failed- Удалить упавшие задачи старше 48 часов
php artisan queue:prune-failed --hours=48- Событие, когда очередь слишком длинная
php artisan queue:monitor redis:default,redis:emails --max=100- Перезапускать воркер, чтобы ограничить рост памяти
php artisan queue:work --max-jobs=1000 --max-time=3600- Обработать очередь и выйти (CI, одноразовые контейнеры)
php artisan queue:work --stop-when-empty- BullMQ: игнорировать дубликаты задач
await queue.add('sync', data, { deduplication: { id: 'customer-' + id } })- BullMQ: расписание по cron
await queue.upsertJobScheduler('nightly-report', { pattern: '0 2 * * *' }, { name: 'report' })- Redis не должен вытеснять задачи
maxmemory-policy noeviction
В моих реальных проектах
- В мультитенантном POS SaaS доменные события проходят через транзакционный outbox в BullMQ (около 20 очередей), а вебхуки с HMAC-подписью доставляются через очередь.
- На платформе зоотоваров синхронизация с ERP и бухгалтерией работает как задачи по расписанию за feature-флагами, с алертами о состоянии и откатом цен.
- В CRM для спортзалов задачи по расписанию выполняются в фиксированном порядке без наложения (разморозка, истечение, активация), а SMS повторно отправляется только при ошибке соединения, чтобы ни одно сообщение не ушло дважды.
- На продакшен-серверах воркеры Laravel работают под supervisor, Node-приложения — под pm2, а планировщики запускает cron.
Вопросы и ответы
Q1Что значит идемпотентность для задачи и как её добиться?
Идемпотентная задача даёт один и тот же результат, выполнится она один раз или пять. Это важно, потому что очереди доставляют at-least-once: воркер может упасть после выполнения работы, но до подтверждения, и задача запустится снова. Я добиваюсь идемпотентности уникальными ключами и проверками состояния: создавать счёт, только если для этого заказа его ещё нет, использовать upsert, записывать id обработанных событий под уникальным ограничением и передавать idempotency key платёжным провайдерам и сервисам сообщений. Также я оставляю побочные эффекты в конце задачи и избегаю частичных записей с помощью транзакций.
Q2Как вы проектируете повторы для задачи, которая вызывает внешний API?
Сначала классифицирую ошибки. Таймауты, ошибки соединения, ответы 429 и 5xx обычно временные — их повторяем. Ошибки валидации 4xx постоянные — падаем сразу и записываем причину. Для временных ошибок я использую экспоненциальный backoff с jitter и ограниченным числом попыток, учитывая заголовки Retry-After. У каждого вызова таймаут короче таймаута задачи. После последней попытки задача попадает в хранилище проваленных или dead-letter-задач с полным контекстом, срабатывает алерт, и есть способ перезапустить её после устранения причины. Задача должна быть идемпотентной, чтобы повторы были безопасными.
Q3Что такое dead-letter-очередь?
Это место, куда попадают сообщения после того, как исчерпаны все повторы: так они перестают блокировать очередь или тратить ресурсы, но не теряются. В Laravel это таблица failed_jobs, в BullMQ — набор failed, в SQS или RabbitMQ — отдельная очередь. Dead-letter-очередь полезна, только если кто-то за ней следит, поэтому я настраиваю алерты на новые записи и отслеживаю их количество. Каждая запись должна содержать payload, ошибку и историю попыток. После исправления бага или окончания сбоя задачи перезапускают. Закономерности в проваленных задачах — ещё и отличный сигнал о проблемах в интеграциях.
Q4Зачем использовать паттерн outbox вместо прямого диспатча задачи?
Потому что запись в базу данных и публикация сообщения — две разные системы, и одна может упасть после того, как другая отработала успешно. Если диспатчить внутри транзакции, а транзакция откатится, воркер обработает событие, которого никогда не было. Если диспатчить после коммита, а процесс упадёт между этими шагами, событие потеряется. С outbox вы вставляете событие в таблицу в той же транзакции, что и бизнес-изменение, поэтому либо коммитится и то и другое, либо ничего. Затем relay читает outbox, публикует в очередь и помечает строки как отправленные. Консьюмеры остаются идемпотентными, потому что доставка — at-least-once.
Q5Как вы запускаете и мониторите воркеры в продакшене?
Воркеры — долгоживущие процессы, поэтому они работают под менеджером процессов: supervisor или systemd для Laravel, pm2 или systemd для Node, или в отдельных контейнерах. Я разделяю очереди по приоритету и задаю конкурентность для каждой, чтобы большой экспорт не мог «задушить» письма для сброса пароля. Воркеры перезапускаются на каждом деплое и корректно завершаются, доделывая текущую задачу. Мониторинг охватывает глубину очереди, возраст задач, долю сбоев и время обработки, с алертами, когда самая старая задача слишком долго ждёт. Horizon или дашборд BullMQ помогают с просмотром, но будят вас, когда что-то застряло, именно алерты.
Вопросы уровня senior
S1Как гарантировать порядок в пределах сущности, когда очередь обрабатывают много воркеров?
Глобальный порядок с параллельными воркерами невозможен без отказа от параллелизма, поэтому я спрашиваю, чему на самом деле нужен порядок. Обычно это события одной сущности: смены статуса заказа или обновления баланса одного клиента. Варианты: направлять все задачи сущности в одну партицию или группу и обрабатывать каждую группу последовательно — это нативно поддерживают партиции Kafka, message groups в SQS FIFO и группы в BullMQ Pro. С обычными очередями Laravel или BullMQ я использую блокировку на сущность, например WithoutOverlapping с ключом по id сущности, плюс номер версии в каждом событии, чтобы обработчик игнорировал всё, что старше сохранённого состояния. Часто самый простой надёжный дизайн — чтобы обработчики читали текущее состояние из базы, а не доверяли payload события; тогда порядок важен меньше.
S2Очереди на Redis, RabbitMQ, SQS или Kafka: как выбирать?
Начинайте с задачи, а не с инструмента. Для фоновой работы внутри одного продукта — письма, синхронизации, экспорты — очереди на Redis вроде очередей Laravel или BullMQ просты, быстры и уже есть в стеке; компромисс в том, что надёжность зависит от настроек персистентности Redis и памяти. SQS полностью управляемый и очень надёжный, с встроенными visibility timeouts и dead-letter queues, но с большей задержкой и привязкой к AWS. RabbitMQ добавляет богатую маршрутизацию, exchanges и подтверждения на каждое сообщение, что подходит множеству сервисов, обменивающихся командами. Kafka — воспроизводимый журнал событий с партициями и consumer groups, идеальный для высоконагруженных потоков, event sourcing и множества независимых потребителей, но в эксплуатации тяжелее. Я выбираю очереди на Redis, пока не появится конкретная потребность: воспроизведение, потоки между командами или огромная пропускная способность.
S3Как вы подбираете размер и автомасштабируете парк воркеров?
Я измеряю скорость поступления и время обработки по каждому типу задач и использую закон Литтла как базу: нужная конкурентность равна задачам в секунду, умноженным на среднее число секунд на задачу, плюс запас на всплески и повторы. Затем проверяю, от чего зависят воркеры, потому что больше воркеров может просто перенести узкое место в базу или в партнёрский API с rate limit. Автомасштабирование должно реагировать на возраст самой старой ожидающей задачи или на глубину, делённую на пропускную способность, а не на CPU, ведь многие задачи ограничены I/O. У каждой очереди своя целевая задержка и свой пул воркеров, с лимитами на тяжёлых очередях, чтобы они не могли заморить голодом критичные. Уменьшение должно быть мягким: воркеры перестают брать задачи, завершают текущую и выходят в пределах grace period, иначе задачи будут повторены.
S4Пакетный импорт на 100,000 строк частично упал. Как должна вести себя система?
Она должна завершить то, что может, чётко сообщить, что упало, и позволить безопасно повторить. Я разбиваю импорт на части или по задаче на группу строк внутри батча, допускающего ошибки, чтобы одна плохая строка не отменяла остальные. Ошибки валидации постоянные, поэтому они записываются по каждой строке с причиной и никогда не повторяются; временные ошибки вроде таймаутов повторяются с backoff. Каждая запись — upsert с ключом по естественному id, поэтому повторный запуск всего импорта или только упавших частей никогда не дублирует данные. Когда батч завершён, пользователь получает сводку — импортировано, пропущено и упало — со скачиваемым файлом ошибок и может исправить и заново загрузить только эти строки. Компромисс — больше учёта, но альтернатива, импорт по принципу «всё или ничего», делает большие файлы почти невозможными для загрузки.
Глоссарий
- Visibility timeout
- Сколько взятая задача остаётся скрытой от других воркеров, прежде чем брокер сочтёт её неудачной и доставит повторно.
- Доставка at-least-once
- Гарантия, что сообщение будет доставлено, возможно, больше одного раза, поэтому обработчики должны выдерживать дубликаты.
- Idempotency key
- Уникальный id, отправляемый с операцией, чтобы получатель мог распознать и проигнорировать повторы.
- Poison message
- Задача, которая падает каждый раз из-за своего содержимого и без лимита повторов блокировала бы очередь или зациклилась бы навсегда.
- Stalled job
- В BullMQ — задача, чей воркер перестал продлевать блокировку; она возвращается в ожидание или помечается упавшей.
- Job batch
- Группа задач, отслеживаемых вместе, с колбэками на случай, когда все успешны, какая-то упала или все завершились.
- Закон Литтла
- Среднее число задач в системе равно скорости поступления, умноженной на среднее время на задачу; используется для расчёта числа воркеров.
- Circuit breaker
- Защита, которая на время перестаёт вызывать падающий сервис, давая ему время восстановиться.
- Справедливость
- Гарантия того, что один тенант или тип задач не может монополизировать воркеры и задерживать всех остальных.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.