Analyser la latence des microservices grâce au traçage distribué avec OpenCensus et Cloud Trace

Dans ce tutoriel, nous vous expliquons comment collecter des informations de trace dans les applications de microservices à l'aide d'OpenCensus et de Cloud Trace. Le traçage distribué est un système qui vous aide à comprendre comment les requêtes traversent des architectures de microservices complexes, et à appréhender la latence associée à chaque étape, ce qui constitue une capacité clé d'observabilité. Les applications sont dans une phase de transition vers les microservices et le nombre de composants et de points de terminaison associés à une seule transaction utilisateur est en hausse. C'est pourquoi l'observabilité reste essentielle pour le bon fonctionnement des services fournis aux utilisateurs.

Pour les services exécutés sur Kubernetes, l'utilisation d'un maillage de services tel que Istio permet un traçage distribué du trafic de service à service sans nécessiter d'instrumentation dédiée. Cependant, il est possible que vous souhaitiez contrôler davantage les traces, que vous deviez collecter des éléments internes de l'application dans les informations de trace ou que vous deviez assurer un suivi du code qui ne s'exécute pas sur Kubernetes. OpenCensus est une bibliothèque Open Source qui permet l'instrumentation des applications de microservices distribuées afin de collecter des traces et des métriques pour une grande variété de langages, de plates-formes et d'environnements.

Ce tutoriel est destiné aux développeurs et aux ingénieurs SRE et DevOps qui souhaitent comprendre les principes de base du traçage distribué et les appliquer à leurs services pour améliorer leur observabilité.

Dans ce tutoriel, nous partons du principe que vous connaissez les éléments suivants :

Les rapports sur l'état du DevOps identifient les fonctionnalités qui améliorent les performances de livraison de logiciels. Ce tutoriel vous permettra d'effectuer les opérations suivantes :

Objectifs

  • Créer un cluster GKE et déployer un exemple d'application
  • Examiner le code d'instrumentation OpenCensus
  • Examiner les traces et les journaux générés par l'instrumentation

Architecture de référence

Le schéma suivant illustre l'architecture déployée dans ce tutoriel.

Architecture du tutoriel avec deux clusters GKE.

Comme illustré dans le schéma ci-dessus, vous créez deux clusters GKE et déployez une application dans chacun de ces clusters. Le trafic utilisateur est envoyé vers l'application de frontend, sur le cluster de frontend. Le pod de frontend du cluster de frontend communique avec le pod de backend du cluster de backend. Le pod de backend appelle un point de terminaison d'API externe.

Vous utilisez Cloud Build, une plate-forme entièrement gérée d'intégration, de diffusion et de déploiement continus, pour créer des images de conteneurs à partir de l'exemple de code et les stocker dans Container Registry. Les clusters GKE extraient les images de Container Registry au moment du déploiement.

Exemple d'application

L'exemple d'application présenté dans ce tutoriel est composé de deux microservices écrits dans le langage Go.

Le service de frontend accepte les requêtes HTTP sur l'URL / et appelle le service de backend. L'adresse du service de backend est définie par la variable d'environnement BACKEND. La valeur de cette variable est définie dans un objet ConfigMap que vous créez.

Le service de backend accepte les requêtes HTTP sur l'URL / et effectue un appel sortant vers une URL externe telle que définie dans la variable d'environnement DESTINATION_URL. La valeur de la variable est définie via un objet ConfigMap. Une fois l'appel externe terminé, le service backend renvoie l'appel d'état HTTP, par exemple 200, à l'appelant.

Comprendre les traces, les délais et le contexte

Une meilleure description du concept de traçage distribué est fournie dans l'étude sur Dapper publiée par Google. Le schéma suivant, issu de cette étude, montre cinq délais dans une trace.

Traçage distribué impliquant cinq délais dans une trace.

Le schéma illustre une requête de frontend unique qui effectue deux requêtes de backend. Le second appel de backend nécessite deux appels d'aide. Chaque appel est associé à son ID de délai et à l'ID du délai parent.

Une trace correspond au total des informations qui décrivent la manière dont un système distribué répond à une requête utilisateur. Les traces sont composées de délais, dont chacun représente une paire spécifique, constituée d'une requête et d'une réponse, impliquée dans le traitement de la demande de l'utilisateur. Le délai parent décrit la latence observée par l'utilisateur final. Chacun des délais enfants décrit la façon dont un service particulier du système distribué a été appelé et traité. Des informations de latence collectées pour chacun d'eux sont également indiquées.

