Разработка · 7 мин чтения

Laravel Queue: настройка воркеров для фоновых задач

Настройка Laravel Queue - первое, что я делаю в проекте, как только в коде появляется отправка писем, генерация PDF или приём вебхуков от Т‑Банка: без очередей такие операции выполняются прямо в HTTP-запросе и упираются в таймаут php-fpm. Пользователь оформил заказ, а сервер в это время генерирует накладную для СДЭК, шлёт письмо и дёргает вебхук в n8n - запрос виснет на 5-8 секунд, а на слабом хостинге падает по 30-секундному лимиту. Очередь выносит всё это в фоновый процесс: клиент получает ответ мгновенно, а воркер обрабатывает задачу отдельно.

Зачем нужны очереди и когда без них не обойтись

Правило простое: если операция дольше 200-300 мс или зависит от внешнего API, её место в очереди. На практике это отправка email и SMS, генерация PDF-счетов и отчётов, обработка загруженных изображений, синхронизация заказов с 1С или CRM, приём вебхуков от платёжных систем и запросы к внешним сервисам вроде расчёта доставки СДЭК. У меня был кейс с интернет-магазином на Laravel и приёмом оплат через Т‑Банк: эквайринг присылал уведомление об оплате, а обработчик сразу пытался сформировать чек через ОФД, обновить остатки и отправить письмо клиентом - всё синхронно. При скачке нагрузки вебхук от банка не укладывался в таймаут, банк слал повтор, и заказ обрабатывался дважды. Перенос логики в job с уникальным ключом идемпотентности снял проблему полностью.

Вторая причина - устойчивость к сбоям. Если внешний сервис (СДЭК, ОФД, SMS-шлюз) недоступен, синхронный запрос просто падает с ошибкой у пользователя. Job из очереди можно ретраить с задержкой, логировать в failed_jobs и разбирать руками, не трогая основной поток заказа.

Настройка драйверов очереди в config/queue.php

Laravel из коробки умеет работать с несколькими драйверами: sync (для локальной отладки, задачи выполняются сразу), database, redis, а также beanstalkd и Amazon SQS. Для боевого проекта я выбираю между database и redis.

Драйвер Где хранятся задачи Когда использую
sync нигде, выполняется сразу только локальная разработка и тесты
database таблица jobs в MySQL/PostgreSQL небольшой проект, нет отдельного Redis, нагрузка низкая
redis списки и хэши в Redis средняя и высокая нагрузка, нужны приоритеты и задержки
sqs Amazon SQS инфраструктура уже в AWS, нужна масштабируемость без своего сервера очередей

Для database-драйвера сначала генерирую таблицы миграций:

php artisan queue:table
php artisan queue:failed-table
php artisan migrate

В .env указываю драйвер и подключение:

QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_QUEUE=default

Database-драйвер проще в развёртывании - не нужен отдельный сервис, но каждая проверка новой задачи это SELECT с блокировкой строки, и на потоке в несколько сотен задач в минуту база начинает тормозить. Redis держит такую нагрузку без проблем и даёт приоритеты очередей, задержки (delay) и явный контроль ретраев без лишних запросов к БД.

Как создать job и запустить воркер через artisan queue:work

Задача создаётся командой php artisan make:job ProcessOrderPayment. Класс реализует интерфейс ShouldQueue, а вся логика уходит в метод handle:

class ProcessOrderPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;
    public $timeout = 90;
    public $backoff = [10, 30, 60];

    public function __construct(public Order $order) {}

    public function handle(CdekService $cdek): void
    {
        $this->order->markAsPaid();
        $cdek->registerShipment($this->order);
        Mail::to($this->order->email)->send(new OrderPaidMail($this->order));
    }

    public function failed(Throwable $exception): void
    {
        Log::error('Order payment job failed', ['order_id' => $this->order->id]);
    }
}

Отправка в очередь: ProcessOrderPayment::dispatch($order). Дальше запускаю воркер, который забирает задачи и выполняет handle:

php artisan queue:work redis --queue=payments,default --tries=3 --timeout=90 --sleep=1

Параметр - queue задаёт приоритет: воркер сначала выгребет всё из payments и только потом перейдёт к default. Отдельные очереди под критичные задачи (оплата, вебхуки) и отдельные под второстепенные (аналитика, логирование) - это то, что почти всегда упускают на старте и потом переделывают, когда письмо с чеком встаёт в очередь после тысячи задач по обновлению кэша каталога.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Supervisor для автозапуска и перезапуска воркеров

Команда queue:work живёт, пока не упадёт процесс или не кончится память - PHP-воркер держит один и тот же процесс часами, и это как раз то место, где утечки памяти в коде вылезают в первую очередь. Без супервизора процесса воркер после падения просто перестанет забирать задачи, и очередь будет копиться молча.

На Ubuntu/Debian ставлю Supervisor (apt install supervisor) и создаю конфиг в /etc/supervisor/conf.d/laravel-worker.conf:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --queue=payments,default --sleep=1 --tries=3 --timeout=90 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=3
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600

