При росте нагрузки в Laravel стандартного демона queue:work под управлением Supervisor перестает хватать: воркеры статично распределены по очередям, внезапный наплыв тяжелых задач парализует выполнение критичных нотификаций, а процессы медленно текут по памяти. Laravel Horizon решает эту проблему за счет супервизора очередей для Redis с динамическим автоскейлингом, веб-дашбордом и мониторингом метрик в реальном времени. Ниже — архитектурный разбор настройки Horizon под современные версии Laravel 11/12 и PHP 8.3/8.4, управление памятью и нюансы деплоя.
Архитектура под капотом: чем Horizon отличается от queue:work
При использовании обычного php artisan queue:work за каждым процессом закреплен фиксированный список очередей. Если воркеров выделено 5, а очередь default забита тяжелыми отчетами, задачи из очереди high будут ждать освобождения процесса, даже если сервер простаивает по CPU.
Horizon работает как мастер-процесс (оркестратор), управляющий пулом дочерних супервизоров:
- Мастер-процесс опрашивает Redis и отслеживает две ключевые метрики: глубину очередей (длину) и ориентировочное время ожидания выполнения (Wait Time).
- На основе выбранной стратегии балансировки Horizon динамически перераспределяет доступные воркеры между очередями без перезапуска всего пула.
- Вся операционная телеметрия (длительность задач, ошибки, метрики пропускной способности throughput) пишется в Redis-ключи со скользящим окном хранения.
Конфигурация продакшен-супервизора: стратегии балансировки
Основной файл конфигурации — config/horizon.php. В секции environments.production настраивается поведение пула воркеров:
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'reports'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'minProcesses' => 2,
'maxProcesses' => 16,
'balanceMaxShift' => 2,
'balanceCooldown' => 3,
'memory' => 256,
'tries' => 3,
'timeout' => 120,
'nice' => 0,
],
],
],
Разбор критических параметров:
balance => 'auto': Horizon динамически выделяет дочерние воркеры тем очередям, где растет задержка. Если очередь пуста, процесс перекидывается туда, где есть нагрузка.autoScalingStrategy => 'time': В Laravel доступны две стратегии масштабирования:'time'(рекомендуется): Horizon оценивает реальное расчетное время ожидания задачи в очереди (Wait Time). Очередь с 5 тяжелыми задачами по 30 секунд получит больше ресурсов, чем очередь со 100 секундными операциями.'size': Балансировка опирается исключительно на количество элементов в очереди.
balanceMaxShiftиbalanceCooldown: Защита от колебаний пула процессов (process flapping).balanceMaxShift => 2ограничивает масштабирование не более чем на 2 воркера за один шаг, аbalanceCooldown => 3задает 3-секундную паузу между пересчетами.minProcessesиmaxProcesses: Базовый порог процессов, которые всегда висят в памяти (чтобы избежать холодного старта), и жесткий потолок, выше которого Horizon не будет поднимать нагрузку на процессор сервера.
Память процессов и борьба с тихими утечками
В долгоживущих процессах PHP (CLI воркеры) любая статическая переменная, Singleton-сервис или накопление событий в памяти фреймворка приводят к медленному раздуванию RSS-памяти (Resident Set Size). Если процесс не перезапускать, он неизбежно выбивает OOM Killer операционной системы.
Для предотвращения утечек в Horizon настраиваются превентивные лимиты:
- Параметр
'memory' => 256в конфиге означает, что если воркер после выполнения очередной задачи потребил более 256 МБ, он аккуратно завершается, а мастер-процесс мгновенно поднимает свежий воркер взамен. - Использование флагов завершения по количеству задач:
'maxJobs' => 500, 'maxTime' => 3600,Ротация процессов каждые 500 выполненных задач или каждый час гарантирует стабильную очистку памяти PHP-рантайма даже при наличии мелких скрытых утечек в сторонних библиотеках.
Настройка Redis: политика maxmemory-policy
Критический нюанс, о котором часто забывают при настройке сервера Redis для Horizon — режим вытеснения ключей при исчерпании RAM.
По умолчанию на многих серверах включена политика volatile-lru или allkeys-lru. Если база Redis переполняется, Redis начинает молча удалять ключи с установленным TTL или старые данные. В случае Horizon удаление метаданных очереди или списка зарезервированных задач приведет к потере задач без вызова механизма failed_jobs.
Для инстанса Redis, обслуживающего очереди, в /etc/redis/redis.conf должна быть установлена жесткая политика:
maxmemory-policy noeviction
При достижении лимита памяти Redis вернет ошибку записи (OOM), что сразу отобразится в логах и алертах, но ни одна задача из очередей не будет удалена втихую.
Для предотвращения раздувания метрик самого Horizon настройте очистку в config/horizon.php:
'trim' => [
'recent' => 60, // Хранить завершенные задачи 60 минут
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080, // Неудачные задачи хранятся 7 дней
'failed' => 10080,
'monitored' => 10080,
],
Бесшовный деплой и graceful shutdown (horizon:terminate)
Horizon не перезагружает измененный PHP-код на лету. После каждого обновления проекта через Git дочерние процессы должны быть перезапущены, но без обрыва выполняющихся в этот момент задач.
Нельзя вызывать supervisorctl restart horizon напрямую в деплой-скрипте — системный Supervisor отправляет сигнал SIGTERM сразу всей группе процессов, что с высокой долей вероятности убьет выполняющиеся транзакции посреди записи в БД.
Правильный сценарий деплоя:
- После раскатки нового кода и миграций вызывается команда:
php artisan horizon:terminate - Мастер-процесс Horizon переходит в режим graceful shutdown:
- Перестает забирать новые задачи из очередей Redis.
- Дает активным дочерним воркерам завершить текущие операции.
- После завершения всех активных задач процесс Horizon штатно выходит с кодом
0.
- Системный Supervisor, контролирующий сам мастер-процесс, фиксирует его штатное завершение и автоматически перезапускает с уже обновленным кодом приложения.
Пример корректного юнита Supervisor для /etc/supervisor/conf.d/horizon.conf:
[program:horizon]
process_name=%(program_name)s
command=php /var/www/yourapp/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/yourapp/storage/logs/horizon.log
stopwaitsecs=3600
Параметр stopwaitsecs=3600 критичен: он указывает Supervisor ждать до 1 часа перед принудительным убийством процесса по SIGKILL, если в очереди есть тяжелые долгоиграющие задачи.
Резюме
- Используйте
balance => 'auto'со стратегиейtimeдля динамического предотвращения пробок в очередях. - Обязательно выставляйте
maxmemory-policy noevictionв Redis, чтобы предотвратить потерю задач при переполнении памяти. - В деплой-пайплайнах перезапускайте пул исключительно через
php artisan horizon:terminateс высокимstopwaitsecsв Supervisor для гарантии целостности данных.