«Воркер» — это не какая-то особая сущность фреймворка. Это обычный долгоживущий 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 для дашборда.