o problema
Em 2007 a Amazon rodava dezenas de milhares de servidores espalhados por vários datacenters, e uma única página do site era montada com chamadas a mais de 150 serviços. Nessa escala, componente quebrado não é exceção: é o modo normal de operação. Sempre tem disco falhando, rota de rede oscilando, datacenter caindo por falta de energia ou refrigeração. O software precisa tratar falha como caso comum, não como incidente.
O jeito padrão de guardar estado era banco relacional com replicação síncrona. Só que a maior parte desses serviços — carrinho, sessão, preferências, ranking de vendas, catálogo — só acessa dado por chave primária. Paga-se o custo inteiro de um RDBMS sem usar quase nada dele: hardware caro, gente especializada, e uma tecnologia de replicação que, quando a rede parte, escolhe consistência. Escolher consistência significa recusar a escrita, ou deixar o dado indisponível até ter certeza de que está certo. Recusar um “adicionar ao carrinho” custa dinheiro na hora e custa confiança do cliente depois.
a ideia
O Dynamo inverte a pergunta. Em vez de “como garanto que todo mundo lê a mesma coisa”, pergunta “como garanto que a escrita nunca é recusada”. A resposta é aceitar a escrita em qualquer réplica alcançável e assumir que, às vezes, vão coexistir duas versões do mesmo objeto.
Daí sai o movimento que sustenta o resto do sistema: a resolução de conflito sai da escrita e vai para a leitura, e sai do banco e vai para a aplicação. Um datastore genérico só consegue aplicar política burra, tipo “last write wins”. A aplicação do carrinho sabe fundir dois carrinhos, porque é ela que conhece o esquema e o que faz sentido para o cliente. Então o get() pode devolver uma lista de versões causalmente não relacionadas, e quem chamou decide o que fazer.
como funciona
As chaves passam por MD5, viram um identificador de 128 bits e caem num anel de consistent hashing. Cada nó ocupa vários pontos do anel — os virtual nodes, ou tokens —, o que permite dar mais tokens para máquina mais forte e espalhar a carga de um nó que cai entre todos os outros, em vez de despejar tudo no vizinho. Cada chave é replicada em N nós; a lista deles é a preference list, montada pulando posições para não repetir nó físico e para atravessar datacenters.
A interface tem duas operações: get(key) e put(key, context, object). O context é metadado opaco para o cliente e carrega o vector clock — uma lista de pares (nó, contador). Comparando dois clocks dá para saber se uma versão descende da outra (a antiga pode ser jogada fora) ou se são ramos paralelos (as duas precisam sobreviver). Quando o cliente reconcilia e escreve de volta, o clock resultante subsume os anteriores e os ramos colapsam.
O nó que atende a requisição é o coordinator. Na escrita ele grava local e manda para os N nós alcançáveis mais bem colocados; com W-1 respostas, o put retorna. Na leitura ele espera R respostas e devolve todas as versões concorrentes. R e W são configuráveis por instância; a combinação mais comum na produção é (3,2,2).
Quando um nó da preference list está fora, a réplica vai para o próximo nó saudável com um “hint” dizendo quem era o dono legítimo — hinted handoff. O nó substituto guarda isso num banco local separado e devolve quando o original volta. Para divergência que o handoff não cobre, existe anti-entropia com Merkle tree por key range: compara-se a raiz, e só desce na árvore onde os hashes diferem, o que reduz o dado trafegado e a leitura de disco. Membership é explícita — um administrador manda o nó entrar ou sair — e se propaga por gossip, com alguns nós atuando como seeds para evitar anéis logicamente partidos. Detecção de falha é local: se B não responde a A, A desvia e tenta B de novo depois.
o que isso custou
O paper é honesto sobre a fatura. Não há isolamento nem transação: só update de chave única, objetos abaixo de 1 MB. O modelo “escrita nunca se perde” implica que item deletado do carrinho pode reaparecer. O vector clock cresce quando muitos nós coordenam escritas da mesma chave, e a solução é truncar o par mais antigo ao chegar num limite (dez, por exemplo) — os autores admitem que isso pode quebrar a relação de descendência e que não investigaram o problema a fundo porque ele não apareceu em produção.
A latência de 99,9 percentil ficava na casa de 200 ms, uma ordem de grandeza acima da média. Para os serviços que precisavam de mais, existe um buffer de escrita em memória que derruba esse percentil por um fator de 5 e troca durabilidade por desempenho: um crash perde o que estava na fila. A primeira estratégia de particionamento fazia o bootstrap de um nó novo demorar quase um dia em época de pico, e complicava arquivamento — foi substituída por partições de tamanho fixo. Tarefas de background brigavam por disco com as operações de cliente e precisaram de um controlador de admissão com feedback. O sistema assume ambiente confiável: não há autenticação nem autorização. E o gossip da tabela de roteamento completa serve para algumas centenas de nós, não para dezenas de milhares.
onde isso aparece hoje
O Dynamo virou o vocabulário de uma geração inteira de bancos distribuídos. Consistent hashing com virtual nodes, sloppy quorum, hinted handoff, anti-entropia com Merkle tree, N/R/W ajustáveis por aplicação: esse conjunto reaparece quase inteiro no Cassandra e no Riak, e o nome sobrevive no DynamoDB da AWS. O termo “eventual consistency” saiu daqui para o vocabulário corrente com um sentido operacional, não acadêmico.
A parte que envelheceu melhor talvez nem seja a arquitetura, e sim a prática: escrever SLA em percentil alto em vez de média, e expor consistência como parâmetro que o dono do serviço regula, em vez de propriedade que o banco decide sozinho.