Analyser les performances de production avec Stackdriver Profiler

Alors que les développeurs d'applications clientes et de sites Web frontend utilisent couramment des outils tels que le profileur de processeur Android Studio ou les outils de profilage inclus dans Chrome pour améliorer les performances de leur code, les techniques équivalentes n'ont pas été aussi accessibles ni aussi bien adoptées par ceux qui travaillent sur les services backend. Stackdriver Profiler offre les mêmes fonctionnalités aux développeurs de services, que leur code s'exécute sur Google Cloud Platform ou ailleurs.

L'outil recueille des informations sur l'utilisation du processeur et l'allocation de mémoire à partir de vos applications de production. Il attribue ces informations au code source de l'application, ce qui vous permet d'identifier les parties du code qui consomment le plus de ressources et de mettre en évidence les caractéristiques de performance du code. Les faibles besoins supplémentaires des techniques de collecte utilisées par l'outil le rendent adapté à une utilisation continue dans les environnements de production.

Dans cet atelier de programmation, vous allez apprendre à configurer Stackdriver Profiler pour un programme Go et à vous familiariser avec les informations sur les performances des applications que l'outil peut présenter.

Points abordés

  • Configurer un programme Go pour le profilage avec Stackdriver Profiler
  • Comment collecter, afficher et analyser les données de performances avec Stackdriver Profiler.

Prérequis

  • Un projet Google Cloud Platform.
  • Un navigateur tel que Chrome ou Firefox
  • Bonne connaissance des éditeurs de texte Linux standards tels que Vim, EMACs ou Nano

Comment allez-vous utiliser ce tutoriel ?

Je vais le lire uniquement Je vais le lire et effectuer les exercices

Quel est votre niveau d'expérience avec Google Cloud Platform ?

Débutant Intermédiaire Expert

Configuration de l'environnement au rythme de chacun

Si vous ne possédez pas encore de compte Google (Gmail ou Google Apps), vous devez en créer un. Connectez-vous à la console Google Cloud Platform (console.cloud.google.com) et créez un projet :

Screenshot from 2016-02-10 12:45:26.png

Mémorisez l'ID du projet. Il s'agit d'un nom unique permettant de différencier chaque projet Google Cloud (le nom ci-dessus est déjà pris ; vous devez en trouver un autre). Il sera désigné par le nom PROJECT_ID tout au long de cet atelier de programmation.

Vous devez ensuite activer la facturation dans la console Cloud pour pouvoir utiliser les ressources Google Cloud.

Suivre cet atelier de programmation ne devrait pas vous coûter plus d'un euro. Cependant, cela peut s'avérer plus coûteux si vous décidez d'utiliser davantage de ressources ou si vous n'interrompez pas les ressources (voir la section "Effectuer un nettoyage" à la fin du présent document).

Les nouveaux utilisateurs de Google Cloud Platform peuvent bénéficier d'un essai sans frais avec 300$de crédits.

Google Cloud Shell

Bien que Google Cloud puisse être utilisé à distance depuis votre ordinateur portable, nous allons nous servir de Google Cloud Shell pour cet atelier de programmation, un environnement de ligne de commande exécuté dans le cloud, afin de simplifier la configuration.

Activer Google Cloud Shell

Depuis la console GCP, cliquez sur l'icône Cloud Shell de la barre d'outils située dans l'angle supérieur droit :

Cliquez ensuite sur "Démarrer Cloud Shell" :

Le provisionnement de l'environnement et la connexion ne devraient pas prendre plus de quelques minutes :

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle intègre un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier dans un simple navigateur ou sur votre Chromebook Google.

Une fois connecté à Cloud Shell, vous êtes en principe authentifié, et le projet est déjà défini avec votre PROJECT_ID.

Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :

gcloud auth list

Résultat de la commande

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

Résultat de la commande

[core]
project = <PROJECT_ID>

Si vous obtenez un résultat différent, exécutez cette commande :

gcloud config set project <PROJECT_ID>

Résultat de la commande

Updated property [core/project].

Dans la console Cloud, accédez à l'interface utilisateur Profiler en cliquant sur "Profiler" dans la barre de navigation de gauche :

Vous pouvez également utiliser la barre de recherche de la console Cloud pour accéder à l'interface utilisateur de Profiler : il vous suffit de saisir "Stackdriver Profiler" et de sélectionner l'élément trouvé. Quelle que soit la méthode choisie, l'interface utilisateur Profiler doit s'afficher avec le message "Aucune donnée à afficher", comme ci-dessous. Le projet est nouveau et ne contient donc pas encore de données de profilage.

Il est maintenant temps de profiler quelque chose !

