Загрузка...

Laravel Horizon: динамический автоскейлинг очередей, изоляция памяти и подводные камни в продакшене

Мониторинг, балансировка и полный контроль над фоновыми задачами
Laravel Horizon: динамический автоскейлинг очередей, изоляция памяти и подводные камни в продакшене

При росте нагрузки в 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,
        ],
    ],
],

Разбор критических параметров:

  1. balance => 'auto': Horizon динамически выделяет дочерние воркеры тем очередям, где растет задержка. Если очередь пуста, процесс перекидывается туда, где есть нагрузка.
  2. autoScalingStrategy => 'time': В Laravel доступны две стратегии масштабирования:
    • 'time' (рекомендуется): Horizon оценивает реальное расчетное время ожидания задачи в очереди (Wait Time). Очередь с 5 тяжелыми задачами по 30 секунд получит больше ресурсов, чем очередь со 100 секундными операциями.
    • 'size': Балансировка опирается исключительно на количество элементов в очереди.
  3. balanceMaxShift и balanceCooldown: Защита от колебаний пула процессов (process flapping). balanceMaxShift => 2 ограничивает масштабирование не более чем на 2 воркера за один шаг, а balanceCooldown => 3 задает 3-секундную паузу между пересчетами.
  4. 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 сразу всей группе процессов, что с высокой долей вероятности убьет выполняющиеся транзакции посреди записи в БД.

Правильный сценарий деплоя:

  1. После раскатки нового кода и миграций вызывается команда:
    php artisan horizon:terminate
  2. Мастер-процесс Horizon переходит в режим graceful shutdown:
    • Перестает забирать новые задачи из очередей Redis.
    • Дает активным дочерним воркерам завершить текущие операции.
    • После завершения всех активных задач процесс Horizon штатно выходит с кодом 0.
  3. Системный 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, если в очереди есть тяжелые долгоиграющие задачи.

Резюме

  1. Используйте balance => 'auto' со стратегией time для динамического предотвращения пробок в очередях.
  2. Обязательно выставляйте maxmemory-policy noeviction в Redis, чтобы предотвратить потерю задач при переполнении памяти.
  3. В деплой-пайплайнах перезапускайте пул исключительно через php artisan horizon:terminate с высоким stopwaitsecs в Supervisor для гарантии целостности данных.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *