Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
MongoDBchevron-right

O custo de não conhecer o MongoDB

Artur Costa23 min read • Published Nov 11, 2024 • Updated Nov 11, 2024
Node.jsEsquemaTypeScriptMongoDB
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Artigo
star-empty
star-empty
star-empty
star-empty
star-empty
O foco principal desta série é mostrar quanto de desempenho você pode obter e, como consequência, o custo que pode economizar ao usar o MongoDB corretamente, seguindo as práticas recomendadas, analisando as necessidades do seu aplicação e usando-o para modelar seus dados.
Para mostrar esses possíveis ganhos, um aplicação fictício será apresentado, e muitas possíveis implementações dele usando o MongoDB serão desenvolver e testar com carga. Haverão implementações para todos os níveis de conhecimento do MongoDB : iniciante, intermediário, sênior e atordoante(m) .
Todo o código e algumas informações extras usados neste artigo podem ser encontrados no Github repositório do .

O aplicação: localização de comportamentos enganosos em transações

O objetivo do aplicação é identificar o comportamento malicioso em um sistema de transações financeiros analisando os status das transações durante um período de tempo para um determinado usuário. Os possíveis status de transação são approved, noFunds, pendinge rejected.Cada usuário é exclusivamente identificável por um valor dekey 64caracteres hexadecimais.
O aplicação receberá o status da transação por meio de um documentoevent. Um event sempre fornecerá informações para uma transação para um usuário em um dia específico e, por isso, sempre terá apenas um dos campos de status possíveis, e esse campo de status terá o valor numérico 1. Como exemplo, o documentoeventa seguir documento uma transação com o status de pending para o usuário com identificação key de ...0001 que ocorreu no date/dia 2022-02-01:
1const event = {
2 key: '0000000000000000000000000000000000000000000000000000000000000001',
3 date: new Date('2022-02-01'),
4 pending: 1,
5};
Os status das transações serão analisados comparando os totais de status nos últimos oneYear, threeYears,fiveYears, sevenYearse tenYears para qualquer usuário. Estes totais serão fornecidos em um documentoreports, que pode ser solicitado fornecendo ao usuário key e o final date do relatório.
O documento a seguir é um exemplo de um documentoreports para o usuário do key ...0001 e data final de 2022-06-15:
1export const reports = [
2 {
3 id: 'oneYear',
4 end: new Date('2022-06-15T00:00:00.000Z'),
5 start: new Date('2021-06-15T00:00:00.000Z'),
6 totals: { approved: 4, noFunds: 1, pending: 1, rejected: 1 },
7 },
8 {
9 id: 'threeYears',
10 end: new Date('2022-06-15T00:00:00.000Z'),
11 start: new Date('2019-06-15T00:00:00.000Z'),
12 totals: { approved: 8, noFunds: 2, pending: 2, rejected: 2 },
13 },
14 {
15 id: 'fiveYears',
16 end: new Date('2022-06-15T00:00:00.000Z'),
17 start: new Date('2017-06-15T00:00:00.000Z'),
18 totals: { approved: 12, noFunds: 3, pending: 3, rejected: 3 },
19 },
20 {
21 id: 'sevenYears',
22 end: new Date('2022-06-15T00:00:00.000Z'),
23 start: new Date('2015-06-15T00:00:00.000Z'),
24 totals: { approved: 16, noFunds: 4, pending: 4, rejected: 4 },
25 },
26 {
27 id: 'tenYears',
28 end: new Date('2022-06-15T00:00:00.000Z'),
29 start: new Date('2012-06-15T00:00:00.000Z'),
30 totals: { approved: 20, noFunds: 5, pending: 5, rejected: 5 },
31 },
32];

O teste de carga

Duas funções para cada versão do aplicação foram criadas para serem executadas simultaneamente e testarem o desempenho de cada versão do aplicação . Uma função é chamada Bulk Upsert, que insere os documentos do evento . O outro é chamado Get Reports, que gera o reports para um usuário específico key e date. A paralelização da execução de cada função foi feita usando threads de trabalho, com 20 trabalhadores alocados para cada função. A duração do teste para cada versão do aplicação é 200 minutos, com diferentes parâmetros de execução sendo usados durante o teste de carga.
A funçãoBulk Upsert receberá lotes de 250 documentos de evento a serem registrados. Como o nome indica, esses registros serão feitos usando a funcionalidadeupsertdo MongoDB. Ele tentará atualizar um documento e, se ele não existir, criará um novo com os dados disponíveis na operação de atualização. Cada iteraçãoBulk Upsert será programada e registrada em um banco de banco de dados secundário. A taxa de processamento em lote será dividida em quatro fases, cada uma com 50 minutos, totalizando 200 minutos. A taxa começará com uma inserção em lote por segundo e aumentará em um a cada 50 minutos, terminando com quatro inserções em lote por segundo ou 1000 documentos de evento por segundo.
A funçãoGet Reports gerará um documentoreports por execução. Cada execuçãoreports será temporizada e registrada no banco de banco de dados secundário. A taxa de geração de reports será dividida em 40 fases, 10 fases para cadafaseBulk Upsert. Em cada fase de Bulk Upsert, a taxa começará com 25 solicitações de relatório por segundo e aumentará 25 solicitações de relatório por segundo a cada cinco minutos, terminando com 250 relatórios completos por segundo na mesma faseBulk Upsert .
O gráfico a seguir representa as taxas de Bulk Upsert e Get Reports para o cenário de teste apresentado acima: Gráfico mostrando as taxas desejadas de Bulk Upsert e Obter Relatórios para o cenário de teste de carga.

