Instagram: Ampliação da infraestrutura

instagramEm 2013, um ano após o Instagram se juntar ao Facebook, 200 milhões de pessoas usavam o Instagram todo mês e foram armazenadas 20 bilhões de fotos. Sem um abrandamento à vista, o Instagram começou a mudança, dos seus servidores na Amazon AWS para a infraestrutura do Facebook. Dois anos depois, o Instagram cresceu e virou uma comunidade com mais de 400 milhões de acessos por mês e 40 bilhões de fotos e vídeos, atendendo a um milhão de solicitações por segundo. Para continuar suportando esse crescimento e ter a certeza de que a comunidade do Instagram esteja tendo a melhor experiência possível com o aplicativo, eles decidiram dimensionar sua infraestrutura geograficamente. Neste post você saberá o porque deles ampliarem sua infraestrutura de um para três datacenters e quais os desafios técnicos eles encontraram ao longo do caminho.

A Motivação do Instagram

Mike Krieger, co-fundador do Instagram e CTO, recentemente escreveu um post em seu site que incluía uma história sobre o clima em 2012, quando uma enorme tempestade na Virgínia derrubou quase metade do seu datacenter. O time deles era pequeno, levaram 36 horas para reconstruir a infraestrutura perdida na tempestade. Desastres naturais como este tem um potencial para fazer estragos temporários ou permanentes nos datacenters; e eles querem ter a certeza de que irão conseguir passar por esses danos com o mínimo de impacto possível para os seus usuários.

Outras motivações para eles quererem aumentar sua infraestrutura incluem:

  • Resiliência em questões regionais: Mais comum que desastres naturais são as desconexões, problemas de energia, etc. Por exemplo, assim que eles expandiram os servidores para o Oregon, um dos racks contendo memcache e servidor async foi desligado, o que causou grandes falhas para as solicitações de usuários. Com a nova infraestrutura, eles são capazes de desviar o tráfego da região para contornar o erro até a energia ser recuperada.
  • Flexibilidade com a capacidade de expansão: O Facebook tem vários datacenters. É muito mais fácil expandir a capacidade do Instagram quando ele está disponível e quando sua infraestrutura está pronta para expandir além de uma região, mesmo quando há uma considerável latência na rede. Isso ajudou a tomar decisões rápidas sobre como obter novos recursos para os usuários sem ter que lutar por recursos de infraestrutura para apoiá-los.

De um para dois

Então, como eles conseguiram iniciar essa ampliação? Primeiro veremos como funciona a infraestrutura do Instagram.

Instagram: Ampliação da infraestrutura

A chave para a expansão em múltiplos datacenters é distinguindo os dados globais e os dados locais. Os dados globais precisam ser replicados em todos os centros de dados, e os dados locais podem ser diferentes para cada região (por exemplo, os async jobs criados por um servidor web apenas podem ser vistos naquela região).

A próxima consideração são os recursos de hardwares. Eles podem ser divididos em três tipos: armazenamento, processamento e cache.

Armazenamento
O Instagram utiliza principalmente dois sistemas de banco de dados back-end: PostgreSQL e Cassandra. Os dois tem estruturas de replicação consistentes que funcionam bem como armazenamento de dados global.

Os dados globais mapeiam perfeitamente aos dados armazenados nesses servidores. O objetivo é ter uma eventual consistência desses dados através do datacenter, mas com um atraso em potencial. Como a maioria das operações são de leitura; ler a réplica de cada região evita o cruzamento dos diferentes datacenter com os servidores web.

Escrever para o PostgreSQL, ainda é feito através dos diferentes datacenters pois eles sempre vão escrever para o servidor primário.

Processamento
Servidores web e servidores async são dois recursos facilmente distribuídos e que, só precisam ser acessados localmente. Os servidores Web podem criar async jobs em fila por intermédio de mensagens async que serão em seguida consumidos por servidores async. Todos na mesma região.

Cache

