Detectando e lidando com erros relacionados ao Redis

7 min de leitura
Patrocinado
Imagem de: Detectando e lidando com erros relacionados ao Redis
Avatar do autor

Equipe TecMundo

Falhas acontecem na pilha de aplicativos. Faz parte do processo. Independentemente de você estar rodando testes internos ou conectado a uma API na nuvem, é necessário se certificar de que o design da sua aplicação seja resiliente e tolerante a falhas. Entretanto, no que se refere a apontar os potenciais culpados por esses problemas, como falhas transitórias na rede ou recursos sobrecarregados, é mais fácil falar do que fazer. Além do mais, com microsserviços, as soluções possuem um número crescente de interdependências de componentes que devem diminuir para manter as aplicações operacionais e os usuários, satisfeitos.

Recomendo iniciar com um padrão de repetição para manter seu sistema saudável e diminuir o tempo gasto — em vez de se dedicar a produzir várias soluções para problemas específicos, como a já citada falha transitória de rede. Este artigo mostra um exemplo de como usar a lógica de repetição com uma client library do Redis para ilustrar os passos que você pode seguir para desenvolver uma conexão autorreparável e garantir armazenamento de dados ou cache.

Requisitos

Redis deployment

– Client library ioredis para Node.js

Cloud native designs para aplicar lógica de repetição com Redis

Redis é uma base de dados em memória principal extremamente rápida que possibilita desenvolver armazenamento em cache, session stores ou índices customizados com seus comandos intuitivos. Normalmente, os códigos de aplicações carregam elementos do Redis que permitem dialogar com o protocolo binário da ferramenta. A leitura e a escrita direcionada a essa função é simples:

// create a key (z) and store a value against it (somedata)

await redisClient.set('z', 'somedata')

// retrieve a value from a known key (z)

const data = await redisClient.get('z')

// 'somedata'

Perceba que cada operação que envolve o Redis é uma chamada dessincronizada, mesmo se o servidor estiver em sua máquina local. A interação de rede entre seu código e o Redis pode levar menos de meio milissegundo ou travar indefinidamente — chegando, às vezes, a nunca ser finalizada.

Isso nos leva à necessidade de detectar erros e lidar com o Redis. Como verificar se o servidor do Redis ao qual você estava conectado caiu? Como lidar com um serviço indisponível?

Por sorte, o client library vai executar grande parte da tarefa pesada. Vamos conferir alguns exemplos com o ioredis para Node.js. (A library do Redis é mais popular, mas é raramente atualizada e oferece poucas opções resilientes comparada à do ioredis).

Conecte-se a um serviço do Redis

Essa etapa é simples. Por exemplo, se você utiliza o IBM Cloud Databases for Redis, confira como conseguir as credenciais do serviço e extrair host, port, senha e certificado TLS:

// load your credentials from a JSON file

const credentials = require('./credentials.json')

// format the credentials in the correct form for the ioredis library

const redisconn = credentials.connection.rediss

const opts = {

host: redisconn.hosts[0].hostname,

port: redisconn.hosts[0].port,

password: redisconn.authentication.password,

tls: {

ca: Buffer.from(redisconn.certificate.certificate_base64, 'base64').toString(),

}

}

// load ioredis

const Redis = require('ioredis')

// configure Promises

Redis.Promise = global.Promise

// make Redis connection

const redisClient = new Redis(opts)

Ao iniciar a library ioredis, é estabelecida uma conexão de rede, credenciais de autenticação são trocadas, certificados são verificados e a rede é mantida por tempo indefinido — um passo trabalhoso que pode ser evitado em algum momento futuro. O client object (redisClient) é garantido e pode ser utilizado diversas vezes para executar comandos do Redis

Então, tudo segue tranquilo até a realidade aparecer: redes ficam instáveis, senhas são rotacionadas, computadores precisam ser reiniciados para atualizações ou manutenções e eventos inesperados surgem a qualquer momento. Então, como lidar com essa incerteza ligada à conexão com um serviço Redis?

