Conselhos para iniciantes em sistemas distribuídos
(somethingsimilar.com)- A principal dificuldade dos sistemas distribuídos não é a latência, mas sim a falha parcial; até um tratamento simples de exceção em uma única máquina passa a fazer parte do desenho do protocolo quando envolve várias máquinas
- Sistemas distribuídos robustos têm bugs que só aparecem em várias máquinas reais, em condições de rede entre datacenters e com grandes volumes de dados, por isso custam mais do que sistemas de máquina única
- Para construir um sistema operável, é preciso reduzir coordenação e aumentar a independência, usando backpressure, disponibilidade parcial, métricas e percentis como ferramentas básicas
- Flags de funcionalidade, desenho do espaço de IDs, localidade de dados e extração de serviços são técnicas práticas para reduzir o escopo de falha e o custo de coordenação durante migrações e expansão
- O teorema CAP está mais para uma ferramenta de crítica de design do que para um ponto de partida para construir sistemas, e em sistemas distribuídos a coordenação social entre times e organizações pode ser tão difícil quanto os problemas técnicos
O que é fácil deixar passar ao aprender sistemas distribuídos pela primeira vez
- Muitas lições de engenheiros de sistemas distribuídos vêm de erros em tráfego de produção e de cicatrizes operacionais, mas engenheiros iniciantes precisam de um contexto mais direto e acionável
- Fallacies of Distributed Computing e o teorema CAP ajudam no autoestudo, mas são abstratos demais para um engenheiro com pouca experiência começar a agir de imediato
- Sistemas distribuídos exigem mais máquinas e mais capital, então tendem a envolver mais times e organizações maiores
- Problemas sociais costumam ser a parte mais difícil do trabalho de desenvolvimento de software, e isso pode ficar especialmente evidente no desenvolvimento de sistemas distribuídos
- Às vezes soluções sociais são mais eficientes e satisfatórias do que soluções técnicas, mas formação, educação e experiência tendem a empurrar engenheiros para soluções técnicas
Projetar assumindo falhas e custos
- Sistemas distribuídos falham com frequência e, pior ainda, falham parcialmente
- Se o
mutex unlockfalhar em um único processo, talvez baste derrubar o processo; mas se ounlockde um mutex distribuído falhar, isso precisa estar embutido no protocolo de lock - Abordagens como “é só mandar o
writepara as duas máquinas” ou “é só tentar de novo até dar certo” não tratam adequadamente situações em que um lado dowritetem sucesso e o outro falha - Acontecem falhas de switch, desaparecimento do líder por
GC pause, falha remota após umsocket writeque pareceu bem-sucedido e casos em que um único disco lento atrasa todo o protocolo de comunicação do cluster
- Se o
- Soluções distribuídas robustas custam mais do que soluções de máquina única
- Há falhas que só ocorrem em várias máquinas, e embora VMs e cloud reduzam custos, ainda não fica tão barato quanto projetar, implementar e testar em um único computador já disponível
- Falhas que só aparecem com datasets grandes demais para caber numa máquina compartilhada ou em condições reais de rede entre datacenters exigem um ambiente distribuído de verdade
- Simulação é útil, mas não substitui todos os bugs de sistemas distribuídos
- Sistemas distribuídos robustos em open source são muito mais raros do que sistemas robustos de máquina única
- O custo de rodar muitas máquinas por muito tempo pesa para a comunidade open source
- Como hobbyistas e amadores constroem open source com as máquinas e o tempo livre que já têm, é difícil encontrar desenvolvedores dispostos a subir, manter e pagar por várias máquinas
- Engenheiros de empresas cobrem parte dessa lacuna, mas as prioridades dessas organizações podem não coincidir com as prioridades da organização usuária
Reduzir coordenação e absorver falhas
- Sempre que possível, deve-se evitar coordenação entre máquinas
- A essência da escalabilidade horizontal é posicionar os dados de forma que as máquinas sejam independentes o bastante para minimizar comunicação e consenso entre si
- Toda vez que duas máquinas precisam concordar sobre algo, a implementação do serviço fica mais difícil
- Há um limite para a velocidade de transmissão de informação, a comunicação de rede é mais instável do que parece, e nossa intuição sobre consenso pode estar errada
- Vale a pena entender Two Generals, Byzantine Generals e a dificuldade de implementar Paxos
- Se o problema cabe em memória, para um engenheiro de sistemas distribuídos ele costuma ser um problema quase trivial
- É muito mais difícil processar rápido quando os dados estão a alguns switches de distância, e não a alguns ponteiros
- Há muita literatura e muitas implementações para algoritmos e sistemas de máquina única, mas bem menos material e implementações para sistemas distribuídos
- “Está lento” é o problema mais difícil de depurar
- Um ou mais sistemas envolvidos em uma requisição do usuário podem estar lentos, ou uma parte de um pipeline de transformação que cruza várias máquinas pode estar lenta
- A própria descrição do problema quase não dá pistas sobre onde está o defeito, e pode haver uma falha parcial escondida que não aparece nos gráficos de sempre
- Até a degradação ficar muito evidente, costuma ser difícil conseguir recursos suficientes — tempo, dinheiro e ferramentas
- Dapper e Zipkin foram criados por esse motivo
- É preciso implementar backpressure em todo o sistema
- Backpressure é quando o sistema que atende sinaliza falha ao sistema que solicita, e o sistema solicitante trata essa falha para evitar sobrecarga nele mesmo e no sistema do outro lado
- Limitar uso de recursos durante sobrecarga ou falhas do sistema é um componente básico de sistemas distribuídos robustos
- Na prática, isso costuma significar descartar novas mensagens ou devolver erro ao usuário quando os recursos se esgotam ou quando há falha, e em ambos os casos as métricas devem ser incrementadas
- Timeouts e backoff exponencial são obrigatórios em conexões e requisições para outros sistemas
- Sem backpressure, falhas em cascata ou perda não intencional de mensagens se tornam muito mais prováveis
- É preciso encontrar formas de oferecer disponibilidade parcial
- Disponibilidade parcial é a capacidade de retornar alguns resultados mesmo quando parte do sistema falha
- Um sistema de busca pode fazer trade-off entre qualidade dos resultados e tempo de espera do usuário, retornando apenas o que conseguiu reunir quando o limite de tempo expira
- Em mensagens privadas, pode ser melhor que a falha afete apenas alguns usuários do que fazer algumas mensagens desaparecerem para todos os usuários
- Também é preciso decidir até que ponto separar domínios de falha para que uma pane em mensagens privadas não afete uma função não relacionada, como upload público de imagens
Usar observabilidade, capacidade e deploy como ferramentas operacionais
- Métricas são um meio essencial para saber o que o sistema realmente faz em produção
- Métricas como percentis de latência, contadores incrementais de certos comportamentos e taxas de mudança reduzem a distância entre o comportamento que acreditamos que o sistema tem e o comportamento real
- Saber como o comportamento do sistema no dia 20 difere do dia 15 separa engenharia bem-sucedida de feitiçaria fracassada
- Métricas são necessárias para entender problemas e comportamento, mas não bastam para dizer o que fazer em seguida
- Arquivos de log são úteis, mas mentem com facilidade
- Mesmo que certas classes de erro ocupem grande parte do arquivo, a proporção real de requisições afetadas pode ser baixa
- Logs de sucesso geralmente são redundantes, podem estourar o disco, e engenheiros frequentemente erram ao imaginar quais logs de erro serão úteis
- É melhor escrever logs presumindo que serão lidos por alguém que nunca viu o código
- Supervalorizar algo estranho visto nos logs sem confirmar com métricas pode prolongar uma indisponibilidade
- Deve-se usar percentis em vez de médias
- Percentis 50th, 99th, 99.9th e 99.99th são, na maioria dos sistemas distribuídos, mais exatos e úteis do que médias
- A média assume que a métrica avaliada segue uma distribuição em forma de sino, mas isso raramente vale para as métricas que importam aos engenheiros
- Se a latência de um sistema distribuído não segue uma distribuição em forma de sino, a latência média leva a decisões e interpretações erradas
- É preciso desenvolver habilidade para estimar capacidade
- Saber quantas máquinas um trabalho exige separa um sistema duradouro de um sistema que precisa ser substituído em três meses
- No fim de 2012, uma máquina comum tinha 24GB de memória, dos quais 4 a 5GB iam para o OS, alguns GB no mínimo para processar requisições, e um
tweet idocupava 8 bytes; esse tipo de conta aproximada é necessário - Numbers Everyone Should Know, de Jeff Dean, ajuda a calibrar expectativas
- Flags de funcionalidade são uma forma de fazer rollout de infraestrutura
- Elas não servem só para testes A/B de frontend, mas também são muito poderosas para trocar infraestrutura
- A abordagem de “big cutover” já levou muitos projetos ao fracasso, com rollback forçado por bugs descobertos tarde demais
- Ao migrar de um banco único para um serviço que esconde um novo armazenamento, é possível aumentar aos poucos o envio paralelo de
writepara o novo serviço, testar leituras sem usá-las na resposta ao usuário após concluir o backfill e comparar leituras entre sistema antigo e novo antes de aumentar gradualmente o uso real deread - Se surgir problema, dá para reduzir o valor da flag ou voltar para 0 imediatamente, ajustando o volume de tráfego para depurar e experimentar
- Flags de funcionalidade são um trade-off: trocam complexidade local no código por simplicidade global e resiliência
- É preciso aceitar que várias versões de infraestrutura e dados são a regra, não a exceção
Modelagem de dados e desenho de fronteiras
- O espaço de IDs define a forma do sistema
- Quanto mais IDs forem necessários para chegar a um dado, mais opções haverá para particionar os dados
- Quanto menos IDs forem necessários para um dado, mais fácil será consumir a saída do sistema
- A API v1 do Twitter consultava, criava e removia tweets por um único ID numérico de 64 bits, e esse ID não estava ligado a outros dados
- À medida que o número de tweets cresce, é possível armazenar os tweets do mesmo usuário na mesma máquina para montar com eficiência a timeline de tweets do usuário e a timeline de assinaturas; mas, como a API pública permitia endereçar qualquer tweet apenas pelo
tweet id, foi necessário um serviço de consulta que soubesse qualuserera dono de qualtweet id - Como alternativa, seria possível exigir
user idpara consultar tweets, ou embutir ouser idno própriotweet id; mas a segunda opção tem o custo de fazer com que otweet iddeixe de ser um numérico k-sortable - A estrutura dos IDs pode afetar desanonimização de dados privados, crawling inesperado, problemas com IDs auto-incrementais e ataques como Insecure Direct Object References
- É preciso aproveitar a localidade dos dados
- Quanto mais perto do armazenamento persistente estiverem o processamento e o cache dos dados, mais eficiente tende a ser o processamento, e mais fácil fica manter consistência e velocidade do cache
- A rede falha mais e tem latência maior do que um dereference de ponteiro ou um
fread(3) - Localidade não vale só para espaço, mas também para tempo
- Se vários usuários fizerem quase ao mesmo tempo a mesma requisição cara, ela pode ser consolidada em uma só; e se requisições do mesmo tipo chegarem próximas no tempo, podem ser agrupadas em uma única requisição maior
- Isso reduz overhead de comunicação e facilita o gerenciamento de falhas
- Reescrever no armazenamento persistente dados vindos do cache é uma má ideia
- Esse tipo de defeito pode aparecer especialmente em sistemas desenhados pela primeira vez por alguém com pouca experiência em sistemas distribuídos
- Implementações que falam em “Russian-doll caching” têm grande chance de apresentar bugs visíveis
- Um sintoma comum é informação do usuário — por exemplo
screenname,email,hashed password— voltar para um valor antigo
- Computadores conseguem fazer mais do que parece
- Mesmo um servidor web modesto no fim de 2012 tinha 6 ou mais processadores, 24GB de memória e espaço em disco muito grande
- Até aplicações CRUD relativamente complexas em runtimes modernos conseguem, em uma única máquina, processar milhares de requests por segundo em algumas centenas de milissegundos
- Na maioria dos casos, algumas centenas de requests por segundo por máquina não é um número do qual se orgulhar do ponto de vista operacional
- Se a aplicação for perfilada e otimizações baseadas em medição forem introduzidas, obter desempenho maior não costuma ser difícil
- O teorema CAP deve ser usado como ferramenta de crítica de design, não como receita de construção de sistemas
- O teorema CAP é genérico demais e o espaço de soluções possíveis é amplo demais para derivar um sistema funcional a partir de primeiros princípios
- Revisar repetidamente um design de sistema à luz das restrições que CAP impõe aos subsistemas pode levar a um design melhor
- Entre C, A e P, CA não é uma escolha possível
- Extrair serviços traz vantagens de encapsulamento e deploy
- Aqui, serviço significa um sistema distribuído com lógica de nível mais alto do que um sistema de armazenamento e geralmente com API no estilo request-response
- Vale revisar constantemente se certo código não ficaria mais fácil de mudar se estivesse em um serviço separado, e não dentro do sistema principal
- Um serviço extraído oferece encapsulamento como uma biblioteca, mas torna muito mais rápido e fácil implantar mudanças do que atualizar a biblioteca nos sistemas clientes
- Serviços pequenos têm menos dependências de código e de operação, e fronteiras rígidas reduzem atalhos que seriam tolerados em bibliotecas
- Quando há vários sistemas clientes, bibliotecas compartilhadas exigem coordenação dos deploys de cada cliente, e isso fica ainda mais difícil se corrupção de dados puder ocorrer conforme a ordem de deploy
- Se os mantenedores dos sistemas clientes forem diferentes, o custo de coordenação social para conseguir consenso sobre upgrades também aumenta, porque as prioridades podem não estar alinhadas
- Um caso de uso clássico é esconder uma camada de armazenamento em mudança atrás de um serviço com API mais conveniente e com menor área de superfície
1 comentários
Opiniões no Hacker News
O texto deveria ter tratado obrigatoriamente de CALM (Consistência como Monotonicidade Lógica). É muito mais fácil de entender do que CAP, é um resultado mais fundamental e permite que até pessoas com pouca experiência construam sistemas distribuídos muito robustos
Idempotência, CRDT, WAL e Raft são todos casos especiais dos princípios de CALM
https://arxiv.org/pdf/1901.01930
Algo que ficou de fora aqui, mas de que gosto como princípio geral: entrega exatamente uma vez é impossível
No máximo uma vez ou pelo menos uma vez são possíveis, mas você precisa escolher qual modo de falha vai aceitar e projetar de acordo
Em um banco de dados em cluster, é possível mover dados de uma linha para outra, e isso também pode ser interpretado como uma mensagem entregue
Se o sistema inteiro for idempotente, ou se você puder tratar todo o sistema distribuído como uma única unidade que pode sofrer rollback em conjunto, então é possível obter entrega exatamente uma vez. Ou seja, não pode haver efeitos colaterais em outro sistema fora do domínio
Ambas as coisas são formas de monotonicidade lógica. A idempotência é fácil de reconhecer, e a transacionalidade também se baseia em monotonicidade por meio de algoritmos como WAL e Raft
O texto deveria ter tratado de CALM (Consistência como Monotonicidade Lógica). É muito mais fácil de entender do que CAP e é um resultado mais fundamental
https://arxiv.org/pdf/1901.01930
An Overview of End-to-End Exactly-Once Processing in Apache Flink (with Apache Kafka, too!) — https://flink.apache.org/2018/02/28/an-overview-of-end-to-en...
Flink's Fault Tolerance Guarantees — https://nightlies.apache.org/flink/flink-docs-release-1.20/d...
Como corolário de “se você consegue colocar o problema na memória, provavelmente é um problema trivial”, o escopo do que cabe na memória é muito maior do que se imagina
Eu achava que sabia o que era muita RAM, mas mudei de ideia quando os principais provedores de nuvem começaram a oferecer VMs de 12 TB para SAP HANA
O texto também aborda isso muito brevemente com “computadores conseguem fazer mais do que você pensa”, mas ali fala apenas de uma máquina de 24 GB. Mesmo considerando que era 2012, provavelmente já havia bastantes máquinas com 10 vezes essa RAM naquela época
Compartilho este documento com as pessoas mais promissoras com quem trabalho
Quando eu trabalhava na Lookout, Jeff Hodges compartilhou este ensaio como uma apresentação e, no final, acrescentou um corolário: “não finja que engenharia não é política”
Pessoas que acham que o código fala por si só estão perdendo um aspecto importante de influenciar como algo será construído e de obter resultados reais
Mesmo 10 anos depois, não há muita gente que entenda de forma tão concisa a interseção entre liderança em engenharia e aquilo que considero competências básicas comuns de SRE/DevOps
Discussões antigas
https://news.ycombinator.com/item?id=5055371
346 points|jcdavis|12 years ago|42 comments
https://news.ycombinator.com/item?id=12245909
386 points|kiyanwang|8 years ago|133 comments
É uma ótima lista, e gosto das explicações práticas e realistas. Sem buzzwords e sem “microsserviços”
Acho que boa parte desses conselhos também se aplica a sistemas de uma única máquina. Pode haver muitos subcomponentes distribuídos em alguma medida, como IPC entre programas ou a coordenação de threads dentro de um processo
A ideia de memória unificada em uma única máquina também é, até certo ponto, falsa, mas ainda assim o hardware consegue oferecer garantias melhores do que um ambiente distribuído “de verdade”
Muitos dos conselhos do texto que comparam “distribuído” e “máquina única” também se encaixam muito bem na comparação entre multithread e single-thread
Em outro eixo, quando você cria um programa e o distribui para várias pessoas executarem, isso também vira uma espécie de situação “distribuída”. Existem versões diferentes soltas por aí, e é preciso se preocupar com compatibilidade e upgrades; por isso, as feature flags mencionadas no texto também são relevantes
A distribuição é mais um espectro que vai de uma única CPU a múltiplas CPUs, múltiplos computadores fortemente conectados e múltiplos computadores distribuídos pelo mundo, com vários pontos e várias dimensões no meio
Vários processos na mesma máquina se comunicando por localhost também são um exemplo muito conhecido de sistema distribuído, e de fato muita gente aprende sistemas distribuídos primeiro nesse contexto
Nesse caso, todos estaríamos escrevendo todo o código como belos programas MPI de memória distribuída. O falso compartilhamento desapareceria, e seríamos obrigados a pensar explicitamente e em profundidade sobre os padrões de comunicação
Tive a oportunidade de trabalhar por um breve período com o autor deste texto nos últimos anos. Jeff foi uma das pessoas mais perspicazes e positivas com quem já aprendi
Ele era surpreendentemente franco sobre as dificuldades que enfrentava e também era agradavelmente acessível para mentoria e conselhos
Em “sistemas distribuídos são diferentes porque falham com frequência”, o ponto central não é a simples taxa de falhas, mas a taxa de falhas em um sistema com vários nós
E “problemas de sistemas distribuídos” não surgem apenas em vários servidores conectados por rede. Se for um conjunto de nós relacionados entre si, arquivos em discos logicamente conectados ou buffers de diferentes dispositivos de entrada e saída sofrem problemas semelhantes
Alguns veteranos zombam da complexidade excessiva que aparece ao mitigar esses problemas e reclamam que tudo seria muito mais simples se o software rodasse em um único servidor
Na prática, isso quase nunca foi verdade nem nos tempos do AS/400 ou do VAXft, e se aplica menos ainda ao mundo mais caótico do Unix multiusuário e multiprocesso
“Se você consegue colocar o problema na memória, provavelmente é um problema trivial” é um erro comum entre alguns engenheiros de sistemas distribuídos
Não é nada trivial; os problemas a enfrentar apenas se deslocam para uma área complementar
Esse erro leva facilmente a situações em que algo que poderia ser feito em uma única máquina, se otimizado corretamente, acaba sendo processado por um cluster de 100 máquinas