Como fazer seu código escalar

Cenário

Esta postagem se origina em meu blog pessoal, em http://www.mullie.eu/why-your-code-doesnt-scale/

Construir software escalonável significa que você está preparado para acomodar o crescimento. Existem basicamente 2 coisas que você precisa considerar à medida que seus dados aumentam:

  • As solicitações serão atendidas em um ritmo mais rápido do que chegam?
  • Meu hardware será capaz de armazenar todos os dados?

Obviamente, você precisará de mais infraestrutura à medida que crescer. Você precisará de mais máquinas. Você provavelmente também precisará / desejará introduzir aplicativos adicionais para ajudar a aliviar a carga, como servidores de cache, balanceadores de carga, …

Introdução

Imagine um vilarejo autossuficiente de 1.000 habitantes que chega a um milhão de habitantes. Embora a rede de fonte de alimentação inicial fosse de última geração, ela não era adequada para esta magnitude. Para atender a mais residentes, sua usina terá que produzir mais energia.

Horizontal vertical

A escala vertical significa que você aumenta sua capacidade geral, aumentando a capacidade de suas máquinas. Ex: se estiver ficando sem espaço em disco, você pode adicionar mais discos rígidos ao servidor de banco de dados.

A escala horizontal significa adicionar mais máquinas à sua configuração.

Em nossa analogia com a vila, escalar verticalmente seria adicionar um reator nuclear à sua usina, para aumentar a quantidade de energia que pode ser gerada. Escalar horizontalmente significaria construir uma segunda usina de energia. A escala vertical é a mais fácil: o restante da infraestrutura existente ainda funciona. A escala horizontal significa que você precisará de circuitos de energia adicionais de e para a nova planta, novo software para controlar o fluxo de energia entre as duas plantas, …

Em hardware de computador, escalar horizontalmente (um cluster de máquinas de camada inferior) é geralmente mais barato em comparação com escalar verticalmente (um supercomputador). Ele também fornece failover: se uma máquina morre, as outras podem assumir. No entanto, o dimensionamento horizontal costuma ser mais difícil do ponto de vista do software, porque os dados agora são distribuídos em várias máquinas.

Leitura / gravação pesada

Aplicativos de leitura pesada geralmente são mais fáceis de escalar. Ter leitura pesada significa que há muito mais solicitações que precisam apenas buscar (e produzir) dados, em comparação com aquelas que armazenam dados.

Aplicativos de leitura pesada são principalmente sobre a capacidade de atender a solicitações. O que você precisa aqui é de máquinas suficientes para lidar com a carga: servidores de aplicativos suficientes para fazer a computação e / ou escravos de banco de dados suficientes para ler (mais sobre ambos mais tarde).

Aplicativos de gravação pesada precisarão de um planejamento ainda mais cuidadoso. Eles provavelmente não apenas terão problemas de leitura, mas você também precisará armazenar os dados em algum lugar. Se houver muito e crescendo continuamente, pode ultrapassar o tamanho da sua máquina.

Servidores de aplicativos e armazenamento

Os servidores de aplicativos são aqueles que hospedam seu código PHP, Python, …. Isso não é tão difícil de escalar: o código normalmente não muda com base na entrada do usuário (esse material está no banco de dados). Se você executar seu código na máquina A ou na máquina B, ele fará exatamente a mesma coisa.

Escalar seu código é tão fácil quanto adicioná-lo a mais máquinas. Coloque seu código em 10 máquinas com um balanceador de carga na frente deles para distribuir uniformemente as solicitações para todas as 10 máquinas, e agora você pode lidar com 10x mais tráfego. O importante em seu aplicativo é que ele seja o mais eficaz possível e, na verdade, seja capaz de lidar com muitas solicitações.

Os problemas de armazenamento são completamente diferentes. Bem, os problemas de leitura são comparáveis ​​(ter cópias suficientes em máquinas suficientes) – os problemas de gravação requerem um pensamento duro sobre como, exatamente, você armazenará seus dados.

Inscrição

CPU

Um aplicativo consiste em muitas “instruções” (= seu código). Se uma solicitação chegar, seu código começará a fazer um monte de coisas (= processamento) para finalmente responder com a saída apropriada.

A quantidade de solicitações às quais seu servidor é capaz de responder é limitada pela capacidade do hardware, então você deseja manter “o que a máquina tem que fazer para atender a solicitação” o mais simples possível, mesmo se a máquina for capaz de fazer muito. Dessa forma, ele pode lidar com mais solicitações.

