Quão eficiente pode ser o Meteor ao compartilhar uma enorme coleção entre muitos clientes?

100

Imagine o seguinte caso:

  • 1.000 clientes estão conectados a uma página Meteor exibindo o conteúdo da coleção "Somestuff".

  • "Somestuff" é uma coleção com 1.000 itens.

  • Alguém insere um novo item na coleção "Somestuff"

O que vai acontecer:

  • Todos os Meteor.Collections em clientes serão atualizados, ou seja, a inserção será encaminhada para todos eles (o que significa uma mensagem de inserção enviada para 1.000 clientes)

Qual é o custo em termos de CPU do servidor para determinar qual cliente precisa ser atualizado?

É verdade que apenas o valor inserido será encaminhado aos clientes, e não toda a lista?

Como isso funciona na vida real? Existem benchmarks ou experimentos dessa escala disponíveis?

Flavien Volken
fonte

Respostas:

119

A resposta curta é que apenas novos dados são enviados pela rede. É assim que funciona.

Existem três partes importantes do servidor Meteor que gerenciam as assinaturas: a função de publicação , que define a lógica de quais dados a assinatura fornece; o driver Mongo , que observa o banco de dados em busca de alterações; e a caixa de mesclagem , que combina todas as assinaturas ativas de um cliente e as envia pela rede para o cliente.

Publicar funções

Cada vez que um cliente Meteor se inscreve em uma coleção, o servidor executa uma função de publicação . O trabalho da função de publicação é descobrir o conjunto de documentos que seu cliente deve ter e enviar cada propriedade do documento para a caixa de mesclagem. Ele é executado uma vez para cada novo cliente assinante. Você pode colocar qualquer JavaScript que desejar na função de publicação, como o uso de controle de acesso arbitrariamente complexo this.userId. A função de publicação envia dados para a caixa de mesclagem chamando this.added, this.changede this.removed. Veja a documentação de publicação completa para mais detalhes.

A maioria dos publicar funções não tem que mexer com o de baixo nível added, changede removedAPI, no entanto. Se a publicar função retorna um cursor Mongo, o servidor Meteor conecta automaticamente a saída do controlador de Mongo ( insert, updatee removedchamadas de retorno) para a entrada da caixa de merge ( this.added, this.changede this.removed). É muito legal que você possa fazer todas as verificações de permissão antecipadamente em uma função de publicação e, em seguida, conectar diretamente o driver de banco de dados à caixa de mesclagem sem nenhum código de usuário no caminho. E quando a publicação automática está ativada, até mesmo este pequeno detalhe fica oculto: o servidor configura automaticamente uma consulta para todos os documentos em cada coleção e os coloca na caixa de mesclagem.

Por outro lado, você não está limitado a publicar consultas de banco de dados. Por exemplo, você pode escrever uma função de publicação que lê uma posição GPS de um dispositivo dentro de um Meteor.setIntervalou pesquisa uma API REST legada de outro serviço da web. Nesses casos, você emitem alterações na caixa de junção, chamando o de baixo nível added, changede removedDDP API.

O motorista Mongo

O trabalho do driver Mongo é observar o banco de dados Mongo em busca de alterações nas consultas ao vivo. Essas consultas funcionar continuamente e voltar atualizações como a mudança resultados chamando added, removede changedretornos de chamada.

Mongo não é um banco de dados em tempo real. Então, o motorista faz pesquisas. Ele mantém uma cópia na memória do último resultado da consulta para cada consulta ativa ativa. Em cada ciclo de sondagem, ele compara o novo resultado com o resultado anterior salvo, computando o conjunto mínimo de added, removede changed eventos que descrevem a diferença. Se vários chamadores registrarem retornos de chamada para a mesma consulta ao vivo, o driver observará apenas uma cópia da consulta, chamando cada retorno de chamada registrado com o mesmo resultado.

Cada vez que o servidor atualiza uma coleção, o driver recalcula cada consulta ativa nessa coleção (versões futuras do Meteor irão expor uma API de escalonamento para limitar quais consultas ao vivo recalculam na atualização.) O driver também consulta cada consulta ativa em um cronômetro de 10 segundos para capturar atualizações de banco de dados fora da banda que contornaram o servidor Meteor.

A caixa de mesclagem

O trabalho da caixa de junção é combinar os resultados ( added, changede removed chamadas) de todos os ativos publicar funções de um cliente em um único fluxo de dados. Há uma caixa de mesclagem para cada cliente conectado. Ele contém uma cópia completa do cache de minimongo do cliente.

