Escalando para 5M RPM

Na Trendyol, temos a responsabilidade de servir páginas de conteúdo e dados. Também fornecemos dados de conteúdo em massa para favoritos, coleções ou até mesmo páginas de checkout. Em breve, nos envolveremos sempre que você vir as telas abaixo.


Quais são as nossas necessidades ou cargas esperadas?
Como esperado, geralmente recebemos mais tráfego em épocas de compras ou eventos/descontos. Enquanto chegamos a cerca de 1M rpm (solicitações por minuto) em um dia típico, a carga sobe para cerca de 3M rpm em horários de pico. Naquela época, não conseguíamos atender com precisão aos requisitos. Nosso tempo de resposta estava dobrando o normal e ainda tínhamos mais tempos limite do que poderíamos ignorar.
Nossa próxima meta e expectativa era atingir pelo menos 8M rpm com, no máximo, os mesmos recursos. Portanto, o problema era que não podíamos escalar tanto quanto precisávamos, muito menos o que esperávamos a seguir.
Quem gosta de desafios? Sim, certamente nós :)

Como descobrimos a causa do problema?
Então fizemos um perfil para entender qual parte do código leva mais tempo e usa mais recursos. Primeiro, usamos um criador de perfil Java para restringir quais partes rastrear. Depois disso, adicionamos alguns rastreamentos personalizados do New Relic no código. Quando analisamos os resultados, vimos que uma parte específica do código consumia mais tempo e recursos.
Essa parte era uma lógica de conversão para converter o documento Couchbase nos dados de que precisamos para executar a lógica de negócios e servir como massa. Ele estava rodando para cada solicitação e cada conteúdo. Começamos a pensar se poderíamos fazer essa lógica de conversão de forma assíncrona. Poderíamos executar essa lógica uma vez para cada conteúdo. Para a maioria das partes, a resposta foi: SIM!
Não basta fazer o seu melhor; você deve saber o que fazer e, então, fazer o seu melhor. —W Edwards Deming

Podemos lidar com os dados de forma assíncrona?
Estamos usando o Couchbase como fonte de dados. Quando vimos como podemos converter nossos dados do Couchbase de forma assíncrona, vimos que o Couchbase tem um protocolo de alteração de banco de dados (DCP). O DCP pode nos fornecer um fluxo para as mutações. Assim, podemos ouvir nossa fonte de dados antiga, fazer a conversão assíncrona e gravar em nossa nova fonte de dados. Você pode verificar o artigo de Ahmet Hatipoglu se quiser saber sobre os detalhes.
Agora, é hora de determinar se podemos otimizar os dados.
Precisamos de todos os dados?
Usamos o mesmo modelo de dados para a página de conteúdo e solicitações em massa. Para solicitações em massa, vários clientes precisam de dados diferentes. Então precisávamos de uma análise para entender quem precisa de qual parte dos dados. Fizemos uma análise abrangente com todos os nossos clientes juntos.
Após a análise, percebemos que precisávamos apenas de alguns dados e quase todos os nossos clientes exigiam quase as mesmas peças. Mas não pudemos mexer no esquema porque alguns de nossos clientes usam a mesma fonte de dados diretamente. Estamos fortemente acoplados. Em conclusão, separar as fontes de dados pode ser um passo em direção a uma solução.
O que temos agora até agora?
Um serviço que escuta as alterações de uma fonte de dados de conteúdo executa a conversão pesada e grava em uma nova fonte de dados. E sabemos pela análise que precisamos apenas de alguns dados. Assim, podemos omitir todas as partes desnecessárias enquanto fazemos a conversão.
Você está pronto? Salvamos %77 dos dados (1,28 TB). Que alivio!

E agora?
Precisamos de algo que use os dados que preparamos. Queríamos separar as necessidades de conteúdo em massa e as necessidades da página de detalhes do produto apenas porque seus clientes, necessidades de escala e conjuntos de regras de negócios são diferentes. Então, é hora de escrever um novo serviço.
O que pode ser feito diferente?
Talvez tecnologias que usamos. O serviço antigo estava usando Java 8 e inicialização Spring. Tivemos uma experiência estável com o Quarkus . Talvez possamos reduzir o uso de recursos e o tempo de inicialização para sermos mais escaláveis. Ou estávamos usando o Flux para adquirir os documentos do Couchbase, e talvez pudéssemos usar o CompletableFuture para variar. Depois de listar nossas opções, desenvolvemos muitos aplicativos POC e executamos muitos testes de carga. Mas os resultados poderiam ter sido melhores. Pelo menos não pelo esforço que vamos precisar. A pesquisa deve continuar.
A propósito, sou esquilo há quatro anos. Acredito que podemos obter os documentos efetivamente com goroutines em paralelo. Então eu queria tentar isso. Em seguida, examinei o SDK oficial do Couchbase Go, que possui um recurso de “obtenção de documentos em massa”. Isso foi ótimo! As necessidades eram adequadas para o uso do Go. E eu queria tentar as duas opções.
Executei testes de carga em goroutines paralelas e no recurso em massa. Os tempos de resposta e o uso de recursos foram significativamente melhorados para ambos. O recurso em massa foi melhor do que nossa implementação simples. Mas algo estava errado. O uso da rede foi quatro vezes maior. Poderíamos dar uma configuração para a conexão Couchbase no Java SDK que permite a compressão. Mas o Go SDK não possui uma configuração de compactação em suas opções de configuração de cluster. Então eu perdi a configuração de acordo. Infelizmente, o recurso de compactação não pode ser controlado por meio do Couchbase Go SDK, ao contrário do Couchbase Java SDK. Então, examinei seu código, fiz algumas depurações dolorosas e descobri que a compactação poderia ser controlada por meio de uma variável de consulta no final da string de conexão.
couchbase://{HOST_HERE}?compression=true
- Temos um enorme ganho de uso de memória RAM e o uso de memória diminuiu de 800 MB para 60 MB por pod.
- O tempo de resposta caiu pela metade. 10ms a 5ms . E é mais estável contra alto rendimento.
- O uso da CPU é reduzido drasticamente. Para atingir 5 milhões com o serviço antigo, precisávamos de mais pods. Portanto, esta será uma comparação do uso total da CPU. Agora são 130 núcleos em vez de 300 .
- O tamanho total do documento agora é menos de um quarto do anterior, de 1,67 TB para apenas 386 GB .
- Naturalmente, a carga da rede é muito menor.
- O tempo de inicialização agora é de 85 milissegundos em vez de 12 segundos !

Obrigado a Emre Odabas por seu apoio e encorajamento ao escrever este artigo.