O problema com aplicativos que usam muito a CPU é que, se algo levar um tempo cada vez maior para ser computado, o tempo de resposta será atrasado, a quantidade de solicitações que você pode manipular diminuirá e, eventualmente, você não poderá atender a todas as solicitações.

Se o seu trabalho intensivo de CPU não for crítico para a resposta (você não precisa exibir imediatamente o resultado dele), você deve considerar o adiamento do trabalho para uma fila de tarefas, para ser agendado para execução posterior.

Memória

O problema com os aplicativos que usam muita memória é duplo:

  • Seu processador pode ficar ocioso até que possa acessar a memória ocupada, atrasando assim o seu tempo de resposta (mesmo problema que com aplicativos que usam muita CPU)
  • Você pode estar tentando encaixar mais na memória do que é possível e a solicitação falhará.

Sempre tente limitar suas incógnitas: se você não tem ideia de quanto uma solicitação pode custar (= de quantos recursos ela precisa), provavelmente não está fazendo um bom trabalho ao dimensioná-la.

Normalmente, você pode limitar o uso de memória processando dados em lotes menores de um tamanho conhecido. Se você estiver carregando milhares de linhas do banco de dados para processar, divida-as em lotes menores de, digamos, 100 linhas, processe-as e depois faça o próximo lote. Isso garante que você nunca esgote sua memória, uma vez que a quantidade de linhas fica muito grande para caber nela: nunca terá que conter mais de 100 linhas de uma vez!

Exemplo

Digamos que queremos recuperar uma linha aleatória do banco de dados:

MAU

SELECT *
FROM tablename

ORDER BY RAND
()
LIMIT
1;

Esta é principalmente uma tarefa que exige muito da CPU: o MySQL irá iterar todas as linhas e calcular um número aleatório por linha. Conforme a quantidade de linhas aumenta, essa operação leva mais tempo para ser concluída. Como a máquina leva mais tempo para responder, ela processará as solicitações em um ritmo mais lento.

Na verdade, à medida que aumenta ainda mais, o MySQL também não será capaz de manter os números aleatórios na memória e os salvará em uma tabela temporária em seu disco rígido. O acesso será muito mais lento do que os dados na memória, de modo que, em certo ponto, começará a afetar drasticamente o tempo de resposta.

PIOR

Pior ainda seria buscar todas as linhas do banco de dados, passando-as para o seu aplicativo e chamando array_randou algo semelhante nele. Isso não escalaria por causa da memória: uma vez que o tamanho da sua mesa exceda a memória disponível, você travará.

MELHOR

SELECT @rand := ROUND((RAND() * MAX(id)))
FROM tablename
;

SELECT
*
FROM tablename

WHERE id
>= @rand
LIMIT
1;

Independentemente da quantidade de linhas (5 ou 5 bilhões), sempre obteremos a id máxima do banco de dados, geraremos 1 número aleatório menor ou igual a isso e obteremos 1 registro (que o MySQL buscará facilmente através do índice PK ) Isso escala! Não importa quantas linhas crescemos, esse recurso nunca será mais difícil de calcular.

Observe como eu faço> = em vez de =, e tenho um LIMITE 1: isso é apenas para ignorar possíveis lacunas nos ids. Poderíamos gerar um id aleatório “32434” que não existe mais no banco de dados – essa consulta também será definida como 32435 nesse caso.

Armazenamento

Embora imagens e vídeos geralmente consumam muito espaço de armazenamento, o maior problema de armazenamento geralmente é o banco de dados. Contanto que você saiba em qual máquina você salva suas imagens / vídeos, você pode recuperá-los facilmente.

A maioria dos principais sites lida com tantos dados que dificilmente cabem em um banco de dados. Mas distribuir os dados por vários servidores é mais fácil de falar do que fazer, pois os dados geralmente estão vinculados uns aos outros.

À medida que seu banco de dados cresce, você pode considerar mover algumas tabelas problemáticas para seu próprio servidor dedicado. No entanto, os dados em várias tabelas geralmente precisam ser unidos uns contra os outros, o que é totalmente impossível se estiver espalhado entre várias máquinas (físicas ou virtuais).

Sharding

Em vez de distribuir tabelas específicas por várias máquinas (o que causa problemas de JOIN), geralmente é melhor encontrar uma coluna comum compartilhada, pela qual você decide como distribuir seus dados.

Você pode ter vários servidores de banco de dados, cada um deles com o esquema de banco de dados completo. Mas cada servidor manterá dados diferentes.