Cenário inicial e gerador de dados

Para fazer uma comparação justa entre as versões do aplicação , o cenário inicial/ conjunto de trabalho usado nos testes tinha que ser maior do que a memória da máquina que executa o servidor MongoDB , forçando a atividade de cache e evitando a situação em que todo o conjunto de trabalho caberia o cache. Para isso, foram escolhidos os seguintes parâmetros:
  • 10 anos de dados, de 2010-01-01 a 2020-01-01
  • 50 milhões de eventos por ano, totalizando 500 milhões para o conjunto de trabalho
  • 60 eventos por usuário/key por ano
Considerando o número de eventos por ano e o número de eventos por usuário por ano, o número total de usuários é 50.000.000/60=833.333. O gerador do usuário key foi ajustado para produzir chaves que se aproximam de uma distribuição normal/gaussiano para simular um cenário do mundo real em que alguns usuários terão mais eventos do que outros. O gráfico a seguir mostra a distribuição de 50 milhões de chaves geradas pelo geradorkey.
Gráfico mostrando as taxas desejadas de Bulk Upsert e Obter Relatórios para o cenário de teste de carga.
Para também abordar um cenário do mundo real, a distribuição dos status do evento é:
  • 80% approved.
  • 10% noFunds.
  • 7.5% pending.
  • 2.5% rejected.

A configuração de instâncias

A instância do EC2 que executa o servidor MongoDB é um c7a.large na nuvem do Amazon Web Services . Tem 2vCPU e 4GB de memória. Dois discos foram anexados a ele: um para o sistema operacional com 15GB de tamanho e GP3 tipo, e o outro para o servidor MongoDB , que armazena seus dados com 300GB de tamanho, IO2 tipo e 10.000IOPS. O sistema operacional instalado na instância é Ubuntu 22.04, com todas as atualizações e upgrades disponíveis no momento. Todas as notas de produção recomendadas foram aplicadas à máquina para permitir que o MongoDB extraia o desempenho máximo do hardware disponível.
A instância do EC2 que executa o servidor de aplicação é um c6a.xlarge na nuvem do Amazon Web Services . Tem 4vCPU e 8GB de memória. Dois discos foram anexados a ele: um para o sistema operacional com 10GB de tamanho e GP3 tipo, e o outro para o servidor MongoDB secundário, que armazena seus dados com 10GB de tamanho e GP3 tipo. O sistema operacional instalado na instância é Ubuntu 22.04, com todas as atualizações e upgrades disponíveis no momento. Todas as notas de produção recomendadas foram aplicadas à máquina para permitir que o MongoDB extraia o desempenho máximo do hardware disponível.

Versão do Aplicativo 1 (appV1)

A primeira versão do aplicação e o caso base de nossa comparação teria sido desenvolvido por alguém com um nível de conhecimento júnior do MongoDB , que acabou de dar uma rápida olhada na documentação e aprender que cada documento em uma coleção deve ter um campo_id e este o campo é sempre indexado único.
Para aproveitar o campo e índice obrigatórios_id, o desenvolvedor decide armazenar os valores de key e date em um documento incorporado no campo _id. Com isso, cada documento registrará os totais de status para um usuário, especificado pelo campo _id.key, em um dia, especificado pelo campo _id.date.

Esquema

