Projeto Final: Projetando um Sistema de Armazenamento em Nuvem para uma Carga de Trabalho
Objetivos de Aprendizagem
- Dada a descrição de uma carga de trabalho realista, identificar qual dos sistemas desta disciplina (ou qual mecanismo específico de um deles) é o encaixe certo, e justificar essa escolha em termos dos dois eixos de abertura da disciplina: consistência versus disponibilidade, e vazão versus latência.
- Explicar, para cada projeto escolhido, com precisão qual sistema alternativo desta disciplina teria sido o encaixe errado, e qual garantia específica essa alternativa teria sacrificado ou deixado de oferecer.
- Sintetizar a lição central e recorrente da disciplina, a de que nenhum sistema estudado aqui é universalmente correto, numa recomendação de projeto concreta e defensável para três cargas de trabalho genuinamente diferentes.
Contexto e Motivação
Esta disciplina começou argumentando que a teoria de Sistemas Distribuídos I (consenso, replicação, modelos de consistência) é necessária, mas não suficiente: é o julgamento de engenharia real sobre os requisitos de fato de uma carga de trabalho específica que determina qual sistema real e funcional é o certo para construir ou adotar. Todo conceito desde então estudou um sistema específico (GFS, MapReduce, hashing consistente, Dynamo, Bigtable, Chubby, ZooKeeper, two-phase commit, Chain Replication, Spanner, Memcached e computação serverless) como um estudo de caso de um trade-off deliberado e específico, nunca como uma resposta universalmente correta. Este projeto final é o retorno da disciplina: dadas três cargas de trabalho genuinamente diferentes e realistas, a tarefa é escolher e justificar um projeto real para cada uma, usando o vocabulário e os trade-offs específicos que esta disciplina desenvolveu e, tão importante quanto, ser explícito sobre quais projetos alternativos teriam sido a escolha errada, e exatamente por quê.
Teoria Central
Carga de trabalho 1: um carrinho de compras que nunca pode perder uma escrita
Uma escrita no carrinho de compras (adicionar ou remover um item) precisa sempre ter sucesso, mesmo durante uma partição de rede ou uma breve queda de nó, já que uma escrita rejeitada custa diretamente a capacidade de um cliente de concluir uma compra, e uma leitura desatualizada ocasional (ver brevemente um carrinho um pouco desatualizado) é um custo muito menor do que uma escrita rejeitada. Esta é, quase por definição, exatamente a carga de trabalho para a qual o Dynamo foi construído, e o seu próprio artigo usa precisamente este exemplo. A escolha de projeto certa é a abordagem do Dynamo: hashing consistente para particionar os carrinhos entre os nós, quóruns relaxados e hinted handoff para que uma escrita sempre tenha sucesso mesmo quando alguns dos nós de réplica designados de um carrinho estão brevemente inalcançáveis, e relógios vetoriais para detectar e expor escritas genuinamente concorrentes (dois dispositivos adicionando itens diferentes ao mesmo carrinho quase ao mesmo tempo), para que a aplicação possa realizar uma mescla correta para o domínio, uma união de itens, em vez de perder silenciosamente uma escrita.
A escolha errada aqui seria o projeto do Spanner: o commit wait e o two-phase commit entre shards do Spanner são construídos especificamente para garantir consistência externa, ao custo de uma transação às vezes não se completar durante uma partição severa, exatamente o resultado "por favor, tente de novo mais tarde" que o requisito real desta carga de trabalho (uma escrita precisa sempre ter sucesso) descarta. Usar o Spanner aqui significaria recusar ocasionalmente uma escrita no carrinho especificamente para proteger uma garantia de consistência forte de que esta carga de trabalho de fato não precisa, escolhendo o ponto errado do primeiro eixo da disciplina para este requisito específico.
Carga de trabalho 2: um livro-razão financeiro global que nunca pode mostrar dois saldos conflitantes
Um livro-razão financeiro tem a prioridade oposta à da Carga de trabalho 1: duas visões diferentes do saldo de uma conta, visíveis a dois observadores diferentes no mesmo momento, são uma falha de corretude séria e inaceitável (poderiam permitir que os mesmos fundos fossem gastos duas vezes, de dois lugares diferentes, antes que qualquer um dos débitos ficasse visível para o outro), enquanto uma pequena latência adicional em cada transação é um custo aceitável e menor. Esta é exatamente a carga de trabalho alvo do Spanner, e o seu próprio artigo cita dados financeiros e outros dados críticos em consistência como um caso de uso principal. A escolha de projeto certa é a abordagem do Spanner: cada shard do livro-razão replicado via Paxos para tolerância a falhas, transferências entre shards coordenadas via two-phase commit com um coordenador replicado por Paxos (evitando o problema de bloqueio do two-phase commit simples que um coordenador único, que pode travar individualmente, introduziria), e o commit wait do TrueTime para garantir consistência externa: uma transação que termina antes de outra começar tem a garantia de um timestamp estritamente anterior, em todo lugar do sistema globalmente distribuído.
A escolha errada aqui seria a abordagem do Dynamo: os quóruns relaxados e os conflitos expostos por relógios vetoriais do Dynamo são construídos especificamente para deixar uma escrita ter sucesso mesmo quando as réplicas discordam, exatamente a propriedade que deixaria duas atualizações de saldo conflitantes terem sucesso de forma independente, precisamente o risco de gasto duplo que esta carga de trabalho não pode tolerar. O próprio modelo de resolução de conflitos do Dynamo assume que a aplicação consegue mesclar corretamente versões concorrentes (como na união de itens de um carrinho de compras), mas não existe jeito correto, para o domínio, de "mesclar" dois saldos de conta conflitantes como existe para o conteúdo de um carrinho, o que torna todo o modelo de conflitos do Dynamo um encaixe ruim aqui, e não meramente um descompasso de estilo.
Carga de trabalho 3: análise em lote sobre petabytes de logs, com um prazo, mas sem nenhum usuário ao vivo esperando qualquer operação individual
Um job noturno em lote que varre petabytes de dados de log para calcular estatísticas agregadas diárias tem uma forma inteiramente diferente das duas cargas de trabalho acima: nenhuma leitura ou escrita individual é sensível à latência (nada está esperando de forma síncrona por uma operação individual, do jeito que um usuário ao vivo espera por uma escrita no carrinho ou uma consulta de saldo), mas o job como um todo precisa terminar dentro de algum prazo (digamos, antes do próximo dia útil), e a carga de trabalho diz respeito esmagadoramente à vazão agregada sobre dados sequenciais enormes, e não a qualquer garantia por operação. Esta é exatamente a carga de trabalho para a qual o GFS e o MapReduce foram construídos: dados de log armazenados de forma durável e barata no GFS, com os seus chunks grandes de 64MB e o seu modelo de consistência relaxado ajustados ao padrão de escrita desses dados, pesado em anexações e raramente acessado de forma aleatória, e a computação de agregação em si expressa como as funções map e reduce do MapReduce, com o escalonamento consciente da localidade do runtime, a reexecução determinística em caso de falha e a execução de tarefas de backup para stragglers absorvendo as falhas de máquina rotineiras que o conceito de abertura desta disciplina estabeleceu como a condição esperada e permanente nessa escala.
A escolha errada aqui seria o Chain Replication ou o Spanner: os dois são construídos para otimizar a consistência forte de operações individuais e sensíveis à latência, exatamente a propriedade de que esta carga de trabalho não precisa (nada aqui é uma leitura ou escrita individual e sensível à latência esperando uma resposta fortemente consistente), enquanto o projeto voltado primeiro à vazão do GFS e do MapReduce, discutido explicitamente no eixo de abertura desta disciplina de vazão versus latência, trata do gargalo real com que esta carga de trabalho se importa. Usar o projeto transacional, externamente consistente e que paga latência por operação do Spanner para processar uma varredura em lote na escala de petabytes acrescentaria uma sobrecarga substancial e inteiramente desnecessária por uma garantia (consistência externa de operações individuais) que esta carga de trabalho nunca pediu.
Exemplos Resolvidos
Exemplo 1: uma quarta carga de trabalho, resolvida explicitamente, para praticar o mesmo raciocínio
Problema: Um serviço precisa de um mecanismo de coordenação para garantir que exatamente uma instância de um executor de jobs em segundo plano esteja ativa por vez numa frota de instâncias redundantes, com failover automático se a instância ativa travar. Usando os sistemas desta disciplina, identifique o projeto certo e justifique-o.
Raciocínio: Esta não é uma carga de trabalho de armazenamento de dados: é um problema de coordenação, especificamente eleição de líder com failover automático em caso de travamento, exatamente o problema para o qual o ZooKeeper (ou o Chubby, o seu predecessor de escopo mais estreito) é construído. O projeto certo usa os znodes efêmeros e sequenciais do ZooKeeper: cada instância cria um znode efêmero e sequencial sob um caminho compartilhado ao iniciar, a instância que detém o znode de menor número é o líder ativo e, se essa instância trava, o seu znode efêmero é removido automaticamente quando a sua sessão expira, fazendo com que a instância de próximo menor número (que tinha definido um watch especificamente no seu predecessor imediato, conforme o padrão de eleição de líder desenvolvido no conceito sobre o ZooKeeper) se torne o novo líder. Nem o Dynamo, nem o Spanner, nem o GFS são construídos para esse tipo de problema: eles resolvem problemas de armazenamento e de processamento de dados, e não de coordenação de propósito geral e eleição de líder. Isso confirma que a ferramenta certa aqui não é simplesmente "o sistema desta disciplina que parece mais sofisticado", mas especificamente aquele construído para exatamente este tipo de problema.
Exemplo 2: reconhecendo quando uma única carga de trabalho de fato precisa combinar dois sistemas desta disciplina
Problema: Um serviço de rede social precisa armazenar posts de usuários (dados estruturados e esparsos, já que os posts têm campos opcionais extremamente variados, como imagens anexadas, marcações de localização ou opções de enquete) em escala de petabytes, e o próprio armazenamento subjacente precisa ser durável e tolerante a falhas. Explique por que a resposta para esta carga de trabalho recorre a dois sistemas desta disciplina juntos, em vez de só um.
Resolução: O modelo de dados esparso e semiestruturado (posts com conjuntos esparsos e extremamente variados de campos opcionais) é exatamente o modelo de dados do Bigtable (um mapa esparso, distribuído e ordenado com famílias de colunas, em vez de um esquema rígido e totalmente preenchido), o que torna o Bigtable a escolha certa para como esses dados devem ser organizados e consultados. Mas o próprio Bigtable, como o seu conceito nesta disciplina desenvolve, não implementa a sua própria camada de armazenamento durável nem a sua própria replicação e tolerância a falhas para os bytes reais: ele armazena as suas SSTables e commit logs no GFS, contando com a replicação de chunks do GFS para a durabilidade real dos bytes subjacentes, e conta com o Chubby (ou, numa implantação aberta, fora do Google, um equivalente baseado no ZooKeeper) puramente para a eleição do master e a coordenação de vivacidade dos servidores de tablets. A resposta correta para esta carga de trabalho, portanto, não é "escolher exatamente um sistema desta disciplina": é reconhecer que o próprio Bigtable já é uma composição do GFS (armazenamento durável) e de um serviço de coordenação (o Chubby ou o seu equivalente), precisamente a arquitetura em camadas que os conceitos sobre o Bigtable nesta disciplina desenvolveram, e não algo a reinventar do zero.
Equívocos Comuns e Armadilhas
- "O 'melhor' sistema estudado nesta disciplina é o que oferece as garantias mais fortes, então ele é sempre a escolha padrão mais segura." A consistência externa do Spanner é uma garantia estritamente mais forte que a consistência eventual do Dynamo, mas a Carga de trabalho 1 mostra concretamente por que escolher a garantia mais forte é a escolha errada quando o requisito real da carga de trabalho é disponibilidade, e não consistência. A garantia mais forte disponível de um sistema não é automaticamente a certa a buscar; os requisitos reais da carga de trabalho determinam qual garantia de fato vale a pena pagar.
- "Cada um dos sistemas desta disciplina resolve um problema completamente separado e sem sobreposição, então sempre existe exatamente uma resposta obviamente correta para qualquer carga de trabalho." O Exemplo 2 mostra uma carga de trabalho realista cuja resposta correta é uma composição de dois sistemas (o Bigtable construído sobre o GFS e um serviço de coordenação), e não uma única escolha limpa e separada. Reconhecer quando a resposta correta de uma carga de trabalho é ela mesma uma composição em camadas, em vez de um único sistema escolhido de uma lista, faz parte da habilidade real que este projeto final pede.
- "Uma vez escolhido um sistema para uma carga de trabalho, os seus parâmetros específicos (como o N, o R e o W do Dynamo, ou o tamanho de chunk do GFS) não precisam ser reconsiderados, já que o projeto padrão do sistema já está totalmente especificado." Os conceitos desta disciplina desenvolveram vários parâmetros ajustáveis precisamente porque as implantações reais os ajustam para a sua situação específica: o próprio artigo do Dynamo descreve configurações diferentes de N, R e W para casos de uso diferentes, mesmo dentro da Amazon. Um exercício de projeto real deve incluir o raciocínio sobre esses parâmetros para a carga de trabalho específica em questão, e não só nomear qual sistema usar em abstrato.
Resumo
Este projeto final aplica toda a sequência de estudos de caso da disciplina a três cargas de trabalho genuinamente diferentes e realistas. Um carrinho de compras que nunca pode perder uma escrita pede o projeto voltado primeiro à disponibilidade do Dynamo (hashing consistente, quóruns relaxados, hinted handoff e relógios vetoriais), escolhendo deliberadamente a disponibilidade em vez da consistência externa do Spanner, mais forte, mas que custa disponibilidade. Um livro-razão financeiro global que nunca pode mostrar saldos conflitantes pede a escolha oposta: os shards replicados por Paxos do Spanner, o two-phase commit com um coordenador tolerante a falhas e o commit wait do TrueTime, pagando deliberadamente um pequeno custo de latência por uma garantia de consistência forte que o projeto do Dynamo não consegue oferecer. A análise em lote sobre petabytes de logs não pede nenhum desses projetos transacionais, mas sim a arquitetura voltada primeiro à vazão do GFS e do MapReduce, ajustada a uma carga de trabalho sem nenhuma operação individual sensível à latência, mas com um prazo agregado real. Nas três, e nos dois exemplos resolvidos adicionais (coordenação via ZooKeeper, e um projeto em camadas de Bigtable sobre GFS e Chubby), vale a mesma lição com que esta disciplina começou: nenhum sistema estudado aqui é a escolha universalmente correta, cada um é uma resposta deliberada e bem fundamentada aos requisitos reais de uma carga de trabalho específica, e julgamento de engenharia real significa casar a carga de trabalho com o sistema, e conseguir dizer com precisão o que daria errado com qualquer uma das outras escolhas.