Capítulo 12 – Apresentando o projeto de sistemas não-abstratos em grande escala

Por Salim Virji, James Youngman, Henry Robertson,

Stephen Thorne, Dave Rensin e Zoltan Egyed 

com Richard Bondi

 

Com responsabilidades que abrangem operações de produção e engenharia de produtos, o SRE (Engenharia de Confiabilidade de Site) está em uma posição única para alinhar os requisitos do caso de negócio e os custos operacionais. As equipes de engenharia de produtos podem não estar cientes do custo de manutenção dos sistemas que projetam, especialmente se essa equipe estiver construindo um único componente que faz parte de um ecossistema de produção maior.

Com base na experiência do Google no desenvolvimento de sistemas, consideramos a confiabilidade como o aspecto mais crítico de qualquer sistema de produção. Descobrimos que adiar questões de confiabilidade durante o design é semelhante a aceitar menos recursos a custos mais altos. Ao seguir um estilo iterativo de design e implementação de sistemas, chegamos a designs robustos e escaláveis com baixos custos operacionais. Chamamos esse estilo de Projeto de Sistemas Não-Abstratos em Grande Escala (NALSD).

O que é NALSD?

Este capítulo apresenta uma abordagem NALSD: começamos com a declaração do problema, reunimos requisitos e iteramos através de designs que se tornam cada vez mais sofisticados até chegarmos a uma solução viável. No final, chegamos a um sistema que se defende contra muitos modos de falha e satisfaz tanto os requisitos iniciais quanto os detalhes adicionais que surgiram durante as iterações.

O NALSD descreve uma habilidade crítica para o SRE: a capacidade de avaliar, projetar e avaliar sistemas grandes. Na prática, o NALSD combina elementos de planejamento de capacidade, isolamento de componentes e degradação graciosa do sistema, que são cruciais para sistemas de produção altamente disponíveis. Espera-se que os SREs do Google sejam capazes de iniciar o planejamento de recursos com um diagrama básico em uma lousa de um sistema, pensar nos vários domínios de escalabilidade e falha, e focar seu design em uma proposta concreta de recursos. Como esses sistemas mudam ao longo do tempo, é vital que um SRE seja capaz de analisar e avaliar os aspectos-chave do design do sistema.

Por que “Não-Abstrato”?

Todos os sistemas eventualmente terão que ser executados em computadores reais em datacenters reais usando redes reais. O Google aprendeu (da maneira difícil) que as pessoas que projetam sistemas distribuídos precisam desenvolver e exercitar continuamente a habilidade de transformar um design de lousa em estimativas concretas de recursos em várias etapas do processo. Sem essa rigidez, é muito tentador criar sistemas que não se traduzem completamente no mundo real.

Este trabalho adicional inicial geralmente leva a menos mudanças de design de sistema de última hora para dar conta de alguma restrição física não prevista.

Por favor, note que, enquanto conduzimos esses exercícios para resultados discretos (por exemplo, número de máquinas), exemplos de raciocínio sólido e suposições são mais importantes do que quaisquer valores finais. As suposições iniciais influenciam fortemente os resultados dos cálculos, e fazer suposições perfeitas não é um requisito para o NALSD. O valor deste exercício está em combinar muitos resultados imperfeitos, mas razoáveis, para uma melhor compreensão do design.

Exemplo do AdWords

O serviço Google AdWords exibe anúncios de texto na Pesquisa na Web do Google. A métrica de taxa de cliques (CTR) informa aos anunciantes o desempenho de seus anúncios. A CTR é a razão entre o número de vezes que o anúncio é clicado e o número de vezes que o anúncio é exibido.

Este exemplo do AdWords tem como objetivo projetar um sistema capaz de medir e relatar uma CTR precisa para cada anúncio do AdWords. Os dados que precisamos para calcular a CTR são registrados em logs dos sistemas de pesquisa e de veiculação de anúncios. Esses logs registram os anúncios exibidos para cada consulta de pesquisa e os anúncios clicados, respectivamente.

Processo de design

O Google utiliza uma abordagem iterativa para projetar sistemas que atendam aos nossos objetivos. Cada iteração define um design potencial e examina seus pontos fortes e fracos. Essa análise alimenta a próxima iteração ou indica quando o design é bom o suficiente para ser recomendado.

Em termos gerais, o processo do NALSD tem duas fases, cada uma com duas a três perguntas.

Na fase de design básico, tentamos inventar um design que funcione em princípio. Fazemos duas perguntas:

É possível?

O design é viável? Se não precisássemos nos preocupar com RAM suficiente, CPU, largura de banda de rede, e assim por diante, o que projetaríamos para satisfazer os requisitos?

Podemos melhorar?

Para qualquer design desse tipo, perguntamos: “Podemos melhorar?” Por exemplo, podemos tornar o sistema significativamente mais rápido, menor, mais eficiente? Se o design resolver o problema em tempo O(N), podemos resolvê-lo mais rapidamente, digamos, em tempo O(ln(N))?

Na próxima fase, tentamos ampliar nosso design básico – por exemplo, aumentando drasticamente um requisito. Fazemos três perguntas:

É viável?

É possível escalar este design, considerando restrições de dinheiro, hardware, e assim por diante? Se necessário, qual design distribuído satisfaria os requisitos?

É resiliente?

O design pode falhar de forma graciosa? O que acontece quando este componente falha? Como o sistema funciona quando um datacenter inteiro falha?

Podemos fazer melhor?

Embora geralmente abordemos essas fases e perguntas nesta ordem aproximada, na prática, saltamos entre as perguntas e fases. Por exemplo, durante a fase de design básico, muitas vezes temos o crescimento e a escalabilidade em mente. 

Então, iteramos. Um design pode passar com sucesso pela maioria das fases, apenas para fracassar mais tarde. Quando isso acontece, começamos novamente, modificando ou substituindo componentes. O design final é o fim de uma história de reviravoltas.

Com esses conceitos em mente, vamos percorrer o processo iterativo do NALSD.

Requisitos Iniciais

Cada anunciante pode ter vários anúncios. Cada anúncio é identificado por um ad_id e está associado a uma lista de termos de pesquisa selecionados pelo anunciante.

Ao exibir um painel para um anunciante, precisamos saber o seguinte para cada anúncio e termo de pesquisa:

  • Com que frequência este termo de pesquisa ativou este anúncio para ser exibido
  • Quantas vezes o anúncio foi clicado por alguém que viu o anúncio

Com essas informações, podemos calcular a CTR: o número de cliques dividido pelo número de impressões.

Sabemos que nossos anunciantes se preocupam com duas coisas: que o painel seja exibido rapidamente e que os dados sejam recentes. Portanto, ao iterar no design, consideraremos nossos requisitos em termos de SLOs (consulte o Capítulo 2 para mais detalhes):

  • 99,9% das consultas do painel são concluídas em < 1 segundo.
  • 99,9% do tempo, os dados da CTR exibidos têm menos de 5 minutos de idade.

Esses SLOs fornecem um objetivo razoável que devemos ser capazes de atender consistentemente. Eles também fornecem um orçamento de erro (consulte o Capítulo 4 em Engenharia de Confiabilidade de Site), que vamos comparar nossa solução em cada iteração do design. 

Nosso objetivo é criar um sistema que possa atender nossos SLOs e também suportar milhões de anunciantes que desejam ver suas CTRs em um painel. Para taxas de transação, vamos supor 500.000 consultas de pesquisa por segundo e 10.000 cliques de anúncio por segundo.

Uma Máquina

O ponto de partida mais simples é considerar a execução de toda a nossa aplicação em um único computador.

Para cada consulta de pesquisa na web, registramos:

time

O horário em que a consulta ocorreu

query_id

Um identificador único da consulta (ID da consulta)

search_term

O conteúdo da consulta

ad_id

Os IDs dos anúncios do AdWords mostrados para a pesquisa

Juntas, essas informações formam o registro de consultas. Cada vez que um usuário clica em um anúncio, registramos o time do clique, o ID da consulta (query id) e o ID do anúncio (ad id) no registro de cliques (click log).

Você pode estar se perguntando por que não adicionamos o search_term ao registro de cliques para reduzir a complexidade. No escopo arbitrariamente reduzido do nosso exemplo, isso poderia ser viável. No entanto, na prática, a CTR é na verdade apenas uma das muitas informações calculadas a partir desses registros. Os registros de cliques são derivados de URLs, que têm limitações de tamanho inerentes, tornando o registro de consulta separado uma solução mais escalável. Em vez de provar esse ponto adicionando requisitos adicionais semelhantes à CTR ao exercício, simplesmente reconheceremos essa suposição e avançaremos.

