Otimização do aplicativo node.js.

Dado: aplicativo http node.js antigo e aumento da carga nele.

Soluções padrão para o problema: saia de servidores, reescreva tudo a partir de 0, otimize o que já foi escrito.

Vamos tentar passar pela otimização e descobrir como encontrar e melhorar os pontos fracos do aplicativo. E talvez acelerar sem tocar em uma única linha de código :)

Todos os interessados ​​são bem-vindos sob o gato!

Primeiro, vamos decidir sobre uma técnica de teste de desempenho. Estaremos interessados ​​no número de solicitações atendidas em 1 segundo: rps.

Executaremos o aplicativo no modo 1 de trabalho (1 processo), medindo o desempenho do código antigo e do código com otimizações - desempenho absoluto não é importante, desempenho comparativo é importante.

Em um aplicativo típico com muitas rotas diferentes, é lógico encontrar primeiro os pedidos mais carregados, cujo processamento leva a maior parte do tempo. Utilitários como request-log-analizer ou muitos similares permitem extrair essas informações dos logs.

Por outro lado, você pode pegar uma lista real de solicitações e marcá-las todas (por exemplo, usando o yandex-tank) - obtemos um perfil de carga confiável.

Mas, ao fazer muitas iterações de otimização de código, é muito mais conveniente usar uma ferramenta mais simples e rápida e um tipo específico de solicitação (e depois de otimizar uma solicitação, estudar a próxima etc.). Minha escolha é errada . Além disso, no meu caso, o número de rotas não é grande - não é difícil verificar tudo um por um.

Deve-se notar imediatamente que, em termos de bloqueio de consultas, expectativas de bancos de dados, etc. o aplicativo já está otimizado, tudo depende da CPU: durante os testes, o trabalhador consome 100% da CPU.

Os servidores vendidos usam o node.js. versão 6 - vamos começar com ele:

Pedidos / s: 1210

Tentamos no oitavo nó:
Pedidos / s: 2308
10ª nota:
Pedidos / s: 2590

A diferença é óbvia. O papel principal aqui é desempenhado pela atualização da versão v8 - muitos códigos v8 pouco otimizados estão no passado. E, para não lidar com os moinhos de vento que desapareceram no node.js v8, é melhor atualizar imediatamente e depois otimizar o código.

Voltamos à busca real por gargalos: na minha opinião, a melhor ferramenta para isso é o flamegraph. E com o advento do projeto 0x , obter um flamegraph era muito simples - iniciar 0x em vez do nó: 0x -o YOURScript.js, faça um teste, pare o script e observe o resultado no navegador.

O flamegraph do código testado se parece com isso antes das otimizações:


Abaixo dos filtros, deixe o aplicativo, deps - apenas o código do aplicativo e dos módulos de terceiros.

Quanto maior a faixa, mais tempo é gasto na execução dessa função (incluindo chamadas aninhadas).

Lidaremos com a maior parte central.

Primeiro, destacamos as funções não otimizadas. Encontrei alguns deles no aplicativo.

Além disso, as principais funções são candidatas típicas à otimização. As demais funções são alinhadas com etapas relativamente uniformes - cada função contribui com uma pequena fração dos atrasos, não há um líder óbvio.

Então, é possível um simples algoritmo de ações: otimizar as funções mais amplas, passando de uma para outra. Mas eu escolhi uma abordagem diferente: otimizar a partir do ponto de entrada para o aplicativo (manipulador de solicitações em http.createServer). No final da função em estudo, em vez de chamar as seguintes funções, concluo o processamento da solicitação com uma resposta simulada e estudo o desempenho dessa função específica. Após sua otimização, a resposta dummy se move mais adiante na pilha de chamadas para a próxima função, etc.

Uma conseqüência conveniente dessa abordagem: você pode ver rps sob condições ideais (com apenas uma função inicial, rps está próximo dos rps máximos do aplicativo hellow world node.js) e, com mais movimentos da resposta stub profundamente no aplicativo, observe a contribuição da função em estudo para a queda de desempenho em rps-ah.

Então, deixamos apenas a função start, obtemos:

Pedidos / s: 16176



Ao conectar os principais filtros da v8, você pode ver que quase toda a função sob investigação consiste em enviar uma resposta, fazer logon e outras coisas pouco otimizadas - vamos mais longe.

Passamos para a seguinte função:

Pedidos / s: 16111
Nada mudou - mergulhe ainda mais:
Pedidos / s: 13330


Nosso cliente! Pode-se observar que a função getByUrl envolvida ocupa uma parte significativa da função start - que se correlaciona bem com a subsidência de rps.

Analisamos atentamente o que está acontecendo (ative o core, v8):

Muitas coisas estão acontecendo ... fumamos o código, otimizamos:

for (var i in this.data) { if (this[i]._options.regexp_obj.test(url)) return this[i]; } return null; 

transformar em

 let result = null; for (let i=0; i<this.length && !result; i++) { if (this[i]._options.regexp_obj.test(url)) result = this[i]; } 

Nesse caso, um simples para é muito mais rápido do que para .. em

Receba Solicitações / s: 16015



Visualmente, a função "esvazia" e ocupa uma fração muito menor da função inicial.
Nas informações detalhadas sobre a função, tudo também foi bastante simplificado:

Passamos para a próxima função.

Pedidos / s: 13316



Essa função possui muitas funções de matriz e, apesar da aceleração significativa nas versões recentes do node.js, elas ainda são mais lentas que os loops simples: altere [] .map e filter. regular para e obter

Pedidos / s: 15067



E assim, vez após vez, para cada função subseqüente.

Algumas otimizações mais úteis: para hashes com um conjunto de chaves que muda dinamicamente, o novo Map () pode ser 40% mais rápido que o normal {};

Math.round (el * 100) / 100 é 2 vezes mais rápido que toFixed (2).

No flamegraph para funções principais e v8, você pode ver as entradas obscuras e as palavras StringPrototypeSplit ou v8 :: internal :: Runtime_StringToNumber, e se essa é uma parte significativa da execução do código, tente otimizar, por exemplo, simplesmente reescrever o código que não as executa. operações.

Por exemplo, substituir a divisão por várias chamadas indexOf e substring pode gerar ganhos de desempenho duplos.

Um tópico grande e complexo separado é a otimização de jit, ou melhor, funções desoptimizadas.
Se houver uma grande proporção de tais funções, será necessário lidar com elas.

Um estudo cuidadoso da saída do nó --trace_file_names --trace_opt_verbose --trace-deopt --trace_opt pode ajudar aqui.

Por exemplo, linhas do formulário

desoptimização (DEOPT soft): inicie 0x2bcf38b2d079 <Função JS getTime ... Feedback do tipo insuficiente para operação binária levou à linha

valor de retorno> = 10? val: '0' + val;

Substituição para

return (val> = 10? '': '0') + val;

corrigiu a situação.

Há muitas informações para o antigo mecanismo v8 por razões e maneiras de combater a desoptimização de funções:

github.com/P0lip/v8-deoptimize-reasons - lista,
www.netguru.co/blog/tracing-patterns-hinder-performance - análise de causas típicas,
www.html5rocks.com/en/tutorials/speed/v8 - sobre otimizações para a v8, acho que o mesmo vale para o atual mecanismo da v8.

Mas muitos dos problemas não são mais relevantes para o novo v8.

De qualquer forma, depois de todas as otimizações, consegui obter solicitações / s: 9971 , ou seja, ele será acelerado cerca de duas vezes devido à transição para a versão mais recente do node.js e outras quatro vezes devido à otimização do código.

Espero que esta experiência seja útil para outra pessoa.

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


All Articles