A implementação do aplicação apresentado acima teria o seguinte esquema de documento Typescript denominado ScemaV1:
1type SchemaV1 = {
2 _id: {
3 key: string;
4 date: Date;
5 };
6 approved?: number;
7 noFunds?: number;
8 pending?: number;
9 rejected?: number;
10};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const operation = {
2 updateOne: {
3 filter: {
4 _id: { date: event.date, key: event.key },
5 },
6 update: {
7 $inc: {
8 approved: event.approved,
9 noFunds: event.noFunds,
10 pending: event.pending,
11 rejected: event.rejected,
12 },
13 },
14 upsert: true,
15 },
16};

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operaçãoGet Reports. Cada intervalo de datas terá o seguinte pipeline, com apenas o intervalo_id.date no filtro$matchsendo diferente:
1const pipeline = [
2 {
3 $match: {
4 '_id.key': request.key,
5 '_id.date': { $gte: Date.now() - oneYear, $lt: Date.now() },
6 },
7 },
8 {
9 $group: {
10 _id: null,
11 approved: { $sum: '$approved' },
12 noFunds: { $sum: '$noFunds' },
13 pending: { $sum: '$pending' },
14 rejected: { $sum: '$rejected' },
15 },
16 },
17];

Índices

Conforme apresentado na introdução desta implementação do aplicação , o principal objetivo de incorporar os campos key e date no campo _id campo aproveitar sua existência e índice obrigatórios. Mas, após alguns testes e pesquisas prévias, verificou-se que o índice no campo _id campo suportaria os critérios de filtragem/correspondência na função Get Reports. Com isso, o seguinte índice extra foi criado:
1const keys = { '_id.key': 1, '_id.date': 1 };
2const options = { unique: true };
3
4db.appV1.createIndex(keys, options);
Para aqueles que se perguntam por que precisamos de um índice extra nos campos do documento incorporado no campo _id, que já é indexado por padrão, uma explicação detalhada pode ser encontrada em Índice de documentos incorporados.

Estatísticas do cenário inicial

Inserindo os 500 milhões de documentos de evento para o cenário inicial na collection appV1 com o esquema e a funçãoBulk Upsert apresentados acima, temos o seguinte collection stats:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359,639,62239.58GB119B8.78GB220.06GB
Outra métrica interessante que podemos acompanhar por meio das versões do aplicação é o tamanho do armazenamento necessário, os dados e o índice para armazenar um dos 500 milhões de eventos — vamos chamá-lo event stats. Podemos obter esse valor dividindo o Tamanho dos dados e o Tamanho do índice das estatísticas iniciais do cenário pelo número de documentos do evento . Para appV1, temos o seguinte event stats:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B

Carregar resultados do teste

Executando o teste de carga para appV1, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Obter relatórios para a appV1.
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Bulk Upsert para appV1.
Os gráficos acima mostram que em quase nenhum momento o appV1 conseguiu atingir as taxas desejadas. O primeiro estágio do Bulk Upsert dura 50 minutos com uma taxa desejada de 250 eventos por segundo. A taxa de evento só é atingida nos primeiros 10 minutos do teste de carga. O primeiro estágio de Obter relatórios dura 10 minutos com uma taxa desejada de 20 relatórios por segundo. A taxa de relatórios nunca é atingida, e o valor mais alto é de 16.5 relatórios por segundo. Como esta é nossa primeira implementação e teste, não há muito mais sobre o que raciocinar.

Problemas e melhorias

O primeiro problema que pode ser destacado e melhorado nesta implementação é o esquema do documento em combinação com os dois índices. Como os campos key e date estão em um documento incorporado no campo _id, seus valores são indexados duas vezes: pelo índice padrão/obrigatório no campo _id e pelo índice que criamos para suportar Bulk Upserts e operaçõesGet Reports.
Como o campokeyé uma string de 64caracteres e o campodateé do tipo data, esses dois valores usam pelo menos 68 bytes de armazenamento. Como temos dois índices, cada documento contribuirá para 136 bytes de índice em um cenário não compactado.
A melhoria aqui é extrair os campos key e date do campo _id e permitir que o campo _id campo seu valor padrão do tipo ObjectId. O tipo de dados ObjectId ocupa apenas 12 bytes de armazenamento.
Essa primeira implementação pode ser vista como um cenário forçado do pior caso para fazer com que as soluções mais otimizadas tenham uma aparência melhor. Infelizmente, esse não é o caso. Não é difícil encontrar implementações como essa na internet e trabalhei em um grande projeto com um esquema como esse, de onde tirar a ideia para este primeiro caso.

Versão do Aplicativo 2 (appV2)

Conforme discutido nos problemas e melhorias de appV1, incorporar os campos key e date como um documento no campo _id, tentando aproveitar seu índice obrigatório, não é uma boa solução para nosso aplicação porque ainda precisariamos criar um índice extra, e o índice no campo _id campo mais armazenamento do que o necessário.
Para resolver o problema do índice no campo _id campo maior do que o necessário, a solução é mover os campos key e date para fora do documento incorporado no campo _id e deixar o _id campo tem seu valor padrão do tipo ObjectId. Cada documento ainda registraria os totais de status para um usuário, especificado pelo campo key, em um dia, especificado pelo campo date, assim como em appV1.
Esta segunda versão do aplicação e suas melhorias ainda teriam sido desenvolvida por alguém com um nível de conhecimento júnior do MongoDB , mas que tem mais profundidade na documentação relacionada a índices no MongoDB, especialmente ao indexar campos de documentos do tipo.

