Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Архитектура очередей: как мы убрали "тормоза" из HTTP-запросов и спасли нервы пользователей

(0 reviews)

Пользователи не любят ждать. Если кнопка "Отправить" не реагирует три секунды — они уже в Twitter пишут что ваш сайт сломан. Один из самых мощных инструментов для улучшения perceived performance — асинхронная обработка через очереди.


Что уходит в очередь

В 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, там собираются те, кто делает, а не только говорит.

0 Comments

Recommended Comments

There are no comments to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.