Практичний досвід використання черг Laravel у високонавантажених проектах
Що таке Queue і навіщо воно потрібно
Queue (черги) в Laravel – це механізм для виконання важких завдань у фоновому режимі. Замість того щоб змушувати користувача чекати 30 секунд поки система відправить email, ми додаємо завдання в чергу і повертаємо миттєву відповідь.
Типові use cases:
- Відправка email/SMS
- Обробка зображень
- Синхронізація з зовнішніми API
- Генерація звітів
- Backup операції
Архітектура Laravel Queue
Laravel Queue складається з трьох основних компонентів:
|
1 2 3 4 |
[Job] → [Queue Driver] → [Worker Process] ↓ ↓ ↓ Завдання Зберігання Виконання |
Job – клас з логікою що треба виконати Queue Driver – механізм зберігання (database, Redis, SQS) Worker – процес що виймає завдання з черги та виконує
Налаштування Queue
1. Конфігурація (config/queue.php)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
return [ 'default' => env('QUEUE_CONNECTION', 'database'), 'connections' => [ 'database' => [ 'driver' => 'database', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, ], 'redis' => [ 'driver' => 'redis', 'connection' => 'default', 'queue' => 'default', 'retry_after' => 90, ], ], ]; |
2. Створення таблиць
|
1 2 3 4 |
php artisan queue:table php artisan queue:failed-table php artisan migrate |
3. Environment налаштування
|
1 2 3 |
QUEUE_CONNECTION=database QUEUE_FAILED_DRIVER=database |
Створення Job’ів
Базовий Job
|
1 2 |
php artisan make:job ProcessUserData |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class ProcessUserData implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; private $userId; private $data; public function __construct($userId, $data) { $this->userId = $userId; $this->data = $data; } public function handle() { // Логіка обробки $user = User::find($this->userId); $user->processData($this->data); } } |
Advanced Job з retry логікою
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class SyncWithExternalAPI implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries = 5; public $backoff = [10, 30, 60, 120, 300]; // Exponential backoff public $timeout = 120; private $apiData; public function __construct($apiData) { $this->apiData = $apiData; $this->onQueue('external-api'); // Окрема черга } public function handle() { try { $response = Http::timeout(60)->post('https://api.example.com', $this->apiData); if (!$response->successful()) { throw new \Exception("API returned {$response->status()}"); } // Обробка успішної відповіді } catch (\Exception $e) { // Логування помилки Log::error('API sync failed', [ 'attempt' => $this->attempts(), 'error' => $e->getMessage(), 'data' => $this->apiData ]); throw $e; // Перекидаємо для retry механізму } } public function failed(\Throwable $exception) { // Логіка для остаточної невдачі Log::critical('API sync permanently failed', [ 'data' => $this->apiData, 'error' => $exception->getMessage() ]); // Можна відправити notification адміну Notification::route('mail', 'admin@example.com') ->notify(new JobFailedNotification($this, $exception)); } } |
Запуск Job’ів
Синхронний vs Асинхронний
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Синхронно (виконується одразу) ProcessUserData::dispatchSync($userId, $data); // Асинхронно (додається в чергу) ProcessUserData::dispatch($userId, $data); // З затримкою ProcessUserData::dispatch($userId, $data) ->delay(now()->addMinutes(5)); // В конкретну чергу ProcessUserData::dispatch($userId, $data) ->onQueue('high-priority'); |
Batch Jobs (Laravel 8+)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
use Illuminate\Bus\Batch; use Illuminate\Support\Facades\Bus; $batch = Bus::batch([ new ProcessUser(1), new ProcessUser(2), new ProcessUser(3), ])->then(function (Batch $batch) { // Всі завдання виконані })->catch(function (Batch $batch, Throwable $e) { // Перше завдання провалилося })->finally(function (Batch $batch) { // Завжди виконується })->dispatch(); |
Запуск Worker’ів
Базовий worker
|
1 2 |
php artisan queue:work |
Продакшн налаштування
|
1 2 3 4 5 6 7 8 9 10 11 |
# Обробляти тільки конкретну чергу php artisan queue:work --queue=high-priority,default # З таймаутами і лімітами php artisan queue:work \ --sleep=3 \ --tries=3 \ --max-time=3600 \ --memory=512 \ --timeout=60 |
Supervisor конфігурація
|
1 2 3 4 5 6 7 8 9 10 |
[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/artisan queue:work --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true user=www-data numprocs=4 redirect_stderr=true stdout_logfile=/var/log/laravel-worker.log |
Docker інтеграція
docker-compose.yml
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
services: app: # основний контейнер queue-worker: build: . container_name: queue-worker restart: unless-stopped command: php artisan queue:work --verbose --tries=3 --timeout=90 volumes: - .:/var/www depends_on: - redis - mysql environment: - QUEUE_CONNECTION=redis |
Multiple workers для різних черг
|
1 2 3 4 5 6 7 8 9 |
high-priority-worker: command: php artisan queue:work --queue=high-priority --tries=5 default-worker: command: php artisan queue:work --queue=default --tries=3 email-worker: command: php artisan queue:work --queue=emails --tries=1 |
Моніторинг та відладка
Artisan команди
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Стан черги php artisan queue:size php artisan queue:size high-priority # Невдалі завдання php artisan queue:failed # Повторити невдале завдання php artisan queue:retry 5 php artisan queue:retry all # Очистити невдалі завдання php artisan queue:flush # Статистика worker'а php artisan queue:monitor redis:default --max=100 |
Dashboard для моніторингу
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// В AdminController public function queueStats() { $stats = [ 'pending' => Queue::size('default'), 'failed' => DB::table('failed_jobs')->count(), 'processed_today' => DB::table('jobs') ->whereDate('created_at', today()) ->count(), ]; return view('admin.queue-stats', compact('stats')); } |
Horizon (для Redis)
|
1 2 3 4 |
composer require laravel/horizon php artisan horizon:install php artisan horizon |
Horizon надає веб-інтерфейс для моніторингу Redis черг.
Практичні поради
1. Правильне використання пам’яті
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ProcessLargeDataset implements ShouldQueue { public function handle() { // НЕ завантажуйте всі дані одразу User::chunk(1000, function ($users) { foreach ($users as $user) { $user->process(); } }); } } |
2. Ідемпотентність
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class CreateUserProfile implements ShouldQueue { public function handle() { // Перевіряємо чи профіль вже існує if (UserProfile::where('user_id', $this->userId)->exists()) { return; // Не створюємо дублікат } UserProfile::create([...]); } } |
3. Graceful degradation
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class SendNotification implements ShouldQueue { public function handle() { try { // Спробувати основний канал $this->sendViaPrimaryChannel(); } catch (\Exception $e) { // Fallback на альтернативний $this->sendViaFallbackChannel(); } } } |
Поширені проблеми та рішення
Worker “завмирає”
Проблема: Worker перестає обробляти завдання Рішення:
- Додати
--max-time=3600для перезапуску worker’а - Використовувати Supervisor для автоматичного перезапуску
- Моніторити memory usage
Job’и накопичуються
Проблема: Завдання додаються швидше ніж обробляються Рішення:
- Збільшити кількість worker’ів
- Оптимізувати логіку Job’ів
- Використовувати пріоритетні черги
Втрата Job’ів
Проблема: Завдання зникають без обробки Рішення:
- Перевірити налаштування
retry_after - Додати детальне логування
- Використовувати
failedметоди
Альтернативи та порівняння
Database vs Redis
Database:
- Простіше налаштувати
- Транзакційна цілісність
- Повільніше для високих навантажень
Redis:
- Значно швидше
- Підтримка pub/sub
- Потребує додаткового сервіса
Laravel vs інші рішення
Laravel Queue – добре інтегрується, але може бути надлишковим для простих задач
Альтернативи:
- Beanstalkd – легкий і швидкий
- RabbitMQ – enterprise рівень
- SQS – для AWS інфраструктури
Висновки
Laravel Queue – потужний інструмент для фонових завдань. Ключові принципи успішного використання:
- Правильно налаштуйте retry логіку – не всі API завжди доступні
- Моніторьте черги – проблеми треба виявляти швидко
- Тестуйте під навантаженням – продакшн може сильно відрізнятися
- Логируйте все – debugging асинхронного коду складний
- Плануйте масштабування – додавання worker’ів має бути простим
Правильно налаштовані черги дозволяють створювати швидкі та надійні додатки навіть при складній бізнес-логіці.