A exibição de um painel requer os dados de ambos os logs. Precisamos ser capazes de mostrar que podemos alcançar nosso SLO de exibir dados atualizados no painel em menos de um segundo. Alcançar esse SLO requer que a velocidade de cálculo de uma CTR permaneça constante à medida que o sistema lida com grandes quantidades de cliques e consultas.

Para atender nosso SLO de exibir nosso painel em menos de um segundo, precisamos de pesquisas rápidas do número de “query_ids” clicados e mostrados por “search_term” para um determinado “ad_id”. Podemos extrair a divisão de “query_ids” mostrados por “search_term”

‘ad_id” do registro de consulta. Um painel de CTR precisa de todos os registros tanto do registro de consulta quanto do registro de cliques para os “ad_ids”.

Se tivermos mais do que alguns anunciantes, percorrer o registro de consulta e o registro de cliques para gerar o painel será muito ineficiente. Portanto, nosso design prevê que nossa única máquina crie uma estrutura de dados apropriada para permitir cálculos rápidos de CTR à medida que recebe os registros. Em uma única máquina, usando um banco de dados SQL com índices em “query_id” e “search_term”, deve ser possível fornecer respostas em menos de um segundo. Ao unir esses registros em “query_id” e agrupar por “search_term”, podemos relatar a CTR para cada pesquisa.

Cálculos

Precisamos calcular quantos recursos precisamos para analisar todos esses registros. Para determinar nossos limites de escalabilidade, precisamos fazer algumas suposições, começando pelo tamanho do registro de consulta:

time

Inteiro de 64 bits, 8 bytes

query_id

Inteiro de 64 bits, 8 bytes

ad_id

Três inteiros de 64 bits, 8 bytes no total

search_term

Uma string longa, até 500 bytes

Outros metadados

500 a 1.000 bytes de informações, como qual máquina serviu os anúncios, em que idioma a pesquisa foi feita e quantos resultados o termo de pesquisa retornou.

Para garantir que não atingimos um limite prematuramente, arredondamos agressivamente para cima e tratamos cada entrada no registro de consulta como 2 KB. O volume do registro de cliques deve ser consideravelmente menor do que o volume do registro de consultas: como a CTR média é de 2% (10.000 cliques / 500.000 consultas), o registro de cliques terá 2% do número de registros do registro de consultas. Lembre-se de que escolhemos números grandes para ilustrar que esses princípios se aplicam a implementações arbitrariamente grandes. Essas estimativas parecem grandes porque devem ser.

Por fim, podemos usar a notação científica para limitar erros causados por aritmética em unidades inconsistentes. O volume de registros de consulta gerados em um período de 24 horas será:

(5 x 10^5 consultas/seg) × (8,64 x 10^4 segundos/dia) × (2 x 10^3 bytes) = 86,4 TB/dia

Como recebemos 2% dos cliques em relação às consultas, e sabemos que nossos índices de banco de dados acrescentarão alguma quantidade razoável de sobrecarga, podemos arredondar nossos 86,4 TB/dia para 100 TB de espaço necessário para armazenar os dados de log de um dia.

Com um requisito de armazenamento agregado de ~100 TB, temos algumas novas suposições a fazer. Esse design ainda funciona com uma única máquina? Embora seja possível anexar 100 TB de discos a uma única máquina, provavelmente estaremos limitados pela capacidade da máquina de ler e gravar em disco.

Por exemplo, um HDD comum de 4 TB pode ser capaz de sustentar 200 operações de entrada/saída por segundo (IOPS). Se cada entrada de log puder ser armazenada e indexada em uma média de uma gravação de disco por entrada de log, vemos que o IOPS é um fator limitante para nossos registros de consulta:

(5 x 10^5 consultas/seg) / (200 IOPS/disco) = 2,5 x 10^3 discos ou 2.500 discos

Mesmo que possamos agrupar nossas consultas em uma proporção de 10:1 para limitar as operações, no melhor cenário, ainda precisaríamos de várias centenas de HDDs. Considerando que as gravações de log de consulta são apenas um componente dos requisitos de E/S do design, precisamos usar uma solução que lide com altos IOPS melhor do que os HDDs tradicionais.

Para simplificar, vamos direto para a avaliação da RAM e pular a avaliação de outros meios de armazenamento, como discos de estado sólido (SSD). Uma única máquina não pode lidar com uma pegada de 100 TB inteiramente na RAM: assumindo que tenhamos uma pegada de máquina padrão de 16 núcleos, 64 GB de RAM e 1 Gbps de throughput de rede disponível, precisaremos de:

(100 TB) / (64 GB de RAM/máquina) = 1.563 máquinas

Avaliação

Ignorando nossos cálculos por um momento e imaginando que pudéssemos ajustar este design em uma única máquina, nós realmente gostaríamos disso? Se testarmos nosso design perguntando o que acontece quando este componente falha, identificamos uma longa lista de pontos únicos de falha (por exemplo, CPU, memória, armazenamento, energia, rede, refrigeração). Podemos apoiar razoavelmente nossos SLOs se um desses componentes falhar? Quase certamente não – até mesmo um simples ciclo de energia afetaria significativamente nossos usuários.

Voltando aos nossos cálculos, nosso design de uma única máquina mais uma vez parece inviável, mas este passo não foi um desperdício de tempo. Descobrimos informações valiosas sobre como raciocinar sobre as restrições do sistema e seus requisitos iniciais. Precisamos evoluir nosso design para usar mais de uma máquina.

Sistema Distribuído

Os search_terms que precisamos estão no registro de consulta, e os ad_ids estão no registro de cliques. Agora que sabemos que precisaremos de várias máquinas, qual é o melhor design para uni-los?

MapReduce

Podemos processar e unir os logs com um MapReduce. Podemos periodicamente obter os logs de consulta e de cliques acumulados, e o MapReduce irá produzir um conjunto de dados organizado por ad_id que exibe o número de cliques que cada search_term recebeu. 

O MapReduce funciona como um processador em lote: suas entradas são um grande conjunto de dados, e ele pode usar muitas máquinas para processar esses dados por meio de trabalhadores e produzir um resultado. Assim que todas as máquinas tiverem processado seus dados, suas saídas podem ser combinadas – o MapReduce pode criar diretamente resumos de cada CTR para cada anúncio do AdWords e termo de pesquisa. Podemos usar esses dados para criar os painéis de controle de que precisamos.

Avaliação

MapReduce é um modelo de computação amplamente utilizado que temos confiança de que se escalará horizontalmente. Não importa o quão grandes sejam nossos inputs de registro de consulta e registro de cliques, adicionar mais máquinas sempre permitirá que o processo seja concluído com sucesso sem ficar sem espaço em disco ou RAM.

Infelizmente, este tipo de processo em lote não pode atender nosso SLO de disponibilidade de registros unidos dentro de 5 minutos após a recepção dos logs. Para fornecer resultados dentro de 5 minutos, precisaríamos executar trabalhos do MapReduce em lotes pequenos – apenas alguns minutos de logs por vez.

A natureza arbitrária e não sobreposta dos lotes torna os pequenos lotes impraticáveis. Se uma consulta registrada está no lote 1 e seu clique está no lote 2, o clique e a consulta nunca serão unidos. Embora o MapReduce lide bem com lotes autocontidos, não é otimizado para esse tipo de problema. Neste ponto, poderíamos tentar descobrir possíveis soluções alternativas usando MapReduce. Por uma questão de simplicidade, no entanto, passaremos a examinar outra solução.

LogJoiner

O número de anúncios que os usuários clicam é significativamente menor do que o número de anúncios servidos. Intuitivamente, precisamos focar em dimensionar o maior dos dois: os registros de consulta. Fazemos isso introduzindo um novo componente do sistema distribuído.

Em vez de procurar o query_id em lotes pequenos, como em nosso design do MapReduce, e se criássemos uma loja de todas as consultas que podemos consultar por query_id sob demanda? Vamos chamá-la de QueryStore. Ele contém o conteúdo completo dos registros de consulta, indexado por query_id. Para evitar repetições, vamos supor que nossos cálculos do design de uma única máquina se aplicarão ao QueryStore e limitaremos a revisão do QueryStore ao que já cobrimos. Para uma discussão mais aprofundada sobre como um componente como este poderia funcionar, recomendamos a leitura sobre o Bigtable.

Como os logs de cliques também têm o query_id, a escala do nosso loop de processamento agora é muito menor: ele só precisa percorrer os logs de cliques e buscar as consultas específicas referenciadas. Chamaremos este componente de LogJoiner.