Em seu exemplo, com apenas uma única assinatura, a caixa de mesclagem é essencialmente uma passagem. Mas um aplicativo mais complexo pode ter várias assinaturas que podem se sobrepor. Se duas assinaturas definirem o mesmo atributo no mesmo documento, a caixa de mesclagem decide qual valor tem prioridade e apenas o envia ao cliente. Ainda não expusemos a API para definir a prioridade de assinatura. Por enquanto, a prioridade é determinada pelo pedido em que o cliente assina os conjuntos de dados. A primeira assinatura que um cliente faz tem a prioridade mais alta, a segunda assinatura é a próxima mais alta e assim por diante.

Como a caixa de mesclagem contém o estado do cliente, ela pode enviar a quantidade mínima de dados para manter cada cliente atualizado, independentemente de qual função de publicação o alimenta.

O que acontece em uma atualização

Portanto, agora preparamos o cenário para o seu cenário.

Temos 1.000 clientes conectados. Cada um está inscrito na mesma consulta Mongo ao vivo ( Somestuff.find({})). Como a consulta é a mesma para cada cliente, o driver está executando apenas uma consulta ativa. Existem 1.000 caixas de mesclagem ativas. E a função de publicação de cada cliente registrou um added, changede removednaquela consulta ativa que alimenta uma das caixas de mesclagem. Nada mais está conectado às caixas de mesclagem.

Primeiro, o driver Mongo. Quando um dos clientes insere um novo documento Somestuff, ele dispara uma recomputação. O driver Mongo executa novamente a consulta para todos os documentos em Somestuff, compara o resultado com o resultado anterior na memória, descobre que há um novo documento e chama cada um dos 1.000 insertretornos de chamada registrados .

Em seguida, as funções de publicação. Há muito pouca coisa acontecendo aqui: cada um dos 1.000 insertretornos de chamada envia dados para a caixa de mesclagem chamando added.

Por fim, cada caixa de mesclagem verifica esses novos atributos em relação à sua cópia na memória do cache do cliente. Em cada caso, ele descobre que os valores ainda não estão no cliente e não ocultam um valor existente. Portanto, a caixa de mesclagem emite uma DATAmensagem DDP na conexão SockJS para seu cliente e atualiza sua cópia na memória do lado do servidor.

O custo total da CPU é o custo para comparar uma consulta Mongo, mais o custo de 1.000 caixas de mesclagem verificando o estado de seus clientes e construindo uma nova carga útil de mensagem DDP. Os únicos dados que fluem pela conexão são um único objeto JSON enviado a cada um dos 1.000 clientes, correspondendo ao novo documento no banco de dados, mais uma mensagem RPC para o servidor do cliente que fez a inserção original.

Otimizações

Aqui está o que definitivamente planejamos.

  • Driver Mongo mais eficiente. Nós otimizamos o motorista em 0.5.1 para executar apenas um único observador por consulta distinta.

  • Nem toda mudança no banco de dados deve acionar uma recomputação de uma consulta. Podemos fazer algumas melhorias automatizadas, mas a melhor abordagem é uma API que permite ao desenvolvedor especificar quais consultas precisam ser executadas novamente. Por exemplo, é óbvio para um desenvolvedor que inserir uma mensagem em uma sala de chat não deve invalidar uma consulta ao vivo para as mensagens em uma segunda sala.

  • O driver Mongo, a função de publicação e a caixa de mesclagem não precisam ser executados no mesmo processo ou mesmo na mesma máquina. Alguns aplicativos executam consultas dinâmicas complexas e precisam de mais CPU para monitorar o banco de dados. Outros têm apenas algumas consultas distintas (imagine um mecanismo de blog), mas possivelmente muitos clientes conectados - eles precisam de mais CPU para caixas de mesclagem. Separar esses componentes nos permitirá dimensionar cada peça independentemente.

  • Muitos bancos de dados oferecem suporte a gatilhos que disparam quando uma linha é atualizada e fornecem as linhas novas e antigas. Com esse recurso, um driver de banco de dados pode registrar um gatilho em vez de pesquisar as alterações.

Debergalis
fonte
Existe algum exemplo de como usar Meteor.publish para publicar dados que não são do cursor? Como resultados de uma API de resto legada mencionada na resposta?
Tony
@Tony: Está na documentação. Verifique o exemplo de contagem de salas.
Mitar de
É importante notar que nas versões 0.7, 0.7.1, 0.7.2 Meteor mudou para OpLog Observe Driver para a maioria das consultas (as exceções são skip, $neare $wherecontendo consultas) que é muito mais eficiente na carga da CPU, largura de banda da rede e permite escalar a aplicação servidores.
imslavko
E quando nem todos os usuários veem os mesmos dados. 1. eles se inscreveram para diferentes tópicos .2. eles têm funções diferentes, portanto, dentro do mesmo tópico principal, há algumas mensagens que não deveriam chegar até eles.
tgkprog
@debergalis em relação à invalidação de cache, talvez você encontre ideias em meu artigo vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf que vale a pena
qbolec
29

