Como usar o rastreamento distribuído para observar a latência do microsserviço com o OpenCensus e o Cloud Trace

Neste tutorial, mostramos como capturar informações de rastreamento em aplicativos de microsserviço usando o OpenCensus e o Cloud Trace. O rastreamento distribuído é um sistema que ajuda você a entender como as solicitações percorrem arquiteturas complexas de microsserviços e a latência associada a cada etapa, que é um recurso importante de observabilidade. Os aplicativos são transferidos para microsserviços e o número de componentes e endpoints envolvidos em uma única transação de usuário aumenta. Portanto, a observabilidade permanece essencial para operar os serviços do usuário de maneira confiável.

Para serviços em execução no Kubernetes, usar uma malha de serviço como o Istio permite o rastreamento distribuído do tráfego de serviço a serviço sem a necessidade de instrumentação dedicada. No entanto, se você quer ter mais controle sobre os traces, talvez é necessário capturar os componentes internos do app nas informações de rastreamento ou rastrear o código que não está sendo executado no Kubernetes. O OpenCensus é uma biblioteca de código aberto que permite a instrumentação de aplicativos de microsserviço distribuídos para coletar traces e métricas em uma grande variedade de linguagens, plataformas e ambientes.

Este tutorial é destinado a desenvolvedores, SREs e engenheiros de DevOps que buscam entender os princípios básicos do rastreamento distribuído e aplicá-los aos serviços deles para melhorar a observabilidade do serviço.

Para este tutorial, consideramos que você esteja familiarizado com o seguinte:

O Estado do DevOps informa os recursos identificados que impulsionam o desempenho de entrega de um software. Este tutorial ajudará você com os seguintes recursos:

Objetivos

  • Criar um cluster do GKE e implante um aplicativo de amostra.
  • Revisar o código de instrumentação do OpenCensus.
  • Revisar os traces e registros gerados pela instrumentação.

Arquitetura de referência

O diagrama a seguir mostra a arquitetura implantada neste tutorial.

Arquitetura do tutorial com dois clusters do GKE.

Conforme ilustrado no diagrama anterior, você cria dois clusters do GKE e implanta um aplicativo em cada um deles. O tráfego do usuário é enviado para o aplicativo de front-end no cluster de front-end. O pod de front-end no cluster de front-end se comunica com o pod de back-end no cluster de back-end. O pod de back-end chama um endpoint de API externo.

Use o Cloud Build, uma plataforma de integração contínua, entrega e implantação totalmente gerenciada, para criar imagens de contêiner a partir do código de amostra e armazená-las no Container Registry. Os clusters do GKE extraem as imagens do Container Registry no momento da implantação.

Aplicativo de amostra

O aplicativo de amostra neste tutorial é composto por dois microsserviços escritos em Go.

O serviço de front-end aceita solicitações HTTP no URL / e chama o serviço de back-end. O endereço do serviço de back-end é definido pela variável de ambiente BACKEND. O valor dessa variável é definido em um objeto ConfigMap que você cria.

O serviço de back-end aceita solicitações HTTP no URL / e faz uma chamada de saída para um URL externo, conforme definido na variável de ambiente DESTINATION_URL. O valor da variável é definido por meio de um objeto ConfigMap. Após a conclusão da chamada externa, o serviço backend retorna a chamada de status HTTP, por exemplo, 200, de volta ao autor da chamada.

Noções básicas sobre traces, períodos e contexto

O conceito de rastreamento distribuído é mais bem descrito no documento de pesquisa do Dapper (em inglês) publicado pelo Google. No artigo, o diagrama a seguir mostra cinco períodos em um trace.

Rastreamento distribuído com cinco períodos em um trace.

O diagrama mostra uma única solicitação de front-end que faz duas solicitações de back-end. A segunda chamada de back-end requer que duas chamadas auxiliares sejam concluídas. Cada chamada é identificada com o código do período e o código do período pai.

Um trace é o total de informações que descrevem como um sistema distribuído responde a uma solicitação do usuário. Os traces são compostos por períodos, em que cada período representa uma solicitação e um par de resposta específicos envolvidos na exibição da solicitação do usuário. O período pai descreve a latência conforme observado pelo usuário final. Cada um dos períodos filhos descreve como um determinado serviço no sistema distribuído foi chamado e respondido, com informações de latência capturadas para cada um deles.

Um desafio com o rastreamento em sistemas distribuídos é que as informações sobre a solicitação de front-end original não são transportadas de forma automática ou herdada quando solicitações subsequentes são feitas para vários serviços de back-end. No Go, é possível fazer solicitações com contexto (o endereço IP e as credenciais do cluster), o que significa que informações adicionais são carregadas no cabeçalho HTTP. O conceito de contexto é estendido com o OpenCensus para incluir o contexto de período, em que é possível incluir informações sobre o período pai em cada solicitação subsequente. Em seguida, é possível anexar períodos filhos para compor o rastreamento geral. Assim, é possível ver como a solicitação do usuário passou pelo sistema e, por fim, foi atendida novamente.

O contexto não é específico do Go. Para mais informações sobre as linguagens em que o OpenCensus é compatível com o recurso SpanContext, consulte a matriz de recursos do OpenCensus.

