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