6 lições aprendidas da experiência de otimização de desempenho do serviço Node.js.

A Klarna fez grandes esforços para ajudar os desenvolvedores a criar serviços seguros e de alta qualidade. Uma das ferramentas destinadas aos desenvolvedores é uma plataforma para a realização de testes A / B. O componente mais importante desse sistema é a multiplicidade de processos que, para cada solicitação recebida, decidem em qual tipo de teste (A ou B) enviar a solicitação. Por sua vez, isso determina qual cor exibir o botão, qual layout exibir ao usuário ou mesmo qual pacote de terceiros usar. Essas decisões têm um impacto direto na experiência do usuário.



Klarna usa a plataforma Node.js. O artigo, cuja tradução publicamos hoje, é dedicado às lições que os especialistas da empresa conseguiram aprender com a experiência de otimizar o desempenho de seus serviços.

Lição número 1: o teste de desempenho pode garantir que a velocidade do sistema não diminui a cada release


O desempenho de cada processo desempenha um papel enorme, pois esses processos são usados ​​de forma síncrona nos caminhos críticos de decisão do ecossistema Klarna. O requisito de desempenho usual para essas tarefas é que, para 99,9% dos pedidos, a decisão deve ser tomada com um atraso, cujo tempo é expresso em um dígito. Para garantir que o sistema não se desvie desses requisitos, a empresa desenvolveu um transportador para testes de estresse no serviço.

Lição número 2: “encerrando” independentemente a carga, é possível identificar problemas antes mesmo que eles cheguem à produção


Embora praticamente não tenhamos visto problemas de desempenho nos dois anos em que a plataforma foi usada na produção, os testes indicaram claramente alguns problemas. Dentro de alguns minutos do teste, em um nível moderadamente estável de recebimento de solicitações, a duração do processamento da solicitação aumentou acentuadamente de valores normais para vários segundos.


Informações sobre o tempo necessário para processar a solicitação. Algum tipo de problema detectado

Decidimos, embora isso ainda não tivesse acontecido na produção, que era apenas uma questão de tempo. Se a carga real atingir um certo nível, podemos encontrar algo semelhante. Portanto, foi decidido que esse problema deveria ser investigado.

Lição número 3: o teste de estresse a longo prazo pode identificar uma variedade de problemas. Se tudo parecer bom, tente aumentar a duração do teste.


Outra coisa que vale a pena prestar atenção é que os problemas do nosso sistema aparecem após 2-3 minutos de trabalho sob carga. Inicialmente, executamos o teste por apenas 2 minutos. E o problema só foi visto quando o tempo de execução do teste foi aumentado para 10 minutos.

Lição número 4: não se esqueça de levar em consideração o tempo necessário para a resolução de nomes de DNS, considerando as solicitações de saída. Não ignore a vida útil das entradas de cache - isso pode prejudicar seriamente o aplicativo


Geralmente, monitoramos os serviços usando as seguintes métricas: número de solicitações recebidas por segundo, duração do processamento das solicitações recebidas, nível de erros. Isso nos dá bons indicadores do estado do sistema, indicando se há algum problema nele.

Mas essas métricas não fornecem informações valiosas durante o mau funcionamento do serviço. Quando algo dá errado, você precisa saber onde está o gargalo do sistema. Para esses casos, você precisa monitorar os recursos usados ​​pelo tempo de execução do Node.js. É óbvio que a composição dos indicadores, cujo estado é monitorado em situações problemáticas, inclui o uso do processador e da memória. Mas, às vezes, a velocidade do sistema não depende deles. No nosso caso, por exemplo, o nível de utilização do processador era baixo. O mesmo poderia ser dito sobre o nível de consumo de memória.

Outro recurso que determina o desempenho dos projetos do Node.js. é o loop de eventos. Assim como é importante sabermos quanta memória é usada pelo processo, precisamos saber quantas "tarefas" são necessárias para processar o loop de eventos. O loop de eventos do Node.js. é implementado na biblioteca C ++ libuv ( aqui está um bom vídeo sobre isso). As "tarefas" são aqui referidas como "Solicitação ativa". Outra métrica importante é o número de “identificadores ativos” representados por descritores de arquivo aberto ou soquetes usados ​​pelos processos do Node.js. Uma lista completa dos tipos de descritores pode ser encontrada na documentação do libuv. Como resultado, se o teste usar 30 conexões, pode-se esperar que o sistema tenha 30 descritores ativos. O indicador que caracteriza o número de solicitações ativas indica o número de operações aguardando na fila por um descritor específico. Quais são essas operações? Por exemplo, operações de leitura / gravação. Uma lista completa deles pode ser encontrada aqui .

