Resolvendo problemas do Redis para mais estabilidade e velocidade
Às vezes, as coisas dão errado na Transloadit. Quando isso acontece, queremos ser transparentes e assumir nossos erros. Por isso, estou divulgando um problema que afetou 81 dos nossos clientes na semana passada.
No início da semana passada, recebemos relatos de um aumento no número de erros WORKER_JOB_ERROR. Isso
indica que há um problema de comunicação entre nossos Uploaders (as máquinas que recebem arquivos e
orquestram Assemblies) e nossos Drones (as máquinas que executam as tarefas de codificação).
Compartilhamos o problema no Twitter e no transloaditstatus.com e começamos a investigar.
Como autores do módulo retry, implementamos novas tentativas para muitas coisas na Transloadit. Isso também significa que quase todas as Assemblies podem ser reexecutadas sem perda de dados. Em alguns casos, porém, não foi possível reexecutar as Assemblies e, em muitos outros, elas ficaram simplesmente muito mais lentas, porque as novas tentativas com backoff exponencial entravam em ação quando havia erros.
Depois de vasculhar nossos logs e depurar o código, percebemos que estávamos perdendo Jobs enfileirados no cluster Redis regional us-east, assim como heartbeats entre os Uploaders e Drones mencionados acima. Monitorar os heartbeats nos permite passar um Job para outro Drone, caso o Drone original pare de emiti-los.
Aplicamos imediatamente uma solução paliativa, readicionando as mensagens perdidas e tornando nosso monitoramento de heartbeats menos rigoroso. Essa abordagem, no entanto, apenas combatia os sintomas, em vez de atacar a raiz do problema. Além disso, ela também aumentava os tempos de execução das Assemblies. Em outras palavras, precisávamos fazer mais.
O plano de ação
A equipe então se reuniu, e vários membros seguiram caminhos diferentes para identificar a causa raiz do problema. Mais especificamente, nós:
- Verificamos se havíamos deixado de aplicar algum patch crítico do Redis e atualizamos para a versão estável mais recente.
- Fizemos uma auditoria completa do código da nossa biblioteca do lado do cliente e avaliamos a forma como a utilizamos.
- Aprimoramos nosso monitoramento, métricas, alertas e entradas de log. Já monitoramos inúmeras métricas, mas só conseguimos configurar alertas sensatos para um número limitado delas. É possível que nossa seleção tenha sido insuficiente.
Quando a equipe se reuniu novamente para compartilhar as descobertas, havia problemas em várias frentes:
- Não estávamos rodando a versão mais recente do Redis, na qual vários bugs alarmantes tinham sido
corrigidos, alguns deles marcados com
[FIX] Fixed data loss. - A equipe não encontrou problemas na versão recente da biblioteca do lado do cliente que estávamos usando. ✅
- Estávamos acompanhando o número de comandos, conexões e uso de swap do Redis, entre outros, mas não a largura de banda. Rodávamos um cluster de 3 nós nos EUA, e parecia improvável que saturássemos todas as placas de rede deles. Olhando mais de perto, porém, descobrimos que era exatamente isso que estava acontecendo.

Assim que ficou claro que estávamos limitados pela largura de banda, dobramos o tamanho do nosso cluster. Isso deveria distribuir a carga de trabalho entre seis nós Redis, resultando em uma vazão total de leitura de ~6Gbit/s. Estávamos convencidos de que isso certamente seria suficiente.

Depois disso, notamos um aumento imediato no consumo de largura de banda:

