Expansão do LunaDb, o nosso sistema interno de carregamento de dados declarativos

Retrato de contribuidor da equipe AsanaArvind Vijayakumar
5 de março de 2026
16 minutos de leitura
facebookx-twitterlinkedin
Gemini said Two female professionals collaborating at a desk in a bright, modern office. One woman sits while the other leans in, smiling and using the computer mouse, with a male colleague working in the background.

Aqui na Asana, nós criamos um sistema de carregamento de dados chamado LunaDb, que serve como a coluna vertebral do nosso aplicativo Web. Apesar do nome, não é um banco de dados. Na verdade, é um sistema semelhante ao GraphQL para obter dados de forma declarativa — basicamente, uma maneira de carregar a versão mais recente dos dados e todas as atualizações futuras.

Inicialmente, lançamos o LunaDb em 2015 como uma reescrita radical da nossa infraestrutura de back-end¹. O componente central deste novo sistema era o servidor de sincronização, um monólito que executa tudo, desde a sincronização do cliente até o carregamento de dados e o controle de acesso. Sem mudanças significativas, essa arquitetura inicial foi dimensionada muito além dos níveis inicialmente esperados, chegando a milhões de usuários ativos semanalmente e bilhões de consultas diárias. 

Embora o desempenho permanecesse forte, à medida que o tráfego e a complexidade dos recursos aumentavam, tornava-se cada vez mais difícil operar e melhorar o LunaDb devido às restrições do servidor de sincronização.

Visão geral da nossa infraestrutura de carregamento de dados

visão geral da nossa infraestrutura de carregamento de dados

Por que era difícil de operar?

A alternância de tráfego é cara

O servidor de sincronização gerenciava diretamente as conexões websocket persistentes com os clientes. Cada WebSocket era respaldado por uma sessão de cliente com estado. Quando uma conexão era interrompida, todo esse estado era descartado e o cliente tinha que se inscrever novamente para receber todos os dados de que precisava. Quando isso acontece com muitas sessões, o custo aumenta rapidamente. Portanto, tivemos que ter cuidado ao deslocar essas conexões, dado o grande aumento do trabalho decorrente das reconexões.

alternância de tráfego

alternância de tráfego

Implementar servidores de sincronização significa alternar o tráfego

É claro que você não pode impedir perpetuamente a mudança de tráfego. Sempre que você quiser enviar um novo código ou aumentar/diminuir a escala, será necessário desativar os servidores de sincronização, e isso requer a transferência de todo o tráfego das instâncias de terminação.

atualizações contínuas

atualizações contínuas

Os servidores de sincronização não têm bom desempenho na inicialização

Ao mesmo tempo, os servidores de sincronização só se tornariam eficientes após uma quantidade não trivial de aquecimento do processo. A gestão desses dois problemas tem sido um equilíbrio bastante frágil e exigiu um extenso trabalho de engenharia no passado.

Os servidores de sincronização são complexos e difíceis de monitorar

Por fim, como os servidores de sincronização executavam muitos bits arbitrários de código do produto (por meio de funções personalizadas do lado do servidor), eles eram muito vulneráveis a regressões de desempenho baseadas em “vizinhos barulhentos” que eram difíceis de atribuir². 

o problema do vizinho barulhento

o problema do vizinho barulhento


Uma pergunta razoável a ser feita é: “Por que os servidores de sincronização são complexos e não têm bom desempenho na inicialização?”

Uma causa comum desses problemas é o nosso código de produto do lado do servidor. Os principais trechos do código do servidor de sincronização são escritos em Scala.  Apesar de algumas complexidades relacionadas ao gerenciamento do estado da sessão e aos vários aspectos da estrutura Luna, este código de estrutura/plataforma se comporta principalmente como esperamos (há relativamente poucos problemas operacionais e de desempenho). 

Por outro lado, esses valores computados pelo servidor do produto (nós os chamamos de SCVs, mas pense em resolvedores personalizados) são escritos em Typescript. Ambos os conjuntos de código são executados juntos no GraalVM, uma VM poliglota que permite o uso de múltiplas linguagens por meio do seu framework Truffle. Como são escritos em TypeScript, os SCVs são essencialmente interpretados na inicialização, o que previsivelmente resulta em desempenho e uso de CPU inaceitáveis. O GraalVM tentará realizar a compilação just-in-time nos SCVs invocados. Isso é bom! O GraalVM/Truffle pode otimizar muito o desempenho deles, mas isso não é gratuito. A compilação de SCV pode ser bastante cara (em CPU, cache de código, etc.). 

