Optimisation de l'application node.js

Éléments fournis: ancienne application http node.js et augmentation de la charge.

Solutions standard au problème: quitter les serveurs, tout réécrire à partir de 0, optimiser ce qui a déjà été écrit.

Essayons de passer par l'optimisation et de découvrir comment trouver et améliorer les faiblesses des applications. Et peut-être accélérer sans toucher à une seule ligne de code :)

Bienvenue à tous sous le chat!

Tout d'abord, décidons d'une technique de test de performance. Nous serons intéressés par le nombre de requêtes servies en 1 seconde: rps.

Nous exécuterons l'application en mode 1 de travailleur (1 processus), en mesurant les performances de l'ancien code et du code avec optimisations - les performances absolues ne sont pas importantes, les performances comparatives sont importantes.

Dans une application typique avec de nombreux itinéraires différents, il est logique de rechercher d'abord les demandes les plus chargées, dont le traitement prend la plupart du temps. Des utilitaires tels que request-log-analizer ou de nombreux outils similaires vous permettront d'extraire ces informations des journaux.

D'un autre côté, vous pouvez prendre une vraie liste de demandes et les balancer toutes (par exemple, en utilisant yandex-tank) - nous obtenons un profil de charge fiable.

Mais en faisant de nombreuses itérations d'optimisation de code, il est beaucoup plus pratique d'utiliser un outil plus simple et plus rapide et un type spécifique de requête (et après avoir optimisé une requête, étudiez la suivante, etc.). Mon choix est wrk . De plus, dans mon cas, le nombre d'itinéraires n'est pas important - il n'est pas difficile de tout vérifier un par un.

Il convient de noter tout de suite qu'en termes de blocage des requêtes, des attentes des bases de données, etc. l'application est déjà optimisée, tout dépend du cpu: lors des tests, le travailleur consomme 100% cpu.

Les serveurs vendus utilisent node.js version 6 - commençons par:

Demandes / sec: 1210

Nous essayons sur le 8ème nœud:
Demandes / sec: 2308
10e note:
Demandes / sec: 2590

La différence est évidente. Le rôle clé ici est joué par la mise à jour de la version v8 - beaucoup de code v8 mal optimisé est dans le passé. Et afin de ne pas gérer les moulins à vent qui ont disparu dans node.js v8, il est préférable de mettre à niveau immédiatement, puis de faire l'optimisation du code.

Nous passons à la recherche réelle de goulots d'étranglement: à mon avis, le meilleur outil pour cela est le graphique à flamme. Et avec l'avènement du projet 0x , obtenir un graphique de flamme était très simple - démarrer 0x au lieu de node: 0x -o yourscript.js, faire un test, arrêter le script, regarder le résultat dans le navigateur.

Le graphique en flammes du code testé ressemble à ceci avant les optimisations:


Sous les filtres, quittez l'application, deps - uniquement le code de l'application et des modules tiers.

Plus la bande est large, plus vous passez de temps à exécuter cette fonction (y compris les appels imbriqués).

Nous traiterons de la partie centrale la plus importante.

Tout d'abord, nous mettons en évidence des fonctions non optimisées. J'en ai trouvé quelques-uns dans l'application.

De plus, les fonctions supérieures sont des candidats typiques pour l'optimisation. Les fonctions restantes sont alignées avec des étapes relativement égales - chaque fonction contribue à une petite fraction des retards, il n'y a pas de leader évident.

Un algorithme simple d'actions est alors possible: optimiser les fonctions les plus larges, passer de l'une à l'autre. Mais j'ai choisi une approche différente: pour optimiser à partir du point d'entrée vers l'application (gestionnaire de requêtes dans http.createServer). À la fin de la fonction à l'étude, au lieu d'appeler les fonctions suivantes, je termine le traitement de la demande avec une réponse fictive et j'étudie les performances de cette fonction particulière. Après son optimisation, la réponse fictive se déplace le long de la pile d'appels vers la fonction suivante, etc.