Pela minha experiência, usar muitos clientes enquanto compartilha uma coleção enorme no Meteor é essencialmente impraticável, a partir da versão 0.7.0.1. Vou tentar explicar o porquê.

Conforme descrito na postagem acima e também em https://github.com/meteor/meteor/issues/1821 , o servidor do meteoro deve manter uma cópia dos dados publicados para cada cliente na caixa de mesclagem . Isso é o que permite que a mágica do Meteor aconteça, mas também resulta em qualquer grande banco de dados compartilhado sendo repetidamente mantido na memória do processo do nó. Mesmo ao usar uma possível otimização para coleções estáticas como em ( Existe uma maneira de dizer ao meteoro que uma coleção é estática (nunca mudará)? ), Tivemos um grande problema com o uso de CPU e memória do processo Node.

Em nosso caso, estávamos publicando uma coleção de 15k documentos para cada cliente que era completamente estático. O problema é que copiar esses documentos para uma caixa de mesclagem do cliente (na memória) na conexão basicamente trouxe o processo do Node para 100% da CPU por quase um segundo e resultou em um grande uso adicional de memória. Isso é inerentemente não escalável, porque qualquer cliente conectado deixará o servidor de joelhos (e as conexões simultâneas se bloquearão) e o uso de memória aumentará linearmente no número de clientes. Em nosso caso, cada cliente causou um uso adicional de ~ 60 MB de memória, embora os dados brutos transferidos fossem de apenas 5 MB.

Em nosso caso, como a coleção era estática, resolvemos esse problema enviando todos os documentos como um .jsonarquivo, que foi compactado pelo nginx, e carregando-os em uma coleção anônima, resultando em apenas uma transferência de dados de aproximadamente 1 MB sem CPU adicional ou memória no processo do nó e um tempo de carregamento muito mais rápido. Todas as operações sobre esta coleção foram feitas usando _ids de publicações muito menores no servidor, permitindo reter a maioria dos benefícios do Meteor. Isso permitiu que o aplicativo se expandisse para muitos mais clientes. Além disso, como nosso aplicativo é principalmente somente leitura, melhoramos ainda mais a escalabilidade executando várias instâncias do Meteor atrás do nginx com balanceamento de carga (embora com um único Mongo), já que cada instância do Node é de thread único.

No entanto, a questão de compartilhar grandes coleções graváveis ​​entre vários clientes é um problema de engenharia que precisa ser resolvido pelo Meteor. Provavelmente existe uma maneira melhor do que manter uma cópia de tudo para cada cliente, mas isso requer uma reflexão séria como um problema de sistemas distribuídos. Os problemas atuais de uso massivo de CPU e memória simplesmente não escalam.

Andrew Mao
fonte
@Harry oplog não importa nesta situação; os dados eram estáticos.
Andrew Mao,
Por que não faz diffs das cópias minimongo do lado do servidor? Talvez tudo isso tenha mudado no 1.0? Quero dizer, geralmente eles são os mesmos que eu espero, mesmo as funções que ele chama de volta seriam semelhantes (se eu estou seguindo que para é algo que está armazenado lá também e potencialmente diferente.)
MistereeDevlord
@MistereeDevlord Diferença de alterações e caches de dados do cliente são separados agora. Mesmo que todos tenham os mesmos dados e apenas um diff seja necessário, o cache por cliente é diferente porque o servidor não pode tratá-los de forma idêntica. Isso definitivamente poderia ser feito de forma mais inteligente em relação à implementação existente.
Andrew Mao
@AndrewMao Como você se certifica de que os arquivos gzipados estão protegidos ao enviá-los para o cliente, ou seja, apenas um cliente conectado pode acessá-los?
FullStack
4

A experiência que você pode usar para responder a esta pergunta:

  1. Instale um meteoro de teste: meteor create --example todos
  2. Execute-o no inspetor Webkit (WKI).
  3. Examine o conteúdo das mensagens XHR que passam pela rede.
  4. Observe que a coleção inteira não é movida pelo fio.

Para obter dicas sobre como usar o WKI, consulte este artigo . Está um pouco desatualizado, mas ainda é válido, especialmente para esta questão.

javajosh
fonte
2
Uma explicação do mecanismo de votação: eventedmind.com/posts/meteor-liveresultsset
cmather