a configuração da nossa VM poliglota

a nossa configuração de VM poliglota

Por que as duas linguagens?

Nosso primeiro design para SCVs foi inteiramente em Scala. Por outro lado, os nossos sistemas de mutação e de tarefas assíncronas são escritos em JavaScript/TypeScript. Embora os SCVs baseados em Scala funcionassem, a duplicação da lógica empresarial entre os nossos sistemas de mutação e de tarefas assíncronas e o LunaDb, bem como a falta de familiaridade dos engenheiros de produtos com o Scala, tornou-se um grande obstáculo para a velocidade do produto. 

Por que usar o GraalVM?

Armazenamos em cache muitos dados em processo para acelerar o processamento (e o reprocessamento) dos resultados da assinatura. O uso do GraalVM nos oferece uma maneira simples de compartilhar esses caches entre linguagens sem as preocupações de correção ou desempenho que poderiam surgir ao dividir as partes em Scala e TypeScript em contêineres separados.


Por que era difícil melhorar?

Como o servidor fazia muitas coisas e era relativamente frágil de operar, tendíamos a evitar mudanças maiores. Não apenas devido à complexidade do código, mas também à alta sobrecarga para implementar novas mudanças com segurança.

Como resolvemos as coisas?

Sim, gosto de fazer perguntas

Dadas as dificuldades de operar e aprimorar o servidor de sincronização, tomamos a difícil decisão de mudar a nossa arquitetura. Basicamente, decidimos substituir o servidor de sincronização monolítico por dois tipos menores de componentes:

  • um agente de sessão que gerencia as conexões do cliente e a resolução de estado

  • um carregador sincronizável responsável pelo carregamento de dados, ou seja, pelo atendimento de consultas dos agentes de sessão

agentes de sessão e carregadores sincronizáveis

agentes de sessão e carregadores sincronizáveis

Por que isso ajuda?

Imediatamente, esta nova arquitetura separa a mudança do tráfego de WebSocket (ou seja, a implantação de agentes de sessão) do aquecimento de novos processos (ou seja, a implantação de carregadores sincronizáveis). Como resultado, podemos minimizar as interrupções ao implementar os carregadores sincronizáveis separadamente dos agentes de sessão.

Cada um desses novos componentes é mais simples.

  • Os agentes de sessão são muito mais leves, não exigem aquecimento de processos e não executam nenhum código de produto. Consequentemente, não precisamos implantá-los com tanta frequência — e, quando o fazemos, é bastante simples.

  • Os carregadores sincronizáveis têm uma interface mais simples (solicitações de assinatura sem estado) que é mais fácil de adaptar ao autoscaling horizontal de pods padrão do Kubernetes. Isso também torna mais rápido e simples aquecê-los — podemos simplesmente usar o espelhamento de tráfego nas solicitações do ambiente que fluem entre os agentes de sessão e os carregadores sincronizáveis

A nova arquitetura nos permite simplificar muito o processo de desenvolvimento de produtos. Desde o início, os cronogramas de implementação independentes do código do produto do lado do servidor e da chamada do código do produto do lado do cliente têm sido um obstáculo à velocidade e uma fonte de trabalho operacional (devido à incompatibilidade de versões). Como os carregadores sincronizáveis são agora o único processo restante que executa o código do produto e a sua implementação já não causa interrupções, podemos reimplementá-los sempre que enviarmos um novo código do produto.

Esta nova arquitetura nos permite escalar melhor para novos recursos, implementando diferentes pools de carregadores sincronizáveis para diferentes tipos de carga de trabalho (como recursos distintos, por exemplo, Caixa de entrada, Tarefa, Metas, etc.). O agente de sessão funciona como um gateway de serviço que pode controlar diretamente como as consultas de dados são encaminhadas para diferentes carregadores sincronizáveis upstream.

Quais são os principais desafios de design?

