<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
DEVOPS · JUNIOR

Як тримати черги й воркери живими в продакшені: supervisor, systemd чи контейнер?

Воркер - це звичайний PHP-процес, який рано чи пізно завершиться, тому поруч має бути менеджер процесів: supervisor із `autorestart=true` і `numprocs`, systemd-юніт із `Restart=always` або сам оркестратор контейнерів. Ключове налаштування - час очікування після SIGTERM (`stopwaitsecs`, `TimeoutStopSec`), більший за найдовшу задачу.

Задачі в черзі не виконуються з ночі, хоча вчора все працювало. Де шукати?
Чим `php artisan queue:work` у продакшені відрізняється від запуску в терміналі?
Що відбувається з задачею, яка виконувалась у момент деплою?
Чому supervisor пише про воркер FATAL і більше не намагається його підняти?
supervisor systemd черги queue:work SIGTERM деплой

php artisan queue:work завантажує фреймворк один раз і далі крутить цикл: узяв задачу з Redis чи бази, виконав, пішов по наступну. Процес живе годинами, але колись обовʼязково завершиться: фатальна помилка в задачі, вбивство від OOM-killer, ребут сервера або штатний вихід по --max-time=3600, --max-jobs=1000, --memory=128. Останні три ставлять навмисно, бо довгоживучий PHP-процес накопичує памʼять і стан. Тобто періодично помирати для воркера нормально, і поруч має бути щось, що це помічає й піднімає новий процес. nohup ... & чи вкладка в screen на цю роль не годяться.

Під Laravel найчастіше беруть supervisor, для junior він і найпростіший. Конфіг кладуть у /etc/supervisor/conf.d/, numprocs задає кількість паралельних воркерів (supervisor сам розводить їх за process_name), autorestart=true піднімає процес після будь-якого коду виходу, включно з нульовим після --max-time. Значення unexpected тут не підходить: штатний вихід воркера помилкою не є, але перезапустити його все одно треба. Сам конфіг після редагування не перечитується, потрібні supervisorctl reread і supervisorctl update, а точковий рестарт групи робиться як supervisorctl restart queue-worker:*. Команда в command= мусить лишатись на передньому плані: якщо запускати обгортку, яка демонізується, supervisor побачить миттєве завершення й піде в цикл перезапусків до статусу FATAL.

Systemd дає те саме без зайвого демона, якщо на сервері й так systemd. Замість numprocs використовують шаблонний юніт queue-worker@.service і вмикають потрібну кількість екземплярів: systemctl enable --now queue-worker@{1..4}. Restart=always разом із RestartSec=5 відповідає autorestart, але тут є пастка, на якій регулярно спотикаються: за замовчуванням пʼять перезапусків за десять секунд переводять юніт у стан failed, і далі він просто лежить, поки хтось не зробить systemctl reset-failed. Для воркера, який іноді падає на старті, StartLimitIntervalSec=0 у секції [Unit] знімає це обмеження. Змінні оточення підвантажують через EnvironmentFile=, а не покладаються на профіль користувача, бо systemd не читає ~/.bashrc.

Обидва менеджери зупиняють воркер через SIGTERM, і Laravel ловить цей сигнал за допомогою ext-pcntl: обробник виставляє внутрішній прапорець, поточна задача доробляється до кінця, і тільки після цього процес виходить. Часу на це дають рівно стільки, скільки написано в stopwaitsecs (supervisor, типово 10 секунд) або TimeoutStopSec (systemd, типово 90). Далі йде SIGKILL, який нічого не питає. Задача, обірвана на SIGKILL, лишається зарезервованою в черзі, і через retry_after секунд її підхопить інший воркер, тобто половина роботи виконається двічі. Звідси два правила: час очікування ставлять більшим за найдовшу задачу, а retry_after у config/queue.php - більшим за --timeout воркера. Якщо ext-pcntl у збірці PHP немає, мʼякої зупинки не існує взагалі, і кожен деплой ріже задачі посеред виконання.

Логи воркерів розʼїжджаються по трьох місцях, і на співбесіді їх краще розділити вголос. Те, що воркер пише в stdout (рядки про оброблені задачі), у supervisor іде в stdout_logfile з вбудованою ротацією по stdout_logfile_maxbytes і stdout_logfile_backups, а в systemd - у journald, звідки читається як journalctl -u 'queue-worker@*' -f. Винятки всередині задач ідуть окремо, у звичайний лог-канал застосунку, і для черг корисно завести власний канал, щоб не змішувати їх із HTTP-трафіком. У контейнері картина простіша: власного менеджера не додають, перезапуском займається Compose або Kubernetes, CMD пишуть в exec-формі (інакше PID 1 стане /bin/sh і проковтне SIGTERM), лог шлють у stderr без файлів і ротації, а час на зупинку піднімають явно: docker stop дає десять секунд, Kubernetes - тридцять, і для задач на кілька хвилин цього мало.

