Очереди сообщений: как развязать сервисы и пережить пиковую нагрузку

Когда проект вырастает из одного скрипта в набор сервисов, рано или поздно встанет вопрос: как передать работу от одной части системы другой, чтобы ничего не потерялось и не зависло. В первой статье серии — Монолит против микросервисов — мы говорили, что есть два пути общения между блоками кода: прямой вызов по сети и обмен через очередь. Во второй, Проектирование API, мы разобрали первый путь — как сервис просит у сервиса данные и ждёт ответа.

Третья статья посвящена второму пути — очередям сообщений (message queue, механизм, при котором один участник кладёт задачу в буфер, а другой забирает её оттуда, когда будет готов). Это один из самых полезных инструментов в арсенале архитектора: он спасает сайт от разовых скачков трафика, позволяет делать тяжёлую работу в фоне и не даёт одному упавшему сервису утянуть за собой весь продукт.

Разберём на простых примерах: зачем вообще нужна очередь, чем отличаются RabbitMQ, Kafka и AWS SQS, как работают паттерны «точка-точка» и «подписка», что делать с сообщениями, которые не получилось обработать, и почему «согласованность в итоге» — это нормально. В конце сравним родной WordPress-крон (wp-cron) с настоящей очередью. Без жаргона, только конкретика.


Часть I. Зачем нужны очереди и что они лечат

1. Проблема прямого вызова

Допустим, у вас интернет-магазин. Пользователь нажимает «Оплатить», и ваш PHP-код должен: записать заказ в базу, списать деньги, отправить письмо, начислить бонусы, обновить склад и позвонить в службу доставки. Если делать всё прямо по порядку (синхронно, то есть «пока не закончу — не отвечаю»), пользователь висит на белом экране пять-шесть секунд. А если сервис доставки не отвечает — падает вся кнопка «Оплатить», хотя деньги уже списались.

Прямой сетевой вызов, который мы обсуждали в Проектирование API, хорош, когда нужен немедленный ответ: «дай цену товара прямо сейчас». Но для тяжёлой фоновой работы он превращается в мину замедленного действия: один медленный или упавший сосед тянет за собой весь запрос.

Очередь разрывает эту цепочку. Платёжный код делает ровно одно дело — кладёт в очередь сообщение «заказ оплачен» — и мгновенно отвечает пользователю «спасибо, всё хорошо». Дальше отдельные работники (consumers, «потребители», программы, которые читают очередь) в своё время разбирают задачу: один отправит письмо, другой начислит бонусы, третий вызовет доставку. Если доставка лежит — очередь просто подождёт.

2. Развязка: сервисы не знают друг о друге лишнего

Развязка (decoupling, букв. «расцепление») означает, что отправитель и получатель не обязаны быть подключены друг к другу напрямую. Платёжный модуль не знает ни IP-адреса службы доставки, ни того, на каком языке она написана, сколько у неё копий и жива ли она вообще. Он знает только одно: «надо положить сообщение в очередь orders.paid».

Это даёт огромную свободу. Сегодня доставку обрабатывает один скрипт на PHP. Завтра вы переписали её на Python и запустили в трёх копиях — платёжный модуль можно не трогать вообще. Нужна новая функция — SMS-оповещение? Просто добавили ещё одного потребителя, который читает ту же очередь и рассылает смс. Основной код не менялся ни на строчку.

В прямом вызове так нельзя: там отправитель жёстко прописывает адрес получателя. В очереди отправитель говорит «в ящик», а кто стоит у ящика и что с письмом делает — его не касается.

3. Сглаживание пиков: backpressure простыми словами

Backpressure — это «противодавление» или, по-русски, «лишнее не выкидываем, ставим в очередь». Представьте, что обычно к вам приходит 10 заказов в минуту, а в ночь распродажи — 300. Если обрабатывать синхронно, при 300 запросах сервер захлебнётся: воркеры (рабочие процессы) кончатся, очередь к базе данных встанет, пользователи получат ошибки и таймауты.

