L'analyse de performance et l'optimisation sont des outils puissants pour vérifier la conformité des performances pour les clients.
L'analyse de performance peut ĂȘtre utilisĂ©e pour identifier les goulets d'Ă©tranglement dans le programme, en appliquant une approche scientifique lors de l'Ă©valuation des expĂ©riences d'optimisation. Cet article dĂ©finit une approche gĂ©nĂ©rale pour l'analyse de performance et l'optimisation, en utilisant un serveur web en Go comme exemple.
Go convient particuliĂšrement bien Ă cette fonction, car il dispose d'outils de profilage. pprof dans la bibliothĂšque standard.

Stratégie
Créons une liste récapitulative pour notre analyse structurelle. Nous essaierons d'utiliser des données pour prendre des décisions plutÎt que d'apporter des modifications basées sur l'intuition ou des suppositions. Pour ce faire, procédons comme suit :
- Définissons les limites de l'optimisation (exigences) ;
- Calculons la charge transactionnelle du systĂšme ;
- Effectuons un test (créons des données) ;
- Observons ;
- Analysons â toutes les exigences sont-elles respectĂ©es ?
- Optimisons de maniĂšre scientifique, formulons une hypothĂšse ;
- Réaliser une expérience pour vérifier cette hypothÚse.

Architecture d'un simple serveur HTTP
Pour cet article, nous allons utiliser un petit serveur HTTP en Golang. Tout le code de cet article est disponible. .
L'application analysĂ©e est un serveur HTTP qui interroge PostgreSQL pour chaque requĂȘte. En complĂ©ment, nous avons Prometheus, node_exporter et Grafana pour collecter et afficher les mĂ©triques de l'application et du systĂšme.

Pour simplifier, considérons que pour le scaling horizontal (et pour simplifier les calculs), chaque service et base de données est déployé ensemble :

Définissons les objectifs
Ă cette Ă©tape, nous clarifions notre objectif. Que cherchons-nous Ă analyser ? Comment saurons-nous qu'il est temps de conclure ? Dans cet article, nous supposerons que nous avons des clients et que notre service traitera 10 000 requĂȘtes par seconde.
Dans Nous examinerons en dĂ©tail les mĂ©thodes de sĂ©lection et de modĂ©lisation. ProcĂ©dons de mĂȘme, en construisant des modĂšles :
- Latence : 99 % des requĂȘtes doivent ĂȘtre exĂ©cutĂ©es en moins de 60 ms ;
- Coût : le service doit consommer le montant le plus faible, que nous jugeons raisonnablement possible. Pour cela, nous maximisons le débit ;
- Planification des capacitĂ©s : il est nĂ©cessaire de comprendre et de documenter combien d'instances d'application doivent ĂȘtre lancĂ©es, y compris la fonction globale de mise Ă l'Ă©chelle, ainsi que combien d'instances sont nĂ©cessaires pour rĂ©pondre aux exigences initiales de charge et garantir .
La latence peut nĂ©cessiter des optimisations en plus de l'analyse, mais la bande passante doit clairement ĂȘtre analysĂ©e. Dans le cadre du processus SRE, l'exigence en matiĂšre de latence provient du client et/ou de l'entreprise, reprĂ©sentĂ©e par le propriĂ©taire du produit. Et notre service rĂ©pondra Ă cet engagement dĂšs le dĂ©part, sans aucune configuration !
Configuration d'un environnement de test
Grùce à l'environnement de test, nous pourrons imposer une charge mesurée à notre systÚme. Des données sur les performances du service web seront créées pour l'analyse.
Charge transactionnelle
Cet environnement utilise pour crĂ©er une frĂ©quence de requĂȘtes HTTP personnalisĂ©e jusqu'Ă ce qu'il soit arrĂȘté :
$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta reportSurveillance
Pendant l'exĂ©cution, une charge transactionnelle sera appliquĂ©e. En plus des mĂ©triques de l'application (nombre de requĂȘtes, latences de rĂ©ponse) et du systĂšme d'exploitation (mĂ©moire, CPU, IOPS), un profilage de l'application sera lancĂ© pour comprendre oĂč se trouvent les problĂšmes et comment le temps processeur est consommĂ©.
Profilage
Le profilage est un type de mesure permettant de voir oĂč va le temps processeur lors de l'exĂ©cution de l'application. Il permet de dĂ©terminer oĂč et combien le temps processeur est dĂ©pensé :

