.NET — Timeout com StackExchange.Redis (RedisTimeoutException)

Elvis Fernandes Dias
7 min readFeb 23, 2022

--

Redis é um banco de dados chave valor, in memory, utilizado amplamente como cache distribuído. Assim como qualquer outra tecnologia, ele traz inúmeras vantagens, mas também podem ocorrer alguns problemas na sua utilização. Neste artigo, vou compartilhar com vocês um dos problemas que enfrentamos com a adoção do Redis e como contornamos.

Recentemente me deparei com um problema de timeout em algumas operações de busca de chave no Redis, e no contexto da aplicação, na ocorrência de timeout, deveríamos realizar a busca da informação na fonte primária, sendo ela uma API ou um banco de dados, e o processo para buscar as informações na fonte primária dispendia um tempo crucial para a operação.

Os diagramas abaixo demonstram como o processo de busca de valores no cache funcionava:

1) Fluxo de sucesso, quando não existe dado no cache

2) Fluxo de sucesso, quando existe dado no cache

3) Fluxo de falha, quando não é possível obter o dado do cache

O diagrama 3 representava nosso calcanhar de Aquiles, principalmente quando o cache continha a informação e nós não conseguíamos buscá-la. O sintoma desse problema era uma degradação significativa do tempo de resposta da aplicação pois quando ocorria um timeout, vários outros ocorriam em cascata.

Bom, acredito que nesse ponto ficou claro qual era nosso problema. Resolver os timeouts.

Análise da causa

Timeouts nas operações com Redis é um problema complicado de se encontrar a causa raiz e a solução. A própria exceção RedisTimeoutException, retornada pela lib StackExchange.Redis, indica uma documentação onde podemos obter algumas dicas de como resolver o problema. Para uma exceção ter o link da documentação, imagino quantas issues a galera que suporta a lib teve que responder sobre esse tema.

Exemplo

Timeout performing GET (1000ms), next: GET solution:2097, inst: 0, qu: 0, qs: 1, aw: False, rs: ReadAsync, ws: Idle, in: 0, in-pipe: 102209, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: XXXXXXXXX, IOCP: (Busy=1,Free=999,Min=4,Max=1000), WORKER: (Busy=0,Free=32767,Min=4,Max=32767), v: 2.0.601.3402 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)