Une conséquence pratique de cette approche: vous pouvez voir les rps dans des conditions idéales (avec une seule fonction de démarrage, rps est proche des rps maximaux de l'application hellow world node.js), et avec un mouvement supplémentaire du talon de réponse profondément dans l'application, observez la contribution de la fonction étudiée à la baisse de performance dans rps-ah.

Donc, on ne laisse que la fonction start, on obtient:

Demandes / sec: 16176



En connectant le noyau, les filtres v8, vous pouvez voir que la quasi-totalité de la fonction étudiée consiste à envoyer une réponse, la journalisation et d'autres choses mal optimisées - nous allons plus loin.

On passe à la fonction suivante:

Demandes / sec: 16111
Rien n'a changé - plongez plus loin:
Demandes / sec: 13330


Notre client! On peut voir que la fonction getByUrl impliquée occupe une partie importante de la fonction de démarrage - ce qui correspond bien à la subsidence rps.

Nous regardons attentivement ce qui s'y passe (allumez core, v8):

Il se passe beaucoup de choses ... nous fumons le code, optimisons:

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

se transformer en

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

Dans ce cas, un simple for est beaucoup plus rapide que for..in

Récupérer les requêtes / sec: 16015



Visuellement, la fonction «se dégonfle» et occupe une fraction beaucoup plus petite de la fonction de départ.
Dans les informations détaillées sur la fonction, tout a également été grandement simplifié:

Nous passons à la fonction suivante.

Demandes / sec: 13316



Cette fonction possède de nombreuses fonctions de tableau et, malgré l'accélération significative des dernières versions de node.js, elles sont toujours plus lentes que les boucles simples: changez [] .map et filtre. régulièrement et obtenir

Demandes / sec: 15067



Et donc à chaque fois, pour chaque fonction suivante.

Quelques optimisations plus utiles: pour les hachages avec un jeu de clés à changement dynamique, la nouvelle Map () peut être 40% plus rapide que la normale {};

Math.round (el * 100) / 100 est 2 fois plus rapide que toFixed (2).

Dans le graphique des flammes pour les fonctions de base et v8, vous pouvez voir à la fois des entrées obscures et StringPrototypeSplit ou v8 :: internal :: Runtime_StringToNumber, et si c'est une partie importante de l'exécution du code, essayez d'optimiser, par exemple, simplement réécrivez le code qui ne les exécute pas opérations.

Par exemple, le remplacement de split par plusieurs appels indexOf et substring peut donner des gains de performances doubles.

Un autre sujet vaste et complexe est l'optimisation jit, ou plutôt les fonctions désoptimisées.
S'il existe une grande proportion de ces fonctions, il sera nécessaire de les traiter.

Une étude approfondie de la sortie du nœud --trace_file_names --trace_opt_verbose --trace-deopt --trace_opt peut aider ici.

Par exemple, les lignes du formulaire

désoptimisation (DEOPT soft): begin 0x2bcf38b2d079 <Fonction JS getTime ... Retour de type insuffisant pour le fonctionnement binaire conduit à la ligne

retour val> = 10? val: '0' + val;

Remplacement pour

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

corrigé la situation.

Il y a beaucoup d'informations sur l'ancien moteur v8 pour les raisons et les moyens de lutter contre la désoptimisation des fonctions:

github.com/P0lip/v8-deoptimize-reasons - list,
www.netguru.co/blog/tracing-patterns-hinder-performance - analyse des causes typiques,
www.html5rocks.com/en/tutorials/speed/v8 - à propos des optimisations pour la v8, je pense que la même chose est vraie pour le moteur v8 actuel.

Mais bon nombre des problèmes ne sont plus pertinents pour la nouvelle v8.

Quoi qu'il en soit, après toutes les optimisations, j'ai réussi à obtenir des requêtes / sec: 9971 , c'est-à-dire il accélérera environ 2 fois en raison de la transition vers la dernière version de node.js, et encore 4 fois en raison de l'optimisation du code.

J'espère que cette expérience sera utile à quelqu'un d'autre.

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


All Articles