Um exemplo simples seria um provedor de plataforma de blog em que você se inscreve em um blog hospedado em seu (s) servidor (es). Atender milhares de blogs em uma máquina pode não ser viável, mas eles podem distribuir facilmente os blogs em várias máquinas. Contanto que todos os dados do meu blog estejam na mesma máquina, as coisas devem ser muito fáceis. Seu blog poderia perfeitamente estar em outra máquina, seus dados nunca terão que ser unidos aos meus.

Um exemplo mais complexo poderia ser uma rede social. Muitos usuários, muitos dados por usuário – muitos para qualquer máquina. Todos os dados da minha conta de usuário (minhas configurações, minhas imagens, minhas mensagens, minhas atualizações de status, …) podem estar em uma máquina, enquanto os seus podem estar armazenados em uma máquina totalmente diferente. Nesse caso, os dados são compartilhados em diferentes servidores com base no ID do usuário.

No entanto, teremos que ser mais cuidadosos sobre como distribuir os dados. Ao visualizar as atualizações de status de uma única pessoa, estamos solicitando dados de apenas uma máquina. No entanto, quando queremos ver um fluxo de atualizações de status de todos os nossos amigos, isso pode estar localizado em 20 máquinas diferentes. Não é uma coisa que você gostaria de fazer.

Não vamos nos precipitar aqui: fragmentar seu banco de dados pode ser incrivelmente complexo e muito complicado de implementar e manter. A lei de Moore pode ter máquinas crescendo a uma taxa mais rápida do que suas necessidades jamais terão, então você provavelmente nem terá que se preocupar com isso .

Para mais detalhes técnicos sobre fragmentação: Jurriaan Persyn escreveu um excelente artigo sobre como fragmentaram o banco de dados da Netlog.

Replicação de banco de dados

A replicação de banco de dados trata da configuração de um banco de dados mestre com vários escravos. Os dados são sempre gravados no banco de dados mestre, que por sua vez os replica para todos os bancos de dados escravos. Todos os dados podem então ser lidos dos bancos de dados escravos.

Se você tiver uma quantidade enorme de leituras, precisará apenas adicionar mais escravos. Seu banco de dados mestre então só precisa lidar com as gravações, das quais, esperançosamente, são relativamente poucas.

A fragmentação provavelmente será a solução para aplicativos com uso intenso de gravação e replicação para problemas de leitura intensa.

Cache

A bala mágica! Bem, não realmente, mas pode ser extremamente útil. Cache é o armazenamento do resultado de uma operação cara, na qual se sabe que o resultado será o mesmo da próxima vez. Como armazenar o resultado de uma chamada para uma API externa em seu banco de dados. Ou armazenar o resultado de alguma consulta ou cálculo caro no memcached.

Se você tem um CNS e armazena todas as páginas no banco de dados, não faz sentido obter essa navegação de seu banco de dados em cada solicitação de página. Ele não mudará apenas a qualquer momento, então você pode simplesmente armazenar uma cópia estática dele no cache. Assim que uma nova página for adicionada à navegação, podemos simplesmente limpar / invalidar esse cache para que, da próxima vez, busquemos a navegação atualizada do armazenamento. Que pode então ser armazenado em cache …

O cache pode vir de várias formas: um servidor memcached ou redis, cache de disco, cache temporário na memória … Tudo o que importa é que ler o resultado dele é mais rápido do que executá-lo novamente.

Não que a introdução do cache aumente a complexidade da sua base de código. Agora você não só precisa ler e gravar dados de e para vários lugares, como também ter certeza de que eles estão sincronizados e que os dados em seu cache sejam atualizados ou invalidados quando os dados em seu armazenamento forem alterados.

Rede

As solicitações de rede geralmente não são realmente consideradas, mas podem tornar seu aplicativo drasticamente lento, desde que você não as limite com cuidado. Em um ambiente de produção, seu servidor de banco de dados, servidor de cache e outros enfeites muito provavelmente estarão em outro recurso de rede. Se quiser ler de seu banco de dados, você se conectará a ele. O mesmo para o seu servidor de cache.

Embora provavelmente muito pouco, a conexão com outros servidores leva tempo. Tente fazer o mínimo possível de solicitações de banco de dados e cache. Isso é especialmente importante para solicitações de cache, pois muitas vezes são consideradas muito baratas. Seu servidor de cache responderá muito rapidamente, mas se você solicitar centenas de valores em cache, o tempo gasto para se conectar ao servidor de cache será acumulado.

Se possível, você pode tentar solicitar seus dados em lote. Isso significa buscar várias chaves de cache de uma vez. Ou buscar todas as colunas de uma tabela de uma vez se você sabe que vai consultar uma coluna específica na função A e outra na função B.