Панель управления делает то, для чего создана: сайт, домен, сертификат, база, версия PHP на выбор, cron. Всего, что связано с фоновой обработкой, в ней нет. Воркер, запущенный в SSH-сессии, живёт до закрытия терминала.
Продолжение темы, начатой в статьях про Composer с нужной версией PHP и про Redis на таком сервере. Здесь — про то, как заставить очередь работать постоянно.
Что даёт панель и чего в ней нет
Есть: несколько версий PHP одновременно, у каждого сайта своя; планировщик заданий с интерфейсом; отдельный системный пользователь на сайт.
Нет: менеджера процессов, интерфейса к очередям, мониторинга фоновых задач.
Отдельная особенность, которая влияет на всё дальнейшее: PHP для сайта и PHP в консоли — разные. В консоли у пользователя обычно системный PHP, часто старее того, на котором работает сайт, и с другим набором расширений.
Три способа держать воркер живым
Cron с повторным запуском. Каждую минуту cron пытается запустить воркер, а блокировка не даёт запустить второй экземпляр.
* * * * * cd /var/www/user/data/www/app && flock -n /tmp/worker.lock /opt/php84/bin/php artisan queue:work --stop-when-empty >> storage/logs/worker.log 2>&1Работает без прав root. Минус: задержка до минуты после падения и на старте.
systemd. Правильный способ на своём сервере, требует root. На арендованном VPS с панелью root обычно есть, и я использую именно его в одном проекте.
Supervisor. Тоже требует root для установки, зато удобнее для нескольких воркеров и не завязан на конкретный дистрибутив.
Я остановился на supervisor. Причина прагматичная: та же конфигурация, что и в Docker локально, только пути другие.
Конфигурация под пользователя панели
[program:app-queue]
command=/opt/php84/bin/php /var/www/user/data/www/app/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
directory=/var/www/user/data/www/app
user=user
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=5
stopwaitsecs=120
stdout_logfile=/var/www/user/data/www/app/storage/logs/worker.log
redirect_stderr=trueТри момента, специфичных для панели.
Полный путь к нужному PHP. Не php, а /opt/php84/bin/php. Системный PHP почти наверняка не тот, что нужен приложению, и воркер упадёт на несовместимости синтаксиса или на отсутствующем расширении.
Пользователь сайта, а не root. Иначе файлы кеша и логов создадутся от root, и веб-сервер потом не сможет в них писать. Симптом — сайт падает с ошибкой прав через несколько часов после того, как всё настроили.
Каталог сайта в directory. Относительные пути в приложении отсчитываются от него.
Конфигурационный файл может потребовать явного указания настроек PHP, если панель хранит их отдельно для каждой версии:
command=/opt/php84/bin/php -c /var/www/user/data/php-bin-isp-php84/php.ini /var/www/user/data/www/app/artisan queue:workБез этого воркер получит настройки по умолчанию, а не те, что у сайта. Разница обычно в лимите памяти и в подключённых расширениях.
Планировщик
В Laravel одна запись cron заменяет весь планировщик:
* * * * * cd /var/www/user/data/www/app && /opt/php84/bin/php artisan schedule:run >> /dev/null 2>&1Добавляется через интерфейс панели в разделе планировщика, а не через crontab -e — панель управляет своей копией и может перезаписать ручные правки.
Тот же полный путь к PHP. Ошибка здесь встречается чаще всего: команда добавлена, cron её выполняет, но системный PHP падает на синтаксисе, а вывод отправлен в никуда. Симптом — «планировщик не работает» без единой записи в логах.
При настройке я всегда сначала пишу вывод в файл и только потом, убедившись, что всё идёт, меняю на отбрасывание.
Перезапуск при выкладке
Воркеры держат код в памяти. Без перезапуска они продолжают работать со старой версией после обновления.
Скрипт обновления заканчивается тремя командами:
/opt/php84/bin/php artisan migrate --force
/opt/php84/bin/php artisan config:cache
/opt/php84/bin/php artisan queue:restartКоманда перезапуска не убивает процессы. Она ставит метку в кеш, воркеры проверяют её после каждой задачи и завершаются штатно. Supervisor поднимает новые.
config:cache эту метку не удаляет: конфигурационный кеш и application cache — разные вещи. Порядок выше нужен, чтобы команда стартовала уже с новой конфигурацией. Если при выкладке меняются cache store или prefix, старые воркеры могут не увидеть метку в новом кеше; один раз перезапускаю их напрямую через supervisorctl restart app-queue:*.
Диагностика: очередь стоит, ошибок нет
Порядок, к которому я пришёл после нескольких таких случаев.
Процессы живы?
supervisorctl status app-queue:*Статус, отличный от RUNNING, — ответ найден. Смотрим лог, там причина падения.
Что в очереди?
Сначала беру database, prefix и имя очереди из конфигурации приложения. Фрагмент ручной проверки выглядит так:
redis-cli -n "$REDIS_DB" --scan --pattern "${REDIS_PREFIX}*queues:*"
redis-cli -n "$REDIS_DB" llen "$QUEUE_KEY"QUEUE_KEY — точный ключ из результата первой команды, а не захардкоженный queues:default. Ноль при ожидающих задачах означает, что приложение пишет в другую базу, очередь или с другим префиксом, чем читает воркер. Классика при нескольких проектах на одном Redis.
Не завис ли воркер на задаче?
ps -o pid,etime,cmd -p $(pgrep -f "queue:work")Время работы больше лимита --max-time означает, что процесс висит на чём-то, что не проверяет флаги — обычно на сетевом запросе без тайм-аута.
Что в проваленных?
/opt/php84/bin/php artisan queue:failedТот ли PHP?
supervisorctl status app-queue:* | head -1
cat /etc/supervisor/conf.d/app-queue.conf | grep commandПроверка занимает секунду и закрывает целый класс проблем, которые выглядят как что угодно, кроме версии интерпретатора.
Ограничения такой схемы
Описанная одноузловая схема масштабируется только вертикально: больше нагрузки — больше процессов на том же сервере. Сама Redis-очередь не мешает поднять воркеры на втором сервере, но это уже отдельный worker-host с общей конфигурацией, кодом и мониторингом.
Мониторинга нет. Длину очереди и возраст старейшей задачи я смотрю руками или простым скриптом, который шлёт письмо при превышении порога.
Нет изоляции. Воркер работает в том же окружении, что и сайт, и тяжёлая задача отъедает ресурсы у веб-запросов. Ограничить это на уровне панели нечем.
Для проекта с десятками тысяч задач в сутки этого достаточно. Для большего нужен либо отдельный сервер под обработку, либо переезд в контейнеры — и второе я обычно и предлагаю, когда доходит до такого объёма.
Итог
Настройка занимает полчаса, из которых двадцать минут — выяснение правильных путей к PHP и к каталогу сайта в конкретной панели.
Главное, что стоит запомнить: пользователь и путь к интерпретатору. Обе ошибки не дают внятной диагностики, обе выглядят как «очередь не работает», и обе решаются одной строкой в конфигурации.