Custos

Neste tutorial, usamos os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custo baseada na projeção de uso deste tutorial, use a calculadora de preços. Novos usuários do Google Cloud podem ser qualificados para uma avaliação gratuita.

Ao concluir este tutorial, exclua os recursos criados para evitar o faturamento contínuo. Para mais informações, consulte Como fazer a limpeza.

Antes de começar

  1. Faça login na sua conta do Google.

    Se você ainda não tiver uma, inscreva-se.

  2. No Console do Cloud, na página do seletor de projetos, selecione ou crie um projeto do Cloud.

    Acessar a página do seletor de projetos

  3. Verifique se a cobrança está ativada para o seu projeto do Google Cloud. Saiba como confirmar se a cobrança está ativada para o seu projeto.

  4. Ative as APIs GKE, Cloud Trace, Cloud Build, Cloud Storage, and Container Registry.

    Ative as APIs

Como configurar o ambiente

Nesta seção, você configurará seu ambiente com as ferramentas usadas ao longo deste tutorial. Todos os comandos de terminal neste tutorial são executados a partir do Cloud Shell.

  1. No Console do Cloud, ative o Cloud Shell.

    Ativar o Cloud Shell

    Na parte inferior do Console do Cloud, uma sessão do Cloud Shell é iniciada e exibe um prompt de linha de comando. O Cloud Shell é um ambiente com o SDK do Cloud pré-instalado com a ferramenta de linha de comando gcloud e os valores já definidos para seu projeto atual. A inicialização da sessão pode levar alguns segundos.

  2. Defina as variáveis de ambiente:
    export PROJECT_ID=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
    
  3. Faça o download dos arquivos necessários para este tutorial clonando o seguinte repositório do GitHub:
    git clone https://github.com/GoogleCloudPlatform/gke-observability-tutorials.git
    cd $HOME/gke-observability-tutorials/gke-opencensus-stackdriver/go/http
    WORKDIR=$(pwd)
    

    A pasta do repositório torna-se sua $WORKDIR e, a partir dela, você realiza todas as tarefas relacionadas a este tutorial para poder excluí-la quando terminá-lo.

Instalar ferramentas

  1. No Cloud Shell, instale kubectx e kubens.

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

    Use essas ferramentas para trabalhar com vários clusters, contextos e namespaces do Kubernetes.

  2. No Cloud Shell, instale o Apache Database, uma ferramenta de geração de carga de código aberto:

    sudo apt-get install apache2-utils
    

Como criar clusters do GKE

Nesta seção, você criará dois clusters do GKE em que implantará o aplicativo de amostra. Por padrão, os clusters do GKE são criados com acesso somente gravação à API Cloud Trace. Portanto, não é necessário definir o acesso ao criar os clusters.

  1. No Cloud Shell, crie os 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
    

    Neste tutorial, os clusters estão na zona us-west1-a. Para mais informações, consulte Geografia e regiões.

  2. Consiga as credenciais do cluster e armazene-as localmente:

    gcloud container clusters get-credentials backend-cluster --zone=us-west1-a
    gcloud container clusters get-credentials frontend-cluster --zone=us-west1-a
    
  3. Renomeie os contextos dos clusters para facilitar o acesso a eles posteriormente no tutorial:

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

Analisar a instrumentação do OpenCensus

Nas seções a seguir, revise o código do aplicativo de amostra para saber como usar a propagação de contexto para permitir que períodos de várias solicitações sejam anexados a um único rastreamento pai.

Revisar o código de front-end

  • Há três importações no código do serviço de front-end:

    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"
    )
    
    • As importações go.opencensus.io/plugin/ochttp e go.opencensus.io/plugin/ochttp/propagation/tracecontext contêm o plug-in ochttp, que propaga o contexto entre as solicitações. O contexto carrega informações sobre o trace ao qual os períodos subsequentes são anexados.
    • A importação contrib.go.opencensus.io/exporter/stackdriver exporta traces para o Trace. Para ver a lista de back-ends compatíveis com o OpenCensus, consulte Exportadores (em inglês).
    • A importação github.com/gorilla/mux é a biblioteca que o app de amostra usa para processar solicitações.
  • A função main() configura a exportação de traces para o Trace e usa um roteador mux para processar solicitações feitas ao 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))
    }
    
    • O gerenciador está usando a propagação HTTP para adicionar contextos às solicitações.
    • Neste tutorial, a amostragem está definida como AlwaysSample. Por padrão, as amostras do OpenCensus rastreiam a uma taxa predeterminada, que pode ser controlada usando esse parâmetro.
  • Revise a função 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)
    }
    
    • Essa função cria o período raiz para capturar a latência geral da solicitação. Você anexa períodos subsequentes ao período raiz.

      // create root span
        ctx, rootspan := trace.StartSpan(context.Background(), "incoming call")
        defer rootspan.End()
      
    • Para cronometrar a chamada backend, a função cria um período filho.

      // create child span for backend call
        _, childspan := trace.StartSpan(ctx, "call to backend")
        defer childspan.End()
      
    • A função cria uma solicitação para o back-end.

      // 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)
      
    • A função adiciona o contexto do período a essa solicitação.

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