O LogJoiner recebe um fluxo contínuo de dados dos logs de cliques, une-o com os dados no QueryStore e, em seguida, armazena essas informações, organizadas por ad_id. Uma vez que as consultas clicadas são armazenadas e indexadas por ad_id, temos metade dos dados necessários para gerar o painel de CTR. Chamaremos isso de ClickMap, porque mapeia de ad_id para os cliques.

Se não encontrarmos uma consulta para um clique (pode haver uma desaceleração na recepção dos logs de consulta), colocamos o clique de lado por algum tempo e tentamos novamente, até um limite de tempo. Se não conseguirmos encontrar uma consulta para ele até esse limite de tempo, descartamos esse clique.

O painel de CTR precisa de dois componentes para cada par ad_id e search_term: o número de impressões e o número de anúncios clicados. O ClickMap precisa de um parceiro para armazenar as consultas, organizadas por ad_id. Chamaremos isso de QueryMap. QueryMap é alimentado diretamente com todos os dados do registro de consulta e também indexa as entradas por ad_id. 

A Figura 12-1 representa como os dados fluem pelo sistema.

O design do LogJoiner introduz vários novos componentes: LogJoiner, QueryStore, ClickMap e QueryMap. Precisamos garantir que esses componentes possam ser escalados.

 

Figura 12-1. Design básico do LogJoiner; os dados de cliques são processados e armazenados para que o painel possa recuperá-los.

Cálculos 

A partir dos cálculos que realizamos em iterações anteriores, sabemos que o QueryStore terá cerca de 100 TB de dados para um dia de logs. Podemos excluir dados que sejam muito antigos para serem úteis.

O LogJoiner deve processar os cliques conforme eles chegam e recuperar os logs de consulta correspondentes do QueryStore.

A quantidade de largura de banda de rede que o LogJoiner precisa para processar os logs é baseada em quantos cliques por segundo temos em nossos logs, multiplicado pelo tamanho do registro de 2 KB:

(10^4 cliques/seg) × (2 × 10^3 bytes) = 2 × 10^7 = 20 MB/seg = 160 Mbps

As consultas ao QueryStore incorrem em uso adicional da rede. Para cada registro de log de clique, buscamos o query_id e retornamos um registro de log completo:

  • (10^4 cliques/seg) × (8 bytes) = 8 × 10^4 = 80 KB/seg = 640 Kbps
  • (10^4 cliques/seg) × (2 × 10^3 bytes) = 2 × 10^7 = 20 MB/seg = 160 Mbps

O LogJoiner também enviará resultados para o ClickMap. Precisamos armazenar o query_id, ad_id, time e search_term. O tempo e o query_id são ambos inteiros de 64 bits, então esses dados serão menos de 1 KB:

(10^4 cliques/seg) × (10^3 bytes) = 10^7 = 10 MB/seg = 80 Mbps

Um agregado de ~400 Mbps é uma taxa de transferência de dados gerenciável para nossas máquinas. 

O ClickMap precisa armazenar o tempo e o query_id para cada clique, mas não precisa de nenhum metadado adicional. Vamos ignorar o ad_id e search_term porque são um fator linear pequeno (por exemplo, número de anunciantes × número de anúncios × 8 bytes). Mesmo 10 milhões de anunciantes com 10 anúncios cada é apenas ~800 MB. Um dia de ClickMap é:

(10^4 cliques/seg) × (8,64 × 10^4 segundos/dia) × (8 bytes + 8 bytes) = 1,4 × 10^10 = 14 GB/dia para o ClickMap

Vamos arredondar o ClickMap para 20 GB/dia para considerar qualquer sobrecarga e nossos ad_ids. Conforme preenchemos o QueryMap, precisamos armazenar o query_id para cada anúncio que é exibido. Nossa necessidade de armazenamento aumenta porque potencialmente existem três ad_ids que poderiam ser clicados para cada consulta de pesquisa, então precisaremos registrar o query_id em até três entradas:

3 × (5 × 10^5 consultas/seg) × (8,64 × 10^4 segundos/dia) × (8 bytes + 8 bytes) = 2 × 10^12 = 2 TB/dia para o QueryMap

2 TB é pequeno o suficiente para ser hospedado em uma única máquina usando HDDs, mas sabemos, a partir de nossa iteração em uma única máquina, que as gravações individuais pequenas são muito frequentes para serem armazenadas em um disco rígido. Embora pudéssemos calcular o impacto do uso de unidades com maior IOPS (por exemplo, SSD), nosso exercício está focado em demonstrar que o sistema pode escalar para um tamanho arbitrariamente grande. Neste caso, precisamos projetar em torno das limitações de E/S de uma única máquina. Portanto, o próximo passo na escalabilidade do design é dividir as entradas e saídas: dividir os registros de consulta recebidos e os registros de cliques em vários fluxos.

LogJoiner particionado

Nosso objetivo nesta iteração é executar várias instâncias do LogJoiner, cada uma em uma partição diferente dos dados. Para isso, precisamos pensar em vários fatores:

Gerenciamento de dados

Para unir os registros de logs de consulta e cliques, devemos associar cada registro de log de clique com seu registro de log de consulta correspondente pelo query_id. O design deve impedir que a largura de banda de rede e de disco restrinja nosso design conforme escalamos.

Confiabilidade

Sabemos que uma máquina pode falhar a qualquer momento. Quando uma máquina executando o LogJoiner falha, como garantimos que não perdemos o trabalho em andamento?

Eficiência

Podemos escalar sem desperdícios? Precisamos usar os recursos mínimos que atendam às nossas preocupações de gerenciamento de dados e confiabilidade.

Nosso design do LogJoiner mostrou que podemos unir nossos logs de consulta e logs de cliques, mas o volume resultante de dados é muito grande. Se dividirmos o trabalho em fragmentos com base no query_id, podemos executar vários LogJoiners em paralelo.

Desde que tenhamos um número razoável de instâncias do LogJoiner, se distribuirmos os logs de forma equitativa, cada instância receberá apenas um pequeno fluxo de informações pela rede. Conforme o fluxo de cliques aumenta, escalamos horizontalmente adicionando mais instâncias do LogJoiner, em vez de escalar verticalmente usando mais CPU e RAM.

Conforme mostrado na Figura 12-2, para que os LogJoiners recebam as mensagens corretas, introduzimos um componente chamado sharder de log, que direcionará cada entrada de log para o destino correto. Para cada registro, nossos sharders de log de cliques fazem o seguinte:

  1. Aplicar a função de hash no query_id do registro.
  2. Fazer o módulo do resultado com N (o número de fragmentos) e adicionar 1 para obter um número entre 1 e N.
  3. Enviar o registro para o fragmento número obtido no passo 2.

 

Figura 12-2. Como deve funcionar o particionamento?

Agora, cada LogJoiner receberá um subconjunto consistente dos logs de entrada divididos pelo query_id, em vez do log completo de cliques.

O QueryMap também precisa ser particionado. Sabemos que será necessário muitos discos rígidos para sustentar o IOPS exigido pelo QueryMap, e que o tamanho de um QueryMap de um dia (2 TB) é muito grande para nossas máquinas de 64 GB armazenarem na RAM. No entanto, em vez de particionar por query_id como o LogJoiner, vamos particionar pelo ad_id. O ad_id é conhecido antes de qualquer leitura ou gravação, então usar a mesma abordagem de hash que o LogJoiner e o painel CTR fornecerá uma visão consistente dos dados.

Para manter as implementações consistentes, podemos reutilizar o mesmo design de sharder de log para o ClickMap como para o QueryMap, já que o ClickMap é menor que o QueryMap.

Agora que sabemos que nosso sistema escalará, podemos avançar para abordar a confiabilidade do sistema. Nosso design deve ser resiliente a falhas do LogJoiner. Se um LogJoiner falhar após receber mensagens de log, mas antes de uni-las, todo o seu trabalho deve ser refeito. Isso atrasa a chegada de dados precisos ao painel, o que afetará nosso SLO.

Se nosso processo de sharder de log enviar entradas de log duplicadas para dois fragmentos, o sistema pode continuar operando em plena velocidade e processar resultados precisos mesmo quando um LogJoiner falhar (provavelmente porque a máquina na qual ele está falha).