L'une des difficultés du traçage dans des systèmes distribués réside dans le fait que les informations relatives à la requête de frontend d'origine ne sont pas automatiquement transférées lorsque des requêtes ultérieures sont envoyées à plusieurs services de backend. Dans Go, vous pouvez effectuer des requêtes en indiquant le contexte (adresse IP et identifiants du cluster), ce qui signifie que des informations supplémentaires sont chargées dans l'en-tête HTTP. Le concept de contexte est étendu avec OpenCensus, qui inclut le contexte de délai dans lequel vous pouvez inclure des informations sur le délai parent dans chaque requête ultérieure. Vous pouvez ensuite ajouter des délais enfants pour composer la trace globale, ce qui vous permet de voir comment la requête utilisateur a traversé le système, jusqu'à son traitement côté utilisateur.

Le contexte n'est pas spécifique à Go. Pour en savoir plus sur les langages offrant une compatibilité de la fonctionnalité SpanContext avec OpenCensus, consultez la matrice des fonctionnalités OpenCensus.

Coûts

Ce tutoriel utilise les composants facturables suivants de Google Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit.

Une fois que vous avez terminé ce tutoriel, vous pouvez éviter de continuer à payer des frais en supprimant les ressources que vous avez créées. Consultez la page Effectuer un nettoyage pour en savoir plus.

Avant de commencer

  1. Connectez-vous à votre compte Google.

    Si vous n'en possédez pas déjà un, vous devez en créer un.

  2. Dans Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Cloud.

    Accéder à la page de sélection du projet

  3. Vérifiez que la facturation est activée pour votre projet Google Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Activer les API GKE, Cloud Trace, Cloud Build, Cloud Storage, and Container Registry.

    Activer les API

Configurer votre environnement