Existem outras documentações sobre o que pode causar esse erro, mas basicamente temos os seguintes pontos a observar:

  1. Consumo excessivo de CPU, client ou server;
  2. Largura de banda, rede;
  3. Problemas de conectividade;
  4. Alto consumo de memória, server;
  5. Comandos de execução longa enviados ao redis (https://redis.io/commands/slowlog-get);
  6. Valores grandes armazenados no Redis;

Investimos um tempo considerável analisando cada uma das possibilidades, CPU, memória, rede, log dos comandos executados para verificar a existência de algum tipo de comando de longa duração, e tudo estava OK, sem nenhum tipo de anormalidade.

Então nos restou avaliar o tamanho das estruturas armazenadas no Redis, por este motivo deixei esse item em destaque na listagem acima. Mas vamos com calma.

A lib StackExchange.Redis utiliza apenas uma conexão com o servidor de cache para realizar as operações, por tanto, um request que retorne um valor “grande” pode impactar toda a fila de comandos. O link abaixo explica muito bem esse ponto:

https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-development#large-request-or-response-size

Mas o que seria um valor “grande” para o conteúdo? Aí meu amigo, é onde temos um problema, pois isso depende de muitos fatores, e acredito que a melhor forma de identificar isso é realizando testes de carga simulando vários tamanhos de conteúdo e verificar quando os erros começam a ser significatvos.

Tínhamos conteúdos que ultrapassavam 100 kb, e, em nosso cenário, isso era suficiente para gerar alguns problemas. Pode ser que para seu cenário e ambiente um valor com esse tamanho não seja problema.

Antes de abordarmos a solução que adotamos, vou deixar alguns links que auxiliaram muito no entendimento do problema e a causa raiz:

Solução

Identificado que o problema era o tamanho do conteúdo salvo no cache, inicialmente pensamos em quebrar os valores em diversas chave menores, assim como está descrito nas documentações dos links acima, porém para essa solução precisaríamos realizar um volume considerável de alterações na aplicação, e buscávamos algo com menos impacto no código.

Segunda hipótese avaliada foi, vamos utilizar o cache local, em memória, e não o cache distribuído. Esse tipo de solução definitivamente resolveria o problema, mas tínhamos alguns impactos, o mais latente era que já existiam algumas rotinas que realizam a limpeza do cache distribuído para forçar um reload de algumas informações na aplicação, e mudando o cache de distribuído para local, todas essas rotinas deveriam ser revistas, portanto não seguimos com essa solução.

Por último pensamos em uma estratégia de cache local e distribuído, heterogênea, onde todos os dados do cache são salvos localmente, mas também é mantido uma referência no cache distribuído e eventualmente a aplicação faz uma busca no cache distribúido, Redis, a fim de validar se seu cache local é válido ainda. Desta forma a aplicação sofreria poucas alterações, reduziríamos muito o volume de chamadas no Redis e as rotinas externas que interagiam com o cache distribuído não seriam impactadas.

Vou demonstrar abaixo, de forma simples, como foi essa implementação de cache heterogêneo, local e distribuído.

Vamos imaginar que temos nossa interface IUserRepository

E temos nossa implementação que realizada a busca das informações em algum repositório, API ou banco de dados por exemplo. No código de exemplo apenas realizamos a leitura de um arquivo.

Bom, dado que temos nossa classe de repositório, UserRepositoty, podemos ter uma nova implementação da interface IUserRepository que recebe como parâmetro no construtor a classe UserRepositoty e implementa a estratégia do cache distribuído:

Podemos observar que a classe UserRemoteCacheRepositoty recebe em seu construtor a implementação de IUserRepository que realiza a busca dos dados na fonte primária e também a classe CacheOperations que realiza operações no cache distribuído. Basicamente essa era a estrutura que estava apresentando o problema de timeouts devido o tamanho do conteúdo armazenado no Redis.

Para solucionarmos, realizamos uma nova implementação da interface IUserRepository , com a estratégia de armazenarmos os dados no cache local e uma referência no cache distribuído.

Já peço desculpa pelos nomes adotados nas classes, sou PÉSSIMO com nomenclaturas :(

Voltando ao que interessa, na classe UserMixLocalRemoteCacheRepositoty, observamos que recebemos no construtor a classe UserRepositoty, que realiza buscas na fonte primária, a classe RedisCacheOperations, que realiza operações no cache distribuído (Redis) e a interface IMemoryCache que corresponde ao cache local (In Memory) do asp.net core.

O diagrama abaixo representa a lógica implementada na classe UserMixLocalRemoteCacheRepositoty.

Abaixo descrevo a lógica da classe UserMixLocalRemoteCacheRepositoty para processar as requisições de informação de usuário:

  1. Primeiramente é realizado duas consultas no cache local, itens 1 e 2 do diagrama. Uma buscando as informações do usuário e a segunda buscando a última data em que foi realizado uma consulta no cache distribuído da respectiva chave;
  2. Primeira condição de retorno (UserInfo <> null and LastSyncDate Ok). Caso o dado exista no cache local e a última consulta no Redis foi realizada a menos de 1 minuto (TTL), então podemos retornar o valor do cache local para a aplicação;
  3. Caso tenhamos a informação do usuário no cache local mas não temos a data da última consulta no Redis (UserInfo <> null and LastSyncDate Nok), então validamos se existe a referência da chave no cache distribuído, item 3 do diagrama. Caso exista a chave de referencia no Redis, então atualizamos a data do último sync no cache local, iten 4 do diagrama, e retornamos o user info obtido do cache local para a aplicação;
  4. Bom, se chegamos nessa etapa é porque não temos a informação no cache local ou não temos mais a referência no cache distribuído, então precisamos consultar a informação na fonte primária, atualizar o cache local e o cache remoto, itens 5 6 7 e 8 do diagrama, e após podemos retornar a informação para a aplicação.

Sei que já está extenso o artigo, mas vale algumas observação adcionais.

Na solução proposta, a chave que armazenamos no Redis apenas contém um valor bool para indicar a existência do dado, e o nome da key deve ser composta por algum identificador da instância da aplicação.

Outra consideração, essa solução traz o cache que ocupava memória no servidor Redis para o host da aplicação, portanto teremos um aumento no comsumo de memória da aplicação.

Essa solução foi bem viável para o problema que estávamos enfrentando e espero que seja útil para alguém.

Fique a vontade para indicar melhores soluções e possíveis melhorias ou correções na solução proposta, o objetivo aqui é agregar.

Abaixo o link de um repositório no github onde deixo uma api em asp.ner core 6 com essa implementação.

Fonte: https://github.com/ElvisFDias/LocalCacheMinimalApi

Obrigado e até mais!!

--

--

Elvis Fernandes Dias

.Net Developer Graduado em Ciência da Computação e Pós Graduado em Engenharia de Software