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!