Dans cette section, vous allez configurer votre environnement à l'aide des outils que vous utiliserez tout au long de ce tutoriel. Dans ce tutoriel, vous allez exécuter toutes les commandes de terminal à partir de Cloud Shell.

  1. Dans Cloud Console, activez Cloud Shell.

    Activer Cloud Shell

    En bas de la fenêtre de Cloud Console, une session Cloud Shell démarre et affiche une invite de ligne de commande. Cloud Shell est un environnement shell dans lequel le SDK Cloud est déjà installé (y compris l'outil de ligne de commande gcloud), et dans lequel des valeurs sont déjà définies pour votre projet actuel. L'initialisation de la session peut prendre quelques secondes.

  2. Définissez les variables d'environnement :
    export PROJECT_ID=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
    
  3. Téléchargez les fichiers nécessaires pour ce tutoriel en clonant le dépôt Git suivant :
    git clone https://github.com/GoogleCloudPlatform/gke-observability-tutorials.git
    cd $HOME/gke-observability-tutorials/gke-opencensus-stackdriver/go/http
    WORKDIR=$(pwd)
    

    Vous définissez le dossier du dépôt en tant que dossier $WORKDIR à partir duquel vous effectuez toutes les tâches liées à ce tutoriel. Vous pourrez supprimer le dossier une fois le tutoriel terminé.

Installer les outils

  1. Dans Cloud Shell, installez kubectx et kubens.

    git clone https://github.com/ahmetb/kubectx $WORKDIR/kubectx
    export PATH=$PATH:$WORKDIR/kubectx
    

    Ces outils permettent de travailler avec plusieurs clusters, contextes et espaces de noms Kubernetes.

  2. Dans Cloud Shell, installez Apache Bench, un outil de génération de charge Open Source :

    sudo apt-get install apache2-utils
    

Créer des clusters GKE

Dans cette section, vous allez créer deux clusters GKE dans lesquels vous allez déployer l'exemple d'application. Par défaut, les clusters GKE sont créés avec un accès en écriture seule à l'API Cloud Trace. Par conséquent, vous n'avez pas besoin de définir un accès lorsque vous créez les clusters.

  1. Dans Cloud Shell, créez les clusters :

    gcloud container clusters create backend-cluster \
        --zone=us-west1-a --enable-stackdriver-kubernetes \
        --verbosity=none --async
    gcloud container clusters create frontend-cluster \
        --zone=us-west1-a --enable-stackdriver-kubernetes \
        --verbosity=none
    

    Dans ce tutoriel, les clusters se trouvent dans la zone us-west1-a. Pour plus d'informations, consultez la page Zones géographiques et régions.

  2. Récupérez vos identifiants de cluster et stockez-les localement :

    gcloud container clusters get-credentials backend-cluster --zone=us-west1-a
    gcloud container clusters get-credentials frontend-cluster --zone=us-west1-a
    
  3. Renommez les contextes de vos clusters afin de pouvoir y accéder plus facilement dans la suite du tutoriel :

    kubectx backend=gke_${PROJECT_ID}_us-west1-a_backend-cluster
    kubectx frontend=gke_${PROJECT_ID}_us-west1-a_frontend-cluster
    

Examiner l'instrumentation OpenCensus

Dans les sections suivantes, vous allez examiner le code de l'exemple d'application pour apprendre à utiliser la propagation contextuelle. Vous pourrez ainsi autoriser l'ajout de délais à une seule trace parente à partir de plusieurs requêtes.

Examiner le code de frontend

  • Le code du service de frontend est constitué de trois importations :

    import (
    	"context"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"time"
    
    	"contrib.go.opencensus.io/exporter/stackdriver"
    	"go.opencensus.io/trace"
    
    	"go.opencensus.io/plugin/ochttp"
    	"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
    
    	"github.com/gorilla/mux"
    )
    
    • Les importations go.opencensus.io/plugin/ochttp et go.opencensus.io/plugin/ochttp/propagation/tracecontext contiennent le plug-in ochttp, qui propage le contexte vers les requêtes. Le contexte contient des informations sur la trace à laquelle les délais suivants sont ajoutés.
    • L'importation contrib.go.opencensus.io/exporter/stackdriver exporte des traces vers Trace. Pour connaître la liste des backends compatibles avec OpenCensus, consultez la page Exportateurs.
    • L'importation github.com/gorilla/mux est la bibliothèque utilisée par l'exemple d'application pour le traitement des requêtes.
  • La fonction main() configure l'exportation des traces vers Trace et utilise un routeur mux pour gérer les requêtes envoyées à l'URL / :

    func main() {
    	// set up Stackdriver exporter
    	exporter, err := stackdriver.NewExporter(stackdriver.Options{ProjectID: projectID, Location: location})
    	if err != nil {
    		log.Fatal(err)
    	}
    	trace.RegisterExporter(exporter)
    	trace.ApplyConfig(trace.Config{
    		DefaultSampler: trace.AlwaysSample(),
    	})
    
    	// handle root request
    	r := mux.NewRouter()
    	r.HandleFunc("/", mainHandler)
    	var handler http.Handler = r
    	handler = &ochttp.Handler{
    		Handler:     handler,
    		Propagation: &tracecontext.HTTPFormat{}}
    
    	log.Fatal(http.ListenAndServe(":8081", handler))
    }
    
    • Le gestionnaire ajoute les contextes aux requêtes à l'aide de la propagation HTTP.
    • Dans ce tutoriel, l'échantillonnage est défini sur AlwaysSample. Par défaut, OpenCensus échantillonne les traces à un taux prédéterminé, que vous pouvez contrôler à l'aide de ce paramètre.
  • Examinez la fonction mainHandler() :

    func mainHandler(w http.ResponseWriter, r *http.Request) {
    	// create root span
    	ctx, rootspan := trace.StartSpan(context.Background(), "incoming call")
    	defer rootspan.End()
    
    	// create child span for backend call
    	ctx, childspan := trace.StartSpan(ctx, "call to backend")
    	defer childspan.End()
    
    	// create request for backend call
    	req, err := http.NewRequest("GET", backendAddr, nil)
    	if err != nil {
    		log.Fatalf("%v", err)
    	}
    
    	childCtx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond)
    	defer cancel()
    	req = req.WithContext(childCtx)
    
    	// add span context to backend call and make request
    	format := &tracecontext.HTTPFormat{}
    	format.SpanContextToRequest(childspan.SpanContext(), req)
    	//format.SpanContextToRequest(rootspan.SpanContext(), req)
    	client := http.DefaultClient
    	res, err := client.Do(req)
    	if err != nil {
    		log.Fatalf("%v", err)
    	}
    
    	fmt.Printf("%v\n", res.StatusCode)
    }
    
    • Pour collecter la latence globale des requêtes, cette fonction crée le délai racine. Vous ajoutez les délais suivants au délai racine.

      // create root span
        ctx, rootspan := trace.StartSpan(context.Background(), "incoming call")
        defer rootspan.End()
      
    • Pour temporiser l'appel backend, la fonction crée un délai enfant.

      // create child span for backend call
        _, childspan := trace.StartSpan(ctx, "call to backend")
        defer childspan.End()
      
    • La fonction crée une requête qu'elle envoie au backend.

      // create request for backend call
        req, err := http.NewRequest("GET", backendAddr, nil)
        if err != nil {
          log.Fatalf("%v", err)
        }
        childCtx, cancel := context.WithTimeout(req.Context(), 1000*time.Millisecond)
        defer cancel()
        req = req.WithContext(childCtx)
      
    • La fonction ajoute le contexte de délai à cette requête.

      // add span context to backend call and make request
        format := &tracecontext.HTTPFormat{}
        format.SpanContextToRequest(rootspan.SpanContext(), req)
      

