Laravel Queues в 2026 году: горизонтальное масштабирование воркеров с Redis и Kubernetes
Введение: когда монолит перестаёт справляться
Очереди задач — одна из первых вещей, которые ломаются при росте нагрузки. Пока трафик умеренный, один процесс php artisan queue:work на сервере справляется. Но когда число заданий в очереди начинает расти быстрее, чем воркер их обрабатывает, возникают классические проблемы: задержки в доставке email, зависшие экспорты, накопившиеся уведомления и переполненные очереди.
В монолитной архитектуре горизонтальное масштабирование воркеров — боль. Нужно вручную запускать новые процессы, следить за их состоянием, балансировать нагрузку. В 2026 году стек Laravel + Redis + Kubernetes закрывает эту проблему элегантно: вы декларируете желаемое состояние, а инфраструктура сама подстраивается под нагрузку.
Эта статья — практическое руководство для middle/senior PHP-разработчиков, которые хотят выстроить надёжную, масштабируемую систему обработки фоновых задач на базе Laravel Queues, Redis Cluster и Kubernetes с HPA.
Архитектура Laravel Queues + Redis в 2026 году
Именованные очереди и приоритеты
Laravel позволяет организовать несколько именованных очередей с разными приоритетами. Воркер обрабатывает очереди в порядке, указанном при запуске:
php artisan queue:work redis --queue=critical,high,default,low --sleep=3 --tries=3 --max-time=3600
В коде задачи приоритет задаётся через свойство $queue или при диспетчеризации:
<?php
// Диспетчеризация в конкретную очередь
SendInvoice::dispatch($order)->onQueue('critical');
// Или через свойство класса
class ProcessPayment implements ShouldQueue
{
public string $queue = 'critical';
public int $tries = 5;
public int $timeout = 120;
public int $backoff = 30;
}
Батчи задач (Job Batching)
Job Batching, появившийся в Laravel 8, в 2026 году стал стандартом для параллельной обработки больших объёмов данных. Батч позволяет запустить тысячи задач и получить колбэк по завершению:
<?php
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$batch = Bus::batch([
new ImportChunk($chunk1),
new ImportChunk($chunk2),
new ImportChunk($chunk3),
])->then(function (Batch $batch) {
// Все задачи выполнены успешно
ImportCompleted::dispatch($batch->id);
})->catch(function (Batch $batch, Throwable $e) {
// Первый сбой в батче
Log::error('Batch failed', ['batch_id' => $batch->id, 'error' => $e->getMessage()]);
})->finally(function (Batch $batch) {
// Всегда выполняется
Cache::forget('import_lock');
})->onQueue('high')
->dispatch();
Конфигурация queue.php для Redis
<?php
// config/queue.php
return [
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'queue', // отдельное Redis-соединение
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => 5, // блокирующий BLPOP вместо поллинга
'after_commit' => true, // диспетч только после коммита транзакции
],
],
];
Параметр after_commit критически важен в production: он гарантирует, что задача попадёт в очередь только после успешной фиксации транзакции в базе данных.
Настройка Redis Cluster как backend для очередей
Для высоконагруженных систем одиночный Redis-экземпляр становится узким местом. Redis Cluster распределяет данные по нескольким нодам и обеспечивает отказоустойчивость.
Настройте отдельное Redis-соединение для очередей в config/database.php:
<?php
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'queue' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', '1'),
// Для Redis Cluster
'cluster' => [
['host' => 'redis-node-1', 'port' => 6379, 'password' => env('REDIS_PASSWORD')],
['host' => 'redis-node-2', 'port' => 6379, 'password' => env('REDIS_PASSWORD')],
['host' => 'redis-node-3', 'port' => 6379, 'password' => env('REDIS_PASSWORD')],
],
'options' => [
'cluster' => 'redis',
],
],
],
Важный момент: при использовании Redis Cluster все ключи одной очереди должны попадать в один слот. Laravel автоматически использует хэш-теги ({queue_name}) для этого, но убедитесь, что версия predis или расширение phpredis поддерживает кластерный режим.
Контейнеризация воркеров: Dockerfile и Supervisor
Dockerfile для воркера
FROM php:8.3-cli-alpine
# Системные зависимости
RUN apk add --no-cache \
supervisor \
git \
curl \
libpng-dev \
oniguruma-dev \
libxml2-dev
# PHP-расширения
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath
# phpredis для лучшей производительности
RUN pecl install redis && docker-php-ext-enable redis
# Opcache для CLI (ускоряет запуск воркера)
RUN docker-php-ext-install opcache
# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
COPY . .
# Конфиг Supervisor
COPY docker/supervisor/worker.conf /etc/supervisor/conf.d/worker.conf
# Права
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache
USER www-data
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
Конфигурация Supervisor
; docker/supervisor/worker.conf
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work redis \
--queue=critical,high,default,low \
--sleep=3 \
--tries=3 \
--max-time=3600 \
--memory=256 \
--timeout=90
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=%(ENV_WORKER_NUMPROCS)s
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stopwaitsecs=3600
Переменная окружения WORKER_NUMPROCS позволяет управлять числом процессов внутри контейнера. В связке с Kubernetes это даёт двухуровневое масштабирование: количество подов × количество процессов внутри пода.
Деплой воркеров как Kubernetes Deployment с HPA
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue-worker
namespace: production
labels:
app: laravel-queue-worker
spec:
replicas: 3
selector:
matchLabels:
app: laravel-queue-worker
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 0 # zero-downtime при обновлении
template:
metadata:
labels:
app: laravel-queue-worker
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9253"
spec:
terminationGracePeriodSeconds: 3600 # ждём завершения длинных задач
containers:
- name: worker
image: your-registry/laravel-app:latest
imagePullPolicy: Always
env:
- name: APP_ENV
value: production
- name: QUEUE_CONNECTION
value: redis
- name: WORKER_NUMPROCS
value: "2"
envFrom:
- secretRef:
name: laravel-secrets
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
lifecycle:
preStop:
exec:
# Graceful shutdown: дождаться завершения текущей задачи
command: ["/bin/sh", "-c", "php artisan queue:restart && sleep 30"]
restartPolicy: Always
Horizontal Pod Autoscaler (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-queue-worker-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-queue-worker
minReplicas: 2
maxReplicas: 20
metrics:
# Масштабирование по CPU
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Масштабирование по кастомной метрике (размер очереди из Prometheus)
- type: External
external:
metric:
name: laravel_queue_size
selector:
matchLabels:
queue: default
target:
type: AverageValue
averageValue: "100" # 1 под на каждые 100 задач в очереди
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 4
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # медленно уменьшаем, чтобы не флаппать
policies:
- type: Pods
value: 1
periodSeconds: 120
Масштабирование по кастомной метрике (размер очереди Redis) — ключевое преимущество этого подхода. Когда очередь растёт, Kubernetes автоматически добавляет воркеры; когда она пустеет — уменьшает их количество, экономя ресурсы.
Мониторинг очередей: Laravel Horizon и Prometheus
Настройка Laravel Horizon
Laravel Horizon предоставляет красивый дашборд и детальную статистику по очередям. Установка:
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Конфигурация config/horizon.php для Kubernetes-окружения:
<?php
// config/horizon.php
return [
'use' => 'default',
'prefix' => env('HORIZON_PREFIX', 'horizon:'),
'middleware' => ['web', 'auth'],
'waits' => [
'redis:critical' => 3, // алерт если задача ждёт >3 сек
'redis:default' => 60,
'redis:low' => 300,
],
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080, // 7 дней
'failed' => 10080,
'monitored' => 10080,
],
'silenced' => [],
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
'fast_termination' => false,
'memory_limit' => 256,
'environments' => [
'production' => [
'supervisor-critical' => [
'connection' => 'redis',
'queue' => ['critical'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'minProcesses' => 2,
'maxProcesses' => 10,
'tries' => 3,
'timeout' => 90,
],
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'autoScalingStrategy' => 'size',
'minProcesses' => 1,
'maxProcesses' => 20,
'balanceMaxShift' => 3,
'balanceCooldown' => 3,
'tries' => 3,
'timeout' => 120,
],
],
],
];
Prometheus-метрики для Kubernetes HPA
Для экспорта метрик Laravel-очередей в Prometheus используйте пакет spatie/laravel-prometheus или напишите собственный эндпоинт:
<?php
// app/Http/Controllers/MetricsController.php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Redis;
class MetricsController extends Controller
{
public function index(): string
{
$queues = ['critical', 'high', 'default', 'low'];
$output = '';
foreach ($queues as $queue) {
$size = Redis::connection('queue')->llen('queues:' . $queue);
$output .= "laravel_queue_size{queue=\"$queue\"} $size\n";
}
// Failed jobs count
$failedCount = Redis::connection('queue')->zcard('failed_jobs');
$output .= "laravel_queue_failed_jobs_total $failedCount\n";
return response($output, 200, ['Content-Type' => 'text/plain; version=0.0.4']);
}
}
Зарегистрируйте роут без middleware в routes/api.php:
Route::get('/metrics', [MetricsController::class, 'index'])
->middleware('throttle:60,1');
Обработка failed jobs и dead letter queue
Конфигурация повторных попыток
В классе задачи настройте стратегию повторных попыток с экспоненциальной задержкой:
<?php
class ProcessWebhook implements ShouldQueue
{
public int $tries = 5;
public int $timeout = 60;
public int $maxExceptions = 3; // сбой после 3 исключений, даже если tries не исчерпаны
// Экспоненциальный backoff: 10, 60, 180, 420 секунд
public function backoff(): array
{
return [10, 60, 180, 420];
}
public function handle(): void
{
// обработка вебхука
}
public function failed(Throwable $exception): void
{
// Уведомление в Slack/PagerDuty при окончательном сбое
Log::error('Webhook processing failed', [
'job_id' => $this->job->getJobId(),
'error' => $exception->getMessage(),
'payload' => $this->webhook->toArray(),
]);
// Помещаем в dead letter queue для ручного анализа
DeadLetterQueue::push($this->webhook, $exception);
}
}
Dead Letter Queue через отдельную очередь
Реализуйте паттерн Dead Letter Queue через именованную очередь dead-letter и отдельный воркер для мониторинга:
<?php
class DeadLetterQueue
{
public static function push(mixed $payload, Throwable $exception): void
{
Redis::connection('queue')->rpush('queues:dead-letter', json_encode([
'payload' => serialize($payload),
'exception' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
'failed_at' => now()->toISOString(),
]));
}
public static function requeue(string $jobId): void
{
// Логика для повторной диспетчеризации из DLQ
Artisan::call('queue:retry', ['id' => [$jobId]]);
}
}
Artisan-команды для управления failed jobs
# Список упавших задач
php artisan queue:failed
# Повторная попытка конкретной задачи
php artisan queue:retry 5d5a4fb3-5e82-4af3-bedf-6df64b4ee59b
# Повторная попытка всех упавших задач
php artisan queue:retry all
# Очистка упавших задач старше 7 дней
php artisan queue:prune-failed --hours=168
# Просмотр размера очередей через Horizon
php artisan horizon:status
Советы по production-настройке и подводные камни
Graceful shutdown в Kubernetes
Один из главных подводных камней — принудительное убийство пода при масштабировании вниз. Настройте terminationGracePeriodSeconds равным максимальному таймауту задачи, и используйте preStop хук для отправки сигнала queue:restart. Воркеры завершат текущие задачи и корректно остановятся.
Memory leaks в долгоживущих воркерах
PHP не освобождает память так эффективно, как Go или Java. Используйте параметры --max-time и --memory чтобы воркер перезапускался до достижения критических значений. Supervisor автоматически запустит новый процесс.
Идемпотентность задач
Всегда проектируйте задачи идемпотентно: повторный запуск одной задачи не должен приводить к дублированию эффектов. Используйте Redis-блокировки для критических секций:
<?php
public function handle(): void
{
$lock = Cache::lock('process-order-' . $this->orderId, 120);
if (! $lock->get()) {
Log::warning('Job already running, skipping', ['order_id' => $this->orderId]);
return;
}
try {
$this->processOrder();
} finally {
$lock->release();
}
}
Разделение очередей по критичности
Не смешивайте платёжные задачи и рассылку новостей в одной очереди. Выделите отдельные Deployment-ы для критических очередей с гарантированными ресурсами и отдельные — для фоновых задач с лимитами.
Мониторинг lag-времени
Настройте алерты на время ожидания задачи в очереди (queue lag). Если задача в очереди critical ждёт больше 5 секунд — это уже инцидент. Laravel Horizon из коробки показывает этот параметр на дашборде.
Redis persistence
Убедитесь, что Redis настроен с AOF-персистентностью (appendonly yes) или используйте Redis Sentinel/Cluster. Потеря данных в очереди при падении Redis без персистентности — реальный production-риск.
Заключение
Связка Laravel Queues + Redis Cluster + Kubernetes HPA в 2026 году — это зрелый, production-готовый подход к горизонтальному масштабированию фоновых задач. Вы получаете автоматическое масштабирование воркеров на основе реального размера очереди, graceful shutdown без потери задач, детальный мониторинг через Laravel Horizon и Prometheus, и надёжную обработку сбоев через dead letter queue.
Ключевые принципы, которые стоит запомнить: задачи должны быть идемпотентными, воркеры — stateless и легко перезапускаемыми, а мониторинг метрик очереди должен быть основой для принятия решений об автоскейлинге. Начните с базовой конфигурации из этой статьи и адаптируйте её под специфику вашей нагрузки.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →