Перейти к основному содержимому

Улучшенные повторные попытки с экспоненциальным отставанием и дрожанием

· 4 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим, как мы можем улучшить повторные попытки клиента с помощью двух разных стратегий: экспоненциальная отсрочка и дрожание.

2. Повторить попытку

В распределенной системе сетевое взаимодействие между многочисленными компонентами может быть нарушено в любое время. Клиентские приложения обрабатывают эти сбои, реализуя повторные попытки .

Предположим, что у нас есть клиентское приложение, которое вызывает удаленную службу — PingPongService .

interface PingPongService {
String call(String ping) throws PingPongServiceException;
}

Клиентское приложение должно повторить попытку, если PingPongService возвращает PingPongServiceException . В следующих разделах мы рассмотрим способы реализации повторных попыток клиента.

3. Resilience4j Повторить попытку

В нашем примере мы будем использовать библиотеку Resilience4j , в частности ее модуль повторных попыток . Нам нужно добавить модуль resilience4j-retry в наш pom.xml :

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
</dependency>

Чтобы узнать больше об использовании повторных попыток, не забудьте ознакомиться с нашим руководством по Resilience4j .

4. Экспоненциальный откат

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

Экспоненциальная отсрочка — это распространенная стратегия обработки повторных попыток неудачных сетевых вызовов. Проще говоря, клиенты ждут постепенно увеличивающиеся интервалы между последовательными повторными попытками :

wait_interval = base * multiplier^n

куда,

  • base — начальный интервал, т. е. ожидание первой повторной попытки
  • n - количество произошедших отказов
  • multiplier — произвольный множитель, который можно заменить любым подходящим значением

При таком подходе мы предоставляем системе передышку для восстановления после периодических сбоев или даже более серьезных проблем.

Мы можем использовать алгоритм экспоненциальной отсрочки в повторной попытке Resilience4j, настроив его IntervalFunction , который принимает начальный интервал и множитель .

IntervalFunction используется механизмом повтора в качестве функции ожидания :

IntervalFunction intervalFn =
IntervalFunction.ofExponentialBackoff(INITIAL_INTERVAL, MULTIPLIER);

RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(MAX_RETRIES)
.intervalFunction(intervalFn)
.build();
Retry retry = Retry.of("pingpong", retryConfig);

Function<String, String> pingPongFn = Retry
.decorateFunction(retry, ping -> service.call(ping));
pingPongFn.apply("Hello");

Давайте смоделируем реальный сценарий и предположим, что у нас есть несколько клиентов, одновременно вызывающих PingPongService :

ExecutorService executors = newFixedThreadPool(NUM_CONCURRENT_CLIENTS);
List<Callable> tasks = nCopies(NUM_CONCURRENT_CLIENTS, () -> pingPongFn.apply("Hello"));
executors.invokeAll(tasks);

Давайте посмотрим на журналы удаленных вызовов для NUM_CONCURRENT_CLIENTS, равного 4:

[thread-1] At 00:37:42.756
[thread-2] At 00:37:42.756
[thread-3] At 00:37:42.756
[thread-4] At 00:37:42.756

[thread-2] At 00:37:43.802
[thread-4] At 00:37:43.802
[thread-1] At 00:37:43.802
[thread-3] At 00:37:43.802

[thread-2] At 00:37:45.803
[thread-1] At 00:37:45.803
[thread-4] At 00:37:45.803
[thread-3] At 00:37:45.803

[thread-2] At 00:37:49.808
[thread-3] At 00:37:49.808
[thread-4] At 00:37:49.808
[thread-1] At 00:37:49.808

Здесь мы видим четкую закономерность — клиенты ждут экспоненциально растущие интервалы, но все они вызывают удаленную службу точно в одно и то же время при каждой повторной попытке (коллизии).

./950a90b70f70937b2b99c00c830565d7.png

Мы решили только часть проблемы — мы больше не забиваем удаленный сервис повторными попытками, а вместо того, чтобы распределять нагрузку по времени, у нас чередуются периоды работы с большим временем простоя. Такое поведение сродни проблеме громоподобного стада .

5. Представляем джиттер

В нашем предыдущем подходе ожидания клиента постепенно увеличиваются, но все еще синхронизируются. Добавление джиттера позволяет нарушить синхронизацию между клиентами, тем самым избегая коллизий . В этом подходе мы добавляем случайность в интервалы ожидания.

wait_interval = (base * 2^n) +/- (random_interval)

где random_interval добавляется (или вычитается), чтобы нарушить синхронизацию между клиентами.

Мы не будем вдаваться в механику вычисления случайного интервала, но рандомизация должна распределять всплески для гораздо более плавного распределения клиентских вызовов.

Мы можем использовать экспоненциальную отсрочку с дрожанием в повторной попытке Resilience4j, настроив экспоненциальную случайную отсрочку IntervalFunction , которая также принимает randomizationFactor :

IntervalFunction intervalFn = 
IntervalFunction.ofExponentialRandomBackoff(INITIAL_INTERVAL, MULTIPLIER, RANDOMIZATION_FACTOR);

Вернемся к нашему реальному сценарию и посмотрим на журналы удаленных вызовов с джиттером:

[thread-2] At 39:21.297
[thread-4] At 39:21.297
[thread-3] At 39:21.297
[thread-1] At 39:21.297

[thread-2] At 39:21.918
[thread-3] At 39:21.868
[thread-4] At 39:22.011
[thread-1] At 39:22.184

[thread-1] At 39:23.086
[thread-5] At 39:23.939
[thread-3] At 39:24.152
[thread-4] At 39:24.977

[thread-3] At 39:26.861
[thread-1] At 39:28.617
[thread-4] At 39:28.942
[thread-2] At 39:31.039

Теперь у нас гораздо лучший спред. Мы устранили как коллизии, так и время простоя, и в итоге получили почти постоянную скорость клиентских вызовов , исключая первоначальный всплеск.

./b5a00dfd7f4eb30df5f98fa1d2f38257.png

Примечание. Мы завысили интервал для иллюстрации, и в реальных сценариях у нас были бы меньшие промежутки.

6. Заключение

В этом учебнике мы рассмотрели, как можно улучшить повторение неудачных вызовов клиентскими приложениями, дополнив экспоненциальную отсрочку дрожанием.

Исходный код примеров, используемых в руководстве, доступен на GitHub .