Examiner le code de backend

  • La fonction main() du code de backend est identique à la fonction main() du service de frontend. Elle configure l'exportateur Trace et utilise un routeur mux pour gérer les requêtes adressées à l'URL / avec la fonction mainHandler().

    func main() {
    	// set up Stackdriver exporter
    	exporter, err := stackdriver.NewExporter(stackdriver.Options{ProjectID: projectID, Location: location})
    	if err != nil {
    		log.Fatal(err)
    	}
    	trace.RegisterExporter(exporter)
    	trace.ApplyConfig(trace.Config{
    		DefaultSampler: trace.AlwaysSample(),
    	})
    
    	// handle incoming request
    	r := mux.NewRouter()
    	r.HandleFunc("/", mainHandler)
    	var handler http.Handler = r
    	// handler = &logHandler{log: log, next: handler}
    
    	handler = &ochttp.Handler{
    		Handler:     handler,
    		Propagation: &tracecontext.HTTPFormat{}}
    
    	log.Fatal(http.ListenAndServe(":8080", handler))
    }
    
  • La fonction mainHandler() utilise la requête entrante pour obtenir à la fois le contexte HTTP fourni par Go et le contexte de trace fourni par OpenCensus. Ensuite, la fonction crée un délai enfant et l'ajoute au contexte de trace. Enfin, elle appelle la fonction callRemoteEndpoint() pour effectuer un autre appel dont la latence doit être collectée.

    func mainHandler(w http.ResponseWriter, r *http.Request) {
    	// get context from incoming request
    	ctx := r.Context()
    	// get span context from incoming request
    	HTTPFormat := &tracecontext.HTTPFormat{}
    	if spanContext, ok := HTTPFormat.SpanContextFromRequest(r); ok {
    		_, span := trace.StartSpanWithRemoteParent(ctx, "call remote endpoint", spanContext)
    		defer span.End()
    		returnCode := callRemoteEndpoint()
    		fmt.Fprintf(w, returnCode)
    	}
    }
    
  • La fonction callRemoteEndpoint() utilise la bibliothèque HTTP pour effectuer un appel de point de terminaison distant. Étant donné que le traçage de cet appel est géré dans la fonction parente, le contexte de trace et la création d'autres délais enfants ne sont pas nécessaires.

    // make an outbound call
    func callRemoteEndpoint() string {
    	resp, err := http.Get(destURL)
    	if err != nil {
    		log.Fatal("could not fetch remote endpoint")
    	}
    	defer resp.Body.Close()
    	body, err := ioutil.ReadAll(resp.Body)
    	if err != nil {
    		log.Fatal("could not read response from Google")
    		log.Fatal(body)
    	}
    
    	return strconv.Itoa(resp.StatusCode)
    }
    

Déployer l'application

Dans cette section, vous allez créer des images de conteneur pour les services de backend et frontend à l'aide de Cloud Build, puis les déployer sur leurs clusters GKE.

Créer le service de backend

  1. Dans Cloud Shell, accédez au répertoire backend :

    cd $WORKDIR/backend/
    
  2. Envoyez la compilation :

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/backend:latest
    
  3. Vérifiez que l'image du conteneur a bien été créée et qu'elle est disponible dans Container Registry :

    gcloud container images list
    

    L'image du conteneur a bien été créée lorsque le résultat ressemble à ce qui suit, où project-id correspond à votre ID de projet Cloud :

    NAME
    gcr.io/project-id/backend
    