Esquema

A implementação do aplicação apresentado acima teria o seguinte esquema de documento Typescript denominado SchemaV2:
1type SchemaV2 = {
2 _id: ObjectId;
3 key: string;
4 date: Date;
5 approved?: number;
6 noFunds?: number;
7 pending?: number;
8 rejected?: number;
9};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const operation = {
2 updateOne: {
3 filter: { key: event.key, date: event.date },
4 update: {
5 $inc: {
6 approved: event.approved,
7 noFunds: event.noFunds,
8 pending: event.pending,
9 rejected: event.rejected,
10 },
11 },
12 upsert: true,
13 },
14};

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operaçãoGet Reports. Cada intervalo de datas terá o seguinte pipeline, com apenas o intervalodate no filtro$matchsendo diferente:
1const pipeline = [
2 {
3 $match: {
4 key: request.key,
5 date: { $gte: Date.now() - oneYear, $lt: Date.now() },
6 },
7 },
8 {
9 $group: {
10 _id: null,
11 approved: { $sum: '$approved' },
12 noFunds: { $sum: '$noFunds' },
13 pending: { $sum: '$pending' },
14 rejected: { $sum: '$rejected' },
15 },
16 },
17];

Índices

Para suportar os critérios de filtro/correspondência de Bulk Upsert e Get Reports, o seguinte índice foi criado na coleçãoappV2:
1const keys = { key: 1, date: 1 };
2const options = { unique: true };
3
4db.appV2.createIndex(keys, options);

Estatísticas do cenário inicial

Inserindo os 500 milhões de documentos de evento para o cenário inicial na collection appV2 com o esquema e a funçãoBulk Upsert apresentados acima, além de apresentar os valores das versões anteriores, temos o seguinte collection stats:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359,639,62239.58GB119B8.78GB220.06GB
appV2359,614,53641.92GB126B10.46GB216.66GB
Calculando o event stats para appV2 e também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
Analisando as tabelas acima, podemos ver que de appV1 para appV2, aumentamos o tamanho dos dados em 6% e diminuímos o tamanho do índice em 17%. Podemos dizer que nosso objetivo de tornar o índice no campo _id campo foi atingido.
Analisando event stats, o tamanho total por valor de evento aumentou apenas 1.8%, de 128.1B para 125.8B. Com essa diferença sendo tão pequena, há uma boa chance de que não vejamos melhorias significativas do ponto de vista do desempenho.

Carregar resultados do teste

Executando o teste de carga para appV2 e plotando-o juntamente com os resultados para appV1, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Obter relatórios para a appV2.
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Bulk Upsert para appV2.
Os gráficos acima mostram que em quase nenhum momentoappV2 atingiu as taxas desejadas, tendo um resultado muito semelhante ao appV1, conforme previsto no Initial Scenario Stats quando obtivemos apenas uma melhoria de 1.7% no event stats. appV2 só atingiu a taxaBulk Upsert desejada de 250 eventos por segundo nos primeiros 10 minutos do teste e obteve apenas 17 relatórios por segundo em Get Reports, abaixo dos 20 relatórios por segundo desejado.
Comparando as duas versões, podemos ver que appV2 teve um desempenho melhor que appV1 nas operaçõesBulk Upsert e pior que as operaçõesGet Reports. A melhoria nas operaçõesBulk Upsert pode ser atribuída aos índices serem menores, e a degradação no Get Reports pode ser atribuída ao fato de o documento ser maior.

Problemas e melhorias

O seguinte documento é uma amostra da collection appV2:
1const document = {
2 _id: ObjectId('6685c0dfc2445d3c5913008f'),
3 key: '0000000000000000000000000000000000000000000000000000000000000001',
4 date: new Date('2022-06-25T00:00:00.000Z'),
5 approved: 10,
6 noFunds: 3,
7 pending: 1,
8 rejected: 1,
9};
Analisando-o com o objetivo de reduzir seu tamanho, podem ser encontrados dois pontos de melhoria. Um é o campo key, que é uma string e sempre terá 64 caracteres de dados hexadecimais, e o outro é o nome dos campos de status, que combinados podem ter até 30 caracteres.
O campo key, conforme apresentado na seção de cenário, é composto por dados hexadecimais, nos quais cada caractere requer quatro bits para ser apresentado. Em nossa implementação até agora, armazenamos esses dados como strings usando codificação UTF-8, na qual cada caractere requer oito bits para ser representado. Então, estamos usando o double do armazenamento necessário. Uma maneira de contornar esse problema é armazenar os dados hexadecimais em seu formato bruto usando os dados binários.
Para os nomes dos campo de status, podemos ver que os nomes dos campos usam mais armazenamento do que o próprio valor. Os nomes dos campo são cadeias de caracteres com pelo menos sete caracteres UTF-8 , que usam pelo menos sete bytes. O valor dos campos de status é um número inteiro 32bits, que usa quatro bytes. Podemos abreviar os nomes de status pelo primeiro caractere, em que approved se torna a, noFunds se torna n, pending se torna pe rejected se torna r.