Ao replicar o trabalho dessa maneira, reduzimos (mas não eliminamos) a chance de perdermos esses registros unidos. Dois fragmentos podem falhar ao mesmo tempo e perder os registros unidos. Distribuindo a carga de trabalho para garantir que nenhum fragmento duplicado aterrisse na mesma máquina, podemos mitigar grande parte desse risco. Se duas máquinas falharem simultaneamente e perdermos ambas as cópias do fragmento, o orçamento de erro do sistema (consulte o Capítulo 4 do primeiro livro de SRE) pode cobrir o restante do risco. Quando ocorre um desastre, podemos reproces‐sar os logs. O painel mostrará apenas dados um pouco mais antigos do que 5 minutos por um curto período de tempo.

A Figura 12-3 mostra nosso design para um fragmento e sua réplica, onde o LogJoiner, ClickMap e QueryMap são construídos em ambos os fragmentos.

A partir dos registros unidos, podemos construir um ClickMap em cada uma das máquinas do LogJoiner. Para exibir nossos painéis de usuário, todos os ClickMaps precisam ser combinados e consultados.

Avaliação

Hospedar os componentes particionados em um único datacenter cria um único ponto de falha: se qualquer par de máquinas azaradas ou o datacenter forem desconectados, perdemos todo o trabalho do ClickMap e os painéis do usuário param de funcionar completamente! Precisamos evoluir nosso design para usar mais de um datacenter.

 

Figura 12-3. Particionamento de logs com o mesmo query_id em fragmentos duplicados

Multidatacenter

Duplicar dados entre datacenters localizados em diferentes locais geográficos permite que nossa infraestrutura de serviço suporte falhas catastróficas. Se um datacenter estiver inativo (por exemplo, devido a uma interrupção de energia ou de rede que dure vários dias), podemos mudar para outro datacenter. Para que a mudança seja bem-sucedida, os dados do ClickMap devem estar disponíveis em todos os datacenters onde o sistema estiver implantado.

Será que um ClickMap desse tipo é possível? Não queremos multiplicar nossos requisitos de computação pelo número de datacenters, mas como podemos sincronizar eficientemente o trabalho entre os sites para garantir replicação suficiente sem criar duplicação desnecessária?

Acabamos de descrever um exemplo do conhecido problema de consenso em engenharia de sistemas distribuídos. Existem diversos algoritmos complexos para resolver esse problema, mas a ideia básica é:

  1. Faça três ou cinco réplicas do serviço que deseja compartilhar (como o ClickMap).
  2. Faça com que as réplicas usem um algoritmo de consenso, como o Paxos, para garantir que possamos armazenar de forma confiável o estado dos cálculos se ocorrer uma falha do tamanho de um datacenter.
  3. Implemente pelo menos um tempo de ida e volta de rede entre os nós participantes para aceitar uma operação de escrita. Esse requisito impõe um limite à taxa de transferência sequencial para o sistema. Ainda podemos paralelizar algumas das escritas para o mapa baseado em consenso distribuído.

Seguindo os passos listados anteriormente, o design de multidatacenter agora parece viável em princípio. Será que também funcionará na prática? Que tipos de recursos precisamos e quantos deles precisamos?

Cálculos

A latência da execução do algoritmo Paxos com datacenters isolados contra falhas significa que cada operação precisa de aproximadamente 25 milissegundos para ser concluída. Essa suposição de latência é baseada em datacenters separados por pelo menos algumas centenas de quilômetros. Portanto, em termos de processos sequenciais, só podemos realizar uma operação a cada 25 milissegundos ou 40 operações por segundo. Se precisarmos realizar processos sequenciais 10^4 vezes por segundo (logs de cliques), precisamos de pelo menos 250 processos por datacenter, particionados por ad_id, para as operações Paxos. Na prática, gostaríamos de adicionar mais processos para aumentar o paralelismo – para lidar com o backlog acumulado após qualquer tempo de inatividade ou picos de tráfego.

Com base em nossos cálculos anteriores para ClickMap e QueryMap, e usando a estimativa de 40 operações sequenciais por segundo, quantas novas máquinas precisamos para nosso design de multidatacenter?

Como nosso design de LogJoiner particionado introduz uma réplica para cada registro de log, dobramos o número de transações por segundo para criar o ClickMap e QueryMap: 20.000 cliques/segundo e 1.000.000 de consultas/segundo.

Podemos calcular o número mínimo de processos ou tarefas necessárias dividindo o total de consultas por segundo pelo nosso máximo de operações por segundo:

(1,02 × 10^6 consultas/segundo) / (40 operações/segundo) = 25.500 tarefas

A quantidade de memória para cada tarefa (duas cópias de 2 TB QueryMap):

(4 × 10^12 bytes) / (25.500 tarefas) = 157 MB/tarefa

Tarefas por máquina:

(6,4 × 10^10 bytes) / (1,57 × 10^8 bytes) = 408 tarefas/máquina

Sabemos que podemos acomodar muitas tarefas em uma única máquina, mas precisamos garantir que não seremos limitados por E/S. O throughput de rede total para ClickMap e QueryMap (usando uma estimativa alta de 2 KB por entrada):

(1,02 × 10^6 consultas/segundo) × (2 × 10^3 bytes) = 2,04 GB/seg = 16 Gbps

Throughput por tarefa:

16 Gbps / 25.500 tarefas = 80 KB/seg = 640 Kbps/tarefa

Throughput por máquina:

408 tarefas × 640 Kbps/tarefa = 256 Mbps

Nossa combinação de 157 MB de memória e 640 Kbps por tarefa é gerenciável. Precisamos de aproximadamente 4 TB de RAM em cada datacenter para hospedar o ClickMap e o QueryMap shardados. Se tivermos 64 GB de RAM por máquina, podemos servir os dados de apenas 64 máquinas, e usaremos apenas 25% da largura de banda de rede de cada máquina.

Avaliação

Agora que projetamos um sistema de múltiplos datacenters, vamos revisar se o fluxo de dados faz sentido.

A Figura 12-4 mostra todo o projeto do sistema. Você pode ver como cada pesquisa e clique em anúncios são comunicados aos servidores e como os logs são coletados e inseridos em cada componente.

Podemos verificar este sistema em relação aos nossos requisitos:

10.000 cliques em anúncios por segundo

O LogJoiner pode escalar horizontalmente para processar todos os cliques em logs e armazenar o resultado no ClickMap.

500.000 consultas de pesquisa por segundo

O QueryStore e o QueryMap foram projetados para lidar com o armazenamento de um dia inteiro de dados nessa taxa.

99,9% das consultas do painel são concluídas em < 1 segundo

O painel de CTR recupera dados do QueryMap e do ClickMap, que são indexados por ad_id, tornando essa transação rápida e simples.

99,9% do tempo, os dados de CTR exibidos têm menos de 5 minutos de idade

Cada componente é projetado para escalar horizontalmente, o que significa que se o pipeline estiver muito lento, adicionar mais máquinas diminuirá a latência do pipeline de ponta a ponta.

Acreditamos que essa arquitetura de sistema escala para atender aos nossos requisitos de throughput, desempenho e confiabilidade.

 

Figura 12-4. Design multidatacenter

Conclusão

O NALSD descreve o processo iterativo de design de sistemas que o Google utiliza para sistemas de produção. Ao decompor o software em componentes lógicos e colocar esses componentes em um ecossistema de produção com infraestrutura confiável, chegamos a sistemas que fornecem metas razoáveis e apropriadas para consistência de dados, disponibilidade do sistema e eficiência de recursos. A prática do NALSD nos permite melhorar nosso design sem começar do zero a cada iteração. Embora várias iterações de design apresentadas neste capítulo tenham atendido à nossa declaração de problema original, cada iteração revelou novos requisitos, que pudemos atender estendendo nosso trabalho anterior.

Ao longo desse processo, separamos os componentes de software com base em como esperávamos que o sistema crescesse. Essa estratégia nos permitiu escalar partes diferentes do sistema de forma independente e eliminar dependências de peças únicas de hardware ou instâncias únicas de software, produzindo assim um sistema mais confiável.

Durante o processo de design, continuamos a melhorar cada iteração fazendo as quatro perguntas-chave do NALSD:

É possível?

Podemos construí-lo sem “mágica”?

Podemos fazer melhor?

É tão simples quanto podemos razoavelmente torná-lo?

É viável?

Se encaixa dentro de nossas restrições práticas (orçamento, tempo, etc.)?

É resiliente?

Sobreviverá a interrupções ocasionais, mas inevitáveis?

O NALSD é uma habilidade aprendida. Como qualquer habilidade, você precisa praticá-la regularmente para manter sua proficiência. A experiência do Google mostrou que a capacidade de raciocinar a partir de um requisito abstrato até uma aproximação concreta de recursos é fundamental para construir sistemas saudáveis e duradouros.