Depois de analisar as métricas de serviço, percebemos que havia algo errado aqui. Enquanto o número de descritores ativos era o que esperávamos (neste teste - cerca de 30), o número de solicitações ativas era desproporcionalmente alto - várias dezenas de milhares.


Descritores ativos e solicitações ativas

É verdade que ainda não sabíamos quais tipos de solicitações estavam na fila. Depois de dividirmos as consultas ativas em tipos, a situação se esclareceu um pouco. Ou seja, UV_GETADDRINFO consultas UV_GETADDRINFO acabaram sendo muito visíveis. Eles são gerados quando o Node.js tenta resolver o nome DNS.

Por que o sistema gera tantas solicitações de resolução de nome DNS? Acabou que o cliente StatsD que estávamos tentando resolver o nome do host para cada mensagem de saída. Deve-se observar que esse cliente oferece a possibilidade de armazenar em cache os resultados das consultas DNS, mas o TTL dos registros DNS correspondentes não é levado em consideração aqui. Os resultados são armazenados em cache por um período indeterminado. Como resultado, se o registro for atualizado depois que o cliente já tiver resolvido o nome correspondente, ele nunca o saberá. Como o balanceador de carga StatsD pode ser reimplementado com um endereço IP diferente e não podemos forçar a reinicialização do serviço para atualizar o cache DNS, essa abordagem, que usa cache por tempo ilimitado, não era adequada para nós.

A solução que encontramos foi usar um meio externo ao cliente para armazenar em cache as consultas DNS. Isso é fácil, executando o "patch de macaco" do módulo DNS. O resultado agora parecia muito melhor do que antes.


Informações sobre o tempo necessário para processar a solicitação. Resultado do uso de um cache DNS externo

Lição # 5: Execute operações de E / S no modo em lote. Tais operações, mesmo assíncronas, são sérias consumidoras de recursos.


Depois de resolver o problema acima, ativamos alguns recursos do serviço que foram desabilitados anteriormente e o testamos novamente. Em particular, incluímos um código que envia uma mensagem para o tópico Kafka para cada solicitação recebida. O teste, mais uma vez, revelou picos significativos nos resultados das medições do tempo de resposta (estamos falando de segundos), observados em grandes intervalos de tempo.


Informações sobre o tempo necessário para processar a solicitação. O teste revelou um aumento acentuado no tempo necessário para a formação de respostas

Esses resultados apontam para um problema óbvio precisamente na função que incluímos antes do teste. Em particular, somos confrontados com o fato de que o envio de mensagens para Kafka leva muito tempo.


Informações sobre o tempo necessário para gerar mensagens para Kafka

Decidimos usar a melhoria mais simples aqui - colocar as mensagens de saída em uma fila na memória e enviar essas mensagens no modo em lote a cada segundo. Ao executar o teste novamente, descobrimos melhorias claras no tempo necessário para o serviço formar uma resposta.


Informações sobre o tempo necessário para processar a solicitação. Melhorias após organizar o processamento de mensagens em lote

Lição número 6: antes de tentar fazer melhorias no sistema, prepare testes cujos resultados podem ser confiáveis


O trabalho acima descrito para otimizar o desempenho de um serviço não seria possível sem um mecanismo de execução de teste que permita obter resultados reproduzíveis e uniformes. A primeira versão do nosso sistema de teste não deu resultados uniformes, portanto não podíamos confiar nele para tomar decisões importantes. Tendo investido na criação de um sistema de teste confiável, pudemos testar o projeto em diferentes modos, experimentar correções. O novo sistema de testes, na maioria das vezes, nos deu confiança de que os resultados obtidos foram algo real, e não alguns números que vieram do nada.

Digamos algumas palavras sobre as ferramentas específicas usadas para organizar os testes.

A carga foi gerada por uma ferramenta interna que facilitou a execução do Locust no modo distribuído. Em geral, tudo se resumia à execução de um único comando, após o qual os geradores de carga foram lançados, o script de teste foi transferido para eles e os resultados visualizados pelo painel de controle da Grafana foram coletados. Os resultados correspondentes são apresentados no material em gráficos com fundo escuro. É assim que o sistema fica no teste do ponto de vista do cliente.

O serviço em teste fornece informações de medição no Datalog. Esta informação é apresentada aqui com gráficos com um fundo brilhante.

Caros leitores! Quais sistemas de teste de serviço do Node.js. você usa?

Source: https://habr.com/ru/post/pt484616/


All Articles