С очередью пик не убивает систему: 300 сообщений просто ложатся в буфер и ждут. Вы держите нагрузку не «по максимуму в пике», а «спокойно в среднем». Пик растягивается по времени — часть задач, которые «хотелось бы сделать сейчас», сделается через минуту-две. Зато сайт жив, деньги списаны, письма не потеряны.

Это критично для WordPress-проектов на одном VPS (виртуальный выделенный сервер): у вас всегда ровно столько PHP-процессов, сколько позволяет память. Когда трафик скачет — единственный способ не лечь — не масштабировать железо под пик (дорого), а складывать тяжёлую работу в очередь и разбирать её тем числом воркеров, которое вы можете себе позволить.

4. Фоновая работа: не заставлять пользователя ждать

Фоновая обработка (async processing, «делаем, не дожидаясь») — прямое следствие первых двух пунктов. Пользователю не нужно знать, сгенерирован ли уже его PDF-чек, обработано ли видео и отправлено ли письмо — ему важно, что кнопка нажалась и деньги списались. Всё остальное можно сделать позже.

Грубое правило: если задача не нужна для ответа браузеру прямо сейчас — она идёт в очередь. Отправка письма, генерация отчёта, ресайз (изменение размера) загруженной картинки, синхронизация с внешним сервисом, начисление баллов — всё это фоновые дела. Очередь даёт им честное место в архитектуре вместо «костыля» на кроне.


Часть II. Основные брокеры: RabbitMQ, Kafka, AWS SQS

Очередь — это идея, а брокер (broker, «посредник») — программа, которая её реализует: хранит сообщения, следит за порядком, раздаёт их потребителям и не даёт потерять. Разберём трёх самых частых представителей на пальцах, не сравнивая их по бенчмаркам (замерам скорости) — их числовые характеристики вы найдёте в документации, а нам важно понять, для чего каждый создан.

5. RabbitMQ — умная почта с правилами

RabbitMQ вырос из протокола AMQP (Advanced Message Queuing Protocol, стандарт обмена сообщениями) и ведёт себя как хорошее почтовое отделение. Отправитель кладёт письмо не прямо в конкретную ячейку, а в exchange (букв. «обменник») — сортировочный узел, который по правилам routing key (ключ маршрутизации, «адресат» письма) раскидывает сообщения по нужным queue (очередям).

producer  ->  [exchange]  --routing key-->  [queue A]  ->  consumer 1
                       \-----routing key-->  [queue B]  ->  consumer 2

Зачем такая сложность? Потому что правила маршрутизации можно менять, не трогая код отправителя. Платёжный модуль говорит «это письмо про оплаченный заказ», а exchange решает — в какие именно очереди его положить, кому разослать копию, а куда не пускать.

Плюсы RabbitMQ: гибкая маршрутизация, поддержка паттерна «подписка», надёжное подтверждение доставки (message acknowledgment, «получатель сказал спасибо — тогда удаляем»), зрелость и огромное комьюнити. Минус: при очень больших потоках (сотни тысяч сообщений в секунду) он «тухнет» быстрее Kafka, потому что каждый раз думает, кому что отправить.

Когда брать: у вас микросервисы, где много разных типов задач и нужны хитрые правила, кому и что доставить. Это брокер по умолчанию для большинства проектов уровня «небольшая компания».

6. Kafka — журнал, который всё помнит

Apache Kafka устроена иначе. Единица хранения здесь — topic (тема), которая состоит из partition (часть, «рулон ленты»). Сообщения дописываются в конец ленты и не удаляются после прочтения — они живут ровно столько дней, сколько задано в настройке retention (срок хранения), и несколько потребителей могут прочитать одни и те же записи заново, каждый со своей отметкой.

HTML
                    [partition 0: msg0 -> msg1 -> msg2 -> ...]
producer --append-> [partition 1: msg0 -> msg1 -> msg2 -> ...]
                    [partition 2: msg0 -> msg1 -> msg2 -> ...]