Capítulo 12 – Apresentando o projeto de sistemas não-abstratos em grande escala

Por Salim Virji, James Youngman, Henry Robertson,

Stephen Thorne, Dave Rensin e Zoltan Egyed 

com Richard Bondi

 

Com responsabilidades que abrangem operações de produção e engenharia de produtos, o SRE (Engenharia de Confiabilidade de Site) está em uma posição única para alinhar os requisitos do caso de negócio e os custos operacionais. As equipes de engenharia de produtos podem não estar cientes do custo de manutenção dos sistemas que projetam, especialmente se essa equipe estiver construindo um único componente que faz parte de um ecossistema de produção maior.

Com base na experiência do Google no desenvolvimento de sistemas, consideramos a confiabilidade como o aspecto mais crítico de qualquer sistema de produção. Descobrimos que adiar questões de confiabilidade durante o design é semelhante a aceitar menos recursos a custos mais altos. Ao seguir um estilo iterativo de design e implementação de sistemas, chegamos a designs robustos e escaláveis com baixos custos operacionais. Chamamos esse estilo de Projeto de Sistemas Não-Abstratos em Grande Escala (NALSD).

O que é NALSD?

Este capítulo apresenta uma abordagem NALSD: começamos com a declaração do problema, reunimos requisitos e iteramos através de designs que se tornam cada vez mais sofisticados até chegarmos a uma solução viável. No final, chegamos a um sistema que se defende contra muitos modos de falha e satisfaz tanto os requisitos iniciais quanto os detalhes adicionais que surgiram durante as iterações.

O NALSD descreve uma habilidade crítica para o SRE: a capacidade de avaliar, projetar e avaliar sistemas grandes. Na prática, o NALSD combina elementos de planejamento de capacidade, isolamento de componentes e degradação graciosa do sistema, que são cruciais para sistemas de produção altamente disponíveis. Espera-se que os SREs do Google sejam capazes de iniciar o planejamento de recursos com um diagrama básico em uma lousa de um sistema, pensar nos vários domínios de escalabilidade e falha, e focar seu design em uma proposta concreta de recursos. Como esses sistemas mudam ao longo do tempo, é vital que um SRE seja capaz de analisar e avaliar os aspectos-chave do design do sistema.

Por que “Não-Abstrato”?

Todos os sistemas eventualmente terão que ser executados em computadores reais em datacenters reais usando redes reais. O Google aprendeu (da maneira difícil) que as pessoas que projetam sistemas distribuídos precisam desenvolver e exercitar continuamente a habilidade de transformar um design de lousa em estimativas concretas de recursos em várias etapas do processo. Sem essa rigidez, é muito tentador criar sistemas que não se traduzem completamente no mundo real.

Este trabalho adicional inicial geralmente leva a menos mudanças de design de sistema de última hora para dar conta de alguma restrição física não prevista.

Por favor, note que, enquanto conduzimos esses exercícios para resultados discretos (por exemplo, número de máquinas), exemplos de raciocínio sólido e suposições são mais importantes do que quaisquer valores finais. As suposições iniciais influenciam fortemente os resultados dos cálculos, e fazer suposições perfeitas não é um requisito para o NALSD. O valor deste exercício está em combinar muitos resultados imperfeitos, mas razoáveis, para uma melhor compreensão do design.

Exemplo do AdWords

O serviço Google AdWords exibe anúncios de texto na Pesquisa na Web do Google. A métrica de taxa de cliques (CTR) informa aos anunciantes o desempenho de seus anúncios. A CTR é a razão entre o número de vezes que o anúncio é clicado e o número de vezes que o anúncio é exibido.

Este exemplo do AdWords tem como objetivo projetar um sistema capaz de medir e relatar uma CTR precisa para cada anúncio do AdWords. Os dados que precisamos para calcular a CTR são registrados em logs dos sistemas de pesquisa e de veiculação de anúncios. Esses logs registram os anúncios exibidos para cada consulta de pesquisa e os anúncios clicados, respectivamente.

Processo de design

O Google utiliza uma abordagem iterativa para projetar sistemas que atendam aos nossos objetivos. Cada iteração define um design potencial e examina seus pontos fortes e fracos. Essa análise alimenta a próxima iteração ou indica quando o design é bom o suficiente para ser recomendado.

Em termos gerais, o processo do NALSD tem duas fases, cada uma com duas a três perguntas.

Na fase de design básico, tentamos inventar um design que funcione em princípio. Fazemos duas perguntas:

É possível?

O design é viável? Se não precisássemos nos preocupar com RAM suficiente, CPU, largura de banda de rede, e assim por diante, o que projetaríamos para satisfazer os requisitos?

Podemos melhorar?

Para qualquer design desse tipo, perguntamos: “Podemos melhorar?” Por exemplo, podemos tornar o sistema significativamente mais rápido, menor, mais eficiente? Se o design resolver o problema em tempo O(N), podemos resolvê-lo mais rapidamente, digamos, em tempo O(ln(N))?

Na próxima fase, tentamos ampliar nosso design básico – por exemplo, aumentando drasticamente um requisito. Fazemos três perguntas:

É viável?

É possível escalar este design, considerando restrições de dinheiro, hardware, e assim por diante? Se necessário, qual design distribuído satisfaria os requisitos?

É resiliente?

O design pode falhar de forma graciosa? O que acontece quando este componente falha? Como o sistema funciona quando um datacenter inteiro falha?

Podemos fazer melhor?

Embora geralmente abordemos essas fases e perguntas nesta ordem aproximada, na prática, saltamos entre as perguntas e fases. Por exemplo, durante a fase de design básico, muitas vezes temos o crescimento e a escalabilidade em mente. 

Então, iteramos. Um design pode passar com sucesso pela maioria das fases, apenas para fracassar mais tarde. Quando isso acontece, começamos novamente, modificando ou substituindo componentes. O design final é o fim de uma história de reviravoltas.

Com esses conceitos em mente, vamos percorrer o processo iterativo do NALSD.

Requisitos Iniciais

Cada anunciante pode ter vários anúncios. Cada anúncio é identificado por um ad_id e está associado a uma lista de termos de pesquisa selecionados pelo anunciante.

Ao exibir um painel para um anunciante, precisamos saber o seguinte para cada anúncio e termo de pesquisa:

  • Com que frequência este termo de pesquisa ativou este anúncio para ser exibido
  • Quantas vezes o anúncio foi clicado por alguém que viu o anúncio

Com essas informações, podemos calcular a CTR: o número de cliques dividido pelo número de impressões.

Sabemos que nossos anunciantes se preocupam com duas coisas: que o painel seja exibido rapidamente e que os dados sejam recentes. Portanto, ao iterar no design, consideraremos nossos requisitos em termos de SLOs (consulte o Capítulo 2 para mais detalhes):

  • 99,9% das consultas do painel são concluídas em < 1 segundo.
  • 99,9% do tempo, os dados da CTR exibidos têm menos de 5 minutos de idade.

Esses SLOs fornecem um objetivo razoável que devemos ser capazes de atender consistentemente. Eles também fornecem um orçamento de erro (consulte o Capítulo 4 em Engenharia de Confiabilidade de Site), que vamos comparar nossa solução em cada iteração do design. 

Nosso objetivo é criar um sistema que possa atender nossos SLOs e também suportar milhões de anunciantes que desejam ver suas CTRs em um painel. Para taxas de transação, vamos supor 500.000 consultas de pesquisa por segundo e 10.000 cliques de anúncio por segundo.

Uma Máquina

O ponto de partida mais simples é considerar a execução de toda a nossa aplicação em um único computador.

Para cada consulta de pesquisa na web, registramos:

time

O horário em que a consulta ocorreu

query_id

Um identificador único da consulta (ID da consulta)

search_term

O conteúdo da consulta

ad_id

Os IDs dos anúncios do AdWords mostrados para a pesquisa

Juntas, essas informações formam o registro de consultas. Cada vez que um usuário clica em um anúncio, registramos o time do clique, o ID da consulta (query id) e o ID do anúncio (ad id) no registro de cliques (click log).

Você pode estar se perguntando por que não adicionamos o search_term ao registro de cliques para reduzir a complexidade. No escopo arbitrariamente reduzido do nosso exemplo, isso poderia ser viável. No entanto, na prática, a CTR é na verdade apenas uma das muitas informações calculadas a partir desses registros. Os registros de cliques são derivados de URLs, que têm limitações de tamanho inerentes, tornando o registro de consulta separado uma solução mais escalável. Em vez de provar esse ponto adicionando requisitos adicionais semelhantes à CTR ao exercício, simplesmente reconheceremos essa suposição e avançaremos.