; /etc/supervisor/conf.d/queue-worker.conf
[program:queue-worker]
command=php /var/www/app/artisan queue:work redis --tries=3 --max-time=3600
user=www-data
numprocs=4                    ; чотири незалежні процеси
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true              ; піднімати після будь-якого виходу, зокрема по --max-time
startsecs=1                   ; менше секунди життя = невдалий старт
startretries=3                ; після трьох невдалих стартів статус FATAL, спроб більше не буде
stopwaitsecs=180              ; довше за найдовшу задачу, інакше прилетить SIGKILL
stopasgroup=true              ; сигнал усій групі процесів (потрібно для horizon і queue:listen)
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/log/queue-worker.log
stdout_logfile_maxbytes=50MB  ; 0 вимикає ротацію: так роблять, коли лог іде в stdout контейнера
; supervisorctl reread && supervisorctl update   - підхопити зміни конфігу
; supervisorctl restart queue-worker:*           - перезапустити всі чотири процеси

# /etc/systemd/system/queue-worker@.service - шаблон, %i це номер екземпляра
[Unit]
Description=Queue worker %i
After=network.target redis.service
StartLimitIntervalSec=0       # без цього 5 падінь за 10 с назавжди лишать юніт у failed

[Service]
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php /var/www/app/artisan queue:work redis --tries=3 --max-time=3600
ExecReload=/usr/bin/php /var/www/app/artisan queue:restart
Restart=always
RestartSec=5
TimeoutStopSec=180            # KillSignal і так SIGTERM; після таймауту буде SIGKILL

[Install]
WantedBy=multi-user.target

# systemctl enable --now queue-worker@{1..4}
# journalctl -u 'queue-worker@*' -f    - логи, бо stdout іде в journald
Що `queue:work` завантажує фреймворк один раз і крутить цикл, тому процес завершується сам - через `--max-time`, `--max-jobs`, `--memory`, фатальну помилку чи OOM - і його має піднімати менеджер, а не людина.
Конкретні директиви supervisor: `numprocs` для кількох воркерів, `autorestart=true`, `stopwaitsecs`, `stopasgroup`, і що після зміни конфігу треба `supervisorctl reread && supervisorctl update`.
Що у systemd воркери роблять шаблонним юнітом `worker@.service` з `Restart=always`, а логи читають через `journalctl -u`, бо stdout іде в journald.
Механіку мʼякої зупинки: Laravel ловить SIGTERM через ext-pcntl, дочікує поточну задачу й виходить; менеджер має дати на це достатньо часу, інакше прилетить SIGKILL.
Що на деплої роблять `php artisan queue:restart`, а не `kill`, і що застарілий код у воркері - наслідок того, що процес не перезапустили.
Запускати воркер через `nohup php artisan queue:work &` або в `screen` і вважати це продакшеном: після ребуту чи фатальної помилки черга просто стоїть.
Поставити `stopwaitsecs=10` (значення за замовчуванням) при задачах на кілька хвилин - supervisor добиває процес SIGKILL посеред роботи.
Не помітити, що в PHP немає `ext-pcntl`: сигнали не обробляються взагалі, кожна зупинка воркера рве поточну задачу.
Забути `queue:restart` після деплою й потім годинами шукати, чому виправлений баг відтворюється: у памʼяті воркера стара версія класів.
Написати в supervisor команду, яка демонізується сама (наприклад, обгортку зі своїм `&`) - supervisor вважає процес завершеним і перезапускає його по колу.
Тримати `retry_after` у `config/queue.php` меншим за `--timeout` воркера: задача повертається в чергу, поки ще виконується, і робиться двічі.
ПОРАДА

Назвіть три рівні відповідальності одним реченням: «фреймворк дбає про мʼякий вихід по SIGTERM, менеджер процесів - про перезапуск і час очікування, деплой - про `queue:restart`». Далі згадайте, що `stopwaitsecs` ставлять більшим за найдовшу задачу - це та деталь, яку називають лише ті, хто справді ловив обрізані задачі.

оновлено 3 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Supervisor надсилає SIGTERM, чекає `stopwaitsecs` (типово 10 с) і добиває процес SIGKILL. Якщо задача триває довше, її обірвуть посеред роботи, і вона повторно виконається після `retry_after`.