O custo de não conhecer o MongoDB (V5RX) - Parte 2
Artur Costa26 min read • Published Jan 29, 2025 • Updated Jan 29, 2025
APLICATIVO COMPLETO
Avalie esse Artigo
Esta é a segunda parte da série "The Cost of Not Knowing MongoDB, ", na qual examinamos várias maneiras de modelar nossos esquemas do MongoDB para o mesmo aplicação e ter desempenhos diferentes. Na primeira parte da série, concatenamos campos, alteramos tipos de dados e reduzimos os nomes de campo para melhorar o desempenho do aplicação . Nesta segunda parte, conforme discutido nos problemas e melhorias do
appV4
, os ganhos de desempenho serão alcançados analisando o comportamento do aplicação e como ele armazena e lê seus dados, levando-nos ao uso do Padrão de Bucket e o Padrão computado.Ao gerar o relatório de
oneYear
totais do, a Get Reports
função precisará recuperar uma média de 60 documentos e, no pior cenário, 365 documentos. Para acessar cada documento, uma entrada de índice deve ser visitada e uma operação de leitura de disco deve ser executada.Uma maneira de reduzir o número de entradas de índice e documentos recuperados para gerar o relatório é usar 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
Bucket Pattern
, até agora, agrupamos nossos dados diariamente por um usuário, cada documento contendo os totais de status de um usuário em um dia. Para as duas versões do aplicação apresentados nesta seção, appV5R0
e , agruparemos os appV5R1
dados por mês (appV5R0
) e por trimestre (appV5R1
).Como essas são nossas primeiras implementações usando o Padrão de Bucket, vamos torná-lo o mais simples possível.
appV5R0
Para, cada documento agrupa os eventos por mês e usuário. Cada documento terá um campo de tipo array chamado items
para o qual cada event
documento será enviado. O documento do evento enviado para a array terá os nomes dos campo de status abreviados para sua primeira letra, da mesma forma que fizemos em,appV3
appV4,
e date
ao qual o evento se refere.O
_id
campo terá uma lógica semelhante à utilizada em appV4
, com os valores de key
e date
concatenados e armazenados como informações hexadecimais/binárias. A diferença é o date
valor de — em vez de ser composto por ano, mês e dia (YYYYMMDD
) — terá apenas ano e mêsYYYYMM
(), pois estamos agrupando os dados por mês.Para
appV5R1
, temos quase a mesma implementação que appV5R0
, com a diferença de que agruparemos os eventos por trimestre, e o date
valor usado para gerar o _id
campo será composto por ano e trimestre (YYYYQQ
) em vez de ano e mês (YYYYMM
). Para construir o
_id
campo com base nos key
valores e para date
appV5R0
, a seguinte função Typescript foi criada:1 const buildId = (key: string, date: Date): Buffer => { 2 const [YYYY, MM] = date.toISOString().split(); 3 4 return Buffer.from(`${key}${YYYY}${MM}`, 'hex'); 5 };
Para construir o
_id
campo com base nos key
valores e para date
appV5R1
, as seguintes funções do Typescript foram criadas:1 const getQQ = (date: Date): string => { 2 const month = Number(getMM(date)); 3 4 if (month >= 1 && month <= 3) return '01'; 5 else if (month >= 4 && month <= 6) return '02'; 6 else if (month >= 7 && month <= 9) return '03'; 7 else return '04'; 8 }; 9 10 const buildId = (key: string, date: Date): Buffer => { 11 const [YYYY] = date.toISOString().split('-'); 12 const QQ = getQQ(date); 13 14 return Buffer.from(`${key}${YYYY}${QQ}`, 'hex'); 15 };
As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript
SchemaV5R0
denominado:1 type SchemaV5R0 = { 2 _id: Buffer; 3 items: Array<{ 4 date: Date; 5 a?: number; 6 n?: number; 7 p?: number; 8 r?: number; 9 }>; 10 };
Com base na especificação apresentada, temos a seguinte operação em massa
updateOne
para cada event
gerado pelo aplicação:1 const opeartion = { 2 updateOne: { 3 filter: { _id: buildId(event.key, event.date) }, 4 update: { 5 $push: { 6 items: { 7 date: event.date, 8 a: event.approved, 9 n: event.noFunds, 10 p: event.pending, 11 r: event.rejected, 12 }, 13 }, 14 }, 15 upsert: true, 16 }, 17 };
A
updateOne
operação acima filtrará/pesquisa o documento com o _id
campo composto pela concatenação do evento key
e do evento month/quarter
e para $push
o campo da items
array um documento com o evento date
e status
.serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operação
Get Reports
. Cada intervalo de data terá o seguinte pipeline, com apenas a data utilizada na função buildId
sendo diferente:1 const pipeline = [ 2 { 3 $match: { 4 _id: { 5 $gte: buildId(request.key, Date.now() - oneYear), 6 $lte: buildId(request.key, Date.now()), 7 }, 8 }, 9 }, 10 { 11 $unwind: { 12 path: '$items', 13 }, 14 }, 15 { 16 $match: { 17 'items.date': { $gte: Date.now() - oneYear, $lt: Date.now() }, 18 }, 19 }, 20 { 21 $group: { 22 _id: null, 23 approved: { $sum: '$items.a' }, 24 noFunds: { $sum: '$items.n' }, 25 pending: { $sum: '$items.p' }, 26 rejected: { $sum: '$items.r' }, 27 }, 28 }, 29 { $project: { _id: 0 } }, 30 ];
O primeiro estágio,
$match
, filtrará a _id
faixa pelo campo . O limite inferior do nosso intervalo, $gte
, será gerado usando a buildId
função com o key
do usuário para o qual queremos o relatório e o date
um ano antes do dia em que o relatório foi solicitado. O limite superior do nosso intervalo, $lte
, será gerado de forma semelhante ao limite inferior, mas o date
fornecido será o dia em que o relatório foi solicitado. Um ponto de atenção nesse estágio é que o resultado de buildId
contém informações por mês/trimestre, não por dia, pois precisamos criar o relatório, portanto, será necessária uma filtragem adicional por dia.O segundo estágio,
$unwind
, desconstruirá o campo de array a items
partir dos documentos de entrada para gerar um documento para cada elemento da array.O terceiro estágio,
$match
, filtrará todos os documentos entre o intervalo de datas do relatório. Pode-se ver que já filtramos por data, mas, conforme apresentado na explicação do primeiro estágio, filtramos por mês/trimestre e, para gerar o relatório, precisamos filtrar por dia.Como essa implementação usará o
_id
campo para suas operações, ela não precisará de um índice extra para dar suporte às operações Bulk Upsert
Get Reports
e.Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections
appV5R0
e appV5R1
com o esquema e a Bulk Upsert
função apresentados acima, além de apresentar os valores das versões anteriores, temos a seguinte collection stats
:coleção | Documentos | tamanho de dados | Tamanho do documento | Tamanho do armazenamento | Índices | Tamanho do Índice |
---|---|---|---|---|---|---|
appV1 | 359.639.622 | 39,58GB | 119B | 8.78GB | 2 | 20.06GB |
appV2 | 359.614.536 | 41.92GB | 126B | 10.46GB | 2 | 16.66GB |
appV3 | 359.633.376 | 28.7GB | 86B | 8.96GB | 2 | 16.37GB |
appV4 | 359.615.279 | 19.66GB | 59B | 6.69GB | 1 | 9.5GB |
appV5R0 | 95.350.431 | 19.19GB | 217B | 5.06GB | 1 | 2.95GB |
appV5R1 | 33.429.649 | 15.75GB | 506B | 4.04GB | 1 | 1.09GB |
Calculando o
event stats
para appV5R0
e e appV5R1
também apresentam os valores das versões anteriores, temos o seguinte:coleção | Tamanho dos Dados/ventos | Tamanho/ventos do Índice | Tamanho total/ventos |
---|---|---|---|
appV1 | 85B | 43.1B | 128.1B |
appV2 | 90B | 35.8B | 125.8B |
appV3 | 61.6B | 35.2B | 96.8B |
appV4 | 42.2B | 20.4B | 62.6B |
appV5R0 | 41.2B | 6.3B | 47.5B |
appV5R1 | 33.8B | 2.3B | 36.1B |
Analisando as tabelas acima, podemos ver que passando
appV4
de para ,appV5R0
praticamente não tivemos melhorias ao analisar Data Size
, mas ao considerar Index Size
, a melhoria foi bastante significativa. O tamanho do índice para appV5R0
é 69% do tamanho de appV4
.Ao considerar ir de
appV4
para appV5R1
, os ganhos são ainda mais expressivos. Nesse caso, reduzimos o Data Size
em 20% e o Index Size
em 89%.Analisando o
event stats
, tivemos melhorias consideráveis no Total Size/events
, mas o que realmente chama a atenção é a melhoria no Index Size/events
, que é três vezes menor para appV5R0
e nove vezes menor para appV5R1
.Essa grande redução no tamanho do índice se deve ao uso do Padrão de Bucket, em que um documento armazenará dados para muitos eventos, reduzindo o número total de documentos e, como consequência, reduzindo o número de entradas de índices.
Com essas melhorias significativas em relação ao tamanho do índice, é bem provável que também vejamos melhorias significativas no desempenho do aplicação . Um ponto de atenção nos valores apresentados acima é que o tamanho do índice das duas novas versões é menor que o tamanho da memória da máquina que executa o banco de dados, permitindo que todo o índice seja mantido no cache, o que é muito bom para um desempenho ponto de vista.
Executando o teste de carga para
appV5R0
e appV0R1
e plotando-o junto com os resultados para appV4
, temos os seguintes resultados para Get Reports
e Bulk Upsert
:![Gráfico mostrando as taxas de appV4, appV5R0 e appV5R1 ao executar o teste de carga para a funcionalidade Obter Relatórios.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fblt2ea4cc0fcfd8aba0%2F679a36ecb4ff916fd2e6b3b8%2Fimage1.png&w=3840&q=75)
![Gráfico mostrando as taxas de appV4, appV5R0 e appV5R1 ao executar o teste de carga para a funcionalidade Bulk Upsert.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fbltf29f7e68ada95e55%2F679a36ec783007bfc6cfc701%2Fimage2.png&w=3840&q=75)
appV5R0
e appV0R1
, são consideravelmente mais eficientes do que appV4
, e appV5R1
é muito melhor do que appV5R0
. Isso mostra que, em nosso caso, agrupar os dados por trimestre é melhor do que agrupar os dados por mês.Para as
Get Reports
operações de, o appV5R0
aplicação conseguiu atingir pelo menos a taxa desejada mais baixa pela primeira vez, com appV5R1
quase atingindo todas as taxas desejadas no primeiro trimestre do teste de carga.Para as
Bulk
Upsert
operações de, ambas as novas versões quase atingiram as taxas desejadas durante todo o teste, com app5R0
queda nos últimos 20 minutos.Como fizemos essa primeira implementação do a
Bucket Pattern
mais simples possível, algumas otimizações possíveis não foram consideradas. A principal é como lidamos com o items
campo de array . Na implementação atual, apenas enviamos os documentos dos eventos para ele, mesmo quando já temos eventos para um dia específico.Uma otimização clara aqui é aquela que estamos usando
appV1
de para appV4
, onde criamos apenas um documento por key
e date
/day
, e quando temos muitos eventos para o mesmo key
e date
/dia, apenas incrementamos o status do documento com base no status do event
.Ao aplicar essa otimização, reduziremos o tamanho dos documentos porque a array de
items
terá menos elementos. Também reduziremos o custo computacional de geração dos relatórios porque estamos pré-calculando os totais de status por dia. Esse padrão de construção da pré-computação é bastante comum, pois tem seu próprio nome, Computed Pattern
.Conforme discutido nas questões e melhorias de
appV5R0
e appV5R1
, podemos usar o Padrão Computado para pré-computar o status total por dia no items
campo de array ao inserir um novo event
. Isso reduz o custo de computação de gerar o reports
e também reduz o tamanho do documento por ter menos elementos no items
campo de array.A maior parte dessa versão do aplicação será igual ao
appV5R1
, onde agrupamos os eventos por trimestre. A única diferença estará na Bulk Upsert
operação, em que atualizaremos um elemento no items
campo de array se um elemento com o mesmo date
do novo event
já existir ou inseriremos um novo elemento em items
se um elemento com o mesmo date
do novo não event
existir.As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript
SchemaV5R0
denominado:1 type SchemaV5R0 = { 2 _id: Buffer; 3 items: Array<{ 4 date: Date; 5 a?: number; 6 n?: number; 7 p?: number; 8 r?: number; 9 }>; 10 };
Com base na especificação apresentada, temos a seguinte operação em massa
updateOne
para cada event
gerado pelo aplicação:1 const sumIfItemExists = itemsArray.buildResultIfItemExists(event); 2 const returnItemsOrCreateNew = itemsArray.buildItemsOrCreateNew(event); 3 const operation = { 4 updateOne: { 5 filter: { 6 _id: buildId(event.key, event.date), 7 }, 8 update: [ 9 { $set: { result: sumIfItemExists } }, 10 { $set: { items: returnItemsOrCreateNew } }, 11 { $unset: ['result'] }, 12 ], 13 upsert: true, 14 }, 15 };
O código completo para esta
updateOne
operação é bastante grande, difícil de entender rapidamente, e também tornaria o processo de navegação pelo artigo um pouco trabalhoso. Por isso, aqui teremos um pseudocódigo disso. O código real que cria a operação pode ser encontrado no Github, e um exemplo de código completo pode ser encontrado neste repositório.Nosso objetivo nessa operação de atualização é incrementar o
status
de um elemento na items
array se já existir um element
com o mesmo date
do novo event
ou criar um novo element
se não há um com o mesmo date
. Não é possível obter essa funcionalidade com os Operadores de Atualização do MongoDB . A solução é usar a Atualização com Aggregation Pipeline, que permite uma instrução de atualização mais expressiva.Para facilitar a compreensão da lógica usada no pipeline, será fornecida uma versão JavaScript simplificada da funcionalidade.
O primeiro estágio,
$set
, definirá o campo result
para a lógica da variável sumIfItemExists
. Como o nome sugere, essa lógica iterará pela items
array procurando elementos com o mesmo date
que o event
. Se houver um, este element
terá o status presente no event
somado/adicionado a ele. Como precisamos de uma maneira de acompanhar se um element
com o mesmo date
do event
foi encontrado e o event
status foi registrado, há uma variável booleana de ambiente chamada found
que manterá o controle dele.O código JavaScript a seguir apresenta uma funcionalidade semelhante ao que acontece no primeiro estágio do pipeline de agregação .
1 const result = items.reduce( 2 (accumulator, element) => { 3 if (element.date === event.date) { 4 element.a += event.a; 5 element.n += event.n; 6 element.p += event.p; 7 element.r += event.r; 8 9 accumulator.found = true; 10 } 11 12 accumulator.items.push(element); 13 14 return accumulator; 15 }, 16 { found: false, items: [] } 17 );
A
result
variável/ campo será gerada usando um método de reduçãono items
campo de array do documento que queremos atualizar. O valor inicial para o método de redução é um objeto com o campo found
items
e. O campo accumulator.found
tem um valor inicial de false
e é responsável por sinalizar se um element
na execução reduzida tinha o mesmo date
que o que event
queremos registrar. Se houver um element
com a mesma data que event
, element.date === event.date
, os valores de status de element
serão incrementados pelo status do event
e o accumulator.found
campo será definido como true
, indicando que o event
foi registrado. O accumulator.items
campo de array terá o element
de cada iteração enviado para ele, tornando-se o novo items
campo de array.O segundo estágio,
$set
, definirá o campo items
para a lógica resultante da variável returnItemsOrCreateNew
. Com um pouco de esforço de imagens, o nome sugere que a lógica presente na variável retornará o items
campo do estágio anterior se um elemento com o mesmo date
do event
foi encontrado, found == true
, ou retornar uma nova matriz gerada pela concatenação do campo items
de matriz do estágio anterior com um novo campo de matriz contendo o event
elemento quando um elemento com o mesmo date
do event
não foi encontrado durante a redução iterações, found == false
.O código JavaScript a seguir apresenta a lógica acima mais próxima do que acontece no agregação pipeline.
1 let items = []; 2 3 if (result.found == true) { 4 items = result.items; 5 } else { 6 items = result.items.concat([event]); 7 }
O terceiro estágio,
$unset
, removerá apenas o campo result
que foi criado durante o primeiro estágio e usado no segundo estágio do pipeline.serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operação
Get Reports
. Cada intervalo de data terá o seguinte pipeline, com apenas a data utilizada na função buildId
sendo diferente:1 const pipeline = [ 2 { 3 $match: { 4 _id: { 5 $gte: buildId(request.key, Date.now() - oneYear), 6 $lte: buildId(request.key, Date.now()), 7 }, 8 }, 9 }, 10 { 11 $unwind: { 12 path: '$items', 13 }, 14 }, 15 { 16 $match: { 17 'items.date': { $gte: Date.now() - oneYear, $lt: Date.now() }, 18 }, 19 }, 20 { 21 $group: { 22 _id: null, 23 approved: { $sum: '$items.a' }, 24 noFunds: { $sum: '$items.n' }, 25 pending: { $sum: '$items.p' }, 26 rejected: { $sum: '$items.r' }, 27 }, 28 }, 29 { $project: { _id: 0 } }, 30 ];
Esse pipeline de agregação é o mesmo que
appV5R0
e appV5R1
. Uma explicação detalhada pode ser encontrada na seção anterior "Obter relatórios".Como essa implementação usará o
_id
campo para suas operações, ela não precisará de um índice extra para dar suporte às operações Bulk Upsert
Get Reports
e.Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections
appV5R2
com o esquema e a Bulk Upsert
função apresentados acima, além de apresentar os valores das versões anteriores, temos o collection stats
seguinte:coleção | Documentos | tamanho de dados | Tamanho do documento | Tamanho do armazenamento | Índices | Tamanho do Índice |
---|---|---|---|---|---|---|
appV1 | 359.639.622 | 39,58GB | 119B | 8.78GB | 2 | 20.06GB |
appV2 | 359.614.536 | 41.92GB | 126B | 10.46GB | 2 | 16.66GB |
appV3 | 359.633.376 | 28.7GB | 86B | 8.96GB | 2 | 16.37GB |
appV4 | 359.615.279 | 19.66GB | 59B | 6.69GB | 1 | 9.5GB |
appV5R0 | 95.350.431 | 19.19GB | 217B | 5.06GB | 1 | 2.95GB |
appV5R1 | 33.429.468 | 15.75GB | 506B | 4.04GB | 1 | 1.09GB |
appV5R2 | 33.429.649 | 11.96GB | 385B | 3.26GB | 1 | 1.16GB |
Calculando o
event stats
para appV5R2
e também apresentam os valores das versões anteriores, temos o seguinte:coleção | Tamanho dos Dados/ventos | Tamanho/ventos do Índice | Tamanho total/ventos |
---|---|---|---|
appV1 | 85B | 43.1B | 128.1B |
appV2 | 90B | 35.8B | 125.8B |
appV3 | 61.6B | 35.2B | 96.8B |
appV4 | 42.2B | 20.4B | 62.6B |
appV5R0 | 41.2B | 6.3B | 47.5B |
appV5R1 | 33.8B | 2.3B | 36.1B |
appV5R2 | 25.7B | 2.5B | 28.2B |
Analisando as tabelas acima, temos o resultado esperado apresentado na introdução, de
appV5R1
a appV5R2
. A única diferença notável é a 24redução de % no Data Size
.Essa redução no
Data Size
e Document
size
ajudará no desempenho de nosso aplicação , reduzindo o tempo gasto lendo o documento a partir do disco e o custo de processamento da descompactação do documento de seu estado compactado.Executando o teste de carga para
appV5R2
e plotando-o juntamente com os resultados para appV5R1
, temos os seguintes resultados para Get Reports
e Bulk Upsert
:![Gráfico mostrando as taxas de appV5R1 e appV5R2 ao executar o teste de carga para a funcionalidade Obter Relatórios.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fblt0ab9f95a434959d7%2F679a36ec1dbe446b64b11dd1%2Fimage3.png&w=3840&q=75)
![Gráfico mostrando as taxas de appV5R1 e appV5R2 ao executar o teste de carga para a funcionalidade Bulk Upsert.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fblt77daefad8abf9391%2F679a36ecb4ff913551e6b3b6%2Fimage4.png&w=3840&q=75)
Get Reports
e um pouco pior de desempenho no Bulk Upsert
ao passar de appV5R1
para appV5R2
. Reconheço que estava esperando mais.Para entender por que obtivemos esse resultado, precisamos levar em consideração as taxas nas quais cada operação é executada e seu custo de processamento. A taxa média do
Bulk Upsert
é de 625 operações por segundo e do Get Reports
é 137 de,5 operações por segundo. Com Bulk Upsert
sendo executado em 4 média,5 vezes mais do que Get Reports
, quando aumentamos sua complexidade usando o Computed Pattern
, com o objetivo de diminuir o custo de computação de Get Reports
, provavelmente mudamos seis para meia dezenas. O custo computacional geral provavelmente manteve-se inalterado.Para aqueles que já se depararam com a definição do
Computed Pattern
na documentação MongoDB, o resultado anterior não foi uma grande novidade. Ali, temos, “”If reads are significantly more common than writes, the computed pattern reduces the frequency of data computation. Em nosso caso, as escritas são muito mais comuns do que as leituras, então o Computed Pattern
pode não ser uma boa opção.Vamos tentar extrair mais desempenho de nosso aplicação procurando melhorias em nossas operações atuais. Analisando o pipeline de agregação de
Get Reports
, encontramos um antipadrão muito comum quando campos de array estão envolvidos. Esse antipadrão é o $unwind
seguido por um $match
, que acontece no segundo e no terceiro estágios de nossa agregação pipeline.Essa combinação de estágios pode prejudicar o desempenho do agregação pipeline porque estamos aumentando o número de documentos no pipeline com o estágio
$unwind
para, posteriormente, filtrar os documentos com $match
o. Em outras palavras, para chegar a um estado final com menos documentos, passaremos por um estado intermediário em que aumentamos o número de documentos.Na próxima revisão do aplicação , veremos como podemos alcançar o mesmo resultado final usando apenas um estágio e sem ter um estágio intermediário com mais documentos.
Conforme apresentado nos problemas e melhorias de
appV5R2
, temos um antipadrão no agregação pipeline de Get Reports
que pode reduzir o desempenho da query. Esse antipadrão écaracterizado por um $unwind
estágio seguido por um $match
. Essa combinação de etapas aumentará primeiro o número de documentos, $unwind
, para posteriormente filtrá-los, $match
. De forma simplificada, para chegar a um estado final, estamos passando por um estado intermediário caro.Uma solução possível para contornar esse padrão é usar o
$addFields
estágio com o $filter
operador no items
campo de array. Com essa combinação, substituímos o items
campo de array usando o $addFields
estágio por um novo campo de array gerado pelo $filter
operador na items
array, onde filtramos todos os elementos onde date
é dentro do intervalo de datas do relatório.Mas, considerando nosso pipeline de agregação com a otimização apresentada acima, existe uma solução ainda melhor. Com o
$filter
operador, percorreremos todos os elementos no items
campo e comparamos apenas suas datas com as datas do relatório para filtrar os elementos. Como o objetivo final de nossa query é obter os totais de status de todos os elementos dentro do intervalo de datas do relatório, em vez de apenas percorrer os elementos em items
para filtrá-los, já poderíamos começar a calcular os totais de status. Podemos obter essa funcionalidade usando o $reduce
operador em vez do $filter
.As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript
SchemaV5R0
denominado:1 type SchemaV5R0 = { 2 _id: Buffer; 3 items: Array<{ 4 date: Date; 5 a?: number; 6 n?: number; 7 p?: number; 8 r?: number; 9 }>; 10 };
Com base na especificação apresentada, temos a seguinte operação em massa
updateOne
para cada event
gerado pelo aplicação:1 const sumIfItemExists = itemsArray.buildResultIfItemExists(event); 2 const returnItemsOrCreateNew = itemsArray.buildItemsOrCreateNew(event); 3 const operation = { 4 updateOne: { 5 filter: { 6 _id: buildId(event.key, event.date), 7 }, 8 update: [ 9 { $set: { result: sumIfItemExists } }, 10 { $set: { items: returnItemsOrCreateNew } }, 11 { $unset: ['result'] }, 12 ], 13 upsert: true, 14 }, 15 };
Esta
updateOne
operação é igual ao appV5R2
. Uma explicação detalhada pode ser encontrada na seção anterior "Upsert em massa".serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para realizar a
Get Reports
operação:1 const pipeline = [ 2 { $match: docsFromKeyBetweenDate }, 3 { $addFields: itemsReduceAccumulator }, 4 { $group: groupSumStatus }, 5 { $project: { _id: 0 } }, 6 ];
O código completo para esse pipeline de agregação é bastante complicado. Por isso, teremos apenas um pseudocódigo para isso aqui. O código real que cria a operação pode ser encontrado neste arquivo Typescript e um exemplo de código completo pode ser encontrado neste arquivo Markdown.
O primeiro estágio,,
$match
e o último estágio,, $project
do agregação pipeline acima são iguais appV5R2
ao.Conforme apresentado na introdução desta revisão, e considerando o nome da variável no segundo estágio,
$addFields
, ele adicionará um novo campo ao documento chamado totals
que terá os totais de status para os elementos de items
campo de array onde o date
está no date
intervalo do relatório. Este totals
campo será gerado pelo $reduce
operador que terá uma lógica equivalente ao seguinte código JavaScript:1 const totals = items.reduce( 2 (accumulator, element) => { 3 if (element.date > (Date.now() - oneYear) && element.date < Date.now()) { 4 accumulator.a += element.a; 5 accumulator.n += element.n; 6 accumulator.p += element.p; 7 accumulator.r += element.r; 8 } 9 10 return accumulator; 11 }, 12 { a: 0, n: 0, p: 0, r: 0 } 13 );
A
reduce
operação terá como valor inicial um documento/ objeto com os campos de status possíveis definidos 0 como. À medida que a função itera sobre os elementos de items
, ela verificará se element
date
está na date
faixa do relatório. Se o element
date
pertencer ao date
intervalo do relatório, seus valores de status serão somados ao accumulator
status.O terceiro estágio,
$group
, terá uma lógica semelhante à que usamos até agora. A única diferença é que somaremos os valores presentes no campo totals
em vez de items
. O seguinte código é a lógica real usada no pipeline acima:1 const groupSumStatus = { 2 _id: null, 3 approved: { $sum: '$items.a' }, 4 noFunds: { $sum: '$items.n' }, 5 pending: { $sum: '$items.p' }, 6 rejected: { $sum: '$items.r' }, 7 };
Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections
appV5R3
com o esquema e a Bulk Upsert
função apresentados acima, além de apresentar os valores das versões anteriores, temos o collection stats
seguinte:coleção | Documentos | tamanho de dados | Tamanho do documento | Tamanho do armazenamento | Índices | Tamanho do Índice |
---|---|---|---|---|---|---|
appV1 | 359.639.622 | 39,58GB | 119B | 8.78GB | 2 | 20.06GB |
appV2 | 359.614.536 | 41.92GB | 126B | 10.46GB | 2 | 16.66GB |
appV3 | 359.633.376 | 28.7GB | 86B | 8.96GB | 2 | 16.37GB |
appV4 | 359.615.279 | 19.66GB | 59B | 6.69GB | 1 | 9.5GB |
appV5R0 | 95.350.431 | 19.19GB | 217B | 5.06GB | 1 | 2.95GB |
appV5R1 | 33.429.468 | 15.75GB | 506B | 4.04GB | 1 | 1.09GB |
appV5R2 | 33.429.649 | 11.96GB | 385B | 3.26GB | 1 | 1.16GB |
appV5R3 | 33.429.492 | 11.96GB | 385B | 3,24GB | 1 | 1.11GB |
Calculando o
event stats
para appV5R3
e também apresentam os valores das versões anteriores, temos o seguinte:coleção | Tamanho dos Dados/ventos | Tamanho/ventos do Índice | Tamanho total/ventos |
---|---|---|---|
appV1 | 85B | 43.1B | 128.1B |
appV2 | 90B | 35.8B | 125.8B |
appV3 | 61.6B | 35.2B | 96.8B |
appV4 | 42.2B | 20.4B | 62.6B |
appV5R0 | 41.2B | 6.3B | 47.5B |
appV5R1 | 33.8B | 2.3B | 36.1B |
appV5R2 | 25.7B | 2.5B | 28.2B |
appV5R3 | 25.7B | 2.4B | 28.1B |
Como o esquema do documento e as
Bulk Upsert
operações para appV5R3
são os mesmos que appV5R2
, não há nada sobre o que raciocinar nesta seção entre as duas revisões.Executando o teste de carga para
appV5R3
e plotando-o juntamente com os resultados para appV5R2
, temos os seguintes resultados para Get Reports
e Bulk Upsert
:![Gráfico mostrando as taxas de appV5R2 e appV5R3 ao executar o teste de carga para a funcionalidade Obter Relatórios.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fblt033dcf257404ae2b%2F679a36ec605c9bb7178ec5f0%2Fimage5.png&w=3840&q=75)
![Gráfico mostrando as taxas de appV5R2 e appV5R3 ao executar o teste de carga para a funcionalidade Bulk Upsert.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fblt0ac73749886011d1%2F679a36ec68a1029cf95ad57c%2Fimage6.png&w=3840&q=75)
appV5R3
é apenas um pouco melhor do que appV5R2
em algumas partes de ambas as funções, Get Reports
Bulk Upsert
e.Do ponto de vista lógico, é claro que o pipeline de agregação de
Get Reports
de appV5R3
é mais otimizado e menos intensivo em CPU e memória do que appV5R2
, mas não gerou resultados melhores. Isso pode ser explicado pelo gargalo atual da instância que executa o MongoDB, seu disco. Não entraremos em detalhes sobre isso para manter este artigo apenas grande, não enormes. Esse gargalo de disco ficará muito claro quando appV6R3
formos de para appV6R4
no próximo artigo.Para aqueles que estão preocupados em saber como podemos verificar ou validar se o disco é o fator limitante em nossa instância, o gráfico de
System CPU
no Atlas Metrics tem a IOWAIT
métrica, que mostra o quanto a CPU está esperando o disco. Nesta pasta do Github, você pode encontrar prints de algumas métricas para todos os testes de versão do aplicação e verificar as System CPU
prints para appV5R2
e appV5R3
e ver a IOWAIT
métrica atinge quase 15% do uso da CPU.Acabamos de ver que a limitação da nossa implementação é o disco. Para resolver isso, temos duas opções: atualizar o disco onde o MongoDB armazena dados ou alterar nossa implementação para reduzir o uso do disco.
Como o objetivo desta série é mostrar quanto desempenho podemos obter com o mesmo hardware modelando como nosso aplicação armazena e lê dados do MongoDB, não atualizaremos o disco. Uma alteração na modelagem do aplicação para o MongoDB será deixada para o próximo artigo,
appV6Rx
.appV5R4
Para ,Computed Pattern
duplicaremos o e pré-calcularemos os totais de status por trimestre, não apenas por dias. Embora saibamos que provavelmente não fornecerá melhor desempenho para os itens discutidos no "Resultado do teste de carga" de `appV5R2, vamos flexionar nosso conhecimento do MongoDB e do pipeline de agregação e também fornecer um exemplo de código de referência para os casos em que o Computed Pattern
é um bom ajuste.Conforme apresentado nas edições e melhorias de
appV5R3
, para esta revisão, duplicaremos o mesmo Computed Pattern
tendo boas pesquisas de que ele não proporcionará um desempenho melhor — mas, você sabe, para a ciência.Também usaremos o
Computed Pattern
para pré-calcular os totais de status para cada documento. Como cada documento armazena os eventos por trimestre e usuário, nosso aplicação terá em cada documento os totais de status por trimestre e usuário. Esses totais pré-calculados serão armazenados em um campo chamado totals
.Um ponto de atenção nessa implementação é que estamos adicionando um novo campo ao documento, o que também aumentará o tamanho médio do documento . Conforme visto na revisão anterior,
appV5R3
, nosso gargalo atual é o disco, outra indicação de que esta implementação não terá melhor desempenho.As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript
SchemaV5R1
denominado:1 type SchemaV5R1 = { 2 _id: Buffer; 3 totals: { 4 a?: number; 5 n?: number; 6 p?: number; 7 r?: number; 8 }; 9 items: Array<{ 10 date: Date; 11 a?: number; 12 n?: number; 13 p?: number; 14 r?: number; 15 }>; 16 };
Com base na especificação apresentada, temos a seguinte operação em massa
updateOne
para cada event
gerado pelo aplicação:1 const newReportFields = itemsArray.buildNewTotals(event); 2 const sumIfItemExists = itemsArray.buildResultIfItemExists(event); 3 const returnItemsOrCreateNew = itemsArray.buildItemsOrCreateNew(event); 4 const operation = { 5 updateOne: { 6 filter: { 7 _id: buildId(event.key, event.date), 8 }, 9 update: [ 10 { $set: newReportFields }, 11 { $set: { result: sumIfItemExists } }, 12 { $set: { items: returnItemsOrCreateNew } }, 13 { $unset: ['result'] }, 14 ], 15 upsert: true, 16 }, 17 };
Conforme já explicado em outro código complicado que tornaria o artigo um pouco trabalhoso de ler, aqui temos apenas um pseudocódigo da implementação. O código que cria a operação pode ser encontrado neste arquivo Typescript , e um exemplo de código completo pode ser encontrado neste arquivo Markdown.
A única diferença entre a
updateOne
operação acima e a para appV5R3
é o primeiro estágio, $set
. Este é o estágio em que pré-calcularemos o totals
campo para o trimestre para o qual o documento armazena eventos. O seguinte código JavaScript apresenta uma funcionalidade semelhante ao que acontece no estágio:1 if (totals.a != null) totals.a += event.a; 2 else totals.a = event.a; 3 4 if (totals.n != null) totals.n += event.n; 5 else totals.n = event.n; 6 7 if (totals.p != null) totals.p += event.p; 8 else totals.p = event.p; 9 10 if (totals.r != null) totals.r += event.r; 11 else totals.r = event.r;
Os últimos três estágios são estritamente iguais a, do
appV5R3
qual você pode ver uma explicação detalhada na operação "Bulk upsert".serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para realizar a
Get Reports
operação:1 const pipeline = [ 2 { $match: docsFromKeyBetweenDate }, 3 { $addFields: itemsReduceAccumulator }, 4 { $group: groupSumStatus }, 5 { $project: { _id: 0 } }, 6 ];
Conforme já explicado em outro código complicado que tornaria o artigo um pouco trabalhoso de ler, aqui temos apenas um pseudocódigo da implementação. O código que cria a operação pode ser encontrado neste arquivo Typescript , e um exemplo de código completo pode ser encontrado neste arquivo Markdown.
A maior parte da lógica do pipeline de agregação acima é igual à de
appV5R3
. Há uma pequena diferença: apenas uma if
lógica, no segundo estágio, $addFields
. Conforme visto em Get Reports explanation of appV5R3
, neste estágio criaremos um novo campo chamado totals
percorrendo os elementos do campo de array items
e adicionando o status. Em nossa implementação atual, já temos o totals
campo pré-computado no documento, portanto, se o intervalo de datas do trimestre estiver dentro dos limites do intervalo de datas do relatório, podemos usar o pré-computado .totals
O seguinte código JavaScript apresenta uma funcionalidade semelhante ao que acontece no estágio:1 let totals; 2 3 if (documentQuarterWithinReportDateRage) { 4 totals = document.totals; 5 } else { 6 totals = document.items.reduce( 7 (accumulator, element) => { 8 if (element.date > Date.now() - oneYear && element.date < Date.now()) { 9 accumulator.a += element.a; 10 accumulator.n += element.n; 11 accumulator.p += element.p; 12 accumulator.r += element.r; 13 } 14 15 return accumulator; 16 }, 17 { a: 0, n: 0, p: 0, r: 0 } 18 ); 19 }
Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections
appV5R4
com o esquema e a Bulk Upsert
função apresentados acima, além de apresentar os valores das versões anteriores, temos o collection stats
seguinte:coleção | Documentos | tamanho de dados | Tamanho do documento | Tamanho do armazenamento | Índices | Tamanho do Índice |
---|---|---|---|---|---|---|
appV1 | 359.639.622 | 39,58GB | 119B | 8.78GB | 2 | 20.06GB |
appV2 | 359.614.536 | 41.92GB | 126B | 10.46GB | 2 | 16.66GB |
appV3 | 359.633.376 | 28.7GB | 86B | 8.96GB | 2 | 16.37GB |
appV4 | 359.615.279 | 19.66GB | 59B | 6.69GB | 1 | 9.5GB |
appV5R0 | 95.350.431 | 19.19GB | 217B | 5.06GB | 1 | 2.95GB |
appV5R1 | 33.429.468 | 15.75GB | 506B | 4.04GB | 1 | 1.09GB |
appV5R2 | 33.429.649 | 11.96GB | 385B | 3.26GB | 1 | 1.16GB |
appV5R3 | 33.429.492 | 11.96GB | 385B | 3.24GB | 1 | 1.11GB |
appV5R4 | 33.429.470 | 12.88GB | 414B | 3.72GB | 1 | 1.24GB |
Calculando o
event stats
para appV5R3
e também apresentam os valores das versões anteriores, temos o seguinte:coleção | Tamanho dos Dados/ventos | Tamanho/ventos do Índice | Tamanho total/ventos |
---|---|---|---|
appV1 | 85B | 43.1B | 128.1B |
appV2 | 90B | 35.8B | 125.8B |
appV3 | 61.6B | 35.2B | 96.8B |
appV4 | 42.2B | 20.4B | 62.6B |
appV5R0 | 41.2B | 6.3B | 47.5B |
appV5R1 | 33.8B | 2.3B | 36.1B |
appV5R2 | 25.7B | 2.5B | 28.2B |
appV5R3 | 25.7B | 2.4B | 28.1B |
appV5R4 | 27.7B | 2.7B | 30.4B |
Conforme discutido nesta introdução de revisão, o
totals
campo adicional em cada documento na coleção aumentou o tamanho do documento e o tamanho geral do armazenamento. O Data Size
de appV5R4
é,7 7% maior que appV5R3
e o Total Size/events
é,8 2%. Como o disco é nosso fator limitante, o desempenho do appV5R4
provavelmente será pior do que appV5R3
.Executando o teste de carga para
appV5R4
e plotando-o juntamente com os resultados para appV5R3
, temos os seguintes resultados para Get Reports
e Bulk Upsert
:![Gráfico mostrando as taxas de appV5R3 e appV5R4 ao executar o teste de carga para a funcionalidade Obter Relatórios.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fbltc3f01b064ce11d69%2F679a36ed925b1637701df35e%2Fimage7.png&w=3840&q=75)
![Gráfico mostrando as taxas de appV5R3 e appV5R4 ao executar o teste de carga para a funcionalidade Obter Relatórios.](/developer/_next/image/?url=https%3A%2F%2Fimages.contentstack.io%2Fv3%2Fassets%2Fblt39790b633ee0d5a7%2Fblt3239f4fc3a15ae02%2F679a36ed2408e4d328d5f730%2Fimage8.png&w=3840&q=75)
appV5R4
é pior do que appV5R3
em ambas as funções, Get Reports
e Bulk Upsert
.Conforme mostrado nos problemas e melhorias anteriores , para melhorar o desempenho do nosso aplicativo, precisamos alterar a implementação do MongoDB de uma forma que reduza o uso do disco. Para isso, precisamos reduzir o tamanho do documento .
Você pode pensar que não é possível reduzir o tamanho do documento e o tamanho geral da collection/índice ainda mais porque já estamos usando apenas um índice, concatenando dois campos em um, usando nomes de campo abreviados e usando
Bucket Pattern
o. Mas há uma coisa chamada Dynamic Schema
que pode nos ajudar.No
Dynamic Schema
, os valores de um campo se tornam nomes de campo . Assim, os nomes dos campo também armazenam dados e, como consequência, reduzem o tamanho do documento . Como esse padrão exigirá grandes alterações em nosso esquema atual do aplicação , iniciaremos uma nova versão, appV6Rx
, com a qual trabalharemos na terceira parte desta série.Esse é o fim da segunda parte da série. Cobrimos
Bucket Pattern
e Computed Pattern
e as muitas maneiras de usar esses padrões para modelar como nosso aplicação armazena seus dados no MongoDB e os grandes ganhos de desempenho que eles podem fornecer quando usados corretamente.Aqui está uma rápida revisão das melhorias feitas entre a versão do aplicação :
appV4
aappV5R0/appV5R1
: essa é a implementação mais simples possível deBucket Pattern
, agrupandoevents
por mês paraappV5R0
e por trimestre paraappV5R1
.appV5R1
aappV5R2
: em vez de apenas enviar oevent
documento para aitems
array, passamos a pré-calcular os totais de status por dia, usando oComputed Pattern
.appV5R2
aappV5R3
: isso melhorou o pipeline de agregação paraGet Reports
, evitando um estágio intermediário caro. Ela não forneceu melhorias de desempenho porque nossa instância do MongoDB está atualmente limitada por disco.appV5R3
aappV5R4
:Computed Pattern
Dobramos para pré-calcular ototals
campo, embora saibamos que o desempenho não seria melhor — mas apenas para a ciência.
Obtivemos melhorias notáveis na versão apresentada nesta segunda parte da série em comparação com as versões da primeira parte da série.
appV0
a appV4
. appV5R3
exibiu o melhor desempenho de todos, mas ainda não conseguiu atingir todas as taxas desejadas. Para a terceira e última versão desta série, as versões do nosso aplicação serão criadas em torno do Dynamic Schema Pattern
, o que reduzirá o tamanho geral do documento e ajudará na limitação atual do disco.Para ter mais dúvidas, você pode acessar o Fórum da MongoDB Community ou, se quiser criar seu aplicação usando o MongoDB, o MongoDB Developer Center tem muitos exemplos e tutoriais em diversas linguagens de programação.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.