Melhores tentativas com retração exponencial e instabilidade

Melhores tentativas com retração exponencial e instabilidade

1. Overview

Neste tutorial, vamos explorar como podemos melhorar as tentativas do cliente com duas estratégias diferentes: backoff exponencial e jitter.

2. Repetir

Em um sistema distribuído, a comunicação de rede entre os inúmeros componentes pode falhar a qualquer momento. Client applications deal with these failures by implementingretries.

Vamos supor que temos um aplicativo cliente que invoca um serviço remoto - oPingPongService.

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

O aplicativo cliente deve tentar novamente sePingPongService retornar umPingPongServiceException. Nas seções a seguir, veremos maneiras de implementar novas tentativas do cliente.

3. Resilience4j Repetir

Para o nosso exemplo, usaremos a bibliotecaResilience4j, particularmente seu móduloretry. Precisamos adicionar o módulo resilience4j-retry ao nossopom.xml:


    io.github.resilience4j
    resilience4j-retry

Para relembrar o uso de novas tentativas, não se esqueça de verificar nossoGuide to Resilience4j.

4. Retorno Exponencial

Os aplicativos clientes devem implementar tentativas de forma responsável. When clients retry failed calls without waiting, they may overwhelm the system, e contribuir para degradar ainda mais o serviço que já está em perigo.

O backoff exponencial é uma estratégia comum para lidar com novas tentativas de chamadas de rede com falha. Em termos simples,the clients wait progressively longer intervals between consecutive retries:

wait_interval = base * multiplier^n

Onde,

  • base é o intervalo inicial, ou seja, aguarde a primeira tentativa

  • n é o número de falhas que ocorreram

  • multiplier é um multiplicador arbitrário que pode ser substituído por qualquer valor adequado

Com essa abordagem, fornecemos um espaço para a respiração do sistema para recuperar falhas intermitentes ou problemas ainda mais graves.

Podemos usar o algoritmo de backoff exponencial na nova tentativa do Resilience4j configurando seuIntervalFunction t que aceitainitialIntervalemultiplier.

OIntervalFunction é usado pelo mecanismo de nova tentativa como uma função de suspensão:

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

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

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

Vamos simular um cenário do mundo real e assumir que temos vários clientes invocandoPingPongService simultaneamente:

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

Vejamos os logs de invocação remota paraNUM_CONCURRENT_CLIENTS igual a 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

Podemos ver um padrão claro aqui - os clientes esperam intervalos exponencialmente crescentes, mas todos chamam o serviço remoto exatamente ao mesmo tempo em cada nova tentativa (colisões). image

Abordamos apenas uma parte do problema - não martelamos mais o serviço remoto com novas tentativas,but instead of spreading the workload over time, we have interspersed periods of work with more idle time. Esse comportamento é semelhante aoThundering Herd Problem.

5. Apresentando o Jitter

Em nossa abordagem anterior, o cliente espera é progressivamente mais longo, mas ainda sincronizado. Adding jitter provides a way to break the synchronization across the clients thereby avoiding collisions. Nesta abordagem, adicionamos aleatoriedade aos intervalos de espera.

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

onde,random_interval é adicionado (ou subtraído) para interromper a sincronização entre os clientes.

Não entraremos na mecânica de calcular o intervalo aleatório, mas a randomização deve espaçar os picos para uma distribuição muito mais suave das chamadas do cliente.

Podemos usar o backoff exponencial com jitter na nova tentativa Resilience4j configurando um backoff exponencial aleatórioIntervalFunction t que também aceita umrandomizationFactor:

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

Vamos voltar ao nosso cenário do mundo real e olhar para os logs de invocação remota com jitter:

[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

Agora temos uma propagação muito melhor. Temoseliminated both collisions and idle time, and end up with an almost constant rate of client calls, barrando o pico inicial.

image

Observação: superestimamos o intervalo para ilustração e, em cenários do mundo real, teríamos lacunas menores.

6. Conclusão

Neste tutorial, exploramos como podemos melhorar como os aplicativos cliente tentam novamente chamadas com falha, aumentando o backoff exponencial com jitter.

O código-fonte para as amostras usadas no tutorial está disponívelover on GitHub.