O consumo, no entanto, rapidamente atingiu novos pontos de saturação, e muitas Assemblies continuaram lentas. A princípio, achamos que ainda estávamos subestimando o tamanho necessário do nosso cluster. Estávamos sendo limitados pela capacidade de três nós Redis e, como seis nós Redis também não pareciam suficientes, adicionamos mais três nós Redis para ver como isso afetaria o consumo de largura de banda.
Para nossa surpresa, isso só pareceu piorar a situação. Olhando mais de perto, notamos que a maior parte do tráfego de saída era gerada pelo líder do cluster. Com ~4Gbit/s, ele agora emitia muito mais tráfego do que a Amazon anuncia como sequer possível (o que pode render um post interessante no futuro), enquanto o tráfego nos outros nós Redis permanecia bem dentro dos limites.
Isso era estranho, porque o tráfego de saída (leituras) deveria ser distribuído por todo o cluster. Por que o líder gerava tanto tráfego? Além de lidar com escritas e Pub/Sub, ele deveria emitir apenas cerca de um nono do tráfego de saída total.
Menos é mais
Só então nos demos conta: replicação. Toda escrita ia para o líder, que agora, em vez de sincronizar os bits escritos com dois nós Redis, precisava sincronizar esses dados com oito nós Redis. Como o Redis não tem um protocolo sofisticado como o gossip, tudo o que nossos Drones enviavam agora precisava ser copiado para oito nós em vez de dois (o que já era problemático). São quatro vezes mais trabalho e tráfego de saída para esse único líder Redis emitir.
Para piorar um pouco as coisas, nossos dados são intensivos em escrita e altamente voláteis. Eles só são úteis por um breve momento. Então, depois de copiados, os dados também precisavam ser excluídos novamente pouco tempo depois.
Como conseguimos readicionar dados ausentes, reconsideramos se a ideia aparentemente boa da replicação era realmente um requisito.
O ideal seria ter zero tráfego de replicação e fazer failover para um nó Redis vazio. Com o AWS Elasticache, no entanto, a replicação é obrigatória para o failover automático. Com esse novo conhecimento em mente, reduzimos o número de réplicas de leitura para uma. Nossos logs mostraram imediatamente uma queda tanto no tráfego quanto nas taxas de falha, e as Assemblies também voltaram a ficar mais rápidas.
Um respiro
Com a causa raiz finalmente encontrada, pudemos respirar de novo e começamos a nos perguntar como isso poderia ter acontecido. Era um tanto intrigante, já que não encontramos nenhuma alteração no nosso código ou na nossa infraestrutura que explicasse a saturação repentina da capacidade do Redis.
Notamos, no entanto, duas mudanças no ambiente:
- Um aumento acentuado no tráfego HLS. O HLS divide vídeos grandes em muitos fragmentos pequenos, permitindo que usuários de dispositivos móveis baixem o próximo trecho do vídeo na qualidade ideal para a conexão atual (reduzindo tudo gradualmente até ficar só o áudio quando a conexão fica muito ruim). Isso pode facilmente fazer com que mil vezes mais metadados circulem para cada vídeo.
- Podemos ter mais Drones online para processar o mesmo volume em menos tempo. Isso, por natureza, deixa os nós centrais mais ocupados.
Foi a combinação dessas duas mudanças que expôs esse gargalo na nossa infraestrutura. Em seguida, tomamos a decisão errada de aumentar o número de nós Redis para aliviar o problema, mas descobrimos que ter mais máquinas na verdade sobrecarregava a vazão. Para conseguir lidar com mais tráfego, tivemos que usar menos nós.
Olhando para o futuro
Como resultado da nossa ampla investigação, todos os componentes de software mencionados acima já foram atualizados, então não estamos mais suscetíveis a bugs conhecidos que poderiam levar à perda de heartbeats ou de dados, além dos problemas de largura de banda que encontramos. Também configuramos logs e alarmes adicionais para o caso de nos aproximarmos novamente dos níveis de saturação.
O tráfego entre os nós Redis e nossos Drones e Uploaders era legítimo e, portanto, precisaremos começar a buscar formas de descentralizá-lo. A redução do tráfego de replicação pode ter nos dado algum tempo, mas queremos conseguir sustentar um crescimento que não é possível com apenas um único nó Redis aceitando as escritas. Mesmo que escalemos verticalmente (usando um único nó Redis maior) sem replicação, isso coloca um novo gargalo desconfortavelmente próximo.
Pensamos em ter réplicas de leitura de réplicas de leitura, permitindo compartilhar o tráfego de replicação, mas, se nosso crescimento continuar, isso também só nos daria mais alguns meses. Além disso, como somos limitados pelas escritas e sabemos que essas escritas precisam ficar em uma única máquina, essa opção fica completamente descartada.
Também pensamos em migrar para outro datastore (distribuído) otimizado exatamente para esse propósito, mas uma migração dessa escala provavelmente levaria semanas, se não meses, para ser concluída. É um tempo que simplesmente não temos. Trocar de datastore com a operação em andamento já é complicado o suficiente, ainda mais quando se está com pressa.
O sharding parece ser nossa melhor aposta neste momento. O sharding permite separar os dados em diferentes grupos lógicos e executar cada grupo em hardware separado (ou no mesmo, mas a decisão é sua). Outras empresas costumam fazer sharding por cliente. Atualmente, já fazemos sharding por região e agora planejamos adicionar sharding por Uploader. Fazer sharding por cliente parece menos ideal na nossa configuração, porque um único cliente com uma importação grande poderia, sozinho, esgotar o Redis. No entanto, já escalamos os Uploaders conforme os picos de tráfego, então vamos ganhar gradualmente mais capacidade de Redis à medida que mais tráfego chegar. O tráfego é marcado com o nome do Uploader, então também ficará claro qual nó Redis precisará de atenção.
O trabalho nisso já começou, e planejamos lançá-lo nesta semana. Enquanto isso, colocamos um limite no escalonamento de Drones em cada região. Isso significa que a conclusão de grandes importações em lote pode demorar um pouco mais do que você espera, mas o sistema continuará estável.
Pedimos desculpas
Entendemos que tempos de execução lentos podem ser muito frustrantes, assim como ter que reexecutar manualmente Assemblies que não foram reexecutadas automaticamente. Queremos pedir nossas mais sinceras desculpas e oferecer uma semana de serviço gratuito a quem foi seriamente afetado por isso. Entre em contato conosco e vamos resolver a situação. É claro que o reembolso provavelmente não é o que mais importa para você. O que importa é um serviço confiável. Ainda assim, esperamos que isso ajude a amenizar um pouco o transtorno e mostre que levamos muito a sério problemas como esses.
