New We are sorting MAX channels into categories and territories. Read the news
Loading image…

Не долбимся как кролики с RabbitMQ и yii2-queue



– retry, delay, exponential backoff и немного jitter без лишнего насилия над очередями –

tl;dr

yii2-queue с amqp_interop может повторно отправлять упавшую задачу практически сразу. Добавляем DeliveryDelay - и получаем retry с паузой.

Ну что же, у нас классика на столе - yii2 и система нотификаций, реализованная через yii2-queue и rabbitmq в качестве брокера очередей.

У очередей настроены попытки - пусть 5 для определённости.

Как будто-бы всё хорошо: джоба падает → yii2-queue делает retry → жизнь продолжается.

Но есть нюанс.

При использовании amqp_interop повторная отправка сообщения идёт через redeliver(). И идёт она сразу.
То есть внешний сервис прилёг, API отвечает ошибкой - а мы такие:

— Не получилось?
— Не получилось.
— А сейчас?
— Нет.
— А сейчас?
— Всё ещё нет.
— А СЕЙЧАС?!


Не retry, а тревожный разработчик какой-то перед дедлайном. 😃

Улучшаем - добавляем паузу

Сам yii\queue\amqp_interop\Queue почти всё уже умеет. Но немножечко надо допилить метод redeliver и добавить отправку сообщений с задержкой.



protected function redeliver(AmqpMessage $message): void
{
$attempt = (int)$message->getProperty(self::ATTEMPT, 1);
$delay = $attempt * 60;

$newMessage = $this->context->createMessage(
$message->getBody(),
$message->getProperties(),
$message->getHeaders()
);
$newMessage->setDeliveryMode($message->getDeliveryMode());
$newMessage->setProperty(self::ATTEMPT, $attempt + 1);
$newMessage->setProperty(self::DELAY, $delay);

$producer = $this->context->createProducer();
$producer->setDeliveryDelay($delay * 1000);
$producer->send($this->context->createQueue($this->queueName), $newMessage);
}

Теперь между попытками появляется простенький linear backoff:
1 retry → 60 секунд
2 retry → 120 секунд
3 retry → 180 секунд
...


Если не нравится линейный вариант, то можно сделать exponential backoff:


$delay = 60 * (2 ** ($attempt - 1));

И тогда будет
attempt 1 → 60 сек
attempt 2 → 120 сек
attempt 3 → 240 сек
attempt 4 → 480 сек
...


Нет предела совершенству - добавляем jitter

Представим, что в очереди 10 000 сообщений. Все получили ошибку отправки уведомления через внешний сервис примерно одновременно и происходит retry для всей очереди.

Йеее - словили thundering herd problem, он же «эффект громового стада»! 😆

Чтобы не положить внешний сервис окончательно или не словить его rate limit, добавим немного случайности:

Full jitter - retry распределится по всему диапазону от 0 до $delay.


$jitter = random_int(0, $delay);

Equal Jitter - гарантированно ждём хотя бы половину задержки, а остальное отдаём на волю рандома.


$half = (int) ($delay / 2);
$jitter = $half + random_int(0, $half);

Decorrelated Jitter - cледующая задержка зависит от предыдущей (оставим для самостоятельного изучения 😄).

🚨 !!! ВАЖНО !!!

Уход на retry с новой попыткой срабатывает только тогда, когда джоба выбрасывает Exception.

Не возвращает false, null или что-нибудь ещё в духе "ну, не шмогла".

А именно падает:


throw new RuntimeException('Notification sending failed');

Иначе для yii2-queue джоба успешно завершилась и повторять там, собственно, нечего.

#php #yii2 #rabbitmq #yii2queue #queues #retry #amqp #backend #web #разработка
Views: 29
Open in MAX Report an inaccuracy

Report an inaccuracy

Describe what needs fixing. If you have a source, add a link.

You are leaving datool.ru

The link leads to an external website. datool.ru is not responsible for its content — check the address before entering passwords or payment details.

Redirecting in 10 s

Go