Versão do Aplicativo 3 (appV3)

Conforme discutido nas questões e melhorias do appV2, para reduzir o tamanho do documento , duas melhorias foram propostas. Uma é converter o tipo de dados do campo key de string para binário, exigindo quatro bits para representar cada caractere hexadecimal em vez dos oito bits de um caractere UTF-8 . A outra é abreviar o nome dos campos de status pela primeira letra, exigindo um byte para cada nome de campo em vez de sete bytes. Cada documento ainda registraria os totais de status para um usuário, especificado pelo campo key, em um dia, especificado pelo campo date, assim como nas implementações anteriores.
Para converter o valorkeyde uma string para binário/buffer, a seguinte função Typescript foi criada:
1const buildKey = (key: string): Buffer => {
2 return Buffer.from(key, 'hex');
3};
A terceira versão do aplicação tem duas melhorias em comparação com a segunda versão. A melhoria de armazenar o campo key como dados binários para reduzir suas necessidades de armazenamento teria sido pensada por um desenvolvedor intermediário a sênior do MongoDB que leu a documentação do MongoDB muitas vezes e trabalhou em projetos diferentes. A melhoria de abreviar o nome dos campos de status teria sido pensada por um desenvolvedor intermediário do MongoDB que passou por parte da documentação do MongoDB .

Esquema

A implementação do aplicação apresentado acima teria o seguinte esquema de documento Typescript denominado SchemaV3:
1type SchemaV3 = {
2 _id: ObjectId;
3 key: Buffer;
4 date: Date;
5 a?: number;
6 n?: number;
7 p?: number;
8 r?: number;
9};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const operation = {
2 updateOne: {
3 filter: { key: buildKey(event.key), date: event.date },
4 update: {
5 $inc: {
6 a: event.approved,
7 n: event.noFunds,
8 p: event.pending,
9 r: event.rejected,
10 },
11 },
12 upsert: true,
13 },
14};

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operaçãoGet Reports. Cada intervalo de datas terá o seguinte pipeline, com apenas o intervalodate no filtro$matchsendo diferente:
1const pipeline = [
2 {
3 $match: {
4 key: buildKey(request.key),
5 date: { $gte: Date.now() - oneYear, $lt: Date.now() },
6 },
7 },
8 {
9 $group: {
10 _id: null,
11 approved: { $sum: '$a' },
12 noFunds: { $sum: '$n' },
13 pending: { $sum: '$p' },
14 rejected: { $sum: '$r' },
15 },
16 },
17];

Índices

Para suportar os critérios de filtro/correspondência de Bulk Upsert e Get Reports, o seguinte índice foi criado na coleçãoappV3:
1const keys = { key: 1, date: 1 };
2const options = { unique: true };
3
4db.appV3.createIndex(keys, options);

Estatísticas do cenário inicial

Inserindo os 500 milhões de documentos de evento para o cenário inicial na collection appV3 com o esquema e a funçãoBulk Upsert apresentados acima, além de apresentar os valores das versões anteriores, temos o seguinte collection stats:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359,639,62239.58GB119B8.78GB220.06GB
appV2359,614,53641.92GB126B10.46GB216.66GB
appV3359,633,37628.7GB86B8.96GB216.37GB
Calculando o event stats para appV3 e também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
appV361.6B35.2B96.8B
Analisando as tabelas acima, podemos ver que de appV2 para appV3, praticamente não houve alteração no tamanho do índice e uma diminuição de 32% no tamanho dos dados. Nosso objetivo de reduzir o tamanho do documento foi atingido.
Analisando event stats, o tamanho total por valor de evento diminuído em 23%, de 125.8B para 96.8B. Com essa redução, provavelmente observaremos melhorias consideráveis.

Carregar resultados do teste