consumer A (offset 1) ---> читает с позиции 1
consumer B (offset 0) ---> читает с позиции 0 (независимо)

Отсюда главное отличие: RabbitMQ раздаёт письмо и выбрасывает его, а Kafka вещает ленту, и каждый слушатель сам решает, до какого места он уже дослушал. Отметка, до которой потребитель дочитал, называется offset (смещение в ленте).

Плюсы Kafka: огромная пропускная способность, хранение истории (можно переиграть события за неделю), удобна для потоковой аналитики и для того, чтобы несколько независимых систем «слушали» одни и те же события. Минус: сложнее в эксплуатации (нужен кластер, следящий за репликами), маршрутизация беднее, чем у RabbitMQ, и для простой задачи «отправить 10 писем в день» это стрельба из пушки по воробьям.

Когда брать: у вас поток событий (клики, действия, телеметрия), который хотят читать несколько независимых потребителей и иногда перематывать назад.

7. AWS SQS — готовый ящик без вашей инфраструктуры

Amazon SQS (Simple Queue Service, «простая очередь») — это облачная managed-очередь (управляемая, то есть Amazon сам держит серверы, а вы только ходите по API). Архитектурно она намеренно примитивна: есть очередь, вы кладёте в неё сообщение, потребитель забирает, подтверждает получение — сообщение удаляется. Никаких exchanges, никаких лент с историей.

producer --sendMessage-->  ( SQS queue )  --receiveMessage-->  consumer
                                |
                          visibility timeout
                        (на время спрятано от других)

Хитрость SQS — visibility timeout (таймаут видимости): когда потребитель забрал сообщение, оно на заданное время «прячется» от остальных. Если потребитель успел обработать и подтвердил удаление — отлично. Если упал и не подтвердил — по истечении таймаута сообщение снова всплывает и его заберёт следующий. Так SQS гарантирует «минимум один раз» доставки (at-least-once) ценой того, что сообщение могут обработать дважды.

Плюсы SQS: ноль своей инфраструктуры, почти неубиваемость, простая модель. Минусы: только point-to-point (для подписки сверху ставят SNS), ограничение на размер сообщения и на частоту, привязка к облаку (vendor lock-in, «зависимость от продавца»).

Когда брать: вы уже живёте в AWS, задач немного, и вы не хотите держать и админить свой брокер.

8. Краткое сравнение простыми словами

ВопросRabbitMQKafkaAWS SQS
На что похожеУмная почтаМагнитофонная лентаОблачный ящик
Что делает с сообщением после чтенияОтдаёт и удаляетХранит, читают по отметкеУдаляет после подтверждения
МаршрутизацияГибкая (exchange)Бедная (по partition)Прямая в очередь
Подписка «один — многие»Да, из коробкиДа, через разные потребителиНет, нужен SNS сверху
История / перемотка назадНетДаНет
АдминистрированиеВаше, среднееВаше, сложноеЧужое, почти ноль
Типовой размер проектаМалый/средний бизнесПотоки, большие данныеОблако, простая фоновая работа

Запомните одно правило выбора: не берите Kafka там, где хватило бы SQS, и не стройте свой RabbitMQ, если задача умещается в «положил — забрал». Самый дешёвый брокер — тот, который вы не обязаны администрировать.


Часть III. Два базовых паттерна: точка-точка и подписка

Весь мир очередей собирается из двух моделей доставки. Понять их — значит понимать, когда что использовать.

9. Point-to-point: один отправитель — один получатель

Point-to-point (сокр. P2P, «точка-точка», букв. «от точки до точки») — это модель «работа на одного». Сообщение лежит в очереди, и его заберёт ровно один потребитель. Если потребителей пять — задача уйдёт одному из них, остальные её не увидят. Так работает обычная очередь задач.