Reconexão automática com um serviço Redis

Por padrão, a library ioredis tenta se reconectar ao serviço Redis quando há perda de conexão. Ao ser bem-sucedida, ela pode tentar se inscrever novamente em qualquer canal de publicação ou assinatura que foi desconectado, tentando, novamente, executar comandos que falharam.

Essa ação é configurada na inicialização da library com o fornecimento de opções adicionais ao opts object:

– autoResubscribe ajustado para false para prevenir inscrição automática em canais de publicação ou assinatura. (default: true)

– maxRetriesPerRequest é o número de tentativas repetidas para retomar um comando Redis antes de apresentar erro. (default: 20)

– enableOfflineQueue indica se é necessário enfileirar comandos Redis executados quando o cliente ainda não está responsivo. (default: true)

– retryStrategy é uma função que determina o delay entre as tentativas de reconexão. Você define se acontecerão em um intervalo fixo ou se adota um recuo exponencial. (default: every two seconds with back-off)

– reconnectOnError é uma função que determina se a reconexão deve ser cancelada no caso de erros originados no Redis. É útil se o nó do Redis ao qual você estiver conectado entrar no modo de leitura devido a uma reconfiguração de um cluster Redis de vários nós.

Aqui está um exemplo de opts object:

const opts = {

host: redisconn.hosts[0].hostname,

port: redisconn.hosts[0].port,

password: redisconn.authentication.password,

tls: {

ca: Buffer.from(redisconn.certificate.certificate_base64, 'base64').toString(),

},

autoResubscribe: true,

maxRetriesPerRequest: 1,  // only retry failed requests once

enableOfflineQueue: true, // queue offline requests

retryStrategy: function(times) {

return 2000 // reconnect after 2 seconds

},

reconnectOnError: function(err) {

// only reconnect on error if the node you are connected to

// has switched to READONLY mode

return err.message.startsWith('READONLY')

}

}

As opções fornecidas aqui dependerão de sua aplicação. Pode ser que você se beneficie por estar conectado a um nó de leitura, já que seu código está sempre lendo dados. Você pode querer que a library realize mais tentativas para retomar comandos com falha, mas tenha em mente que uma série de comandos não realizados vai acabar com a memória da sua aplicação. Trata-se de atingir um equilíbrio entre detectar erros para que seu código não falhe mais que o normal, continuando com o funcionamento esperado, e sobreviver a perdas curtas de conexões.

Entenda os eventos relacionados à conexão

Vale a pena registrar o comportamento da conexão do seu cliente para que você possa diagnosticar padrões de erros. A library ioredis emite eventos de conexão (reconnectOnError), que você pode utilizar para reunir dados:

const debug = require('debug')('myapp')

redisClient

.on('connect', () => {

debug('Redis connect')

})

.on('ready', () => {

debug('Redis ready')

})

.on('error', (e) => {

debug('Redis ready', e)

})

.on('close', () => {

debug('Redis close')

})

.on('reconnecting', () => {

debug('Redis reconnecting')

})

.on('end', () => {

debug('Redis end')

})

Caso você execute sua aplicação com um set de variáveis de DEBUG, você receberá as seguintes mensagens:

$ DEBUG=mapp node myapp.js

myapp Redis connect +0ms

myapp Redis ready +138ms

Além disso, se a variável do DEBUG for ajustada para *, você verá os dados de debugging da própria library ioredis:

$ DEBUG=* node myapp.js

ioredis:redis status[myredisinstance.databases.appdomain.cloud:32006]: [empty] -> connecting +0ms

ioredis:redis status[169.60.159.182:32006]: connecting -> connect +446ms

ioredis:redis write command[169.60.159.182:32006]: 0 -> auth([ 'mypassword' ]) +1ms

ioredis:redis write command[169.60.159.182:32006]: 0 -> info([]) +2ms

myapp Redis connect +0ms

ioredis:redis status[169.60.159.182:32006]: connect -> ready +137ms

myapp Redis ready +137ms