A exibição de um painel requer os dados de ambos os logs. Precisamos ser capazes de mostrar que podemos alcançar nosso SLO de exibir dados atualizados no painel em menos de um segundo. Alcançar esse SLO requer que a velocidade de cálculo de uma CTR permaneça constante à medida que o sistema lida com grandes quantidades de cliques e consultas.

Para atender nosso SLO de exibir nosso painel em menos de um segundo, precisamos de pesquisas rápidas do número de “query_ids” clicados e mostrados por “search_term” para um determinado “ad_id”. Podemos extrair a divisão de “query_ids” mostrados por “search_term”

‘ad_id” do registro de consulta. Um painel de CTR precisa de todos os registros tanto do registro de consulta quanto do registro de cliques para os “ad_ids”.

Se tivermos mais do que alguns anunciantes, percorrer o registro de consulta e o registro de cliques para gerar o painel será muito ineficiente. Portanto, nosso design prevê que nossa única máquina crie uma estrutura de dados apropriada para permitir cálculos rápidos de CTR à medida que recebe os registros. Em uma única máquina, usando um banco de dados SQL com índices em “query_id” e “search_term”, deve ser possível fornecer respostas em menos de um segundo. Ao unir esses registros em “query_id” e agrupar por “search_term”, podemos relatar a CTR para cada pesquisa.

Cálculos

Precisamos calcular quantos recursos precisamos para analisar todos esses registros. Para determinar nossos limites de escalabilidade, precisamos fazer algumas suposições, começando pelo tamanho do registro de consulta:

time

Inteiro de 64 bits, 8 bytes

query_id

Inteiro de 64 bits, 8 bytes

ad_id

Três inteiros de 64 bits, 8 bytes no total

search_term

Uma string longa, até 500 bytes

Outros metadados

500 a 1.000 bytes de informações, como qual máquina serviu os anúncios, em que idioma a pesquisa foi feita e quantos resultados o termo de pesquisa retornou.

Para garantir que não atingimos um limite prematuramente, arredondamos agressivamente para cima e tratamos cada entrada no registro de consulta como 2 KB. O volume do registro de cliques deve ser consideravelmente menor do que o volume do registro de consultas: como a CTR média é de 2% (10.000 cliques / 500.000 consultas), o registro de cliques terá 2% do número de registros do registro de consultas. Lembre-se de que escolhemos números grandes para ilustrar que esses princípios se aplicam a implementações arbitrariamente grandes. Essas estimativas parecem grandes porque devem ser.

Por fim, podemos usar a notação científica para limitar erros causados por aritmética em unidades inconsistentes. O volume de registros de consulta gerados em um período de 24 horas será:

(5 x 10^5 consultas/seg) × (8,64 x 10^4 segundos/dia) × (2 x 10^3 bytes) = 86,4 TB/dia

Como recebemos 2% dos cliques em relação às consultas, e sabemos que nossos índices de banco de dados acrescentarão alguma quantidade razoável de sobrecarga, podemos arredondar nossos 86,4 TB/dia para 100 TB de espaço necessário para armazenar os dados de log de um dia.

Com um requisito de armazenamento agregado de ~100 TB, temos algumas novas suposições a fazer. Esse design ainda funciona com uma única máquina? Embora seja possível anexar 100 TB de discos a uma única máquina, provavelmente estaremos limitados pela capacidade da máquina de ler e gravar em disco.

Por exemplo, um HDD comum de 4 TB pode ser capaz de sustentar 200 operações de entrada/saída por segundo (IOPS). Se cada entrada de log puder ser armazenada e indexada em uma média de uma gravação de disco por entrada de log, vemos que o IOPS é um fator limitante para nossos registros de consulta:

(5 x 10^5 consultas/seg) / (200 IOPS/disco) = 2,5 x 10^3 discos ou 2.500 discos

Mesmo que possamos agrupar nossas consultas em uma proporção de 10:1 para limitar as operações, no melhor cenário, ainda precisaríamos de várias centenas de HDDs. Considerando que as gravações de log de consulta são apenas um componente dos requisitos de E/S do design, precisamos usar uma solução que lide com altos IOPS melhor do que os HDDs tradicionais.

Para simplificar, vamos direto para a avaliação da RAM e pular a avaliação de outros meios de armazenamento, como discos de estado sólido (SSD). Uma única máquina não pode lidar com uma pegada de 100 TB inteiramente na RAM: assumindo que tenhamos uma pegada de máquina padrão de 16 núcleos, 64 GB de RAM e 1 Gbps de throughput de rede disponível, precisaremos de:

(100 TB) / (64 GB de RAM/máquina) = 1.563 máquinas

Avaliação

Ignorando nossos cálculos por um momento e imaginando que pudéssemos ajustar este design em uma única máquina, nós realmente gostaríamos disso? Se testarmos nosso design perguntando o que acontece quando este componente falha, identificamos uma longa lista de pontos únicos de falha (por exemplo, CPU, memória, armazenamento, energia, rede, refrigeração). Podemos apoiar razoavelmente nossos SLOs se um desses componentes falhar? Quase certamente não – até mesmo um simples ciclo de energia afetaria significativamente nossos usuários.

Voltando aos nossos cálculos, nosso design de uma única máquina mais uma vez parece inviável, mas este passo não foi um desperdício de tempo. Descobrimos informações valiosas sobre como raciocinar sobre as restrições do sistema e seus requisitos iniciais. Precisamos evoluir nosso design para usar mais de uma máquina.

Sistema Distribuído

Os search_terms que precisamos estão no registro de consulta, e os ad_ids estão no registro de cliques. Agora que sabemos que precisaremos de várias máquinas, qual é o melhor design para uni-los?

MapReduce

Podemos processar e unir os logs com um MapReduce. Podemos periodicamente obter os logs de consulta e de cliques acumulados, e o MapReduce irá produzir um conjunto de dados organizado por ad_id que exibe o número de cliques que cada search_term recebeu. 

O MapReduce funciona como um processador em lote: suas entradas são um grande conjunto de dados, e ele pode usar muitas máquinas para processar esses dados por meio de trabalhadores e produzir um resultado. Assim que todas as máquinas tiverem processado seus dados, suas saídas podem ser combinadas – o MapReduce pode criar diretamente resumos de cada CTR para cada anúncio do AdWords e termo de pesquisa. Podemos usar esses dados para criar os painéis de controle de que precisamos.

Avaliação

MapReduce é um modelo de computação amplamente utilizado que temos confiança de que se escalará horizontalmente. Não importa o quão grandes sejam nossos inputs de registro de consulta e registro de cliques, adicionar mais máquinas sempre permitirá que o processo seja concluído com sucesso sem ficar sem espaço em disco ou RAM.

Infelizmente, este tipo de processo em lote não pode atender nosso SLO de disponibilidade de registros unidos dentro de 5 minutos após a recepção dos logs. Para fornecer resultados dentro de 5 minutos, precisaríamos executar trabalhos do MapReduce em lotes pequenos – apenas alguns minutos de logs por vez.

A natureza arbitrária e não sobreposta dos lotes torna os pequenos lotes impraticáveis. Se uma consulta registrada está no lote 1 e seu clique está no lote 2, o clique e a consulta nunca serão unidos. Embora o MapReduce lide bem com lotes autocontidos, não é otimizado para esse tipo de problema. Neste ponto, poderíamos tentar descobrir possíveis soluções alternativas usando MapReduce. Por uma questão de simplicidade, no entanto, passaremos a examinar outra solução.

LogJoiner

O número de anúncios que os usuários clicam é significativamente menor do que o número de anúncios servidos. Intuitivamente, precisamos focar em dimensionar o maior dos dois: os registros de consulta. Fazemos isso introduzindo um novo componente do sistema distribuído.

Em vez de procurar o query_id em lotes pequenos, como em nosso design do MapReduce, e se criássemos uma loja de todas as consultas que podemos consultar por query_id sob demanda? Vamos chamá-la de QueryStore. Ele contém o conteúdo completo dos registros de consulta, indexado por query_id. Para evitar repetições, vamos supor que nossos cálculos do design de uma única máquina se aplicarão ao QueryStore e limitaremos a revisão do QueryStore ao que já cobrimos. Para uma discussão mais aprofundada sobre como um componente como este poderia funcionar, recomendamos a leitura sobre o Bigtable.

Como os logs de cliques também têm o query_id, a escala do nosso loop de processamento agora é muito menor: ele só precisa percorrer os logs de cliques e buscar as consultas específicas referenciadas. Chamaremos este componente de LogJoiner.

