Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 29, 2022 08:12 pm GMT

Retries e lidando com erros transientes 101

Se voc leu o primeiro post dessa srie sobre sistemas distribudos voc aprendeu que Sistemas distribudos so estranhos. A partir do momento que separamos os sistemas em computadores diferentes atravs da rede, um mundo
novo de possibilidades e problemas se tornam acessveis a ns. Entre eles, nossos sistemas agora so suscetveis a erros transientes.

Nesse post, ns vamos discutir essas falhas temporrias, como podemos tentar resolver esse problema e quais novos problemas so gerados como parte de nossa soluo.

Problema: Erros transientes

Se voc trabalha com software voc j deve ter esbarrado em um erro persistente, por exemplo, um bug no seu sistema que d erro 100% das vezes que determinada condio ocorre ou quando o servidor sai do ar totalmente.
Uma outra categoria de erro so os erros transientes ou erros temporrios. Esses erros normalmente ocorrem de forma inesperada, muitas vezes duram apenas 1 segundo ou apenas alguns milisegundos, apenas o suficiente pra estregar o sucesso da sua requisio .

Se lembrarmos do post anterior, as falcias dos sistemas distribudos ajudam a explicar os erros transientes:

  1. A rede confivel: A rede vai falhar. Ela pode falhar por apenas 1 segundo. Mas isso pode ser suficiente para impedir aquela requisio que seu sistema tentou fazer com sucesso.
  2. A topologia no muda: Em tempos de cloud, mquinas so adicionadas e removidas da rede o tempo inteiro. Imagine o que acontece se o sistema A depende do sistema B e, no momento que a requisio A -> B feita, o sistema B est executando um deploy e a mquina responsvel por atender a requisio removida da rede.
  3. A topologia no muda: Com aparelhos mobile, usurios esto desconectando e reconectando em redes diferentes o tempo inteiro. O que acontece quando o usurio est executando uma requisio nos segundos de desconexo da rede?

Os exemplos so infindveis. Em resumo, sempre haver a chance que a sua primeira tentativa de executar uma requisio vai falhar.

Exemplo

Disclaimer: O cdigo abaixo no passa na qualidade pra ir pra produo .

Ok, muita lorota mas que tal simularmos o nosso problema? O cdigo completo pode ser encontrado no github.

shut up and show me the code

O mtodo abaixo roda em uma pequena App com Spring Boot. Em resumo, o mtodo falha todo minuto entre 00 e 05 segundos.

@RequestMapping("/")public ResponseEntity<String> home() throws InterruptedException {  final var now = LocalDateTime.now();  logger.info("Failure flag value: " + now);  // Sempre envie um erro durante os primeiros 5s de cada minuto.  if (now.getSecond() >= 0 && now.getSecond() <= 5)    throw new IllegalStateException();  return ResponseEntity.status(200).body("Current time: " + now);}

Eu escrevi um cliente que invoca essa app continuamente, porm, se o cliente percebe 1 erro, o cliente interrompe a execuo.

public static void main(String[] args) throws URISyntaxException, InterruptedException {  final HttpRequest request = HttpRequest.newBuilder()      .uri(new URI("http://localhost:8080"))      .GET()      .build();  while (NUM_ERRORS < 1) {    try {      var response = callService(request)      NUM_ERRORS = 0;      System.out.println("--------------------------------------------");      System.out.println(Thread.currentThread().getName());      System.out.println(response.statusCode());      System.out.println(response.body());      System.out.println("--------------------------------------------");    } catch (RuntimeException ex) {      System.out.println(ex.getMessage());      NUM_ERRORS++;    }    Thread.sleep(500);  }}private static HttpResponse<String> callService(HttpRequest request) {  try {    System.out.println("Executing request at" + LocalDateTime.now());    final HttpResponse<String> response =        HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());    if (response.statusCode() == 500)      throw new RuntimeException("Failed to call API");    return response;  } catch (IOException e) {    System.out.println("Operation failed, exception IO");    throw new RuntimeException(e);  } catch (InterruptedException e) {    System.out.println("Operation failed, exception Interrupted");    throw new RuntimeException(e);  }}

Nossa execuo pode gerar o seguinte log:

main200Current time: 2022-07-26T18:19:59.238087--------------------------------------------Executing request at2022-07-26T18:19:59.743644--------------------------------------------main200Current time: 2022-07-26T18:19:59.744904--------------------------------------------Executing request at2022-07-26T18:20:00.250584Failed to call API

