Arquitetura real · Pleno
O cache "sempre-verde": saldo por event sourcing sem estourar a memória
O problema é concreto: qualquer instância atendia qualquer cliente, então cada uma ocupava cerca de 100 GB e sofria pausas de GC. A solução usa a chave account-id + event-counter, com o contador incrementado na mesma transação. O caso explica por que o basis-t global foi rejeitado.
Resposta curta
O problema é concreto: qualquer instância atendia qualquer cliente, então cada uma ocupava cerca de 100 GB e sofria pausas de GC. A solução usa a chave account-id + event-counter, com o contador incrementado na mesma transação. O caso explica por que o basis-t global foi rejeitado.
01 · CONTEXTO
Saldo como soma de eventos
Em event sourcing a ideia é, em vez de guardar o estado atual do sistema num banco, guardar cada passo da história até agora. Para o saldo de uma conta, isso significa que o saldo numa data é a soma de todos os depósitos até ela menos a soma de todos os saques até ela.
A pergunta previsível sobre essa abordagem era se o serviço percorre a lista inteira a cada consulta de saldo. Com o tempo, o que se mostrou um problema foi o requisito de memória.
O Datomic, banco usado pelo serviço, separa as tarefas de escrever, armazenar e consultar dados. Todas as escritas passam por um único processo, o transactor, que as serializa e valida. Os processos de consulta são os peers, e no Nubank os peers são as próprias instâncias dos microsserviços, que carregam dados do DynamoDB para um grande cache em memória. O Nubank também lista Datomic e DynamoDB entre as tecnologias em que guarda dados.
02 · O PROBLEMA
Instâncias que não cabiam na memória
As máquinas começaram a travar: ficavam sem memória e paravam por pausas de coleta de lixo que duravam minutos. A primeira resposta foi aumentar a memória das instâncias e, no início, funcionou. Ao chegar a instâncias de 100GB penduradas por um fio, a equipe concluiu que não dava para continuar assim.
O problema real repousava em dois fatos sobre o sistema. O primeiro: a abordagem com event sourcing exigia carregar muitos dados do armazenamento para o cache em memória dos peers. O segundo: qualquer instância do serviço podia ser chamada para responder a qualquer pedido.
Um cliente que faz quatro pedidos de saldo ao navegar pelo app pode forçar quatro instâncias diferentes a carregar os mesmos dados.
Cada instância carrega dados do armazenamento para um grande cache em memória, e qualquer uma pode ser chamada para responder a qualquer pedido.
Nenhuma instância poderia guardar o banco inteiro no cache em memória, mas todas tinham de guardar dados de todos os clientes ativos a cada momento.
Com instâncias de 100GB penduradas por um fio, as máquinas ficavam sem memória e paravam por pausas de coleta de lixo de minutos.
Na prática, todas as instâncias gastavam tempo e memória refazendo trabalho que outra máquina já tinha feito.
03 · A IDEIA
Um cache que nunca precisa ser invalidado
O autor observa que o difícil em cache não é cachear, e sim invalidar. A saída foi contornar o difícil em vez de resolvê-lo: um cache em que toda referência aponta para nada (um miss) ou para algo imutável, portanto válido para um instante. Quando o mundo muda, muda-se a referência, não o valor.
A inspiração veio do próprio Datomic, que guarda dados como uma grande árvore de nós imutáveis. Se um nó já está no cache em memória, ele é válido por ser um valor imutável.
A receita tem três peças: um armazenamento imutável que mapeia referências a valores, um jeito de derivar o valor atual do banco e empurrá-lo ao armazenamento, e um jeito de obter uma referência válida para o valor mais recente. Qualquer armazenamento chave-valor serve como armazenamento imutável, e a equipe escolheu o DynamoDB.
A estratégia de leitura é read-through: procura-se no cache; se o dado falta, calcula-se do zero, grava-se no cache e devolve-se.
04 · A CHAVE
Da versão global à versão por conta
O Datomic tem um número de versão que a aplicação pode usar, o basis-t, uma referência a um ponto no tempo do banco. A equipe considerou usá-lo como referência da chave, mas toda transação incrementa esse número. Perder o cache de uma conta por causa de um depósito em outra conta é tolice, diz o autor.
A solução foi adicionar um atributo à entidade da conta no Datomic. Ele começa em zero e, a cada ação relacionada àquela conta, é incrementado em 1 na mesma transação. Como conta cada evento da conta, ganhou o nome event-counter, e dá um número de versão dos dados da conta, usável como chave de cache.
| Basis-t global | Contador por conta | |
|---|---|---|
| Versão que entra na chave | Relógio global do Datomic | Atributo na entidade da conta |
| O que a faz mudar | Toda transação | Toda ação da conta, na mesma transação |
| Efeito no cache | Perde o cache por depósito em outra conta | Sem motivos bobos: o saldo da conta de fato mudou |
O app pede o saldo de uma conta e o pedido pode chegar a qualquer instância do serviço.
A instância pergunta ao Datomic o valor atual do event-counter da conta, e esse número serve de versão para a chave de cache.
Se a chave não está no armazém imutável, é miss: a instância consulta o Datomic, calcula o saldo e o grava no armazém sob a chave.
Em outro pedido do saldo da mesma conta, o event-counter continua o mesmo e a chave de cache também.
Quando a conta recebe um novo depósito ou saque, o event-counter aumenta e o pedido seguinte cai em miss.
05 · TRADE-OFFS E RESULTADO
O banco primeiro, o cache depois
Num CDN, consulta-se o cache primeiro e só depois o servidor real. No esquema descrito, a primeira coisa é consultar o banco para obter o event-counter. Só então se procura no cache e, depois, volta-se ao banco para buscar o restante.
Para o autor, a escolha faz sentido porque disponibilidade a qualquer custo não ajuda o cliente: de que serve ver um saldo velho se ele não consegue agir porque os servidores estão fora do ar?
O objetivo principal do cache era permitir que o serviço rodasse com menos recursos. Cada instância precisava de 90GB de RAM e passou a precisar de 55GB. O serviço também passou a escalar horizontalmente, e a latência caiu para a maioria dos pedidos, tudo sem abrir mão da consistência.
Perguntas frequentes
O que é O cache "sempre-verde": saldo por event sourcing sem estourar a memória?
O problema é concreto: qualquer instância atendia qualquer cliente, então cada uma ocupava cerca de 100 GB e sofria pausas de GC. A solução usa a chave account-id + event-counter, com o contador incrementado na mesma transação. O caso explica por que o basis-t global foi rejeitado.
O que o design de O cache "sempre-verde": saldo por event sourcing sem estourar a memória cobre?
Ele está organizado nestas seções: Saldo como soma de eventos, Instâncias que não cabiam na memória, Um cache que nunca precisa ser invalidado, Da versão global à versão por conta e O banco primeiro, o cache depois.
Quanto tempo leva O cache "sempre-verde": saldo por event sourcing sem estourar a memória e qual é o nível?
Leva cerca de 15 minutos para ler e tem nível Pleno.
Agora projete você mesmo
Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.