Ótimo! Essa nova arquitetura parece muito melhor, mas como a tornamos realidade? O servidor de sincronização é basicamente um monólito, pois engloba várias funções, e dividir monólitos quase sempre é complicado. No nosso caso, tivemos de superar alguns obstáculos importantes de design. 

Divisão do PubSub

O PubSub, nosso sistema para implementar a reatividade, foi projetado em torno de um único processo (o servidor de sincronização) responsável por carregar novos dados e enviá-los ao cliente. Tivemos que redesenhar o PubSub de forma a garantir a correção nesses dois tipos de processos agora independentes (carregadores sincronizáveis e agentes de sessão). 

Vamos analisar brevemente como ele é implementado nos servidores de sincronização. Observação: pode ser útil ler a nossa publicação anterior sobre o pipeline de invalidação, mas forneceremos uma visão do sistema sem pré-requisitos.

No servidor de sincronização, acompanhamos as assinaturas por sessão. Usamos o pipeline de invalidação para monitorar continuamente as assinaturas em busca de atualizações. A cada nova mensagem de invalidação, o servidor de sincronização recarregará todas as assinaturas afetadas. 

Os servidores de sincronização armazenam em cache, de forma intensiva, os resultados de objetos/consultas do banco de dados, os resultados do resolvedor personalizado e os resultados de assinaturas anteriores para otimizar as recarregadas de assinaturas (ou seja, usando um padrão de leitura direta). Os artefatos em cache são invalidados passivamente pelo mesmo pipeline de invalidação usado para as assinaturas. Sempre que tentarmos usar dados em cache, verificaremos sua validade e recorreremos ao recálculo, se necessário.

Podemos observar uma clara dependência entre recarregar assinaturas e invalidar dados em cache. Ao receber uma invalidação, se recarregarmos uma assinatura antes que os dados em cache tenham sido invalidados, potencialmente calcularemos um resultado obsoleto. Quando o carregamento de dados e o gerenciamento de assinaturas acontecem no mesmo processo, é simples garantir essa dependência: basta invalidar os caches antes de recarregar. 

A condição de corrida pode gerar dados desatualizados na atualização

A condição de corrida pode gerar dados desatualizados na atualização

Na nossa nova arquitetura proposta, os intermediários de sessão e os carregadores sincronizáveis são consumidores independentes do pipeline de invalidação. Como, então, podemos garantir que os caches sejam invalidados antes que as assinaturas sejam recarregadas?

Versionamento de solicitação e resposta

Poderíamos ter resolvido isso fazendo com que o pipeline de invalidação entregasse mensagens em sincronia. Ou poderíamos ter criado um mecanismo para impor a garantia de ordenação de que nenhuma mensagem do pipeline de invalidação chegasse ao agente de sessão antes do carregador sincronizável. No entanto, ambas as soluções tinham desvantagens que não eram ideais: fundamentalmente, elas aumentavam o acoplamento dos brokers de sessão e dos carregadores sincronizáveis.

Em vez disso, resolvemos esse problema aumentando o nosso protocolo de carregamento de dados com versões de solicitação e resposta com base no seu progresso relativo nos fluxos de invalidação. Como o fluxo representa uma ordenação total das atualizações nos bancos de dados, o progresso do nosso fluxo pode ser usado como um contador de versão global.

versões de solicitação e resposta

versões de solicitação e resposta

Recarregamentos de invalidação

Os servidores de sincronização carregam uma grande quantidade de dados — a maioria de todas as leituras para o site. A nossa nova arquitetura exige que os session brokers e o syncable loader troquem muitos dados pela rede. Para novas assinaturas, essa sobrecarga de rede é relativamente insignificante. No entanto, isso é particularmente ineficiente para recarregamentos de invalidação, pois muitas vezes não precisamos retornar a resposta completa, apenas os dados atualizados³. Certos casos são particularmente ruins nesse aspecto. Imagine um caso em que um usuário paginou 10.000 tarefas em um projeto e outro usuário está constantemente alterando as descrições das tarefas neste projeto: todas as tarefas precisariam ser enviadas a cada invalidação! Claramente, o ideal é enviar de volta apenas os dados atualizados, mas como implementar isso de forma eficiente?