Executando o teste de carga para appV3 e plotando-o juntamente com os resultados para appV2, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Obter relatórios para a appV3. Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Bulk Upsert para appV3.
Os gráficos acima mostram explicitamente que appV3 é mais eficiente do que appV2, e estamos começando a nos aproximar de algumas taxas desejadas. appV3 conseguiu fornecer as taxas desejadas para os primeiros 100 minutos de operaçõesBulk Upsert: 250 eventos por segundo de 0 a 50 minutos e 500 eventos por segundo de 50 minutos a 100 minutos. Por outro lado, Get Report operações ainda não conseguem atingir a taxa mais baixa desejada de 20 relatórios por segundo, mas obviamente tiveram melhor desempenho do que appV2, sendo capazes de manter a taxa de 16 relatórios por segundo por metade do teste.
Toda a melhoria de desempenho pode ser atribuída à redução do tamanho do documento , pois foi a única alteração entre appV2 e appV3.

Problemas e melhorias

Observando collection stats de appV3 e refletindo sobre como o MongoDB executa nossas queries e quais índices estão sendo usados, podemos ver que o campo_id e seu índice não estão sendo usados em nosso aplicação. O campo em si não é grande coisa do ponto de vista de desempenho, mas seu índice único obrigatório é. Toda vez que um novo documento é inserido na coleção, a estrutura do índice no campo _id campo ser atualizada.
Retornando à ideia de appV1 de tentar aproveitar o campo_idobrigatório e seu índice, existe uma maneira de podermos usá-lo em nosso aplicação?
Vamos dar uma olhada em nossos critérios de filtragem nas funçõesGet Report e Bulk Upsert:
1const bulkUpsertFilter = {
2 key: event.key,
3 date: event.date,
4};
5
6const getReportsFilter = {
7 key: request.key,
8 date: {
9 $gte: new Date('2021-06-15'),
10 $lt: new Date('2022-06-15'),
11 },
12};
Em ambos os critérios de filtragem, o campokey é comparado usando a igualdade. O campodate é comparado usando igualdade no Bulk Upsert e intervalo no Get Reports. E se combinarmos esses dois valores de campo em apenas um, concatenando-os, e armazená-lo em _id?
Para nos orientar sobre como devemos ordenar os campos no valor concatenado resultante e obter o melhor desempenho do índice nele, vamos seguir a regra de Igualdade, Classificação e Faixa (ESR).
Como visto acima, o campokey é comparado pela igualdade em ambos os casos, e o campodate é comparado pela igualdade apenas em um caso, então, vamos escolher o campokey para a primeira parte do nosso valor concatenado e o campodate para a segunda parte. Como não temos uma operação de classificação em nossas consultas, podemos ignorá-la. Em seguida, temos a comparação de faixa, que é usada no campo date, então agora faz sentido mantê-la como a segunda parte do nosso valor concatenado. Com isso, a maneira mais ideal de concatenar os dois valores e obter o melhor desempenho de seu índice é key + date.
Um ponto de atenção é como vamos formatar o campodate campo concatenação de forma que o filtro de faixa funcione e não armazenamos mais dados do que precisamos. Uma possível implementação será apresentada e testada na próxima versão do aplicação , appV4.

Versão do Aplicativo 4 (appV4)

Conforme apresentado nas questões e melhorias de appV3, uma maneira de aproveitar o campo e o índice _idobrigatórios é armazenar nele o valor concatenado dekey + date. Uma coisa que precisamos cobrir agora é o tipo de dados que o campo_id campo e como vamos formatar o campodate.
Conforme visto em implementações anteriores, armazenar o campokey como dados binários/hexadecimais melhorou o desempenho. Então, vejamos se também podemos armazenar o campo concatenado resultante , key + date, como binário/hexadecimal.
Para armazenar o campodate em um tipo binário/hexadecimal, temos algumas opções. Um pode convertê-lo em um carimbo de data/hora 4bytes que mede os segundos desde a época do Unix, e o outro pode ser convertido para o formato YYYYMMDD que armazena ano, mês e dia. Ambos os casos exigiriam os mesmos caracteres hexadecimais 32 bits/8 .
Para o nosso caso, vamos usar a segunda opção e armazenar o valordate como YYYYMMDD porque isso ajudará em futuras implementações e melhorias. Considerando um campokey com o valor 0001 e um campodate com o valor 2022-01-01, teriamos o seguinte campo_id:
1const _id = Buffer.from('000120220101', 'hex');
Para concatenar e converter os camposkey e date para o formato e tipo desejados, a seguinte função do Typescript foi criada:
1const buildId = (key: string, date: Date): Buffer => {
2 const day = date.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD
3 return Buffer.from(`${key}${day}`, 'hex');
4};
Cada documento ainda registraria os totais de status para um usuário em um dia, especificados pelo campo _id, da mesma forma que foi feito nas implementações anteriores.