Nous allons utiliser une application Go synthétique simple disponible sur GitHub. Dans le terminal Cloud Shell que vous avez laissé ouvert (et tant que le message "Aucune donnée à afficher" s'affiche dans l'interface utilisateur du Profileur), exécutez la commande suivante :

$ go get -u github.com/GoogleCloudPlatform/golang-samples/profiler/...

Accédez ensuite au répertoire de l'application :

$ cd ~/gopath/src/github.com/GoogleCloudPlatform/golang-samples/profiler/hotapp

Le répertoire contient le fichier "main.go", qui est une application synthétique pour laquelle l'agent de profilage est activé :

main.go

...
import (
        ...
        "cloud.google.com/go/profiler"
)
...
func main() {
        err := profiler.Start(profiler.Config{
                Service:        "hotapp-service",
                DebugLogging:   true,
                MutexProfiling: true,
        })
        if err != nil {
                log.Fatalf("failed to start the profiler: %v", err)
        }
        ...
}

L'agent de profilage collecte les profils de CPU, de tas de mémoire et de thread par défaut. Le code ci-dessous permet de collecter les profils de mutex (également appelés "contention").

Exécutez le programme :

$ go run main.go

Pendant l'exécution du programme, l'agent de profilage collecte périodiquement des profils des cinq types configurés. La collecte est aléatoire dans le temps (avec un taux moyen d'un profil par minute pour chacun des types). Il peut donc s'écouler jusqu'à trois minutes avant que chacun des types ne soit collecté. Le programme vous indique quand il crée un profil. Les messages sont activés par l'indicateur DebugLogging dans la configuration ci-dessus. Sinon, l'agent s'exécute en mode silencieux :

$ go run main.go
2018/03/28 15:10:24 profiler has started
2018/03/28 15:10:57 successfully created profile THREADS
2018/03/28 15:10:57 start uploading profile
2018/03/28 15:11:19 successfully created profile CONTENTION
2018/03/28 15:11:30 start uploading profile
2018/03/28 15:11:40 successfully created profile CPU
2018/03/28 15:11:51 start uploading profile
2018/03/28 15:11:53 successfully created profile CONTENTION
2018/03/28 15:12:03 start uploading profile
2018/03/28 15:12:04 successfully created profile HEAP
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:04 successfully created profile THREADS
2018/03/28 15:12:04 start uploading profile
2018/03/28 15:12:25 successfully created profile HEAP
2018/03/28 15:12:25 start uploading profile
2018/03/28 15:12:37 successfully created profile CPU
...

L'UI se mettra à jour peu de temps après la collecte du premier profil. Il ne se mettra pas à jour automatiquement par la suite. Pour afficher les nouvelles données, vous devrez donc actualiser manuellement l'interface utilisateur du Profileur. Pour ce faire, cliquez deux fois sur le bouton "Maintenant" dans le sélecteur d'intervalle de temps :

Une fois l'interface utilisateur actualisée, un résultat semblable à ce qui suit va s'afficher :

Le sélecteur de type de profil affiche les cinq types de profil disponibles :

Nous allons maintenant passer en revue chacun des types de profils et certaines fonctionnalités importantes de l'UI, puis effectuer quelques tests. À ce stade, vous n'avez plus besoin du terminal Cloud Shell. Vous pouvez donc le quitter en appuyant sur CTRL+C et en saisissant "exit".

Maintenant que nous avons collecté des données, examinons-les de plus près. Nous utilisons une application synthétique (dont la source est disponible sur GitHub) qui simule des comportements typiques de différents types de problèmes de performances en production.

Code nécessitant une utilisation intensive du processeur

Sélectionnez le type de profil du processeur. Une fois l'UI chargée, vous verrez dans le graphique en flammes les quatre blocs de feuilles pour la fonction load, qui représentent collectivement toute la consommation de CPU :

Cette fonction est spécifiquement écrite pour consommer beaucoup de cycles de processeur en exécutant une boucle serrée :

main.go

func load() {
        for i := 0; i < (1 << 20); i++ {
        }
}

La fonction est appelée indirectement à partir de busyloop() via quatre chemins d'appel : busyloop → {foo1, foo2} → {bar, baz} → load. La largeur d'une boîte de fonction représente le coût relatif du chemin d'appel spécifique. Dans ce cas, les quatre chemins ont à peu près le même coût. Dans un programme réel, vous devez vous concentrer sur l'optimisation des chemins d'appel qui comptent le plus en termes de performances. Le graphique en flammes, qui met visuellement en évidence les chemins les plus coûteux avec des boîtes plus grandes, permet de les identifier facilement.

Vous pouvez utiliser le filtre de données de profil pour affiner davantage l'affichage. Par exemple, essayez d'ajouter un filtre "Afficher les piles" en spécifiant "baz" comme chaîne de filtre. Vous devriez voir quelque chose comme la capture d'écran ci-dessous, où seuls deux des quatre chemins d'appel vers load() sont affichés. Ces deux chemins sont les seuls à passer par une fonction dont le nom contient la chaîne "baz". Ce filtrage est utile lorsque vous souhaitez vous concentrer sur une sous-partie d'un programme plus vaste (par exemple, parce que vous n'en possédez qu'une partie).

Code gourmand en mémoire

Passez maintenant au type de profil "Heap". Veillez à supprimer tous les filtres que vous avez créés dans les tests précédents. Vous devriez maintenant voir un graphique de type "flamme" où allocImpl, appelé par alloc, est affiché comme le principal consommateur de mémoire dans l'application :

Le tableau récapitulatif situé au-dessus du graphique de type "flamme" indique que la quantité totale de mémoire utilisée dans l'application est en moyenne d'environ 57,4 Mio, dont la majeure partie est allouée par la fonction allocImpl. Ce n'est pas surprenant, étant donné l'implémentation de cette fonction :

main.go

func allocImpl() {
        // Allocate 64 MiB in 64 KiB chunks
        for i := 0; i < 64*16; i++ {
                mem = append(mem, make([]byte, 64*1024))
        }
}

La fonction s'exécute une fois, en allouant 64 Mio en plus petits blocs, puis en stockant des pointeurs vers ces blocs dans une variable globale pour les protéger contre la récupération de mémoire. Notez que la quantité de mémoire indiquée comme utilisée par le profileur est légèrement différente de 64 Mio : le profileur de segment de mémoire Go est un outil statistique. Les mesures sont donc peu gourmandes en ressources, mais pas précises au byte près. Ne soyez pas surpris si vous constatez une différence d'environ 10 %.

Code gourmand en E/S

Si vous sélectionnez "Threads" dans le sélecteur de type de profil, l'affichage passe à un graphique de type "flamme" où la majeure partie de la largeur est occupée par les fonctions wait et waitImpl :

Dans le récapitulatif au-dessus du graphique de type "flamme", vous pouvez voir qu'il existe 100 goroutines qui développent leur pile d'appels à partir de la fonction wait. C'est tout à fait normal, car le code qui lance ces attentes ressemble à ceci :

main.go

func main() {
        ...
        // Simulate some waiting goroutines.
        for i := 0; i < 100; i++ {
                go wait()
        }

Ce type de profil est utile pour comprendre si le programme passe un temps inattendu dans les attentes (comme les E/S). Ces piles d'appels ne sont généralement pas échantillonnées par le profileur de CPU, car elles ne consomment aucune part importante du temps CPU. Vous souhaiterez souvent utiliser des filtres "Masquer les piles " avec les profils Threads. Par exemple, pour masquer toutes les piles se terminant par un appel à gopark,, car il s'agit souvent de goroutines inactives et moins intéressantes que celles qui attendent des E/S.

Le type de profil "Threads" peut également aider à identifier les points du programme où les threads attendent un mutex appartenant à une autre partie du programme pendant une longue période, mais le type de profil suivant est plus utile à cet effet.

Code à forte contention

Le type de profil "Contention" identifie les verrous les plus "recherchés" dans le programme. Ce type de profil est disponible pour les programmes Go, mais doit être explicitement activé en spécifiant "MutexProfiling: true" dans le code de configuration de l'agent. La collecte fonctionne en enregistrant (sous la métrique "Contentions") le nombre de fois où un verrou spécifique, lorsqu'il est déverrouillé par une goroutine A, avait une autre goroutine B en attente de déverrouillage. Il enregistre également (sous la métrique "Délai") le temps pendant lequel la goroutine bloquée a attendu le verrou. Dans cet exemple, il existe une seule pile de contention et le temps d'attente total pour le verrouillage était de 11,03 secondes :

Le code qui génère ce profil se compose de quatre goroutines qui se disputent un mutex :

main.go

func contention(d time.Duration) {
        contentionImpl(d)
}

func contentionImpl(d time.Duration) {
        for {
                mu.Lock()
                time.Sleep(d)
                mu.Unlock()
        }
}
...
func main() {
        ...
        for i := 0; i < 4; i++ {
                go contention(time.Duration(i) * 50 * time.Millisecond)
        }
}

Dans cet atelier, vous avez appris à configurer un programme Go pour l'utiliser avec Stackdriver Profiler. Vous avez également appris à collecter, afficher et analyser les données de performances avec cet outil. Vous pouvez désormais appliquer vos nouvelles compétences aux services réels que vous exécutez sur Google Cloud Platform.

Vous avez appris à configurer et à utiliser Stackdriver Profiler.

En savoir plus

Licence

Ce document est publié sous une licence Creative Commons Attribution 2.0 Generic.