ioredis:redis write command[169.60.159.182:32006]: 0 -> set([ 'z', 'somedata' ]) +4s

ioredis:redis write command[169.60.159.182:32006]: 0 -> get([ 'z' ]) +134ms

ioredis:redis write command[169.60.159.182:32006]: 0 -> set([ 'z', 'somedata' ]) +5s

ioredis:redis write command[169.60.159.182:32006]: 0 -> get([ 'z' ]) +133ms

Detectando erros enquanto executa comandos Redis

Uma ação comum é utilizar o Redis como cache. Sua aplicação tentará buscar uma cache key. Caso exista, é usada. Do contrário, uma solicitação de busca de dados na database subjacente primária do código é realizada, e eles são gravados em um cache Redis.

const go = async (query) => {

// calculate cache key

const h = hash(query)

const cachedData = await redisClient.get(h)

// if we have cached data

if (cachedData) {

// return it

return JSON.parse(cachedData)

}

// otherwise get the data from the source database

const nonCachedData = await fetchFromSourceDB(query)

// if we got data

if (nonCachedData) {

// write it to the cache for next time

await redisClient.set(h, JSON.stringify(nonCachedData))

}

return nonCachedData

}

Esse é um algoritmo bem simples. Se o serviço Redis estiver com problemas, é possível que você não consiga ler um valor ou gravar dados novamente no cache. Nesse caso, é possível que sua aplicação siga funcionando, mesmo sem o benefício do cache do Redis. Tudo de que você precisa é um código defensivo que possibilite a continuidade das operações. Não tente a operação de gravação se a de leitura falhar.

const go = async (query) => {

// calculate cache key

const h = hash(query)

debug('cache key', h)

let cachedData = null

let readRedisWorked = false

// try to read from Redis

try {

cachedData = await redisClient.get(h)

readRedisWorked = true

} catch (e) {

debug('Redis read error', e)

}

// if we have cached data

if (cachedData) {

// return it

debug('cache hit', h, cachedData)

return JSON.parse(cachedData)

}

// otherwise get the data from the source database

debug('cache miss - fetching from source database')

const nonCachedData = await fetchFromSourceDB(query)

// if we got data

if (nonCachedData && readRedisWorked) {

// write it to the cache for next time

debug('writing to cache', h, nonCachedData)

try {

await redisClient.set(h, JSON.stringify(nonCachedData))

} catch (e) {

debug('Redis write error', e)

}

}

return nonCachedData

}

O código acima coloca as operações do Redis em blocos try/catch. Se o ioredis apresentar um erro (o que vai acontecer depois de uma tentativa de repetição devido à sua configuração opts), seu código absorverá a falha, executará a consulta usando o banco de dados subjacente e não tentará escrever a cache key para o Redis. Em outras palavras, sua aplicação continua funcionando sem o Redis, mas tentará reconexões para que o serviço normal volte o quanto antes. Tenha em mente que o back-end de sua database verá um aumento nas solicitações porque cada leitura de cache será uma "falta"!

Conclusão

A library ioredis possui uma gama de opções para configurar reconexões e lógica automática de repetição para atender muitas necessidades. Uma vez configuradas, códigos defensivos podem proporcionar a continuidade do desenvolvimento mesmo com interrupções temporais do Redis, para que sua aplicação permaneça funcional.

...

Quer ler mais conteúdo especializado de programação? Conheça a IBM Blue Profile e tenha acesso a matérias exclusivas, novas jornadas de conhecimento e testes personalizados. Confira agora mesmo, consiga as badges e dê um upgrade na sua carreira!

.....

Participe da Maratona Behind the Code 2020, um desafio para desenvolvedores e entusiastas da tecnologia! Além de concorrer a prêmios, você ainda tem acesso a conteúdos e serviços gratuitos. Não perca essa chance, as inscrições vão até 7 de agosto!

Você sabia que o TecMundo está no Facebook, Instagram, Telegram, TikTok, Twitter e no Whatsapp? Siga-nos por lá.