A camada de cache é a camada mais acessada do servidor web, e eles precisam ser colocados dentro de um datacenter local para evitar latência de conexão até os usuários. Isso significa que as atualizações para a armazenagem em cache no datacenter não são refletidas no outro datacenter, portanto, criando um desafio para mudar-se para vários datacenters.

Imagine que um usuário comentou uma foto recém-lançada. No caso de um datacenter, o servidor web pode simplesmente atualizar o cache com o novo comentário. Um seguidor vai ver o novo comentário do mesmo cache.

No cenário de vários centros de dados, no entanto, se o comentário e o seguidor são servidos em diferentes regiões, o cache da região do seguidor não vai ser atualizado e o usuário não verá o comentário.

A solução foi usar o PGQ e melhorá-lo para inserir eventos de invalidação de cache para os bancos de dados que estão sendo modificados.

No lado primário:
  • O servidor web insere o comentário para o PostgreSQL DB;
  • O servidor web insere uma entrada de invalidação do cache no mesmo DB.
No lado da réplica:
  • Replicar o DB primário, incluindo tanto o comentário inserido recentemente, quanto a entrada de invalidação do cache.
  • O processo de invalidação do cache lê a entrada de invalidação de cache e invalida os caches regionais.
  • O Django vai ler o DB com o comentário inserido recentemente e vai recarregar o cache.

Isto resolve o problema de coerência do cache. Por outro lado, em comparação com o caso de uma região onde os servidores Django atualizam diretamente o cache sem re-leitura do DB, isso criaria um aumento da carga de leitura dos bancos de dados. Para acabar com esse problema, eles pegaram duas abordagens; 1) reduzir os recursos computacionais necessários para cada leitura para desnormalizar os contadores; 2) reduzir o número de leituras usando locações de cache. >

Desnormalizando Contadores

As peças mais comuns do cache são os contadores. Por exemplo, eles usam um contador para determinar o número de Likes num post específico do Justin Bieber. Quando há apenas uma região, eles atualizam os contadores do Memcache incrementando dos servidores web. Evitando assim um “select count(*)” chamando para o banco de dados, o que levaria centenas de milissegundos.

Mas com duas regiões e a invalidação PgQ, a cada Like novo cria-se um evento de invalidação de cache para o contador. Isso irá criar um monte de “select count(*)”, especialmente em hot objects.

Para reduzir os recursos necessários para cada uma dessas operações, eles desnormalizaram o contador de Likes nos posts. Sempre que um novo Like é realizado, a contagem é aumentada no banco de dados. Portando, cada leitura da contagem será apenas um simples “select” o que é muito mais eficiente.

Existe também um benefício adicional em desnormalizar contadores no mesmo banco de dados, onde o Like é armazenado. Ambas as atualizações podem ser incluídas em uma transação, fazendo com que as atualizações sejam rápidas e consistentes o tempo todo. Considerando que, antes de mudança, o contador no cache pode ser inconsistente, como o que foi armazenado no bando de dados, devido ao tempo de espera, novas tentativas, etc.

Memcache Lease

No exemplo acima de um novo post do Justin Bieber, durante os primeiros minutos do post, tanto a visualização do novo post quanto os Likes tem um pico. Cada novo Like, o contador é excluído do cache. É comum os servidores web tentarem recuperar o mesmo contador do cache, mas ele terá um “cache miss”. Se todos eles forem para o servidor de banco de dados para recuperação, criaria um problema Thundering Herd.

