Todas as boas startups morrem rapidamente ou crescem em escala. Modelaremos essa inicialização, que é primeiro sobre recursos e depois sobre desempenho. Melhoraremos o desempenho com o MongoDB, uma solução popular de armazenamento de dados NoSQL. O MongoDB é fácil de começar e muitos problemas têm soluções prontas para uso. No entanto, quando a carga aumenta, sai um ancinho que ninguém lhe avisou antes ... até hoje!

A modelagem é realizada por
Sergey Zagursky , responsável pela infraestrutura de back-end em geral, e pelo MongoDB em particular, no
Joom . Também foi visto no lado do servidor do desenvolvimento do MMORPG Skyforge. Como Sergei se descreve, ele é "um profissional que toma casquinhas de cone com a própria testa e ancinho". Sob um microscópio, um projeto que utiliza uma estratégia de acumulação para gerenciar dívidas técnicas. Nesta versão em texto do
relatório no HighLoad ++, passaremos em ordem cronológica da ocorrência do problema para a solução usando o MongoDB.
Primeiras dificuldades
Estamos modelando uma startup que enche solavancos. A primeira fase da vida - os recursos são lançados em nossa inicialização e, inesperadamente, os usuários chegam. Nosso servidor MongoDB pequeno e pequeno tem uma carga que nunca sonhamos. Mas estamos na nuvem, somos uma startup! Fazemos as coisas mais simples possíveis: observe as solicitações - ah, e aqui temos toda a correção subtraída para cada usuário, aqui criaremos os índices, adicionaremos o hardware lá e armazenaremos em cache.
Tudo - nós vivemos!
Se os problemas puderem ser resolvidos por meios tão simples, eles devem ser resolvidos dessa maneira.
Mas o caminho futuro de uma inicialização bem-sucedida é um atraso lento e doloroso do momento da escala horizontal. Vou tentar dar conselhos sobre como sobreviver a esse período, chegar à escala e não pisar no ancinho.
Gravação lenta
Este é um dos problemas que você pode encontrar. O que fazer se você a conhecer e os métodos acima não ajudarem? Resposta:
modo de garantia de durabilidade no MongoDB por padrão . Em três palavras, funciona assim:
- Chegamos à linha principal e dissemos: "Escreva!".
- Réplica primária registrada.
- Depois disso, as réplicas secundárias foram lidas e elas disseram primárias: "Nós gravamos!"
No momento em que a maioria das réplicas secundárias fez isso, a solicitação é considerada completa e o controle retorna ao driver no aplicativo. Essas garantias nos permitem ter certeza de que, quando o controle retornar ao aplicativo, a durabilidade não será levada a lugar algum, mesmo se o MongoDB estiver parado, exceto por desastres absolutamente terríveis.
Felizmente, o MongoDB é um banco de dados que permite reduzir as garantias de durabilidade para cada solicitação individual.
Para solicitações importantes, podemos deixar as garantias de durabilidade máxima por padrão e, para algumas solicitações, podemos reduzi-las.
Solicitar Classes
A primeira camada de garantias que podemos remover é
não esperar pela confirmação do registro pela maioria das réplicas . Isso economiza latência, mas não adiciona largura de banda. Mas às vezes é a latência que você precisa, especialmente se o cluster estiver um pouco sobrecarregado e as réplicas secundárias não funcionarem tão rápido quanto gostaríamos.
{w:1, j:true}
Se gravarmos registros com essas garantias, no momento em que tivermos o controle do aplicativo, não saberemos mais se o registro estará vivo após algum tipo de acidente. Mas, geralmente, ela ainda está viva.
A próxima garantia, que afeta também a largura de banda e a latência, está
desativando a confirmação do log . Uma entrada no diário é escrita de qualquer maneira. A revista é um dos mecanismos fundamentais. Se desativarmos a confirmação da gravação, não fazemos duas coisas:
fsync no log e
não esperamos que ele termine . Isso pode
economizar muitos recursos de disco e obter um
aumento múltiplo na taxa de transferência, simplesmente alterando a durabilidade da garantia.
{w:1, j:false}
As garantias de durabilidade mais rigorosas estão
desativando todos os reconhecimentos . Apenas receberemos confirmação de que a solicitação atingiu a réplica primária. Isso economizará latência e não aumentará a taxa de transferência de forma alguma.
{w:0, j:false} — .
Também receberemos várias outras coisas, por exemplo, a gravação falhou devido a um conflito com uma chave exclusiva.
A que operações isso se aplica?
Vou falar sobre o aplicativo para a instalação no Joom. Além da carga dos usuários, na qual não há concessões de durabilidade, existe uma carga que pode ser descrita como carga em lote em segundo plano: atualização, recontagem de classificações, coleta de dados analíticos.
Essas operações em segundo plano podem levar horas, mas são projetadas para que, se uma interrupção, por exemplo, um back-end travar, elas não perderão o resultado de todo o seu trabalho, mas serão retomadas a partir do ponto no passado recente. Reduzir a garantia de durabilidade é útil para essas tarefas, especialmente porque o fsync no log, como qualquer outra operação, aumentará a latência também para leitura.
Ler escala
O próximo problema é a
largura de banda de leitura insuficiente . Lembre-se de que em nosso cluster não existem apenas réplicas primárias, mas também secundárias,
das quais você pode ler . Vamos fazer isso.
Você pode ler, mas há nuances. Dados ligeiramente desatualizados virão de réplicas secundárias - em 0,5 a 1 segundos. Na maioria dos casos, isso é normal, mas o comportamento da réplica secundária é diferente do comportamento das réplicas primárias.
No secundário, há o processo de usar o oplog, que não está na réplica primária. Esse processo não foi projetado para baixa latência - apenas os desenvolvedores do MongoDB não se preocuparam com isso. Sob certas condições, o processo de uso do oplog do primário para o secundário pode causar atrasos de até 10 s.
Réplicas secundárias não são adequadas para consultas do usuário - as experiências do usuário dão um passo rápido na lixeira.
Em clusters não sombreados, esses picos são menos visíveis, mas ainda existem. Os clusters de fragmentos sofrem porque o oplog é particularmente afetado pela exclusão, e a
exclusão faz parte do trabalho do balanceador . O balanceador de maneira confiável exclui documentos com bom gosto por dezenas de milhares em um curto período de tempo.
Número de conexões
O próximo fator a considerar é o
limite no número de conexões nas instâncias do MongoDB . Por padrão, não há restrições,
exceto os recursos do SO - você pode se conectar enquanto isso permitir.
No entanto, quanto mais solicitações simultâneas, mais lentas elas são executadas.
O desempenho diminui de maneira não linear . Portanto, se um pico de solicitações chegar até nós, é melhor atender 80% do que não atender 100%. O número de conexões deve ser limitado diretamente ao MongoDB.
Mas existem bugs que podem causar problemas por causa disso. Em particular, o
pool de conexões no lado do MongoDB é comum para conexões intracluster de usuário e serviço . Se o aplicativo "comeu" todas as conexões desse pool, a integridade pode ser violada no cluster.
Aprendemos sobre isso quando estávamos reconstruindo o índice e, como precisávamos remover a exclusividade do índice, o procedimento passou por vários estágios. No MongoDB, você não pode criar ao lado do índice o mesmo, mas sem exclusividade. Portanto, queríamos:
- Crie um índice semelhante sem exclusividade
- remova o índice com exclusividade;
- Crie um índice sem exclusividade em vez de remoto;
- excluir temporariamente.
Quando o índice temporário ainda estava sendo concluído no secundário, começamos a excluir o índice exclusivo. Nesse ponto, o MongoDB secundário anunciou seu bloqueio. Alguns metadados foram bloqueados e, na maioria, todos os registros pararam: eles ficaram no
pool de conexão e esperaram que eles confirmassem que o registro havia passado. Todas as leituras no secundário também pararam porque o log global foi capturado.
O cluster em um estado tão interessante também perdeu sua conectividade. Às vezes, apareceu e quando dois comentários se conectaram, eles tentaram fazer uma escolha em seu estado que não podiam fazer, porque eles têm uma trava global.
Moral da história: o número de conexões deve ser monitorado.
Existe um conhecido rake MongoDB, que ainda é tão frequentemente atacado que eu decidi dar um breve passeio nele.
Não perca documentos
Se você enviar uma solicitação por índice ao MongoDB, a
solicitação poderá não retornar todos os documentos que satisfaçam a condição e em casos completamente inesperados. Isso se deve ao fato de que, quando vamos para o início do índice, o documento, que no final, passa para o início dos documentos que passamos. Isso se deve apenas
à mutabilidade do índice . Para uma iteração confiável, use
índices em campos não estáveis e não haverá dificuldades.
O MongoDB possui suas próprias visualizações sobre quais índices usar. A solução é simples -
com a ajuda de $ hint, forçamos o MongoDB a usar o índice especificado .
Tamanhos da coleção
Nossa startup está em desenvolvimento, há muitos dados, mas não quero adicionar discos - já adicionamos três vezes no mês passado. Vamos ver o que está armazenado em nossos dados, ver o tamanho dos documentos. Como entender onde na coleção você pode reduzir o tamanho? De acordo com dois parâmetros.
- O tamanho de documentos específicos para brincar com seu comprimento:
Object.bsonsize()
;
- De acordo com o tamanho médio do documento na coleção :
db.c.stats().avgObjectSize
.
Como afetar o tamanho do documento?
Eu tenho respostas não específicas para esta pergunta. Primeiro, um
nome de campo longo aumenta o tamanho do documento. Em cada documento, todos os nomes de campo são copiados; portanto, se o documento tiver um nome longo, o tamanho do nome deverá ser adicionado ao tamanho de cada documento. Se você possui uma coleção com um grande número de documentos pequenos em vários campos, nomeie os campos com nomes abreviados: "A", "B", "CD" - no máximo duas letras.
No disco, isso é compensado pela compactação , mas tudo é armazenado no cache como está.
A segunda dica é que, às vezes,
alguns campos com baixa cardinalidade podem ser colocados no nome da coleção . Por exemplo, esse campo pode ser um idioma. Se tivermos uma coleção com traduções para o russo, inglês, francês e um campo com informações sobre o idioma armazenado, o valor desse campo poderá ser colocado no nome da coleção. Portanto,
reduziremos o tamanho dos documentos e
reduziremos o número e o tamanho dos índices - economia
total ! Isso nem sempre pode ser feito, porque às vezes existem índices dentro do documento que não funcionarão se a coleção estiver dividida em coleções diferentes.
Última dica sobre o tamanho do documento -
use o campo _id . Se seus dados tiverem uma chave exclusiva natural, coloque-a diretamente no id_field. Mesmo se a chave for composta - use um ID composto. É perfeitamente indexado. Existe apenas um pequeno rake - se o seu empacotador às vezes alterar a ordem dos campos, identifique-os com os mesmos valores, mas com ordem diferente será considerado um ID diferente em termos de um índice exclusivo no MongoDB. Em alguns casos, isso pode acontecer no Go.
Tamanhos do índice
O índice armazena uma cópia dos campos incluídos nele . O tamanho do índice consiste nos dados que são indexados. Se estamos tentando indexar campos grandes, prepare-se para que o tamanho do índice seja grande.
O segundo momento aumenta fortemente os índices: os
campos de matriz no índice multiplicam outros campos do documento nesse índice . Cuidado com matrizes grandes em documentos: não indexe outra coisa à matriz ou brinque com a ordem na qual os campos no índice estão listados.
A ordem dos campos é importante ,
especialmente se um dos campos de índice for uma matriz . Se os campos diferem em cardinalidade, e em um campo o número de valores possíveis é muito diferente do número de valores possíveis em outro, faz sentido construí-los aumentando a cardinalidade.
Você pode economizar 50% do tamanho do índice facilmente se trocar os campos com cardinalidade diferente. A permutação dos campos pode proporcionar uma redução mais significativa no tamanho.
Às vezes, quando o campo contém um valor grande, não precisamos comparar esse valor mais ou menos, mas uma comparação clara de igualdade. Em seguida, o
índice no campo com conteúdo pesado pode ser
substituído pelo índice no hash desse campo . Cópias de hash serão armazenadas no índice, não cópias desses campos.
Excluir documentos
Eu já mencionei que a exclusão de documentos é uma operação desagradável e
é melhor não excluir, se possível. Ao criar um esquema de dados, tente minimizar a remoção de dados individuais ou excluir coleções inteiras. eles podem ser excluídos com coleções inteiras. Remover coleções é uma operação barata e excluir milhares de documentos individuais é uma operação difícil.
Se você ainda precisar excluir muitos documentos,
faça a otimização ; caso contrário, a exclusão em massa de documentos afetará a latência da leitura e será desagradável. Isso é especialmente ruim para a latência no secundário.
Vale a pena fazer algum tipo de "caneta" para acelerar a aceleração - é muito difícil subir de nível pela primeira vez. Passamos por isso tantas vezes que a otimização é adivinhada a partir da terceira, quarta vez. Inicialmente, considere a possibilidade de apertá-lo.
Se você excluir mais de 30% de uma coleção grande, transfira documentos ativos para a coleção vizinha e exclua a coleção antiga como um todo. É claro que existem nuances, porque a carga é alterada da coleção antiga para a nova, mas muda, se possível.
Outra maneira de excluir documentos é o índice
TTL , que indexa o campo que contém o carimbo de data / hora do Mongo, que contém a data em que o documento morreu. Quando chegar a hora, o MongoDB excluirá este documento automaticamente.
O índice TTL é conveniente, mas
não há limitação na implementação. O MongoDB não se preocupa em como remover essas exclusões. Se você tentar excluir um milhão de documentos ao mesmo tempo, por alguns minutos, haverá um cluster inoperável que lida apenas com a exclusão e nada mais. Para impedir que isso aconteça, adicione alguma
aleatoriedade ,
espalhe o TTL o máximo que a lógica de negócios e os efeitos especiais na latência permitirem. A mancha do TTL é imprescindível se você tiver razões naturais da lógica de negócios que concentram a exclusão em um determinado momento.
Sharding
Tentamos adiar esse momento, mas chegou - ainda temos que escalar horizontalmente. Para o MongoDB, isso é fragmentação.
Se você duvida que precisa de sharding, não precisa.
O sharding complica a vida de um desenvolvedor e devops de várias maneiras. Em uma empresa, chamamos isso de imposto de fragmentação. Quando fragmentamos uma coleção, o
desempenho específico da coleção diminui : O MongoDB requer um índice separado para fragmentação, e parâmetros adicionais devem ser passados para a solicitação, para que ela possa ser executada com mais eficiência.
Algumas coisas de sharding simplesmente não funcionam bem. Por exemplo, é uma má idéia usar consultas com
skip
, especialmente se você tiver muitos documentos. Você dá o comando: "Pule 100.000 documentos".
O MongoDB pensa assim: “Primeiro, segundo, terceiro ... cem milésimos, vamos além. E nós devolveremos isso ao usuário. ”
Em uma coleção não compartilhada, o MongoDB executará uma operação em algum lugar dentro de si. No tipo shard - ela realmente lê e envia todos os 100.000 documentos para um proxy de sharding - em
mongos , que já estão de alguma forma filtram e descartam os primeiros 100.000. Um recurso desagradável a ser lembrado.
O código certamente ficará mais complicado com o sharding - você precisará arrastar a chave do sharding para muitos lugares. Isso nem sempre é conveniente e nem sempre é possível. Algumas consultas serão transmitidas ou multicast, o que também não adiciona escalabilidade. Venha para a escolha de uma chave pela qual o sharding será mais preciso.
Nas coleções de fragmentos, a operação de count
interrompida . Ela começa a retornar um número mais do que na realidade - ela pode mentir 2 vezes. O motivo está no processo de balanceamento, quando os documentos são derramados de um fragmento para outro. Quando os documentos derramarem no fragmento vizinho, mas ainda não foram excluídos no original, a
count
qualquer maneira. Os desenvolvedores do MongoDB não chamam isso de bug - esse é um recurso. Não sei se eles vão consertar ou não.
Um cluster embaralhado é muito mais difícil de administrar . Os devops deixarão de cumprimentá-lo, porque o processo de remoção de um backup se torna radicalmente mais complicado. Ao compartilhar, a necessidade de automação da infraestrutura pisca como um alarme de incêndio - algo que você nunca poderia ter feito antes.
Como o sharding funciona no MongoDB
Existe uma coleção, queremos de alguma forma espalhá-la em torno de fragmentos. Para fazer isso, o
MongoDB divide a coleção em partes usando a chave shard, tentando dividi-las em partes iguais no espaço da chave shard. Em seguida, vem o balanceador, que diligentemente
distribui esses pedaços de acordo com os fragmentos do cluster . Além disso, o balanceador não se importa com o peso desses blocos e com a quantidade de documentos, pois o balanceamento é feito em partes por peça.
Chave de Fragmento
Você ainda decide o que estilhaçar? Bem, a primeira pergunta é como escolher uma chave de fragmentação. Uma boa chave possui vários parâmetros:
alta cardinalidade ,
não estabilidade e se
encaixa bem em solicitações frequentes .
A escolha natural de uma chave de fragmentação é a chave primária - o campo id. Se o campo id for adequado para fragmentação, é melhor fragmentá-lo diretamente. Essa é uma excelente escolha - ele tem boa cardinalidade, não é estável, mas quão bem ele se encaixa em solicitações frequentes é a especificidade de seu negócio. Construa sobre sua situação.
Vou dar um exemplo de falha na chave de fragmentação. Eu já mencionei a coleção de traduções - traduções. Tem um campo de idioma que armazena o idioma. Por exemplo, a coleção suporta 100 idiomas e nós compartilhamos o idioma. Isso é ruim - cardinalidade, o número de valores possíveis é de apenas 100 peças, o que é pequeno. Mas isso não é o pior - talvez a cardinalidade seja suficiente para esses propósitos. Pior, assim que mudamos o idioma, descobrimos imediatamente que temos três vezes mais usuários que falam inglês do que o resto. Três vezes mais solicitações chegam ao infeliz fragmento em que o inglês está localizado do que a todos os outros combinados.
Portanto, deve-se ter em mente que às vezes uma chave de fragmento tende naturalmente a uma distribuição de carga desigual.
Balanceamento
Chegamos ao sharding quando a necessidade amadureceu para nós - nosso cluster MongoDB range, tritura com seus discos, processador - com tudo o que podemos. Para onde ir? Em nenhum lugar, e nós heroicamente misturamos os saltos das coleções. Fragmentamos, lançamos e de repente descobrimos que o
balanceamento não é gratuito .
O balanceamento passa por várias etapas. O balanceador escolhe pedaços e fragmentos, de onde e para onde ele será transferido. O trabalho adicional ocorre em duas fases: primeiro, os
documentos são copiados da origem para o destino e, em seguida, os documentos copiados
são excluídos .
Nosso shard está sobrecarregado, contém todas as coleções, mas a primeira parte da operação é fácil para ele. Mas o segundo - a remoção - é bastante desagradável, porque coloca um estilhaço nas omoplatas e já sofre com a carga.
O problema é agravado pelo fato de que, se equilibrarmos muitos pedaços, por exemplo, milhares, com as configurações padrão, todos esses pedaços serão copiados primeiro e, em seguida, um removedor entrará e começará a excluí-los em massa. Nesse ponto, o procedimento não é mais afetado e você só precisa observar com tristeza o que está acontecendo.
Portanto, se você estiver se aproximando para fragmentar um cluster sobrecarregado, precisará planejar, pois o
balanceamento leva tempo. É aconselhável levar esse tempo não no horário nobre, mas em períodos de baixa carga. Balanceador - uma peça de reposição desconectada. Você pode abordar o balanceamento primário no modo manual, desligá-lo no horário nobre e ativá-lo quando a carga diminuir para permitir mais.
Se os recursos da nuvem ainda permitirem a escala vertical, é melhor melhorar a fonte do fragmento antecipadamente, a fim de reduzir um pouco todos esses efeitos especiais.
O sharding deve ser cuidadosamente preparado.O HighLoad ++ Siberia 2019 chegará a Novosibirsk nos dias 24 e 25 de junho. O HighLoad ++ Siberia é uma oportunidade para os desenvolvedores da Sibéria ouvirem relatórios, falarem sobre tópicos sobrecarregados e mergulharem no ambiente "onde todos têm seus próprios", sem voar mais de três mil quilômetros para Moscou ou São Petersburgo. Das 80 inscrições, o Comitê do Programa aprovou 25, e falamos sobre todas as outras mudanças no programa, anúncios de relatórios e outras notícias em nossa lista de discussão. Inscreva-se para se manter informado.