recarregamentos de invalidação

recarregamentos de invalidação

Impressão digital

Para que o carregador sincronizável calcule o delta dos dados atualizados, ele precisa saber quais dados o solicitante já possui. Mas passar os dados mais recentes com a solicitação seria tão oneroso quanto retornar o resultado completo.  Precisamos representar os dados de uma maneira mais eficiente em termos de espaço.

Bem, o hash é uma ótima forma de economizar espaço. Cada bit de dados granulares que nos importa é chamado de syncable. Podemos calcular um hash murmur de 128 bits de cada syncable serializado para usar como uma impressão digital ⁴. Especificamente, esta impressão digital é um identificador para essa versão do syncable.

Onde quer que rastreemos dados sincronizáveis, podemos usar suas impressões digitais. Agora, quando quisermos monitorar uma resposta completa de assinatura, podemos simplesmente usar um conjunto de impressões digitais sem ter que transmitir todos os dados!


Observação: O que é um syncable e como ele se relaciona com as assinaturas?

Os syncables são o conteúdo do resultado de uma assinatura. Quando carregamos uma assinatura, os resultados são retornados como um conjunto de syncables. Mais especificamente, um syncable pode ser um objeto, uma consulta ou um resultado SCV.

mapeamento de sincronizáveis para assinatura

mapeamento de sincronizáveis para assinatura

Claramente, cada assinatura é mapeada para vários syncables. No entanto, os sincronizáveis podem ser compartilhados por múltiplas assinaturas (quando carregam dados sobrepostos). Portanto, na verdade, há um mapeamento muitos-para-muitos entre assinaturas e syncables.


Passamos o conjunto dessas impressões digitais com cada solicitação do agente de sessão. No carregador de sincronizáveis, calculamos a resposta completa, calculamos o seu conjunto de impressões digitais, excluímos quaisquer dados que se sobreponham à solicitação e retornamos o delta.

Como fazemos isso?

Dado o tamanho e a criticidade da mudança, dividimos a implementação em aproximadamente 4 etapas. 

Etapa 1 - Refatoração do monólito

  • Dividir o nosso código de gestão de sessões e carregamento de dados altamente acoplado em componentes independentes

Etapa 2 - Carregador sincronizável local

  • Usar o novo componente de carregamento de dados para criar um servidor gRPC local e migrar o carregamento de dados

estágios 1–2

Etapa 3 - Syncable-loader remoto

  • Criar um novo binário e implantação do syncable-loader

  • Migrar todo o carregamento de dados do sync-server para a nossa nova implementação do syncable-loader

Etapa 4 - Binário separado do session-broker

  • Criar um novo binário e implantação do session-broker

  • Migrar todo o tráfego dos servidores de sincronização para os agentes de sessão

estágios 3–4

Quais foram os desafios enfrentados?

Muitos. Por onde começar?

Respostas grandes

Um problema que encontramos rapidamente foi o grande tamanho das respostas. Como o carregamento de dados nos servidores de sincronização ocorria dentro do mesmo processo, isso não tinha sido um grande problema até agora⁵. No entanto, quando começamos a carregar dados através de um limite local do gRPC, passamos a encontrar muitos problemas. 

O tempo todo, suspeitávamos que algumas respostas pudessem ser grandes, mas, quando começamos a investigar isso, encontramos resultados realmente surpreendentes. Tínhamos milhares de carregamentos por dia que regularmente ultrapassavam 100 MiB! Fisicamente, não podíamos retornar respostas tão grandes por meio de um método gRPC unário (você começa a atingir o tamanho máximo de quadro http2). O que fazer?

Consideramos algumas maneiras de corrigir isso sistematicamente, mas, no fim, concluímos que precisávamos abordar as causas principais. Poderíamos ter implementado o streaming do gRPC no lado do servidor, mas os altos custos de serialização incorridos e a elevada contenção de socket seriam bastante regressivos para a latência e a taxa de transferência. Poderíamos simplesmente rejeitar essas respostas, mas a taxa de ocorrência era muito alta para que isso fosse aceitável. 

Optamos por uma abordagem de três fases em que marcamos todas as respostas grandes, analisamos e eliminamos cada caso problemático e, em seguida, impomos um limite superior estrito ao tamanho da resposta.