E a? Como a gente pode evitar que nossa aplicao pare com apenas 1 erro?

Retries lineares

A forma mais simples de contornamos um erro transiente tentar a mesma requisio de novo. Existem diversas bibliotecas que ajudam com isso como
sprint-boot-retry e o resilience4j. No nosso exemplo aqui, eu implementei com resilience4j.

O cdigo principal segue abaixo:

 public static void main(String[] args) throws URISyntaxException, InterruptedException {    final HttpRequest request = HttpRequest.newBuilder()        .uri(new URI("http://localhost:8080"))        .GET()        .build();    Function<HttpRequest, HttpResponse> service = (HttpRequest x) -> callService(x);    var config = RetryConfig.custom().retryExceptions(Exception.class).maxAttempts(3).waitDuration(        Duration.ofSeconds(3)).build();    var registry = RetryRegistry.of(config);    var retry = registry.retry("retry");    final var retryableServiceCall = Retry.decorateFunction(retry, service);    while (NUM_ERRORS < 1) {      try {        var response = retryableServiceCall.apply(request);        NUM_ERRORS = 0;        System.out.println("--------------------------------------------");        System.out.println(Thread.currentThread().getName());        System.out.println(response.statusCode());        System.out.println(response.body());        System.out.println("--------------------------------------------");      } catch (RuntimeException ex) {        System.out.println(ex.getMessage());        NUM_ERRORS++;      }      Thread.sleep(500);    }  }

Um possvel resultado de executar o cdigo acima seria:

main200Current time: 2022-07-27T17:05:59.227065--------------------------------------------Executing request at2022-07-27T17:05:59.731737--------------------------------------------main200Current time: 2022-07-27T17:05:59.733363--------------------------------------------Executing request at2022-07-27T17:06:00.238117Executing request at2022-07-27T17:06:03.303864Executing request at2022-07-27T17:06:06.316072--------------------------------------------main200Current time: 2022-07-27T17:06:06.318999

Observe como a execuo roda a cada 500ms mas o seguinte comportamente acontece:

  1. s 2022-07-27T17:06:00.238117 ns recebemos 1 falha.
  2. Logo em seguida, vemos que nosso client tenta novamente 2022-07-27T17:06:03.303864, mais 1 falha pois ainda estamos dentro do intervalo de falha (00-05s).
  3. Finalmente a nossa 3 tentativa executada com sucesso 2022-07-27T17:06:06.316072
  4. Nossa execuo continua como se no houvesse nenhuma falha. Afinal, a chamada foi feita com sucesso com o uso de retries.

Bacana n? Mas como tudo em engenharia de software, a soluo acima tem alguns problemas.

Problemas com retries

Imagine a seguinte situao.

  1. o incio de uma black friday e todo mundo decide acessar o seu servio ao mesmo tempo.
  2. Ao tentar processar esse fluxo de requisies ao mesmo tempo o nosso pobre servidor no aguenta e manda um erro temporrio.
  3. Recebendo o erro temporrio, todos os clientes decidem tentar a requisio novamente.
  4. As requisies com retry chegam todas de uma vez.
  5. O nosso servidor continua sobrecarregado e continua falhando as requisies.
  6. O usurio fica infeliz porque perdeu aquela promoo bacana...

Ser que d pra fazer melhor?

Retries com backoff and jitter

Nesse post de Maro de 2015 Marc Brooker, Senior Principal Engineer no AWS, introduz a idia de solucionar esse problema utilizando um algoritmo de retry com backoff e jitter. Como funciona essa ideia?

  1. Ao invs de executar a requisio de retry imediatamente aps a falha o cliente espera um pouco
  2. Esse tempo de espera determinado seguindo uma frmula que leva em conta um pouco de aleatoriedade( essa parte chamada jitter) e uma parte exponencial relacionada ao nmero de falhas (essa a parte de backoff).

Por qu backoff?

A ideia do backoff no tentar a requisio imediatamente. Ao invs disso, para casa falha, ns dobramos o tempo de espera. Por exemplo, na primeira falha esperamos 2s pra tentar de novo, na segunda falha ns esperamos 4s, na terceira falha 8s e assim sucessivamente. Essa estratgia faz com que os retries no continuem colocando presso em um servidor j sobrecarregado.

Por qu Jitter?

