Analyser les performances de production avec Cloud Profiler

1. Présentation

Les développeurs d'applications clientes et d'interfaces Web 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. Toutefois, des techniques équivalentes n'ont pas été aussi accessibles ni aussi bien adoptées par les personnes qui travaillent sur des services de backend. Cloud Profiler offre ces mêmes fonctionnalités aux développeurs de services, que leur code soit exécuté sur Google Cloud Platform ou ailleurs.

95c034c70c9cac22.png

L'outil collecte 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. Peu d'efforts liés aux techniques de collecte employées par l'outil, il est adapté à une utilisation continue dans les environnements de production.

Dans cet atelier de programmation, vous apprendrez à configurer Cloud Profiler pour un programme Go et vous vous familiariserez avec le type d'insights sur les performances des applications que l'outil peut fournir.

Points abordés

  • Configurer un programme Go pour le profilage avec Cloud Profiler
  • Collecter, afficher et analyser les données de performances avec Cloud 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

Comment évalueriez-vous votre expérience avec Google Cloud Platform ?

<ph type="x-smartling-placeholder"></ph> Débutant Intermédiaire Expert
.

2. Préparation

Configuration de l'environnement d'auto-formation

  1. Connectez-vous à la console Cloud, puis créez un projet ou réutilisez un projet existant. (Si vous ne possédez pas encore de compte Gmail ou Google Workspace, vous devez en créer un.)

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.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.

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

L'exécution de cet atelier de programmation est très peu coûteuse, voire gratuite. Veillez à suivre les instructions de la section "Nettoyer" qui indique comment désactiver les ressources afin d'éviter les frais une fois ce tutoriel terminé. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai sans frais pour bénéficier d'un crédit de 300$.

Google Cloud Shell

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

Activer Cloud Shell

  1. Dans Cloud Console, cliquez sur Activer Cloud Shell 4292cbf4971c9786.png.

bce75f34b2c53987.png

Si vous n'avez jamais démarré Cloud Shell auparavant, un écran intermédiaire (en dessous de la ligne de flottaison) vous explique de quoi il s'agit. Dans ce cas, cliquez sur Continuer (elle ne s'affiche plus jamais). Voici à quoi il ressemble :

70f315d7b402b476.png

Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.

fbe3a0674c982259.png

Cette machine virtuelle contient tous les outils de développement dont vous avez besoin. Elle comprend 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.

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

  1. 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
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que la commande gcloud connaît votre projet:
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].

3. Accéder à Cloud Profiler

Dans la console Cloud, cliquez sur "Profiler" pour accéder à l'interface utilisateur de Profiler. dans la barre de navigation de gauche:

37ad0df7ddb2ad17.png

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 "Cloud Profiler" et sélectionnez l'élément trouvé. Dans tous les cas, l'interface utilisateur de Profiler devrait s'afficher avec le message "Aucune donnée à afficher". comme indiqué ci-dessous. Le projet est nouveau. Aucune donnée de profilage n'a donc encore été collectée.

d275a5f61ed31fb2.png

Il est maintenant temps de profiler quelque chose !

4. Profiler le benchmark

Nous allons utiliser une application Go synthétique simple disponible sur GitHub. Dans le terminal Cloud Shell qui est encore ouvert (et que le message "Aucune donnée à afficher" s'affiche toujours dans l'interface utilisateur de Profiler), exécutez la commande suivante:

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

Passez 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)
        }
        ...
}

Par défaut, l'agent de profilage collecte des profils de processeur, de tas de mémoire et de thread. Le code utilisé ici active la collecte de profils de mutex (également appelés profils de « contention »).

Exécutez maintenant le programme:

$ go run main.go