Déployer le service de backend

  1. Dans Cloud Shell, définissez votre contexte kubectx sur le cluster backend :

    kubectx backend
    
  2. Déployez le fichier configmap :

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < backend-configmap.yaml | kubectl apply -f -
    
  3. Créez le fichier de déploiement backend :

    envsubst < backend-deployment.yaml | kubectl apply -f -
    
  4. Vérifiez que les pods sont en cours d'exécution :

    kubectl get pods
    

    Le résultat affiche un Status de Running :

    NAME                       READY   STATUS    RESTARTS   AGE
    backend-645859d95b-7mx95   1/1     Running   0          52s
    backend-645859d95b-qfdnc   1/1     Running   0          52s
    backend-645859d95b-zsj5m   1/1     Running   0          52s
    
  5. Exposez le déploiement backend à l'aide d'un équilibreur de charge :

    kubectl expose deployment backend --type=LoadBalancer
    
  6. Récupérez l'adresse IP du service backend :

    kubectl get services backend
    

    Le résultat ressemble à ce qui suit :

    NAME      TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
    backend   LoadBalancer   10.11.247.58   34.83.88.143   8080:30714/TCP   70s
    

    Répétez cette commande jusqu'à ce que le champ EXTERNAL-IP de votre service passe de <pending> à une adresse IP.

  7. Collectez l'adresse IP de l'étape précédente en tant que variable :

    export BACKEND_IP=$(kubectl get svc backend -ojson | jq -r '.status.loadBalancer.ingress[].ip')
    

Créer le service de frontend

  1. Dans Cloud Shell, accédez au répertoire frontend :

    cd $WORKDIR/frontend/
    
  2. Envoyez la compilation :

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/frontend:latest
    
  3. Vérifiez que l'image du conteneur a bien été créée et qu'elle est disponible dans Container Registry :

    gcloud container images list
    

    L'image du conteneur a bien été créée lorsque le résultat ressemble à ce qui suit :

    NAME
    gcr.io/qwiklabs-gcp-47a7dcba55b334f7/backend
    gcr.io/qwiklabs-gcp-47a7dcba55b334f7/frontend
    

Déployer le service de frontend

  1. Dans Cloud Shell, définissez votre contexte kubectx sur le cluster de backend :

    kubectx frontend
    
  2. Déployez le fichier configmap :

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < frontend-configmap.yaml | kubectl apply -f -
    
  3. Créez le fichier de déploiement frontend :

    envsubst < frontend-deployment.yaml | kubectl apply -f -
    
  4. Vérifiez que les pods sont en cours d'exécution :

    kubectl get pods
    

    Le résultat affiche un Status de Running :

    NAME                        READY   STATUS    RESTARTS   AGE
    frontend-747b445499-v7x2w   1/1     Running   0          57s
    frontend-747b445499-vwtmg   1/1     Running   0          57s
    frontend-747b445499-w47pf   1/1     Running   0          57s
    
  5. Exposez le déploiement frontend à l'aide d'un équilibreur de charge :

    kubectl expose deployment frontend --type=LoadBalancer
    
  6. Récupérez l'adresse IP du service frontend :

    kubectl get services frontend
    

    Le résultat ressemble à ce qui suit :

    NAME       TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
    frontend   LoadBalancer   10.27.241.93   34.83.111.232   8081:31382/TCP   70s
    

    Répétez cette commande jusqu'à ce que le champ EXTERNAL-IP de votre service passe de <pending> à une adresse IP.

  7. Collectez l'adresse IP de l'étape précédente en tant que variable :

    export FRONTEND_IP=$(kubectl get svc frontend -ojson | jq -r '.status.loadBalancer.ingress[].ip')
    

Charger l'application et examiner les traces

Dans cette section, vous allez créer des requêtes pour votre application à l'aide de l'utilitaire Apache Bench et examiner les traces obtenues dans Trace.

  1. Dans Cloud Shell, utilisez Apache Bench pour générer 1 000 requêtes avec trois threads simultanés :

    ab -c 3 -n 1000 http://${FRONTEND_IP}:8081/
    
  2. Dans Cloud Console, accédez à la page Liste de traces.

    Accéder à la page Liste de traces

  3. Pour consulter la chronologie, cliquez sur l'un des URI libellés incoming call.

    Graphique en nuage de points représentant les traces.

    Cette trace contient trois délais portant les noms suivants :

    • Le délai incoming call collecte la latence de bout en bout observée par le client.
    • Le délai call to backend collecte la latence de l'appel de backend observée par le frontend.
    • Le délai call remote endpoint collecte la latence de l'appel de point de terminaison externe observée par le backend.

    Graphique à barres des délais.

Effectuer un nettoyage

Le moyen le plus simple d'éviter la facturation consiste à supprimer le projet Cloud que vous avez créé pour le tutoriel. Vous pouvez également supprimer les différentes ressources.

Supprimer le projet

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer .
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Étapes suivantes