Marcamos todos os carregamentos maiores do que 1 MB e registramos eventos detalhados sobre a origem, o uso e o detalhamento dos dados. Houve alguns casos de uso diferentes e dispendiosos, mas o mais notório foi provavelmente o dos blobs de miniaturas de anexos. Acontece que eles estavam sendo codificados como strings base64 e incluídos nas respostas serializadas. Eram toleráveis em pequenos números, mas rapidamente se tornavam enormes quando carregados em massa, como ao carregar uma visualização em grade que renderizava miniaturas de anexos para cada tarefa.

Conseguimos corrigir progressivamente problemas como esses restringindo as miniaturas para respostas enormes, usando miniaturas menores e, por fim, removendo os dados binários da resposta. Após mitigações simples como essas, o número de respostas enormes desapareceu e, posteriormente, conseguimos impor limitações de tamanho de resposta no nível da estrutura⁶.

Colisões de tópicos

Outro problema estranho que encontramos foram as colisões de tópicos do Pubsub. Acontece que tínhamos usos de estrutura não compatíveis que geravam o mesmo tópico de assinatura, independentemente do domínio. Quando o PubSub ocorria apenas em um único tipo de processo, os efeitos eram relativamente inofensivos. Normalmente, um único tópico pubsub corresponde a dados de um único domínio. No entanto, com o Pubsub agora dividido entre session-brokers e syncable-loaders, era possível que os dois tipos de processo discordassem sobre o domínio de um tópico específico. Quando isso acontecia, víamos uma taxa elevada e estável de recarregamentos de invalidação devido a essa “incompatibilidade de domínio”. Felizmente, a correção foi bastante simples, mas é interessante como esse bug sobreviveu na nossa estrutura por tanto tempo sem ser detectado. 

Reajuste dos volumes de trabalho

Os agentes de sessão e os carregadores sincronizáveis têm cargas de trabalho consideravelmente diferentes das dos servidores de sincronização originais. Os agentes de sessão são responsáveis apenas pelo gerenciamento de sessões e os carregadores sincronizáveis são responsáveis apenas pelo carregamento de dados. 

Não tínhamos certeza de como isso afetaria seus requisitos de recursos, então começamos ambos com solicitações de recursos (cpu/mem) e configurações de horizontal pod autoscaler (HPA) semelhantes. 

session-brokers

Conforme observamos, ficou claro que os agentes de sessão eram consideravelmente mais leves. Eles estavam operando de forma confiável com uma CPU muito baixa (na verdade, estavam muito mais limitados pela memória⁷). Algumas réplicas pareciam suficientes para atender ao tráfego de uma célula de infraestrutura inteira. No entanto, quando realmente reduzimos as minReplicas no HPA, observamos que as recargas de dados e a latência de recarga aumentaram. O que estava acontecendo?

Em suma, deixamos de considerar todas as nossas configurações compartilhadas relacionadas ao dimensionamento do cache e aos limitadores. Com apenas algumas réplicas, cada agente de sessão estava lidando com muito mais sessões por pod (cerca de 3,5 vezes) do que um servidor de sincronização típico. Como cada um deles estava recebendo muito mais dados, estavam preenchendo completamente seus caches de tópico pubsub ⇔ assinatura, e cada remoção estava acionando uma recarga (por segurança). Aumentar adequadamente esse limite em cerca de 6x corrigiu as taxas elevadas de remoção do cache e de recarregamento. Da mesma forma, descobrimos que os nossos limitadores de recarga hierárquica estavam muito mal configurados para as novas taxas de tráfego que chegavam. O ajuste dessas configurações do limitador também levou a uma redução drástica na latência de recarregamento e no atraso de reatividade de ponta a ponta (ou seja, quanto tempo leva para um aplicativo Web ver a sua própria gravação) de cerca de 5 a 10 vezes.

syncable-loaders

Por outro lado, os carregadores sincronizáveis eram consideravelmente mais pesados do que o esperado. Cada servidor carregaria mais assinaturas por segundo (cerca de 1,5x) do que um servidor de sincronização com recursos equivalentes. Ao contrário dos agentes de sessão, eles estavam muito mais vinculados à CPU ⁸.

