Архитектура очередей: как мы убрали "тормоза" из HTTP-запросов и спасли нервы пользователей
Истории DevOps-инженера · IThub · 03/21/26 04:01 PM
Что уходит в очередь
В HTTP-запросе должно происходить только то, что нужно пользователю для немедленного ответа.
Что НЕ нужно пользователю немедленно:
Отправка email
Генерация PDF
Пересчёт статистики
Синхронизация с внешними системами
Изменение размеров изображений
Push-уведомления
Webhook-уведомления партнёров
Всё это — в очередь. HTTP отвечает за 50ms. Фоновый воркер делает остальное.
Базовая архитектура очередей на Redis + CI4
<?php namespace App\Libraries\Queue; class Queue { private \Redis $redis; private string $prefix = 'queue:'; public function push(string $queueName, BaseJob $job, int $delay = 0): string { $payload = [ 'id' => $id = uniqid('job_', true), 'class' => get_class($job), 'data' => serialize($job), 'attempt' => 0, 'created_at' => time(), ]; if ($delay > 0) { $this->redis->zAdd( $this->prefix . 'delayed:' . $queueName, time() + $delay, json_encode($payload) ); } else { $this->redis->lPush($this->prefix . $queueName, json_encode($payload)); } return $id; } public function pop(string $queueName, int $timeout = 5): ?array { $this->promoteDelayedJobs($queueName); $result = $this->redis->brPop($this->prefix . $queueName, $timeout); return $result ? json_decode($result[1], true) : null; } private function promoteDelayedJobs(string $queueName): void { $delayedKey = $this->prefix . 'delayed:' . $queueName; $jobs = $this->redis->zRangeByScore($delayedKey, '-inf', time()); foreach ($jobs as $job) { $this->redis->multi(); $this->redis->zRem($delayedKey, $job); $this->redis->lPush($this->prefix . $queueName, $job); $this->redis->exec(); } } } Worker — CI4 CLI команда
<?php namespace App\Commands; use App\Libraries\Queue\Queue; class QueueWorker extends BaseCommand { protected $name = 'queue:work'; protected $description = 'Process jobs from the queue'; private bool $shouldStop = false; public function run(array $params): void { $queueName = $params[0] ?? 'default'; $maxJobs = (int) ($this->getOption('max-jobs') ?? 0); $processed = 0; if (function_exists('pcntl_signal')) { pcntl_signal(SIGTERM, fn() => $this->shouldStop = true); pcntl_signal(SIGINT, fn() => $this->shouldStop = true); } $queue = new Queue(); while (!$this->shouldStop) { if (function_exists('pcntl_signal_dispatch')) { pcntl_signal_dispatch(); } $payload = $queue->pop($queueName); if (!$payload) continue; $this->processJob($queue, $queueName, $payload); $processed++; if ($maxJobs > 0 && $processed >= $maxJobs) break; } } private function processJob(Queue $queue, string $queueName, array $payload): void { try { $job = unserialize($payload['data']); $job->setAttempt($payload['attempt'] + 1); set_time_limit($job->getTimeout()); $job->handle(); } catch (\Throwable $e) { $payload['attempt']++; if ($payload['attempt'] < $job->getMaxAttempts()) { $delay = pow(2, $payload['attempt']) * 10; $queue->push($queueName, $job, $delay); } else { $job->failed($e); $queue->bury($queueName, $payload); } } } } Реальный пример: отправка email
<?php namespace App\Jobs; use App\Libraries\Queue\BaseJob; class SendWelcomeEmailJob extends BaseJob { protected int $maxAttempts = 5; protected int $timeout = 30; public function __construct( private readonly int $userId, private readonly string $email, private readonly string $name ) {} public function handle(): void { service('email')->send( to: $this->email, subject: 'Добро пожаловать!', view: 'emails/welcome', data: ['name' => $this->name] ); model('EmailLogModel')->insert([ 'user_id' => $this->userId, 'type' => 'welcome', 'sent_at' => date('Y-m-d H:i:s'), 'status' => 'sent', ]); } } Использование в Controller:
// Вместо: $emailService->send(...); // ждём 500ms // Делаем: $queue = new Queue(); $queue->push('emails', new SendWelcomeEmailJob($user->id, $user->email, $user->name)); // HTTP response за 15ms вместо 515ms return $this->response->setJSON(['status' => 'registered', 'message' => 'Check your email']); Результаты
Endpoint
До
После
POST /register
580ms
45ms
POST /upload-photo
2300ms
120ms
POST /generate-report
8000ms
80ms (async)
POST /checkout
1200ms
340ms
Bounce rate на страницах с "тяжёлыми" действиями упал на 34%. Конверсия регистрации выросла на 12%. Просто потому что форма теперь отвечает мгновенно.
Пользователи не знают о ваших очередях. Они просто чувствуют что сайт быстрый. Это и есть цель. ⚡
Максим — продуктовый DevOps с горящими глазами и умеренно сгоревшими нервами. Пишу про реальный highload, реальные ошибки и реальные решения. Больше таких историй — на ithub.uno, там собираются те, кто делает, а не только говорит.
- Read more...
- 0 comments
- 61 views
-