Ces donnĂ©es peuvent ĂȘtre utilisĂ©es lors de l'analyse pour obtenir un aperçu du temps processeur dĂ©pensĂ© inutilement et des tĂąches exĂ©cutĂ©es sans nĂ©cessitĂ©. Go (pprof) peut gĂ©nĂ©rer des profils et les visualiser sous forme de flame graph, en utilisant un ensemble d'outils standard. Je parlerai de leur utilisation et de la configuration dans un moment dans l'article.
Exécution, surveillance, analyse.
Nous allons mener une expĂ©rience. Nous exĂ©cuterons, observerons et analyserons jusqu'Ă ce que la performance nous satisfasse. Nous choisirons une faible charge de maniĂšre alĂ©atoire pour l'utiliser afin d'obtenir les rĂ©sultats des premiĂšres observations. Ă chaque Ă©tape suivante, nous augmenterons la charge avec un certain facteur d'Ă©chelle, choisi avec une certaine dispersion. Chaque exĂ©cution de test de charge se fait avec un ajustement du nombre de requĂȘtes : make load-test LOAD_TEST_RATE=X.
50 requĂȘtes par seconde

Notez les deux graphiques en haut. Le supĂ©rieur gauche montre que notre application traite 50 requĂȘtes par seconde (Ă son avis), tandis que le supĂ©rieur droit indique la durĂ©e de chaque requĂȘte. Ces deux paramĂštres nous aident Ă observer et Ă analyser : sommes-nous dans nos limites de performance ou non. La ligne rouge sur le graphique Latence des requĂȘtes HTTP indique un SLO de 60 ms. On voit sur la ligne que nous sommes bien en dessous de notre temps de rĂ©ponse maximal.
Voyons du cÎté des coûts :
10000 requĂȘtes par seconde / 50 requĂȘtes par serveur = 200 serveurs + 1
Nous pouvons encore améliorer ce chiffre.
500 requĂȘtes par seconde
Des choses plus intĂ©ressantes commencent Ă se produire lorsque la charge atteint 500 requĂȘtes par seconde :

Encore une fois, sur le graphique supĂ©rieur gauche, on voit que l'application enregistre une charge normale. Si ce n'est pas le cas, il y a un problĂšme avec le serveur sur lequel l'application est exĂ©cutĂ©e. Le graphique de latence de rĂ©ponse en haut Ă droite montre que 500 requĂȘtes par seconde ont conduit Ă une latence de rĂ©ponse de 25-40 ms. Le 99Ăšme percentile reste largement dans le SLO de 60 ms choisi prĂ©cĂ©demment.
Du point de vue des coûts :
10000 requĂȘtes par seconde / 500 requĂȘtes par serveur = 20 serveurs + 1
Il est encore possible d'améliorer.
1000 requĂȘtes par seconde

Excellente exĂ©cution ! L'application montre qu'elle a traitĂ© 1000 requĂȘtes par seconde, cependant la limite de latence a Ă©tĂ© dĂ©passĂ©e du cĂŽtĂ© du SLO. Cela se voit sur la ligne p99 du graphique supĂ©rieur droit. Bien que la ligne p100 soit bien plus haute, les latences rĂ©elles dĂ©passent le maximum de 60 ms. Plongeons dans le profilage pour voir ce que fait rĂ©ellement l'application.
Profilage
Pour le profilage, nous plaçons la charge Ă 1000 requĂȘtes par seconde, puis utilisons pprof pour collecter des donnĂ©es afin de dĂ©terminer oĂč l'application consomme du temps processeur. Cela peut ĂȘtre fait en activant le point de terminaison HTTP. pprof, puis en chargeant les rĂ©sultats avec curl :
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profLes rĂ©sultats peuvent ĂȘtre affichĂ©s comme suit :
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
Le graphique montre oĂč et combien l'application dĂ©pense de temps CPU. D'aprĂšs la description de :
L'axe X représente le remplissage du profil de la pile, trié par ordre alphabétique (ce n'est pas le temps), l'axe Y montre la profondeur de la pile, commençant à zéro dans [top]. Chaque rectangle représente un cadre de la pile. Plus le cadre est large, plus il apparaßt souvent dans les piles. Ce qui est en haut fonctionne sur le CPU, et en bas se trouvent les éléments enfants. Les couleurs, en général, ne signifient rien, elles sont simplement choisies au hasard pour distinguer les cadres.
Analyse â hypothĂšse
Pour la configuration, nous nous concentrerons sur la recherche des gaspillages inutiles de temps CPU. Nous allons chercher les plus grandes sources de dĂ©penses inutiles et les supprimer. Et Ă©tant donnĂ© que le profilage rĂ©vĂšle trĂšs prĂ©cisĂ©ment oĂč l'application dĂ©pense son temps CPU, il faudra peut-ĂȘtre le faire plusieurs fois, et il sera Ă©galement nĂ©cessaire de modifier le code source de l'application, de redĂ©marrer les tests et d'observer que les performances se rapprochent de ce qui est prĂ©vu.
En suivant les recommandations de Brendan Gregg, nous allons lire le graphique de haut en bas. Chaque ligne représente un cadre de la pile (appel de fonction). La premiÚre ligne est le point d'entrée de la programme, parent de tous les autres appels (en d'autres termes, tous les autres appels auront cette ligne dans leur pile). La ligne suivante est déjà différente :
![]()
Si l'on survole le nom de la fonction sur le graphique, le temps total qu'elle a passĂ© dans la pile pendant le dĂ©bogage s'affiche. La fonction HTTPServe y Ă©tait prĂ©sente 65 % du temps, d'autres fonctions runtime, runtime.mcall, mstart et gc, ont pris le reste du temps. Fait intĂ©ressant : 5 % du temps total a Ă©tĂ© consacrĂ© aux requĂȘtes DNS :