Revisar o código de back-end

  • A função main() para o código de back-end é idêntica à função main() do serviço de front-end. Ela configura o exportador do Trace e usa um roteador mux para processar solicitações feitas ao URL / com a função 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))
    }
    
  • A função mainHandler() usa a solicitação recebida para receber o contexto HTTP fornecido pelo Go e o contexto de trace fornecido pelo OpenCensus. A partir daí, a função cria um período filho e anexa ao contexto do trace. Por fim, ela chama a função callRemoteEndpoint() para fazer outra chamada com a latência que precisa ser capturada.

    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)
    	}
    }
    
  • A função callRemoteEndpoint() usa a biblioteca HTTP para fazer uma chamada de endpoint remoto. Como o rastreamento dessa chamada é processado na função pai, ele não precisa consumir o contexto do trace ou criar mais períodos filhos.

    // 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)
    }
    

Implantar o aplicativo

Nesta seção, você usará o Cloud Build para criar imagens de contêiner para os serviços de back-end e front-end e implantá-los nos clusters do GKE.

Criar o serviço de back-end

  1. No Cloud Shell, mude para o diretório backend:

    cd $WORKDIR/backend/
    
  2. Envie o build:

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/backend:latest
    
  3. Confirme se a imagem do contêiner foi criada e está disponível no Container Registry:

    gcloud container images list
    

    A imagem do contêiner foi criada com sucesso quando a saída é semelhante a esta, em que project-id é o código do projeto do Cloud:

    NAME
    gcr.io/project-id/backend
    

Implantar o serviço de back-end

  1. No Cloud Shell, defina o contexto kubectx para o cluster backend:

    kubectx backend
    
  2. Implante o arquivo configmap:

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < backend-configmap.yaml | kubectl apply -f -
    
  3. Crie o arquivo de implantação backend:

    envsubst < backend-deployment.yaml | kubectl apply -f -
    
  4. Confirme se os pods estão em execução:

    kubectl get pods
    

    A saída exibe um 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. Exponha a implantação backend usando um balanceador de carga:

    kubectl expose deployment backend --type=LoadBalancer
    
  6. Consiga o endereço IP do serviço backend:

    kubectl get services backend
    

    A saída será assim:

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

    Repita esse comando até que o campo EXTERNAL-IP do serviço seja alterado de <pending> para um endereço IP.

  7. Capture o endereço IP da etapa anterior como uma variável:

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

Criar o serviço de front-end

  1. No Cloud Shell, mude para o diretório frontend:

    cd $WORKDIR/frontend/
    
  2. Envie o build:

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/frontend:latest
    
  3. Confirme se a imagem do contêiner foi criada e está disponível no Container Registry:

    gcloud container images list
    

    A imagem do contêiner foi criada com sucesso quando a saída é semelhante a esta:

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

Implantar o serviço de front-end

  1. No Cloud Shell, defina o contexto kubectx como o cluster de back-end:

    kubectx frontend
    
  2. Implante o arquivo configmap:

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < frontend-configmap.yaml | kubectl apply -f -
    
  3. Crie o arquivo de implantação frontend:

    envsubst < frontend-deployment.yaml | kubectl apply -f -
    
  4. Confirme se os pods estão em execução:

    kubectl get pods
    

    A saída exibe um 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. Exponha a implantação frontend usando um balanceador de carga:

    kubectl expose deployment frontend --type=LoadBalancer
    
  6. Consiga o endereço IP do serviço frontend:

    kubectl get services frontend
    

    A saída será assim:

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

    Repita esse comando até que o campo EXTERNAL-IP do serviço seja alterado de <pending> para um endereço IP.

  7. Capture o endereço IP da etapa anterior como uma variável:

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

Carregar o app e analisar os traces

Nesta seção, você usará o utilitário Apache Database para criar solicitações para o aplicativo e revisar os traces resultantes no Trace.

  1. No Cloud Shell, use o Apache Database para gerar 1.000 solicitações com três linhas de execução simultâneas:

    ab -c 3 -n 1000 http://${FRONTEND_IP}:8081/
    
  2. No Console do Cloud, acesse a página Lista de traces.

    Acessar a página "Lista de traces"

  3. Para revisar o cronograma, clique em um dos URIs marcados como incoming call.

    Gráfico de dispersão de traces.

    Esse trace contém três períodos com os seguintes nomes:

    • O período incoming call captura a latência completa observada pelo cliente.
    • O período call to backend captura a latência da chamada de back-end observada pelo front-end.
    • O período call remote endpoint captura a latência da chamada do endpoint externo observada pelo back-end.

    Gráfico de barras de períodos.

Como fazer a limpeza

A maneira mais fácil de eliminar o faturamento é excluir o projeto do Cloud que você criou para o tutorial. A outra opção é excluir os recursos individuais.

Exclua o projeto

  1. No Console do Cloud, acesse a página Gerenciar recursos:

    Acessar a página "Gerenciar recursos"

  2. Na lista de projetos, selecione o projeto que você quer excluir e clique em Excluir .
  3. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.

A seguir