Logo, eles usaram o mecanismo de memcache lease para resolver este problema. Ele funciona assim:
  • O servidor web emite um “lease get”, e não o normal “get” do servidor memcache.
  • Servidor memcache retorna ao valor se ele for um sucesso. Neste caso, não é diferente de um normal “get”.
  • Se o servidor memcache não encontrar a chave, ele retorna a “first miss” para apenas um servidor de web dentro de “n” segundos; qualquer outra solicitação “lease get” durante esse tempo terá um “hot miss”. No caso de um “hot miss” onde a chave foi deletada do cache recentemente, irá voltar ao valor obsoleto. Se a chave do cache não surgir em “n” segundos, novamente terá um “first miss” para um pedido de “lease get”.
  • Quando um servidor web recebe um “first miss”, ele vai para o banco de dados para recuperar dados e preencher o cache.
  • Quando um servidor web recebe um “hot miss” com um valor obsoleto, ele pode normalmente utilizar esse valor. Se ele recebe um “hot miss” sem qualquer valor, ele pode optar por esperar o cache ser preenchido pelo servidor web no “first miss”.

Em resumo, com ambas as implementações acima, pode-se reduzir o aumento da carga do banco de dados, reduzindo o número de acessos à base de dados, assim como os recursos necessários para cada acesso.

Ele também melhorou a confiabilidade do backend nos casos em que alguns hot counters caiam para fora do cache; o que não era uma ocorrência pouco frequente nos primeiros dias do Instagram. Cada uma destas ocorrências causaria algum trabalho apressado de um engenheiro para corrigir manualmente o cache. Com essas mudanças, esses incidentes se tornaram história para os engenheiros veteranos.

Latência 10ms para 60ms

Até agora, eles tem se concentrado principalmente na consistência do cache quando os caches se tornaram regionais. A latência na rede entre centros de dados em todo o continente foi outro desafio que impactou vários modelos. Entre os centros de dados, uma latência de 60ms de rede pode causar problemas na replicação de dados, bem como as atualizações de servidores web para o banco de dados. Eles precisaram resolver esses problemas afim de apoiar uma expansão contínua:

Leitor de réplicas PostgreSQL não atualiza
Primeiramente o Postgre faz gravações e depois gera os delta logs. Quanto mais rápida as gravações entrarem, mais frequentemente esses logs serão gerados. Os logs mais recentes são arquivados para necessidades de réplica. É arquivado todos os registros de armazenamento para ter certeza de que eles estão salvos e acessíveis para quaisquer réplicas que precisem de dados mais antigos do que os primários. Desta forma, o primário não fica sem espaço no disco.

Quando é feito a leitura da réplica ela começa a ler o snapshot do banco de dados desde o primário. Uma vez feito isso, ele precisa aplicar os logs que entraram desde o snapshot do banco de dados. Quando todos os logs são aplicados, ele vai estar atualizado e pode transmitir a partir do primário e servir leituras para servidores web.

No entanto, quando a taxa de gravação de um banco de dados grande é bastante elevada, e há uma grande quantidade de latência de rede entre a réplica e o dispositivo de armazenamento, é possível que a velocidade que os logs são lidos seja mais lenta que a velocidade de criação dos logs. A réplica irá cair ainda mais e mais e nunca atualizar.

Eles começaram a corrigir esse problema iniciando um segundo fluxo no novo leitor de réplica logo quando começa a transferência de base snapshot a partir do primário. Isso pega o fluxo de logs e salva eles no disco local. Quando o snapshot acaba de transferir, o leitor vai conseguir ler os logs, deixando esse processo mais rápido

Isso solucionou os problemas de replicação de banco de dados nos EUA, reduzindo o tempo que levaria para construir uma nova réplica. Agora, mesmo que o primário e a réplica estejam na mesma região, a eficiência operacional é aumentada drasticamente.

Resumo
O Instagram opera em vários datacenters pelos EUA, dando para eles capacidade de fazer um planejamento mais flexível; maior confiabilidade e melhor preparação para os desastres naturais. O Facebook testa regularmente seus datacenters, desligando eles em horário de pico. A um mês, quando migraram os dados para o novo datacenter, o Facebook fez um teste e o desligou. Foi uma simulação de alto risco, mas felizmente, eles sobreviveram a perda de capacidade sem que os usuários percebam. A Instagration Parte 2 foi um sucesso.

Lisa Guo, em tradução livre: http://engineering.instagram.com/posts/548723638608102/