Curiosamente, uma parcela não insignificante da CPU foi atribuída a um aumento nas desotimizações do Truffle relacionadas ao nosso código TS SCV. Muito provavelmente, isso foi causado pelo fato de cada carregador sincronizável acessar uma maior parte do nosso código SCV. De qualquer forma, foi necessário um aumento modesto no tamanho do nosso cache de código⁹. 

Aquecimento do processo

O aquecimento de processos para o carregamento de dados tem sido um desafio histórico. Felizmente, na nossa nova arquitetura, é um pouco mais simples. Nossa interface principal de syncable-loaders são consultas de dados sem estado, portanto, podemos aquecê-los simplesmente reproduzindo ou espelhando o tráfego existente entre session-brokers e syncable-loaders.

Por outro lado, ainda enfrentamos muitos dos mesmos desafios do aquecimento dos servidores de sincronização. Principalmente, é preciso muita CPU para aquecer os processos, e isso causa todos os tipos de problemas de “noisy neighbor” (para nós, a métrica relevante aqui é a paralisação parcial, já que não estamos atingindo os limites de limitação do k8s) na inicialização. Uma boa melhoria que fizemos aqui foi usar o redimensionamento de pod no local para limitar os recursos de um syncable-loader na inicialização, mas permitir que ele se expanda após a inicialização. 

Apesar disso, o aquecimento de cada pod ainda leva alguns minutos. Ao analisar os perfis JFR, acreditamos que o principal gargalo seja a compilação insuficiente do código TS SCV durante o aquecimento, e acreditamos que haja mais espaço para melhorias nesse aspecto. Estamos procurando ativamente aquecer com mais precisão os caminhos relevantes com métodos mais direcionados e remodelar as nossas interfaces TS¹⁰ para uma melhor compilação.

Os nossos intermediários de sessão não são responsáveis por nenhum carregamento de dados e, na prática, nunca exigiram nenhum tipo de aquecimento.

Como isso ajudou?

A nossa nova arquitetura reduziu significativamente a complexidade operacional, acelerou as implementações e a velocidade do código e abriu oportunidades futuras de velocidade e escalonamento. 

A nossa nova arquitetura simplesmente se ajusta automaticamente às mudanças no tráfego total de leitura sem exigir um gerenciamento complexo do tráfego. Cada tipo de operação de deslocamento de tráfego é tratado de maneira ordenada, bastando reiniciar os pods. Precisa inicializar todas as sessões? Alterne todos os brokers de sessão. Precisa limpar os nossos caches? Alterne todos os carregadores sincronizáveis. Ambas as operações são seguras em relação ao tempo operacional.

No passado, a velocidade de desenvolvimento do produto era limitada principalmente pela implantação de servidores de sincronização. No nosso novo mundo, só precisamos implantar os carregadores sincronizáveis para implantar o código do produto. Fizemos isso movendo os carregadores sincronizáveis para a sua própria célula implantável, que estamos trabalhando para implantar com mais frequência (e, eventualmente, juntamente com o código do produto). A implementação de carregadores sincronizáveis já é cerca de 40% mais rápida (cerca de 20 minutos mais rápida) do que a de servidores de sincronização (podemos aumentar a velocidade com segurança), e pretendemos obter aumentos ainda maiores no futuro.

É importante ressaltar que as melhorias de desempenho não eram uma meta deste trabalho, mas vale a pena discuti-las, considerando o quanto do design e da implementação envolveram isso. A latência do processamento dos resultados de consultas realmente melhorou no nosso novo sistema (provavelmente devido a um melhor balanceamento de carga/ajuste/autoscaling). Da mesma forma, a latência da assinatura inicial é visivelmente melhor. Por outro lado, a latência de reflexão de mutação de ponta a ponta (ou seja, o tempo que uma alteração leva para ser distribuída para outras sessões) é aproximadamente a mesma. Os rastreamentos mostram que isso provavelmente se deve à distorção no consumo do fluxo PubSub entre os intermediários de sessão e os carregadores sincronizáveis (os carregadores sincronizáveis não podem atender a uma solicitação até que estejam atualizados com a versão da solicitação).

O que vem em seguida?