Esquema

A implementação do aplicação apresentado acima teria o seguinte esquema de documento Typescript denominado SchemaV4:
1type SchemaV4 = {
2 _id: Buffer;
3 a?: number;
4 n?: number;
5 p?: number;
6 r?: number;
7};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const operation = {
2 updateOne: {
3 filter: { _id: buildId(event.key, event.date) },
4 update: {
5 $inc: {
6 a: event.approved,
7 n: event.noFunds,
8 p: event.pending,
9 r: event.rejected,
10 },
11 },
12 upsert: true,
13 },
14};

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operaçãoGet Reports. Cada intervalo de data terá o seguinte pipeline, com apenas a data utilizada na função buildId sendo diferente:
1const pipeline = [
2 {
3 $match: {
4 _id: {
5 $gte: buildId(request.key, Date.now() - oneYear),
6 $lt: buildId(request.key, Date.now()),
7 },
8 },
9 },
10 {
11 $group: {
12 _id: null,
13 approved: { $sum: '$a' },
14 noFunds: { $sum: '$n' },
15 pending: { $sum: '$p' },
16 rejected: { $sum: '$r' },
17 },
18 },
19];

Índices

Como essa implementação usará o campo_id para suas operações, ela não precisará de um índice extra para dar suporte às operaçõesBulk Upsert e Get Reports.

Estatísticas do cenário inicial

Inserindo os 500 milhões de documentos de evento para o cenário inicial na collection appV4 com o esquema e a funçãoBulk Upsert apresentados acima, além de apresentar os valores das versões anteriores, temos o seguinte collection stats:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359,639,62239.58GB119B8.78GB220.06GB
appV2359,614,53641.92GB126B10.46GB216.66GB
appV3359,633,37628.7GB86B8.96GB216.37GB
appV4359,615,27919.66GB59B6.69GB19.5GB
Calculando o event stats para appV4 e também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
appV361.6B35.2B96.8B
appV442.2B20.4B62.6B
Analisando as tabelas acima, podemos ver que de appV3 para appV4, reduzimos o tamanho dos dados em 32% e o tamanho do índice em 42% — grandes melhorias. Também temos um índice a menos para manter agora.
Analisando event stats, o tamanho total por valor de evento aumentou 35%, de 96.8B para 62.6B. Com essa redução, provavelmente notamos algumas melhorias significativas no desempenho.

Carregar resultados do teste

Executando o teste de carga para appV4 e plotando-o juntamente com os resultados para appV3, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Obter relatórios para a appV4.
Gráfico mostrando as taxas obtidas com a execução do teste de carga na funcionalidade Bulk Upsert para appV4.
Os gráficos acima mostram que appV4 é apenas um pouco melhor que appV3. Para as operaçõesBulk Upsert, ambos podem fornecer as taxas desejadas nos primeiros 100 minutos, mas nenhum dos dois pode fornecer as taxas desejadas nos últimos 100 minutos. No entanto, appV4 tem taxas melhores do que appV3. Para as operaçõesGet Reports, ainda não alcançamos a taxa mais baixa desejada, mas appV4 tem taxas melhores, em média, do que appV3.

Problemas e melhorias

chega de focar na redução do tamanho dos documento para melhorar o desempenho – vamos dar uma olhada no comportamento do aplicação .
Ao gerar os oneYear totais, a funçãoGet Reports precisará recuperar algo próximo a 60 documentos em média e, no pior cenário, 365 documentos. Para acessar cada um desses documentos, uma entrada de índice terá que ser visitada e uma operação de leitura de disco deverá ser executada. Como podemos aumentar a densidade de dados dos documentos em nosso aplicação e reduzir as entradas de índice e as operações de leitura necessárias para realizar a operação desejada?
Uma maneira de fazer isso é usando o Padrão de Bucket. De acordo com a documentação do MongoDB, "O padrão de bucket separa longas séries de dados em objetos distintos. A separação de grandes séries de dados em grupos menores pode melhorar os padrões de acesso a queries e simplificar a lógica do aplicação ."
Observando nosso aplicação da perspectiva do padrão de bucket, até agora, agrupamos nossos dados por usuário diário, cada documento contendo os totais de status para um usuário em um dia. Podemos aumentar o intervalo de agrupamento ou nosso esquema e armazenar eventos ou totais de status de uma semana, mês ou até mesmo trimestre em um documento.

Conclusão