Porm o backoff no resolve o problema completamente, imagine que todos os clientes falham ao mesmo tempo. Isso significa que em 2s o nosso servidor vai levar uma sobrecarga de requisies e em 4s de novo...
O Jitter adiciona um pouco de aleatoriedade pra melhorar essa estratgia. Enquanto alguns clientes podem retentar em 2s, outros vo faz-los em 2.1s ou 2.2s. Essa pequena varincia ajudar a espalhar o nmero de requisies ao longo do tempo diminuindo a carga sobre a nossa aplicao.

Se voc quer mais, o zanfranceschi tem uma thread bacana falando mais sobre backoff e jitter.

Mais problemas com retries

Voltando ao nosso problema da black friday imagine a segunda situao.

  1. Ns adicionamos o algoritmo de backoff e jitter ao nossos retries.
  2. Mesmo assim, a carga to alta que o nosso servidor no segue aguentar as requisies que continuam chegando em 2.1, 2.2 ou 2.3s depois.
  3. Com a adio dos retries ao trfego normal, toda vez que o servidor t pra se recuperar ele cai novamente.

A pergunta que fica , se a carga est to alta e um retry sempre vai falhar, por que deveramos continuar enviando retries?

Retries com algoritmo de token bucket

Pra solucionar o problema acima ns vamos utilizar um novo algoritmo de retries que a estratgia de utilizar um algoritmo de token bucket. Como funciona essa ideia?

  1. Para cada request feita com sucesso ns adicionamos uma percentagem de bucket em uma varivel. Por exemplo: 0.1 token para cada sucesso.
  2. Para cada falha ns removes 1 token da varivel.
  3. Ns s podemos realizar chamadas enquanto o token for acima de 0.

Por exemplo:

  1. Suponha que nosso token bucket comea com um valor de 3.
  2. Quando a primeira request falhar, ns subtramos 1 do bucket. Agora o bucket tem o valor de 2.
  3. Como o bucket tem o valor acima de 0, ns executamos uma nova request.
  4. Recebemos uma nova falha e o bucket cai para 1.
  5. Tentamos novamente, acontece uma nova falha o bucket cai pra 0.
  6. Os retries param totalmente.
  7. Novas requisies podem ser feitas mas note que os retries no vai ser feitos j que o bucket atingiu 0.
  8. Quando as requisies comearem a ter sucesso, elas comeam a adicionar 0.1 bucket para cada sucesso.
  9. Depois de 10 requisies com sucesso ns temos um bucket de valor 1 e agora caso haja uma falha ns teremos direito a 1 retry.

Esse algoritmo timo para impedir o problema de retry storm que ocorre quando um servio A chama um servio B que chama um servio C. Um erro em cascata pode dar trigger em um retry de A que pode tentar at 3x, B pode tentar tambm 3x o que faz com que C possa receber at 9x o trfego .

Esse algoritmo pode ser utilizado ao lidar com a AWS SDK utilizando a estratgia de TokenBucketRetryCondition.

Ainda mais problemas com retries

Infelizmente, mesmo o token bucket no soluciona todos os nossos problemas. Por exemplo:

  1. O que acontece quando executamos o retry mas o servidor ainda est processando a 1 requisio?
  2. Se falharmos 2x, vale pena continuar consumindo recursos e tentar uma 3 vez?
  3. Toda vez que fazemos retry, ainda estamos segurando a resposta ao usurio, o que faz com que a latncia da requisio aumente e logo introduz mais erros de timeout.
  4. Devemos fazer retry no nvel mais baixo, por exemplo em uma chamada de API, ou no nvel mais alto apenas para evitar retry storms?

Esse artigo no vai responde todas essas perguntas mas algumas delas podemos explorar em novos artigos no futuro.

Concluso

Nesse artigo ns discutimos:

  1. Erros temporrios e porqu eles acontecem.
  2. Diferentes estratgias de retry como linear, backoff and jitter e Token Bucket.
  3. Diversos problemas ao se utilizar retries.

Como tudo em software, qual estratgia voc vai utilizar depende. As vezes, no fazer o retry pode ser a melhor estratgia. Um post recente do Marc Brooker no blog pessoal dele discute em detalhes os trade-offs de cada estratgia de retry e compara elas com circuit breakers, por exemplo.

Espero que tenham curtido o post, e se voc curtiu, compartilha e d o like!

Referncias

  1. Fixing retries with token buckets and circuit breakers
  2. Timeouts, retries, and backoff with jitter
  3. Exponencial Backoff and Jitter
  4. Summary of retry strategies

Original Link: https://dev.to/hugaomarques/retries-e-lidando-com-erros-transientes-101-5758

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To