Nosso sistema atual está muito melhor, mas ainda limita a velocidade do produto, exigindo considerações sobre compatibilidade com versões anteriores/posteriores em cada lançamento. No entanto, com todas as nossas melhorias na velocidade de implementação, agora é possível implementar todas as nossas alterações no modelo de dados juntas, eliminando assim qualquer necessidade de considerar a compatibilidade com versões anteriores/posteriores. Estamos ativamente buscando desenvolver isso em um futuro próximo.

Uma das nossas principais motivações para este trabalho foram as regressões de desempenho difíceis de atribuir. Notavelmente, esta é uma área em que não fizemos melhorias significativas como resultado deste trabalho. O lado positivo é que agora este é um dos principais problemas que estamos enfrentando daqui para frente. Ao contrário da maior parte do trabalho discutido aqui, trata-se de um problema consideravelmente mais multidisciplinar que envolve o domínio do produto, o modelo de dados, a estrutura e as considerações de infraestrutura. 

pools de trabalho do carregador sincronizáveis por recurso

pools de trabalho do carregador sincronizáveis por recurso

Estamos entusiasmados em explorar soluções em toda a infraestrutura da plataforma (por exemplo, pools de trabalhadores por recurso), estruturas da plataforma e ferramentas da plataforma (por exemplo, testes de caixa preta/caixa branca).


Biografia do autor

Arvind Vijayakumar é engenheiro da equipe LunaDb, onde trabalha para ajudar a construir e dimensionar a plataforma de base de carregamento de dados da Asana — a infraestrutura essencial que garante que os usuários sempre vejam atualizações precisas, reativas e ultrarrápidas nos nossos aplicativos Web e APIs.

Agradecimentos à equipe

Este trabalho para escalar o LunaDb tem sido um esforço de equipe de longa data que se estende pelos últimos anos, envolvendo muitos membros do LunaDb, tanto atuais quanto anteriores: Brandon Zhang, Alex Matevish, Sean Wentzel, Eric Walton, Spencer Yu, Sophia Yao, George Ong, Tyler Prete, Koushik Ghosh, Natan Dubitski, Vinodh Chandra Murthy e outros


Notas de rodapé

  1. É importante ressaltar que o criamos antes do Facebook disponibilizar o GraphQL como código aberto

  2. Há mais detalhes na publicação anterior sobre o aquecimento do processo, mas, em resumo, contamos com a permissão para que os pods de sincronização explodam arbitrariamente como uma maneira de escalar rapidamente para picos de tráfego local

  3. Também fazemos atualizações com granularidade por objeto, em vez de por campo (por razões históricas). Isso aumenta a quantidade de dados envolvidos nas recargas de invalidação.

  4. Nós utilizamos um murmur hash de 128 bits para evitar colisões.

  5. Em particular, usamos a compactação de WebSocket há muito tempo, o que provavelmente atenua os efeitos percebidos pelo cliente.

  6. Em particular, também tivemos inicialmente alguns problemas com a sobrecarga do salto de rede. Após algumas investigações, descobrimos que a maior parte da sobrecarga era, na verdade, causada pela nossa malha de serviços (Istio/Envoy) e fizemos alguns ajustes direcionados para melhorar o desempenho nesse aspecto.

  7. Um fator significativo na memória retida foi reescrever o nosso armazenamento sincronizável do lado do servidor para reter apenas hashes de dados. Não foi um problema simples e talvez façamos uma publicação de acompanhamento sobre isso! Sem isso, os dados armazenados do cliente faziam com que os agentes de sessão usassem consideravelmente mais memória.

  8. Antes disso, todos os servidores de sincronização eram executados em instâncias otimizadas para memória. Os carregadores sincronizáveis, por outro lado, se beneficiam de tipos de nós mais ricos em CPU, como as instâncias mistas.

  9. Um atributo notável da execução do TS no GraalVM dessa maneira é que ele usa um pouco mais de cache de código do que é típico em um aplicativo Scala/Java JVM mais padrão.

  10. Até onde sabemos, o alto polimorfismo torna a compilação do TS mais cara. Estamos analisando a mudança para interfaces monomórficas e o aproveitamento de especializações polimórficas de relatórios