O LogJoiner recebe um fluxo contínuo de dados dos logs de cliques, une-o com os dados no QueryStore e, em seguida, armazena essas informações, organizadas por ad_id. Uma vez que as consultas clicadas são armazenadas e indexadas por ad_id, temos metade dos dados necessários para gerar o painel de CTR. Chamaremos isso de ClickMap, porque mapeia de ad_id para os cliques.

Se não encontrarmos uma consulta para um clique (pode haver uma desaceleração na recepção dos logs de consulta), colocamos o clique de lado por algum tempo e tentamos novamente, até um limite de tempo. Se não conseguirmos encontrar uma consulta para ele até esse limite de tempo, descartamos esse clique.

O painel de CTR precisa de dois componentes para cada par ad_id e search_term: o número de impressões e o número de anúncios clicados. O ClickMap precisa de um parceiro para armazenar as consultas, organizadas por ad_id. Chamaremos isso de QueryMap. QueryMap é alimentado diretamente com todos os dados do registro de consulta e também indexa as entradas por ad_id. 

A Figura 12-1 representa como os dados fluem pelo sistema.

O design do LogJoiner introduz vários novos componentes: LogJoiner, QueryStore, ClickMap e QueryMap. Precisamos garantir que esses componentes possam ser escalados.

 

Figura 12-1. Design básico do LogJoiner; os dados de cliques são processados e armazenados para que o painel possa recuperá-los.

Cálculos 

A partir dos cálculos que realizamos em iterações anteriores, sabemos que o QueryStore terá cerca de 100 TB de dados para um dia de logs. Podemos excluir dados que sejam muito antigos para serem úteis.

O LogJoiner deve processar os cliques conforme eles chegam e recuperar os logs de consulta correspondentes do QueryStore.

A quantidade de largura de banda de rede que o LogJoiner precisa para processar os logs é baseada em quantos cliques por segundo temos em nossos logs, multiplicado pelo tamanho do registro de 2 KB:

(10^4 cliques/seg) × (2 × 10^3 bytes) = 2 × 10^7 = 20 MB/seg = 160 Mbps

As consultas ao QueryStore incorrem em uso adicional da rede. Para cada registro de log de clique, buscamos o query_id e retornamos um registro de log completo:

  • (10^4 cliques/seg) × (8 bytes) = 8 × 10^4 = 80 KB/seg = 640 Kbps
  • (10^4 cliques/seg) × (2 × 10^3 bytes) = 2 × 10^7 = 20 MB/seg = 160 Mbps

O LogJoiner também enviará resultados para o ClickMap. Precisamos armazenar o query_id, ad_id, time e search_term. O tempo e o query_id são ambos inteiros de 64 bits, então esses dados serão menos de 1 KB:

(10^4 cliques/seg) × (10^3 bytes) = 10^7 = 10 MB/seg = 80 Mbps

Um agregado de ~400 Mbps é uma taxa de transferência de dados gerenciável para nossas máquinas. 

O ClickMap precisa armazenar o tempo e o query_id para cada clique, mas não precisa de nenhum metadado adicional. Vamos ignorar o ad_id e search_term porque são um fator linear pequeno (por exemplo, número de anunciantes × número de anúncios × 8 bytes). Mesmo 10 milhões de anunciantes com 10 anúncios cada é apenas ~800 MB. Um dia de ClickMap é:

(10^4 cliques/seg) × (8,64 × 10^4 segundos/dia) × (8 bytes + 8 bytes) = 1,4 × 10^10 = 14 GB/dia para o ClickMap

Vamos arredondar o ClickMap para 20 GB/dia para considerar qualquer sobrecarga e nossos ad_ids. Conforme preenchemos o QueryMap, precisamos armazenar o query_id para cada anúncio que é exibido. Nossa necessidade de armazenamento aumenta porque potencialmente existem três ad_ids que poderiam ser clicados para cada consulta de pesquisa, então precisaremos registrar o query_id em até três entradas:

3 × (5 × 10^5 consultas/seg) × (8,64 × 10^4 segundos/dia) × (8 bytes + 8 bytes) = 2 × 10^12 = 2 TB/dia para o QueryMap

2 TB é pequeno o suficiente para ser hospedado em uma única máquina usando HDDs, mas sabemos, a partir de nossa iteração em uma única máquina, que as gravações individuais pequenas são muito frequentes para serem armazenadas em um disco rígido. Embora pudéssemos calcular o impacto do uso de unidades com maior IOPS (por exemplo, SSD), nosso exercício está focado em demonstrar que o sistema pode escalar para um tamanho arbitrariamente grande. Neste caso, precisamos projetar em torno das limitações de E/S de uma única máquina. Portanto, o próximo passo na escalabilidade do design é dividir as entradas e saídas: dividir os registros de consulta recebidos e os registros de cliques em vários fluxos.

LogJoiner particionado

Nosso objetivo nesta iteração é executar várias instâncias do LogJoiner, cada uma em uma partição diferente dos dados. Para isso, precisamos pensar em vários fatores:

Gerenciamento de dados

Para unir os registros de logs de consulta e cliques, devemos associar cada registro de log de clique com seu registro de log de consulta correspondente pelo query_id. O design deve impedir que a largura de banda de rede e de disco restrinja nosso design conforme escalamos.

Confiabilidade

Sabemos que uma máquina pode falhar a qualquer momento. Quando uma máquina executando o LogJoiner falha, como garantimos que não perdemos o trabalho em andamento?

Eficiência

Podemos escalar sem desperdícios? Precisamos usar os recursos mínimos que atendam às nossas preocupações de gerenciamento de dados e confiabilidade.

Nosso design do LogJoiner mostrou que podemos unir nossos logs de consulta e logs de cliques, mas o volume resultante de dados é muito grande. Se dividirmos o trabalho em fragmentos com base no query_id, podemos executar vários LogJoiners em paralelo.

Desde que tenhamos um número razoável de instâncias do LogJoiner, se distribuirmos os logs de forma equitativa, cada instância receberá apenas um pequeno fluxo de informações pela rede. Conforme o fluxo de cliques aumenta, escalamos horizontalmente adicionando mais instâncias do LogJoiner, em vez de escalar verticalmente usando mais CPU e RAM.

Conforme mostrado na Figura 12-2, para que os LogJoiners recebam as mensagens corretas, introduzimos um componente chamado sharder de log, que direcionará cada entrada de log para o destino correto. Para cada registro, nossos sharders de log de cliques fazem o seguinte:

  1. Aplicar a função de hash no query_id do registro.
  2. Fazer o módulo do resultado com N (o número de fragmentos) e adicionar 1 para obter um número entre 1 e N.
  3. Enviar o registro para o fragmento número obtido no passo 2.

 

Figura 12-2. Como deve funcionar o particionamento?

Agora, cada LogJoiner receberá um subconjunto consistente dos logs de entrada divididos pelo query_id, em vez do log completo de cliques.

O QueryMap também precisa ser particionado. Sabemos que será necessário muitos discos rígidos para sustentar o IOPS exigido pelo QueryMap, e que o tamanho de um QueryMap de um dia (2 TB) é muito grande para nossas máquinas de 64 GB armazenarem na RAM. No entanto, em vez de particionar por query_id como o LogJoiner, vamos particionar pelo ad_id. O ad_id é conhecido antes de qualquer leitura ou gravação, então usar a mesma abordagem de hash que o LogJoiner e o painel CTR fornecerá uma visão consistente dos dados.

Para manter as implementações consistentes, podemos reutilizar o mesmo design de sharder de log para o ClickMap como para o QueryMap, já que o ClickMap é menor que o QueryMap.

Agora que sabemos que nosso sistema escalará, podemos avançar para abordar a confiabilidade do sistema. Nosso design deve ser resiliente a falhas do LogJoiner. Se um LogJoiner falhar após receber mensagens de log, mas antes de uni-las, todo o seu trabalho deve ser refeito. Isso atrasa a chegada de dados precisos ao painel, o que afetará nosso SLO.

Se nosso processo de sharder de log enviar entradas de log duplicadas para dois fragmentos, o sistema pode continuar operando em plena velocidade e processar resultados precisos mesmo quando um LogJoiner falhar (provavelmente porque a máquina na qual ele está falha).