Esse é o fim da primeira parte da série. Abordamos como os índices funcionam em campos de documentos do tipo e vimos algumas pequenas alterações que podemos fazer em nosso aplicação para reduzir suas necessidades de armazenamento e índice e, como consequência, melhorar seu desempenho.
Aqui está uma rápida revisão das melhorias feitas entre as versões do aplicação :
  • appV1 para appV2: moveu os campos key e date para fora de um documento incorporado no campo _id e deixou-o ter seu valor padrão de ObjectId.
  • appV2 para appV3: Reduziu o tamanho do documento abreviando os nomes dos campos de status e alterando o tipo de dados do campo key de string para binário/hexadecimal.
  • appV3 a appV4: removeu a necessidade de um índice extra concatenando os valores de key e date e armazenando-os no campo _id.
Até agora, nenhum dos nossos aplicativos chegou perto das taxas desejadas, mas não vamos desistir. Conforme apresentado nos problemas e melhorias de appV4, ainda podemos melhorar nosso aplicação usando o Padrão de Bucket. O Padrão de Bucket com o Padrão Computado serão os principais pontos de melhoria para a próxima versão do aplicação , appV5, e suas revisões.
Para ter mais dúvidas, você Go acessar o Fórum da MongoDB Community, ou, se quiser construir seu aplicação usando o MongoDB, o MongoDB Developer Center tem muitos exemplos em muitas linguagens de programação diferentes.

Apêndices

Índice de documentos incorporados {#index-on-embedded-documents}

Vamos dar uma olhada em como o MongoDB indexa um campo com um valor do tipo documento e ver por que precisamos de um índice extra para a implementação doappV1.
Primeiro, vamos verificar se o índice no campo _id campo será usado para nossas queries executando uma operaçãofind com os mesmos critérios de filtragem usados nas funçõesBulk Upsert e Get Reports e aplicando a funcionalidade deexplicaçãoa ele.
1// A sample document
2const doc = {
3 _id: { key: '0001', date: new Date('2020-01-01') },
4 approved: 2,
5 rejected: 1,
6};
7
8// Making sure we have an empty collection
9db.appV1.drop();
10
11// Inserting the document in the `appV1` collection
12db.appV1.insertOne(doc);
13
14// Finding a document using `Bulk Upsert` filtering criteria
15const bulkUpsertFilter = {
16 _id: { key: '0001', date: new Date('2020-01-01') },
17};
18db.appV1.find(bulkUpsertFilter).explain('executionStats');
19/*{
20...
21 executionStats: {
22 nReturned: 1,
23 totalKeysExamined: 1,
24 totalDocsExamined: 1,
25 ...
26 executionStages: {
27 stage: 'EXPRESS_IXSCAN',
28 ...
29 }
30 ...
31 },
32 ...
33}*/
34
35// Finding a document using `Get Reports` filtering criteria
36const getReportsFilter = {
37 '_id.key': '0001',
38 '_id.date': { $gte: new Date('2019-01-01'), $lte: new Date('2021-01-01') },
39};
40db.appV1.find(getReportsFilter).explain('executionStats');
41/*{
42...
43 executionStats: {
44 nReturned: 1,
45 totalKeysExamined: 0,
46 totalDocsExamined: 1,
47 ...
48 executionStages: {
49 stage: 'COLLSCAN',
50 ...
51 }
52 ...
53 },
54 ...
55}*/
Conforme mostrado pela saída das queries explicáveis, temos uma verificação de collection (COLLSCAN) para os critérios de filtragemGet Reports, o que indica que um índice não foi usado para executar a query.
A maioria dos tipos de dados suportados no MongoDB será indexada diretamente sem qualquer tratamento ou conversão especial. Os casos especiais são campos do tipo array ou documentos. O caso da array não é nosso foco atual, mas pode ser visto em Criar um índice em um campo de array. A caixa do documento ou documento documento incorporado pode ser vista em Criar um índice em um documento incorporado. Usando o conhecimento do caso do documento em nossa implementação, poderíamos dizer que o valor do campo _id na estrutura do índice seria uma versão em string do documento incorporado.
1const documentValue = { key: '0001', date: '2010-01-01T00:00:00.000Z' };
2const indexValue = "{key:0001,date:2010-01-01T00:00:00.000Z}";
Como o valor do índice é um blob de dados, o MongoDB não é capaz de acessar valores internos/incorporados, pois eles não existem nessa representação e, como consequência, o MongoDB não pode usar o valor do índice para filtrar por _id.key ou _id.date.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Artigo
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Lidar com dados de série temporal com o MongoDB


Nov 19, 2024 | 13 min read
Início rápido

Trabalhando com o padrão de coleção única do MongoDB em Swift


Jan 18, 2023 | 6 min read
Início rápido

Fluxos de alterações do MongoDB com Python


Sep 23, 2022 | 9 min read
Artigo

Construindo com padrões: o padrão polimórfico


Sep 23, 2022 | 4 min read
Sumário