Зачем это нужно: вы хотите, чтобы одну и ту же тяжёлую работу выполнили ровно один раз, но выполняли её несколько параллельных работников. Десять копий обработчика изображений тянут задачи из одной очереди — картинка будет ресайзнута один раз, но десять картинок одновременно.

                    [ worker 1 ]
[ queue: thumbnails ]--[ worker 2 ]   -> задача = ровно один исполнитель
                    [ worker N ]

Ключевое свойство: потребители здесь — взаимозаменяемые работники одной команды. Если один лежит — задачу возьмёт соседний. Это встроенная балансировка нагрузки.

10. Publish-subscribe: один пишет — многие читают

Publish-subscribe (сокр. pub/sub, «публикация-подписка», по-русски «издатель — подписчики») — модель «рассылка события». Отправитель публикует (publish) событие в тему, и каждый, кто на эту тему подписался (subscribe), получает свою копию. Ни один подписчик не «забирает» событие у других — оно доступно всем сразу.

Пример: событие order.paid (заказ оплачен). На него подписаны четыре независимых сервиса: письма, бонусы, склад, аналитика. Каждый получает свою копию сообщения и делает своё дело. Платёжный модуль не знает об их существовании — он просто опубликовал факт.

            [ topic: order.paid ]
          /        |        \         каждый подписчик — отдельная очередь-копия
[ email ]  [ bonuses ]  [ warehouse ]  [ analytics ]

11. Как выбрать между ними

Тест простой и называется «зачем это сообщение»:

  • Это работа, которую нужно выполнить ровно один раз (сгенерировать файл, отправить одно письмо, списать со склада)? → point-to-point, одна очередь, много воркеров.
  • Это факт, который произошёл, и его хотят знать многие независимые стороны (заказ оплачен, пользователь удалился, товар закончился)? → publish-subscribe, тема, разные подписчики.

Частая ошибка новичков — пытаться сделать подписку через point-to-point и наоборот. Не надо: у RabbitMQ для этого есть exchange-«fanout» (веерная рассылка), у SQS — связка SQS+SNS, у Kafka — отдельные потребители с собственными offset. Не изобретайте велосипед, используйте нативный механизм.


Часть IV. Producer и consumer на PHP: как это выглядит в коде

Теория без кода пустой звук. Ниже — рабочий скелет отправителя и получателя на PHP с библиотекой php-amqplib (клиент RabbitMQ). Логика применима к любому брокеру: меняется только способ подключиться и имя команды.

Каждый код-блок ниже предваряется пометкой языка, как и принято в этой серии.

Пример на PHP — producer (отправитель, кладёт сообщение в очередь):

PHP
<?php
// producer.php — кладём задачу в очередь

require __DIR__ . '/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

// Подключаемся к брокеру. В реальности пароли берём из окружения,
// а не пишем прямо в файле.
$connection = new AMQPStreamConnection(
    'localhost', 5672, 'guest', 'guest'
);
$channel = $connection->channel();

// Объявляем очередь. Если её нет — брокер создаст.
$channel->queue_declare(
    'orders.paid',   // имя очереди
    false,           // не пассивный режим (создать, если нет)
    true,            // durable: очередь переживёт перезапуск брокера
    false,           // не исключительная (доступна другим)
    false            // не автоудаляемая
);

// Формируем тело сообщения — полезная нагрузка (payload) в формате JSON.
$payload = json_encode([
    'event'       => 'order.paid',
    'order_id'    => 1024,
    'user_id'     => 7,
    'amount'      => 1990,
    'currency'    => 'RUB',
    'occurred_at' => (new DateTimeImmutable('now'))->format(DateTimeInterface::ATOM),
]);

//persistent = true — сообщение тоже переживёт перезапуск брокера.
$msg = new AMQPMessage($payload, [
    'content_type'  => 'application/json',
    'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);

// Публикуем: exchange '' = напрямую в очередь с именем orders.paid.
$channel->basic_publish($msg, '', 'orders.paid');

$channel->close();
$connection->close();

echo "Сообщение положено в очередь orders.paid\n";

Пример JSON — схема сообщения (payload, полезная нагрузка), которую мы отправили выше:

JSON
{
  "event": "order.paid",
  "order_id": 1024,
  "user_id": 7,
  "amount": 1990,
  "currency": "RUB",
  "occurred_at": "2026-10-14T09:30:00+00:00"
}

Несколько правил к схеме сообщения, которые стоит усвоить один раз:

  • event — короткое имя того, что произошло. По нему потребитель понимает, чья это задача. Пишите его в прошедшем времени (order.paid, user.deleted) — это устоявшаяся традиция.
  • occurred_at — когда событие случилось (не когда вы его отправили; отправка могла задержаться).
  • Используйте идентификаторы, а не целые объекты. В сообщении выше есть order_id, но нет всей корзины. Потребитель по order_id сам достанет данные из базы. Так сообщение остаётся маленьким и не устаревает в полёте.
  • Один формат на всё — JSON. Он читается везде, расширяется без поломки старых потребителей (добавили поле — старые его просто игнорируют).
  • Идемпотентность в закладке. Дайте сообщению id или используйте пару event + order_id, чтобы потребитель мог узнать: «это я уже обработал, повтор пропускаю». Приедет ли сообщение один раз или два — заранее не гарантируете вы, а брокеры с моделью at-least-once (SQS, RabbitMQ при падении) нарочно могут прислать дубль.

Пример на PHP — consumer (получатель, читает очередь и обрабатывает задачу):

PHP
<?php
// consumer.php — забираем задачу из очереди и делаем своё дело

require __DIR__ . '/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('orders.paid', false, true, false, false);

echo "Жду сообщений, Ctrl+C для выхода\n";

$callback = function ($msg) {
    $data = json_decode($msg->body, true);

    // Защита от дублей: проверяем, не обрабатывали ли этот заказ.
    if (alreadyProcessed($data['order_id'])) {
        $msg->ack();               // подтверждаем и удаляем из очереди
        return;
    }

    try {
        // Само полезное дело: отправляем письмо о покупке.
        sendInvoiceEmail($data['order_id'], $data['email']);

        $msg->ack();               // всё вышло — подтверждаем доставку
        markProcessed($data['order_id']);
    } catch (Throwable $e) {
        // null = не подтверждать: брокер вернёт сообщение в очередь
        // или, после лимита повторов, отправит в dead-letter.
        $msg->nack(false);         // false = requeue = false, не возвращать в начало
        error_log('Ошибка обработки: ' . $e->getMessage());
    }
};

// prefetch_count = 1: за раз выдаём воркеру одну задачу.
// Пока не подтвердит текущую — вторую не получит. Это честно
// распределяет работу между несколькими потребителями.
$channel->basic_qos(null, 1, null);
$channel->basic_consume(
    'orders.paid', '', false, false, false, false, $callback
);

// Крутимся, пока есть активные подписки.
while ($channel->is_consuming()) {
    $channel->wait();
}

$channel->close();
$connection->close();

Здесь главное — слова ack() (acknowledge, «подтверждаю, обработал — удаляй») и nack() (negative acknowledge, «не смог — не удаляй»). Пока потребитель не подтвердил, сообщение считается «необработанным» и при падении воркера вернётся в очередь. Это фундамент надёжности: задача теряется только в том случае, если вы сами подтвердите её удаление.


Часть V. Retry и dead-letter queues: что делать с неудачниками

Реальная обработка падает: упал внешний сервис, таймаут, битая строка в данных, не хватило памяти. Что делать с таким сообщением? Выбросить нельзя — данные потеряются. Оставить навсегда тоже нельзя — один «неудачник» (poison message, «ядовитое сообщение») заблокирует очередь, если он всегда возвращается в начало. Решают это два механизма: retry и dead-letter queue.

12. Retry: дать задаче несколько шансов

Retry (повторная попытка) — это «попробуй ещё раз, но умно». Ключевое слово «умно»: повторы идут с backoff (нарастающей паузой). Первый повтор — через 1 секунду, второй — через 5, третий — через 25, и так далее. Это называется exponential backoff (экспоненциальная задержка).

Зачем пауза растёт? Если сервис доставки лежит, стучаться каждые 100 миллисекунд — только добить его. А с растущей паузой вы даёте ему время очнуться и не заспамливаете сеть. После нескольких неудач задачу отправляют в «карантин» (см. ниже), чтобы она не крутилась вечно.

Практический совет: не повторяйте всё подряд. Если ошибка «внешний сервис недоступен» — retry спасает. Если ошибка «в данных не хватает поля, мы не понимаем формат» — повтор ничего не изменит, это баг, а не случайность. Такие задачи сразу отправляйте в dead-letter, а не мучайте повторами.

13. Dead-letter queue: очередь «мёртвых писем»

Dead-letter queue (DLQ, «очередь мёртвых писем») — обычный ящик, куда брокер складывает сообщения, которые:

  • не удалось обработать после N повторов;
  • просрочили время жизни в очереди (TTL);
  • некуда доставить (нет подходящей очереди).

Это ваш карантин и пункт ручной разбора одновременно. Сообщение не пропадает, а лежит отдельно, пока вы не посмотрите, почему оно упало, — и не решите: исправить данные и вернуть обратно в рабочую очередь или выбросить осознанно.

[ рабочая очередь ]  --не обработано N раз-->  [ dead-letter queue ]
       ^                                                   |
       |__________ после починки данных возвращаем _________|

Обязательное правило: на DLQ должен висеть алерт (автоматическое предупреждение, сигнал дежурному). Очередь «мёртвых писем» — это место, где копятся проблемы системы. Если туда что-то легло и никто не заметил, через месяц вы обнаружите сотни потерянных заказов. Правильно: как только в DLQ появляется сообщение — уведомление дежурному.


Часть VI. Eventual consistency: согласованность в итоге

Когда вы переходите на очередь, появляется цена надёжности — eventual consistency (букв. «в конечном счёте согласованность», по-русски точнее «согласованность в итоге»). Разберём это простыми словами, потому что это самое непонятное и самое важное.

14. Почему данные могут быть чуть «вчерашними»

При синхронной работе всё строго: деньги списались — и заказ тут же «оплачен» во всех местах. В асинхронной очереди между событием и его последствиями есть задержка. Ситуация: пользователь оплатил заказ, сообщение легло в очередь, но письмо ещё не ушло, бонусы ещё не начислились, склад ещё не обновлён — потребители не добрались. В этот момент (секунды, иногда минуты) разные части системы показывают разную картину: база говорит «оплачен», а личный кабинет ещё «в обработке».

Это и есть eventual consistency: согласованность наступит, но не мгновенно. Данные придут к одному состоянию «в итоге», когда очередь разберётся.

15. Почему это нормально и как это контролировать

Многим кажется, что «данные неточны» — это баг. На самом деле это осознанный размен: вы жертвуете мгновенной синхронностью ради надёжности, скорости ответа и устойчивости к пикам. Обычный пользователь, у которого бонусы начислились на минуту позже, счастливее пользователя, у которого кнопка «Оплатить» выдала ошибку 500.

Как держать это под контролем:

  • Честно отражайте статус. Пока событие не разобрано, показывайте «обработка идёт», а не ложное «готово». Пользователь не злится на «ждите», он злится на неверную информацию.
  • Идемпотентность повсеместно. Раз данные могут приехать дважды и вразнобой, делайте обработку повторяемой без вреда: повторное начисление бонуса должно быть безопасно (см. проверку alreadyProcessed в consumer-коде выше).
  • Следите за «лагом». Ключевая метрика — как долго сообщение лежит в очереди неразобранным (queue lag, задержка). Растёт лаг — потребители не справляются, пора добавлять воркеров. Как мерить и на что вешать алерты — в Метрики и мониторинг.
  • Порядок там, где он важен. Очередь по умолчанию не гарантирует, что событие А обработается раньше Б. Если порядок критичен (списали деньги раньше, чем отгрузили), используйте один partition/одну очередь на «сущность» — тогда внутри неё порядок сохранится.

Eventual consistency пугает ровно до первого раза, когда вы поймёте: это не «данные врём», а «данные догоняют реальность». Система с очередью почти всегда ближе к правде, чем упавший синхронный сайт.


Часть VII. Примеры и WordPress: wp-cron против настоящей очереди

Соберём всё вместе на конкретных задачах и разберём, как в экосистеме WordPress (самой массовой CMS) уживается её родной крон и настоящая очередь.

16. Три типовых сценария

Обработка платежей. Деньги списываем синхронно (нужен немедленный ответ), а всё остальное — в очередь: письмо-чек, начисление бонусов, отчёт в бухгалтерию, оповещение склада. Пользователь видит «спасибо за покупку» за долю секунды, тяжёлые последствия разбираются фоном. Здесь нужен point-to-point с идемпотентностью: списание бонуса по order_id должно произойти ровно один раз, даже если сообщение приехало дважды.

Уведомления. Событие (заказ оплачен, комментарий одобрен) публикуется в тему, а разные подписчики — email, push, SMS — читают каждый свою копию. Классический publish-subscribe: один факт, много независимых реагирований.

Фоновые задачи сайта. Генерация превью картинок, экспорт большого CSV, переиндексация поиска — всё это примеры задач, которые в принципе не должны выполняться внутри пользовательского запроса. Их место в очереди с воркерами, число которых вы подбираете под своё железо.

17. wp-cron: что это на самом деле

В WordPress есть встроенный планировщик wp-cron. Важно понимать: он не похож на системный cron (фоновый планировщик ОС, который сам просыпается по расписанию). wp-cron срабатывает только когда кто-то открывает страницу сайта: на каждом посещении WordPress проверяет, не пришло ли время задачи, и если да — выполняет её в этом же запросе.

Отсюда три беды wp-cron для тяжёлой фоновой работы:

  • Зависит от трафика. На тихом сайте задача может не выполниться часами — просто потому что никто не зашёл и не «пнул» планировщик.
  • Тормозит пользователя. Тяжёлая задача выполняется прямо в запросе реального посетителя — он стоит на белом экране, пока ваш экспорт CSV доделается.
  • Никакой развязки. Задача живёт в том же PHP-процессе и в той же базе, что и весь сайт. Упала задача — тормозит сайт; выросла нагрузка — задачи падают вперемешку с запросами. Никакого сглаживания пиков и независимых воркеров.

18. Когда wp-cron уместен, а когда пора в очередь

wp-cron хорош для лёгких регулярных дел: отправить один пост-уведомление, подчистить транзитные записи, проверить обновления. Это не требует развязки и терпит задержку в минуты. Не стоит городить брокер ради задачи, которая раз в сутки пересылает дайджест.

Пора переходить на настоящую очередь, когда:

  • задача тяжёлая (генерация файлов, большая рассылка, обработка изображений) и не должна вешать пользовательский запрос;
  • нужен буфер против пиков — при распродаже wp-cron утонет, а очередь переживёт;
  • важна гарантия доставки и retry — письмо должно уйти, даже если сервер почты лежал десять минут, а wp-cron при падении процесса задачу теряет;
  • нужно много независимых потребителей или разные обработчики на разных языках.

На практике WordPress-проекты делают компромисс: оставляют wp-cron для мелочи, а тяжёлые задачи через плагин или свой код отправляют в RabbitMQ/SQS, где их разбирают отдельные воркеры (те же PHP-cli-скрипты, только запущенные как служба). Если вы решите выносить в контейнеры — мы обсудим это в статье про Контейнеры и оркестрация, но очередь полезна и без Docker.

19. Мини-чек-лист переезда с wp-cron на очередь

Признакwp-cronНастоящая очередь
Задача лёгкая и редкая✓избыточно
Задача тяжёлая / долгая✗ вешает запрос✓
Нужен буфер против пика✗✓
Нужны retry и DLQ✗✓
Несколько независимых подписчиков✗✓
Не хотите зависеть от трафика✗ (ждёт посетителей)✓

Чек-лист: готовы ли вы к очередям

  • Поймите, нужна ли вам очередь вообще. Один тихий блог с редкими письмами — это wp-cron или вообще синхронная отправка. Очередь оправдана, когда есть тяжёлая фоновая работа, пики нагрузки или много независимых потребителей.
  • Сначала простой брокер. Не начинайте с Kafka, если задача умещается в «положил — забрал». SQS (если вы в облаке) или RabbitMQ (если держите сами) закроют 90% потребностей.
  • Разделяйте работу и события. Точка-точка — для задач, которые делают один раз; подписка — для фактов, о которых должны узнать многие. Не путайте их.
  • Настройте ack/nack с первого дня. Пока потребитель не подтвердил, сообщение не должно считаться обработанным. Это единственное, что защищает от потери данных.
  • Идемпотентность обязательна. Считайте, что каждое сообщение приедет дважды. Проверка «а не делал ли я уже это» по event + id должна быть в каждом consumer.
  • Включите exponential backoff и лимит повторов. Не бесконечные retry, а 3–5 попыток с растущей паузой, дальше — в DLQ.
  • DLQ с алертом. Очередь «мёртвых писем» без уведомления дежурному — это молча растущая свалка потерянных заказов. См. Метрики и мониторинг.
  • Следите за queue lag. Задержка разбора очереди — ваша главная метрика здоровья. Растёт — добавляйте воркеров, а не надейтесь на авось.
  • Примите eventual consistency. Проектируйте интерфейсы так, чтобы «в обработке» выглядело честным статусом, а не обещанием мгновенного результата.
  • Не тяните в очередь всё подряд. Задача, где нужен немедленный ответ пользователю (проверить баланс, отдать цену), остаётся синхронной — см. Проектирование API. Очередь — для фона, а не вместо прямого вызова.

Итог. Очередь как умение дышать ровно

Очередь сообщений — это не про «стать модной архитектурой», а про умение системы дышать ровно, когда на неё наваливают пиковую нагрузку, и ничего не терять, когда соседний сервис падает. Мы разобрали три её суперсилы: развязку (сервисы не знают друг о друге лишнего), сглаживание пиков (лишнее копится и разбирается спокойно) и фоновую работу (пользователь не ждёт тяжёлую задачу). Увидели, чем отличаются RabbitMQ, Kafka и AWS SQS, и усвоили правило выбора: берите самый простой брокер, который решает вашу задачу, и не администрируйте лишнее. Разложили два базовых паттерна — точка-точка для работы и подписка для событий. Настроили retry с экспоненциальной паузой и dead-letter queue как карантина для неудачников. Приняли eventual consistency как честную плату за надёжность. И сравнили wp-cron с настоящей очередью — поняли, когда первого хватает, а когда он тихо вас подводит.

Очередь связывает сервисы воедино, но чтобы вся эта распределённая машина не слепла в критический момент, её нужно измерять: сколько сообщений лежит неразобранными, как быстро растёт DLQ, где деградирует задержка. Именно об этом — следующая статья серии Нагрузочное тестирование: как проверить под реальной нагрузкой, что выбранные вами брокер, воркеры и балансировка выдержат настоящий пик, а не тот, что в документации.

Материал написан простым языком без излишней терминологии. Все сложные термины расшифрованы по-русски при первом упоминании. Примеры кода на PHP используют библиотеку php-amqplib (клиент RabbitMQ) и носят показательный характер: подключение, имена очередей и лимиты выстраивайте под свой брокер и свои версии.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.