Ao replicar o trabalho dessa maneira, reduzimos (mas não eliminamos) a chance de perdermos esses registros unidos. Dois fragmentos podem falhar ao mesmo tempo e perder os registros unidos. Distribuindo a carga de trabalho para garantir que nenhum fragmento duplicado aterrisse na mesma máquina, podemos mitigar grande parte desse risco. Se duas máquinas falharem simultaneamente e perdermos ambas as cópias do fragmento, o orçamento de erro do sistema (consulte o Capítulo 4 do primeiro livro de SRE) pode cobrir o restante do risco. Quando ocorre um desastre, podemos reproces‐sar os logs. O painel mostrará apenas dados um pouco mais antigos do que 5 minutos por um curto período de tempo.

A Figura 12-3 mostra nosso design para um fragmento e sua réplica, onde o LogJoiner, ClickMap e QueryMap são construídos em ambos os fragmentos.

A partir dos registros unidos, podemos construir um ClickMap em cada uma das máquinas do LogJoiner. Para exibir nossos painéis de usuário, todos os ClickMaps precisam ser combinados e consultados.

Avaliação

Hospedar os componentes particionados em um único datacenter cria um único ponto de falha: se qualquer par de máquinas azaradas ou o datacenter forem desconectados, perdemos todo o trabalho do ClickMap e os painéis do usuário param de funcionar completamente! Precisamos evoluir nosso design para usar mais de um datacenter.

 

Figura 12-3. Particionamento de logs com o mesmo query_id em fragmentos duplicados

Multidatacenter

Duplicar dados entre datacenters localizados em diferentes locais geográficos permite que nossa infraestrutura de serviço suporte falhas catastróficas. Se um datacenter estiver inativo (por exemplo, devido a uma interrupção de energia ou de rede que dure vários dias), podemos mudar para outro datacenter. Para que a mudança seja bem-sucedida, os dados do ClickMap devem estar disponíveis em todos os datacenters onde o sistema estiver implantado.

Será que um ClickMap desse tipo é possível? Não queremos multiplicar nossos requisitos de computação pelo número de datacenters, mas como podemos sincronizar eficientemente o trabalho entre os sites para garantir replicação suficiente sem criar duplicação desnecessária?

Acabamos de descrever um exemplo do conhecido problema de consenso em engenharia de sistemas distribuídos. Existem diversos algoritmos complexos para resolver esse problema, mas a ideia básica é:

  1. Faça três ou cinco réplicas do serviço que deseja compartilhar (como o ClickMap).
  2. Faça com que as réplicas usem um algoritmo de consenso, como o Paxos, para garantir que possamos armazenar de forma confiável o estado dos cálculos se ocorrer uma falha do tamanho de um datacenter.
  3. Implemente pelo menos um tempo de ida e volta de rede entre os nós participantes para aceitar uma operação de escrita. Esse requisito impõe um limite à taxa de transferência sequencial para o sistema. Ainda podemos paralelizar algumas das escritas para o mapa baseado em consenso distribuído.

Seguindo os passos listados anteriormente, o design de multidatacenter agora parece viável em princípio. Será que também funcionará na prática? Que tipos de recursos precisamos e quantos deles precisamos?

Cálculos

A latência da execução do algoritmo Paxos com datacenters isolados contra falhas significa que cada operação precisa de aproximadamente 25 milissegundos para ser concluída. Essa suposição de latência é baseada em datacenters separados por pelo menos algumas centenas de quilômetros. Portanto, em termos de processos sequenciais, só podemos realizar uma operação a cada 25 milissegundos ou 40 operações por segundo. Se precisarmos realizar processos sequenciais 10^4 vezes por segundo (logs de cliques), precisamos de pelo menos 250 processos por datacenter, particionados por ad_id, para as operações Paxos. Na prática, gostaríamos de adicionar mais processos para aumentar o paralelismo – para lidar com o backlog acumulado após qualquer tempo de inatividade ou picos de tráfego.

Com base em nossos cálculos anteriores para ClickMap e QueryMap, e usando a estimativa de 40 operações sequenciais por segundo, quantas novas máquinas precisamos para nosso design de multidatacenter?

Como nosso design de LogJoiner particionado introduz uma réplica para cada registro de log, dobramos o número de transações por segundo para criar o ClickMap e QueryMap: 20.000 cliques/segundo e 1.000.000 de consultas/segundo.

Podemos calcular o número mínimo de processos ou tarefas necessárias dividindo o total de consultas por segundo pelo nosso máximo de operações por segundo:

(1,02 × 10^6 consultas/segundo) / (40 operações/segundo) = 25.500 tarefas

A quantidade de memória para cada tarefa (duas cópias de 2 TB QueryMap):

(4 × 10^12 bytes) / (25.500 tarefas) = 157 MB/tarefa

Tarefas por máquina:

(6,4 × 10^10 bytes) / (1,57 × 10^8 bytes) = 408 tarefas/máquina

Sabemos que podemos acomodar muitas tarefas em uma única máquina, mas precisamos garantir que não seremos limitados por E/S. O throughput de rede total para ClickMap e QueryMap (usando uma estimativa alta de 2 KB por entrada):

(1,02 × 10^6 consultas/segundo) × (2 × 10^3 bytes) = 2,04 GB/seg = 16 Gbps

Throughput por tarefa:

16 Gbps / 25.500 tarefas = 80 KB/seg = 640 Kbps/tarefa

Throughput por máquina:

408 tarefas × 640 Kbps/tarefa = 256 Mbps

Nossa combinação de 157 MB de memória e 640 Kbps por tarefa é gerenciável. Precisamos de aproximadamente 4 TB de RAM em cada datacenter para hospedar o ClickMap e o QueryMap shardados. Se tivermos 64 GB de RAM por máquina, podemos servir os dados de apenas 64 máquinas, e usaremos apenas 25% da largura de banda de rede de cada máquina.

Avaliação

Agora que projetamos um sistema de múltiplos datacenters, vamos revisar se o fluxo de dados faz sentido.

A Figura 12-4 mostra todo o projeto do sistema. Você pode ver como cada pesquisa e clique em anúncios são comunicados aos servidores e como os logs são coletados e inseridos em cada componente.

Podemos verificar este sistema em relação aos nossos requisitos:

10.000 cliques em anúncios por segundo

O LogJoiner pode escalar horizontalmente para processar todos os cliques em logs e armazenar o resultado no ClickMap.

500.000 consultas de pesquisa por segundo

O QueryStore e o QueryMap foram projetados para lidar com o armazenamento de um dia inteiro de dados nessa taxa.

99,9% das consultas do painel são concluídas em < 1 segundo

O painel de CTR recupera dados do QueryMap e do ClickMap, que são indexados por ad_id, tornando essa transação rápida e simples.

99,9% do tempo, os dados de CTR exibidos têm menos de 5 minutos de idade

Cada componente é projetado para escalar horizontalmente, o que significa que se o pipeline estiver muito lento, adicionar mais máquinas diminuirá a latência do pipeline de ponta a ponta.

Acreditamos que essa arquitetura de sistema escala para atender aos nossos requisitos de throughput, desempenho e confiabilidade.

 

Figura 12-4. Design multidatacenter

Conclusão

O NALSD descreve o processo iterativo de design de sistemas que o Google utiliza para sistemas de produção. Ao decompor o software em componentes lógicos e colocar esses componentes em um ecossistema de produção com infraestrutura confiável, chegamos a sistemas que fornecem metas razoáveis e apropriadas para consistência de dados, disponibilidade do sistema e eficiência de recursos. A prática do NALSD nos permite melhorar nosso design sem começar do zero a cada iteração. Embora várias iterações de design apresentadas neste capítulo tenham atendido à nossa declaração de problema original, cada iteração revelou novos requisitos, que pudemos atender estendendo nosso trabalho anterior.

Ao longo desse processo, separamos os componentes de software com base em como esperávamos que o sistema crescesse. Essa estratégia nos permitiu escalar partes diferentes do sistema de forma independente e eliminar dependências de peças únicas de hardware ou instâncias únicas de software, produzindo assim um sistema mais confiável.

Durante o processo de design, continuamos a melhorar cada iteração fazendo as quatro perguntas-chave do NALSD:

É possível?

Podemos construí-lo sem “mágica”?

Podemos fazer melhor?

É tão simples quanto podemos razoavelmente torná-lo?

É viável?

Se encaixa dentro de nossas restrições práticas (orçamento, tempo, etc.)?

É resiliente?

Sobreviverá a interrupções ocasionais, mas inevitáveis?

O NALSD é uma habilidade aprendida. Como qualquer habilidade, você precisa praticá-la regularmente para manter sua proficiência. A experiência do Google mostrou que a capacidade de raciocinar a partir de um requisito abstrato até uma aproximação concreta de recursos é fundamental para construir sistemas saudáveis e duradouros.

Experimente agora, grátis!