Воркеры в Laravel - mcodex

Воркеры в Laravel

«Воркер» — это не какая-то особая сущность фреймворка. Это обычный долгоживущий PHP-процесс, запущенный из командной строки, который крутит бесконечный цикл и не завершается.

Когда вы выполняете php artisan queue:work, PHP-скрипт запускается и просто не возвращает управление. Внутри у него:

while (true) {
    $job = взять задачу из Redis;

    if (!$job) {
        sleep(3);          // очередь пуста
        continue;
    }

    unserialize($job);     // развернуть объект
    $job->handle();        // выполнить

    удалить задачу из Redis (или вернуть при ошибке);

    проверить: не пора ли мне умереть?  // память, --max-jobs, --max-time, сигнал
}

Всё. Воркер — это исполнитель цикла «взять → выполнить → повторить».

Чем воркер принципиально отличается от обычного PHP-запроса

Вот это главное, что нужно уложить в голове.

При работе через PHP-FPM каждый HTTP-запрос — это чистый лист: фреймворк загружается, отрабатывает, умирает, вся память освобождается. Утечки не страшны, «протухшее» состояние невозможно.

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

Утечки памяти накапливаются. Процесс, отработавший сутки, может съесть гигабайт. Поэтому у воркера есть --memory=128: после каждой задачи он проверяет memory_get_usage() и, превысив лимит, сам завершается — чтобы менеджер процессов поднял свежий.

Изменения в коде не подхватываются. Воркер держит в памяти классы, загруженные при старте. Задеплоили новую версию job — процесс продолжит выполнять старую. Именно поэтому после каждого деплоя обязателен php artisan queue:restart (или horizon:terminate).

Состояние приложения «протухает». Синглтоны в контейнере, статические свойства, уже разрешённые сервисы, Auth::user() от предыдущей задачи, изменённая конфигурация — всё это переживает границу между задачами. Классический баг: в одной job поменяли локаль или подключение к БД, и следующие сто задач работают с чужими настройками. Laravel сбрасывает часть состояния между задачами, но не всё — на свои синглтоны полагаться нельзя.

Есть альтернатива — php artisan queue:listen: он поднимает отдельный дочерний процесс на каждую задачу, то есть чистый лист как при HTTP. Ни утечек, ни протухшего кода, ни queue:restart. Расплата — бутстрап фреймворка на каждую задачу, что в разы медленнее. В продакшене используют queue:work, listen — иногда в разработке.

Параллелизм — это просто много процессов

В PHP нет потоков. «Десять воркеров» означает буквально десять независимых процессов ОС, у каждого свой PID и своя память. Они ничего друг о друге не знают и координируются исключительно через Redis.

Отсюда вопрос: почему два воркера не схватят одну задачу? Потому что операция «забрать» атомарна — Laravel выполняет Lua-скрипт, который одним неделимым действием вынимает задачу из списка и кладёт её в sorted set queues:default:reserved с меткой времени. Второй воркер её там уже не увидит. Если первый умрёт, не доделав, — через retry_after секунд задача из reserved вернётся в основной список и достанется кому-то другому.

Как воркер умирает

Штатных причин несколько, и все они — часть дизайна:

  • --timeout — задача выполняется дольше лимита. Реализовано через pcntl_alarm(): перед запуском job взводится будильник, по срабатыванию процесс убивается. Это единственный способ прервать зависший handle() — потому и нужно расширение pcntl.
  • --memory — превышен лимит памяти, воркер выходит после текущей задачи.
  • --max-jobs / --max-time — профилактический перезапуск: отработал 1000 задач или час, вышел, поднялся заново с чистой памятью.
  • СигналSIGTERM при деплое или остановке. Воркер дорабатывает текущую задачу и выходит.

Во всех случаях подразумевается, что процесс кто-то перезапустит. Сам по себе воркер не самовосстанавливается — «умереть и переродиться» и есть его штатный способ обновиться. Именно это делает Horizon (для своих воркеров) или supervisord (для Horizon в целом).

Где воркеры в схеме Очереди в Laravel, Horizon, Supervisor

В тексте статьи воркеры — нижний уровень иерархии, процессы php artisan horizon:work. Это те же самые queue:work по сути, только запущенные не вами вручную, а процессом horizon:supervisor согласно config/horizon.php, и с дополнительной отправкой метрик в Redis для дашборда.