Les adresses que le programme recherche appartiennent Ă Postgresql. Cliquez sur FindByAge:

Il est intĂ©ressant de noter que le programme montre qu'il y a essentiellement trois sources principales qui ajoutent des dĂ©lais : l'ouverture et la fermeture des connexions, la requĂȘte de donnĂ©es et la connexion Ă la base. Sur le graphique, on voit que les requĂȘtes DNS, l'ouverture et la fermeture des connexions reprĂ©sentent environ 13 % du temps d'exĂ©cution total.
HypothĂšse : La rĂ©utilisation des connexions via un pool devrait rĂ©duire le temps d'une requĂȘte HTTP, permettant une plus grande bande passante et des dĂ©lais plus courts..
Configuration de l'application â expĂ©rience.
Nous mettons Ă jour le code source, essayons de supprimer la connexion Ă Postgresql pour chaque requĂȘte. PremiĂšre option â utilisation. au niveau de l'application. Dans cette expĂ©rience, nous un pool de connexions Ă l'aide du pilote SQL pour Go :
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Exécution, observation, analyse.
AprĂšs le redĂ©marrage du test avec 1000 requĂȘtes par seconde, il est clair que le p99 des dĂ©lais est revenu Ă la normale avec un SLO de 60 ms !
Qu'en est-il du coût ?
10000 requĂȘtes par seconde / Ă 1000 requĂȘtes par serveur = 10 serveurs + 1.
Faisons encore mieux !
2000 requĂȘtes par seconde.

Le doublement de la charge montre la mĂȘme chose, le graphique en haut Ă gauche montre que l'application parvient Ă traiter 2000 requĂȘtes par seconde, p100 infĂ©rieur Ă 60 ms, p99 respecte le SLO.
Du point de vue des coûts :
10000 requĂȘtes par seconde / Ă 2000 requĂȘtes par serveur = 5 serveurs + 1.
3000 requĂȘtes par seconde.

Ici, l'application peut traiter 3000 requĂȘtes avec un retard p99 infĂ©rieur Ă 60 ms. Le SLO n'est pas enfreint, et le coĂ»t est pris comme suit :
10000 requĂȘtes par seconde / Ă 3000 requĂȘtes par serveur = 4 serveurs + 1. (l'auteur a arrondi au supĂ©rieur, note du traducteur)
Essayons un autre round d'analyse.
Analyse â hypothĂšse
Nous collectons et affichons les rĂ©sultats du dĂ©bogage de l'application Ă 3000 requĂȘtes par seconde :

6 % du temps est encore consacré à l'établissement des connexions. La configuration du pool a amélioré la performance, mais il est clair que l'application continue de créer de nouvelles connexions avec la base.
HypothÚse : Les connexions, malgré la présence du pool, sont toujours rejetées et nettoyées, donc l'application doit les rétablir. Fixer le nombre de connexions en attente à la taille du pool devrait aider avec le délai en minimisant le temps que l'application consacre à la création de connexions..
Configuration de l'application â expĂ©rience.
Essayons de fixer à la taille du pool (décrit également ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Exécution, observation, analyse.
3000 requĂȘtes par seconde.

p99 inférieur à 60 ms avec un p100 nettement inférieur !

La vĂ©rification du flame graph montre que l'Ă©tablissement de la connexion n'est plus perceptible ! VĂ©rifions en dĂ©tail. pg(*conn).query â nous ne remarquons Ă©galement pas d'Ă©tablissement de connexion ici.

Conclusion
L'analyse de la performance est essentielle pour comprendre si les attentes des clients et les exigences non fonctionnelles sont satisfaites. L'analyse par la mise en correspondance des observations avec les attentes des clients peut aider à déterminer ce qui est acceptable et ce qui ne l'est pas. Go fournit des outils efficaces, intégrés dans la bibliothÚque standard, qui rendent l'analyse simple et accessible.
Source : habr.com
