Il y a 10 ans, Google était confronté à un goulot d’étranglement critique causé par des temps de compilation C++ extrêmement prolongés et avait besoin d’une manière totalement nouvelle de le résoudre. Les ingénieurs de Google ont relevé le défi en créant un nouveau langage appelé Go (alias Golang). Le nouveau langage de Go emprunte les meilleures parties du C++, (plus particulièrement ses performances et ses caractéristiques de sécurité) et les combine avec la vitesse de Python pour permettre à Go d’utiliser rapidement plusieurs cœurs tout en étant capable d’implémenter la concurrence.

Ici, chez Coralogix, nous analysons les logs de nos clients afin de leur donner des aperçus en temps réel, des alertes et des méta-données sur leurs logs. Pour ce faire, la phase de parsing, qui est très complexe et chargée de tonnes de règles pour chaque service de ligne de log, doit être extrêmement rapide. C’est l’une des raisons pour lesquelles nous avons décidé d’utiliser le langage Go.

Le nouveau service fonctionne maintenant à plein temps en production et, même si nous constatons d’excellents résultats, il doit fonctionner sur des machines très performantes. Plus de dizaines de milliards de logs sont analysés chaque jour par ce service Go qui fonctionne sur une instance AWS m4.2xlarge avec 8 CPU et 36 Go de mémoire.

À ce stade, nous aurions pu nous arrêter là en nous sentant bien que tout fonctionne bien, mais ce n’est pas comme cela que nous fonctionnons ici chez Coralogix. Nous voulions plus de fonctionnalités (performances, etc.) en utilisant moins (instances AWS). Afin de nous améliorer, nous devions d’abord comprendre la nature de nos goulots d’étranglement et comment nous pouvons les réduire ou les éliminer complètement.

Nous avons décidé d’exécuter un profilage Golang sur notre service et de vérifier ce qui causait exactement une consommation élevée de CPU pour voir si nous pouvons optimiser.

D’abord, nous avons mis à niveau vers la dernière version stable de Go (une partie clé du cycle de vie du logiciel). Nous étions sur la version Go v1.12.4, et la dernière était 1.13.8. La version 1.13, selon la documentation, a apporté des améliorations majeures à la bibliothèque d’exécution et à quelques autres composants qui ont principalement utilisé la mémoire. Bottom line, travailler avec la dernière version stable était utile et nous a épargné pas mal de travail →

Donc, la consommation de mémoire s’est améliorée d’environ ~800MB à ~180MB.

Deuxièmement, afin d’obtenir une meilleure compréhension de notre processus et de comprendre où nous dépensons du temps et des ressources, nous avons commencé à profiler.

Profiler différents services et langages de programmation peut sembler complexe et intimidant, mais c’est en fait assez facile en Go et peut être décrit en quelques commandes. Go a un outil dédié appelé ‘pprof’ qui devrait être activé dans votre application en écoutant une route (port par défaut- 6060) et utiliser le paquet Go pour gérer les connexions http:

import _ "net/http/pprof"

Puis initialiser ce qui suit dans votre fonction principale ou sous votre paquet route:

go func() {log.Println(http.ListenAndServe("localhost:6060", nil))}()

Maintenant vous pouvez démarrer votre service et vous connecter à

http://localhost:6060/debug/pprof

La documentation complète par Go peut être trouvée ici.

Le profilage par défaut pour pprof sera un échantillonnage de 30 secondes de l’utilisation du CPU. Il y a quelques chemins différents qui permettent l’échantillonnage pour l’utilisation du CPU, l’utilisation du tas et plus encore.

Nous nous sommes concentrés sur notre utilisation du CPU, nous avons donc pris un profilage de 30 secondes en production et avons découvert ce que vous voyez dans l’image ci-dessous (rappel : c’est après avoir mis à niveau notre version de Go et diminué les parties internes de Go au minimum) :

Profilage Go – Coralogix

Comme vous pouvez le voir, nous avons trouvé beaucoup d’activité de paquets d’exécution, ce qui indique spécifiquement une activité GC → Près de 29% de notre CPU (juste le top 20 des objets les plus consommés) est utilisé par GC. Puisque Go GC est assez rapide et plutôt optimisé, la meilleure pratique est de ne pas le changer ou le modifier et puisque notre consommation de mémoire était très faible (par rapport à notre version précédente de Go), le principal suspect était un taux d’allocation d’objets élevé.

Si c’est le cas, il y a deux choses que nous pouvons faire :

  • Tuner l’activité Go GC pour l’adapter au comportement de notre service, c’est-à-dire – retarder son déclenchement afin d’activer le GC moins fréquemment. Cela nous obligera à compenser avec plus de mémoire.
  • Trouver la fonction, la zone ou la ligne de notre code qui alloue trop d’objets.

En regardant notre type d’instance, il était clair que nous avions beaucoup de mémoire à disposition et que nous sommes actuellement liés par le CPU de la machine. Nous avons donc simplement changé ce ratio. Golang, depuis ses premiers jours, a un drapeau que la plupart des développeurs ne connaissent pas, appelé GOGC. Ce drapeau, avec une valeur par défaut de 100, indique simplement au système quand déclencher la GC. La valeur par défaut déclenche le processus GC lorsque le tas atteint 100% de sa taille initiale. En changeant cette valeur pour un nombre plus élevé, le déclenchement de la GC sera retardé et en la diminuant, la GC sera déclenchée plus tôt. Nous avons commencé à tester quelques valeurs différentes et les meilleures performances ont été obtenues en utilisant : GOGC=2000.

Cela a immédiatement augmenté notre utilisation de la mémoire de ~200MB à ~2,7GB (C’est après que la consommation de mémoire ait diminué en raison de notre mise à jour de la version Go) et a diminué notre utilisation du CPU de ~10%.
La capture d’écran suivante démontre les résultats du benchmark:

GOGC =2000 résultats – benchmark Coralogix

Les 4 fonctions les plus consommatrices de CPU sont les fonctions de notre service, ce qui est logique. L’utilisation totale du GC est maintenant de ~13%, moins de la moitié de sa consommation précédente( !)

Nous aurions pu nous arrêter là, mais nous avons décidé de découvrir où et pourquoi nous allouons tant d’objets. Plusieurs fois, il y a une bonne raison pour cela (par exemple dans le cas du traitement des flux où nous créons beaucoup de nouveaux objets pour chaque message que nous recevons et où nous devons nous en débarrasser parce qu’il n’est pas pertinent pour le prochain message), mais il y a des cas dans lesquels il y a un moyen facile d’optimiser et de diminuer considérablement la création d’objets.

Pour commencer, exécutons la même commande que précédemment avec un petit changement pour prendre le heap dump:

http://localhost:6060/debug/pprof/heap

Afin d’interroger le fichier résultat, vous pouvez exécuter la commande suivante dans votre dossier de code afin d’analyser le dump :

go tool pprof -alloc_objects <HEAP.PROFILE.FILE>

notre instantané ressemblait à ceci:

Tout semblait raisonnable sauf pour la troisième ligne, qui est une fonction de surveillance faisant un rapport à notre exportateur prometheus à la fin de chaque phase de parsing de règles Coralogix. Afin d’aller plus en profondeur, nous avons exécuté la commande suivante:

list <FunctionName>

Par exemple:

list reportRuleExecution

Et nous avons obtenu ce qui suit:

.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.