numprocs=3 запускает три параллельных воркера - это число подбираю под нагрузку, а не ставлю наугад: для проекта с редкими письмами хватает одного, для магазина с оплатами и вебхуками в пик обычно беру 3-4 процесса и слежу за очередью через мониторинг. max-time=3600 заставляет воркер перезапускаться раз в час сам по себе, это подстраховка от медленных утечек памяти, которые не успевают вылезти за час работы. После правки конфига:

supervisorctl reread
supervisorctl update
supervisorctl start laravel-worker:*

Отдельный момент - деплой. Воркер держит код в памяти на момент старта, поэтому после каждого деплоя новую версию кода он не увидит, пока процесс не перезапустят. В CI/CD после выкладки кода всегда добавляю php artisan queue:restart - команда не убивает процессы принудительно, а ставит флаг, и воркер завершает текущую задачу и перезапускается сам, Supervisor поднимает его заново с новым кодом.

Laravel Horizon для мониторинга очередей на Redis

Eсли драйвер очереди - Redis, ставлю Horizon (composer require laravel/horizon). Он даёт дашборд с графиками пропускной способности, списком упавших задач, временем выполнения по каждому job и удобным ретраем прямо из интерфейса вместо ковыряния в failed_jobs через tinker.

В config/horizon.php задаю баланс воркеров под окружение:

'production' => [
    'supervisor-1' => [
        'connection' => 'redis',
        'queue' => ['payments', 'default'],
        'balance' => 'auto',
        'minProcesses' => 2,
        'maxProcesses' => 8,
        'tries' => 3,
    ],
],

balance: auto сам поднимает и опускает число процессов в зависимости от длины очереди - удобно, когда нагрузка скачет (распродажа, рассылка по базе), и не хочется вручную считать numprocs как в Supervisor. Сам Horizon тоже запускается через Supervisor одной строкой: php artisan horizon вместо queue:work. Дашборд по умолчанию доступен только в local-окружении, для продакшена доступ ограничиваю через Gate в HorizonServiceProvider, иначе список задач с содержимым payload увидит кто угодно, кто угадает URL.

Если очередь на database-драйвере, Horizon не подключить - для неё смотрю на failed_jobs и на длину очереди через php artisan queue:monitor database:default --max=100, которая пишет в лог и может слать уведомление, если задач скопилось больше порога.

Частые ошибки при настройке воркеров под фоновые задачи

За несколько лет работы с Laravel на разных проектах чаще всего натыкаюсь на одни и те же грабли:

  • Забыли queue:restart после деплоя - воркер месяцами работает на старой версии кода, баг вроде бы поправлен в репозитории, а в проде всё ещё падает.
  • Не задан timeout - тяжёлый job (например, генерация большого PDF-отчёта) виснет и держит воркер часами, а остальные задачи в очереди просто ждут.
  • Один воркер на все очереди без приоритета - критичная задача (подтверждение оплаты) стоит в общей куче после тысячи фоновых обновлений кэша.
  • Нет обработки failed_jobs - задачи падают молча, узнают об этом только когда клиент напишет, что не пришло письмо с чеком.
  • Слишком много numprocs на слабом сервере - три воркера одновременно упираются в память и CPU, обычный HTTP-трафик сайта начинает тормозить.

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

Частые вопросы

Сколько воркеров нужно запускать для проекта на Laravel?

Зависит от объёма фоновых задач и ресурсов сервера. Для небольшого проекта с редкими письмами хватает одного процесса. Для интернет-магазина с оплатами, доставкой и рассылками обычно ставлю 2-4 воркера с разделением по очередям через параметр - queue, а точное число подбираю по факту, глядя на длину очереди в Horizon или через queue:monitor.

Что делать, если очередь Laravel зависла и задачи не выполняются?

Сначала проверяю, жив ли сам процесс воркера: supervisorctl status laravel-worker:*. Если процесс жив, но задачи не двигаются - смотрю на timeout, возможно один job завис на внешнем API и держит воркер. Если процесс упал, Supervisor должен поднять его сам благодаря autorestart=true; если конфиг без Supervisor, значит воркер просто умер после ошибки и нужно перезапускать вручную.

Нужен ли Horizon, если очередь работает на database, а не на Redis?

Нет, Horizon работает только с Redis-подключением. Для database-драйвера мониторю очередь командой queue:monitor и таблицей failed_jobs напрямую. Если проект растёт и очередь начинает нагружать основную базу данных, это обычно сигнал переезжать на Redis и подключать Horizon.

Как перезапустить воркеры после деплоя без потери задач?

Командой php artisan queue:restart. Она не убивает процесс мгновенно - ставит сигнал в кэш, и воркер завершает текущую задачу до конца, только после этого останавливается. Supervisor видит остановку и поднимает процесс заново уже с обновлённым кодом, задачи из очереди при этом не теряются.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно