Когда проект вырастает из одного скрипта в набор сервисов, рано или поздно встанет вопрос: как передать работу от одной части системы другой, чтобы ничего не потерялось и не зависло. В первой статье серии — Монолит против микросервисов — мы говорили, что есть два пути общения между блоками кода: прямой вызов по сети и обмен через очередь. Во второй, Проектирование 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 (срок хранения), и несколько потребителей могут прочитать одни и те же записи заново, каждый со своей отметкой.
[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. Краткое сравнение простыми словами
| Вопрос | RabbitMQ | Kafka | AWS 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
// 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, полезная нагрузка), которую мы отправили выше:
{
"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
// 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) и носят показательный характер: подключение, имена очередей и лимиты выстраивайте под свой брокер и свои версии.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