Pendant l'exécution du programme, l'agent de profilage collecte régulièrement les profils des cinq types configurés. La collecte est aléatoire au fil du temps (avec un taux moyen d'un profil par minute pour chacun des types). Par conséquent, la collecte de chacun des types peut prendre jusqu'à trois minutes. Le programme vous informe lorsqu'il crée un profil. Les messages sont activés par l'option DebugLogging dans la configuration ci-dessus. Sinon, l'agent s'exécute silencieusement:

$ 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'interface utilisateur 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 actualiser manuellement l'interface utilisateur de Profiler. Pour ce faire, cliquez deux fois sur le bouton Maintenant dans l'outil de sélection de l'intervalle de temps:

650051097b651b91.png

Une fois l'interface utilisateur actualisée, le résultat devrait ressembler à ceci:

47a763d4dc78b6e8.png

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

b5d7b4b5051687c9.png

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

5. Analyser les données de Profiler

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

Code nécessitant une utilisation intensive des processeurs

Sélectionnez le type de profil de processeur. Une fois que l'UI l'a chargée, dans le graphique de type "flamme", vous pouvez voir les quatre blocs feuilles de la fonction load, qui représentent ensemble toute la consommation du processeur:

fae661c9fe6c58df.png

Cette fonction est spécifiquement écrite pour consommer de nombreux cycles de processeur en exécutant une boucle étroite:

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 zone 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 les plus importants en termes de performances. Le graphique de type "flamme", qui met visuellement en évidence les chemins les plus coûteux avec des cadres plus grands, facilite l'identification de ces chemins.

Vous pouvez utiliser le filtre des données de profil pour affiner davantage l'affichage. Par exemple, essayez d'ajouter une option "Afficher les piles" filtre spécifiant "baz" comme chaîne de filtre. Vous devriez voir une capture d'écran semblable à 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 avec la chaîne "baz" dans son nom. 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).

eb1d97491782b03f.png

Code exigeant une utilisation intensive de la mémoire

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

f6311c8c841d04c4.png

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, la majeure partie étant allouée par la fonction allocImpl. Ce n'est pas surprenant, compte tenu de 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 seule fois, en allouant 64 Mio en fragments plus petits, puis en stockant les pointeurs vers ces fragments dans une variable globale pour éviter qu'ils ne soient récupérés. Notez que la quantité de mémoire utilisée par le profileur est légèrement différente de 64 Mio: le profileur de segments de mémoire Go est un outil statistique. Par conséquent, les mesures sont peu gourmandes en ressources, mais ne sont pas précises à l'octet. Ne soyez pas surpris de constater une différence d'environ 10% comme celle-ci.

Code à utilisation intensive d'E/S

Si vous choisissez "Fils de discussion" Dans le sélecteur de type de profil, l'écran affiche un graphique de type "flamme" dont la majeure partie de la largeur est extraite par les fonctions wait et waitImpl:

ebd57fdff01dede9.png

Dans le graphique de type "flamme" récapitulatif ci-dessus, vous pouvez constater que 100 goroutines augmentent leur pile d'appels à partir de la fonction wait. C'est tout à fait exact, étant donné que le code qui déclenche ces temps d'attente se présente comme suit:

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 certain temps d'attente imprévu (comme les E/S). Ces piles d'appels ne sont généralement pas échantillonnées par le Profileur de processeur, car elles ne consomment pas une grande partie du temps CPU. Vous utiliserez souvent « Masquer les piles » Filtres avec des 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 les E/S.

Le type de profil des 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 pour cela.

Code générant beaucoup de conflits

Le type de profil "Contention" identifie les profils les plus "soumis" se verrouille 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, est dans lequel une autre goroutine B attend le déverrouillage du verrou. Elle enregistre également (sous la métrique "Delay") le temps d'attente du verrouillage par la goroutine bloquée. Dans cet exemple, il existe une seule pile de conflit et le temps total d'attente pour le verrouillage est de 10,5 secondes:

83f00dca4a0f768e.png

Le code qui génère ce profil est constitué 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)
        }
}

6. Résumé

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

7. Félicitations !

Vous avez appris à configurer et à utiliser Cloud Profiler.

En savoir plus

Licence

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