借助 OpenCensus 和 Cloud Trace 使用分布式跟踪功能观察微服务延迟时间

本教程介绍如何使用 OpenCensus 和 Cloud Trace 在微服务应用上捕获跟踪记录信息。分布式跟踪系统可帮助您了解请求如何遍历复杂的微服务架构以及与每个步骤相关的延迟时间,这是可观测性的关键功能。应用正在向微服务过渡,而且单个用户事务中涉及的组件和端点数量越来越多。因此,可观测性仍然是可靠运行用户服务的关键。

对于在 Kubernetes 上运行的服务,使用 Istio 等服务网格可实现对服务到服务流量的分布式跟踪,而无需专用插桩 (instrumentation)。但是,您可能希望更好地控制跟踪记录,可能需要在跟踪信息中捕获应用内部资料,或者可能需要跟踪未在 Kubernetes 上运行的代码。OpenCensus 是一个开源库,可用于对分布式微服务应用进行插桩,以跨各种语言、平台和环境收集跟踪记录和指标。

本教程面向希望了解分布式跟踪基础知识并将其应用于服务以提高服务可观测性的开发者、SRE 和 DevOps 工程师。

本教程假定您熟悉以下内容:

DevOps 的状态报告确定了可提高软件交付方面表现的功能:本教程将帮助您使用以下功能:

目标

  • 创建 GKE 集群并部署示例应用。
  • 查看 OpenCensus 插桩代码。
  • 查看插桩生成的跟踪记录和日志。

参考架构

下图展示了您在本教程中部署的架构。

具有两个 GKE 集群的教程的架构。

如上图所示,您创建了两个 GKE 集群,并将应用部署到每个集群。用户流量将发送到前端集群上的前端应用。前端集群上的前端 pod 会与后端集群上的后端 pod 通信。后端 pod 调用外部 API 端点。

您可以使用 Cloud Build(一个全代管式持续集成、交付和部署平台)根据示例代码构建容器映像并将其存储在 Container Registry 中。GKE 集群在部署时从 Container Registry 中拉取映像。

示例应用

本教程中的示例应用由两个使用 Go 编写的微服务组成。

前端服务接受对 / 网址的 HTTP 请求并调用后端服务。后端服务的地址由 BACKEND 环境变量定义。该变量的值是在您创建的 ConfigMap 对象中设置。

后端服务接受对 / 网址的 HTTP 请求,并对 DESTINATION_URL 环境变量中定义的外部网址进行拨出调用。变量的值通过 ConfigMap 对象设置。外部调用完成后,backend 服务会将 HTTP 状态调用(例如 200,)返回给调用者。

了解跟踪记录、span 和上下文

如需了解分布式跟踪概念的最贴切描述,请参阅 Google 发布的 Dapper 研究论文。在这篇论文中,下图显示了一个跟踪记录中的五个 span。

一个跟踪记录中包含五个 span 的分布式跟踪。

该图显示了发出两个后端请求的单个前端请求。 第二个后端调用需要两个帮助程序调用才能完成。每个调用都标有其 span ID 和父级 span 的 ID。

跟踪记录是描述分布式系统如何响应用户请求的信息汇总。跟踪记录由 span 组成,其中每个 span 表示为处理用户请求而涉及的特定请求和响应对。父级 span 描述最终用户观察到的延迟时间。每个子级 span 描述如何调用和响应分布式系统中的特定服务,并记录针对每个服务捕获的延迟信息。

在分布式系统中进行跟踪的挑战是,对各种后端服务发出后续请求时,原始前端请求的信息不会自动传递或者这些信息本身不会传递。在 Go 中,您可以使用上下文(集群 IP 地址和凭据)发出请求,这意味着在 HTTP 标头中加载其他信息。上下文概念通过 OpenCensus 进行扩展,以包含 span 上下文,您可以在其中包含每个后续请求的父级 span 信息。然后,您可以附加子级 span 以编写总体跟踪记录,以便查看用户请求如何遍历系统并最终被提供给用户。

上下文不是 Go 特有的。如需详细了解 OpenCensus 支持 SpanContext 功能的语言,请参阅 OpenCensus 功能矩阵

费用

本教程使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用量来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本教程后,您可以删除所创建的资源以避免继续计费。如需了解详情,请参阅清理

准备工作

  1. 登录您的 Google 帐号。

    如果您还没有 Google 帐号,请注册一个新帐号

  2. 在 Cloud Console 的项目选择器页面上,选择或创建 Cloud 项目。

    转到项目选择器页面

  3. 确保您的 Google Cloud 项目已启用结算功能。 了解如何确认您的项目已启用结算功能

  4. 启用 GKE, Cloud Trace, Cloud Build, Cloud Storage, and Container Registry API。

    启用 API

设置环境

在本部分中,您将使用本教程中使用的工具来设置环境。您将通过 Cloud Shell 运行本教程中的所有终端命令。

  1. 在 Cloud Console 中,激活 Cloud Shell。

    激活 Cloud Shell

    Cloud Shell 会话随即会在 Cloud Console 的底部启动,并显示命令行提示符。Cloud Shell 是一个已安装 Cloud SDK 的 Shell 环境,其中包括 gcloud 命令行工具以及已为当前项目设置的值。该会话可能需要几秒钟时间来完成初始化。

  2. 设置环境变量:
    export PROJECT_ID=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
    
  3. 克隆以下 Git 代码库,下载本教程所需的文件:
    git clone https://github.com/GoogleCloudPlatform/gke-observability-tutorials.git
    cd $HOME/gke-observability-tutorials/gke-opencensus-stackdriver/go/http
    WORKDIR=$(pwd)
    

    您可以将代码库文件夹设为 $WORKDIR,然后从中执行与本教程相关的所有任务,这样您就可以在完成本教程时删除该文件夹。

安装工具

  1. 在 Cloud Shell 中,安装 kubectxkubens

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

    您可以使用这些工具来处理多个 Kubernetes 集群、上下文和命名空间。

  2. 在 Cloud Shell 中,安装开源负载生成工具 Apache Bench

    sudo apt-get install apache2-utils
    

创建 GKE 集群

在本部分中,您将创建两个 GKE 集群,然后在其中部署示例应用。默认情况下,创建的 GKE 集群对 Cloud Trace API 拥有只写权限,因此您在创建集群时无需定义访问权限。

  1. 在 Cloud Shell 中,创建集群:

    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
    

    在本教程中,集群位于 us-west1-a 地区中。如需了解详情,请参阅地理位置和区域

  2. 获取集群凭据并将其存储在本地:

    gcloud container clusters get-credentials backend-cluster --zone=us-west1-a
    gcloud container clusters get-credentials frontend-cluster --zone=us-west1-a
    
  3. 重命名集群的上下文,以便在本教程后面的部分中可以更轻松地访问这些上下文:

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

查看 OpenCensus 插桩

在以下部分中,您将查看示例应用的代码,以了解如何使用上下文传播来允许将多个请求的 span 附加到单个父级跟踪记录。

查看前端代码

  • 前端服务代码中有三个导入项:

    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"
    )
    
    • go.opencensus.io/plugin/ochttpgo.opencensus.io/plugin/ochttp/propagation/tracecontext 导入项包含 ochttp 插件,用于在请求之间传播上下文。上下文会携带后续 Span 附加到的跟踪记录的相关信息。
    • contrib.go.opencensus.io/exporter/stackdriver 导入项会将跟踪记录导出到 Trace。如需查看 OpenCensus 支持的后端列表,请参阅导出工具
    • github.com/gorilla/mux 导入项是示例应用用于处理请求的库。
  • main() 函数设置将跟踪记录导出到 Trace 这一流程,并使用 mux 路由器来处理向 / 网址发出的请求:

    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))
    }
    
    • 处理程序使用 HTTP 传播为请求添加上下文。
    • 在本教程中,采样设置为 AlwaysSample。默认情况下,OpenCensus 会以预定的速率对跟踪记录进行采样,您可以使用此参数控制该速率。
  • 查看 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)
    }
    
    • 为了捕获总体请求延迟时间,此函数会创建根 span。将后续 span 附加到根 span。

      // create root span
        ctx, rootspan := trace.StartSpan(context.Background(), "incoming call")
        defer rootspan.End()
      
    • 为了设置 backend 调用的时间,该函数会创建一个子级 span。

      // create child span for backend call
        _, 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(req.Context(), 1000*time.Millisecond)
        defer cancel()
        req = req.WithContext(childCtx)
      
    • 函数将 span 上下文添加到该请求。

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

查看后端代码

  • 后端代码的 main() 函数类似于前端服务的 main() 函数 - 后端代码会设置 Trace 导出工具并使用 mux 路由器来处理用 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))
    }
    
  • mainHandler() 函数使用传入请求来获取 Go 提供的 HTTP 上下文和 OpenCensus 提供的跟踪上下文。然后,该函数会创建子级 span 并将其附加到跟踪上下文。最后,它会调用 callRemoteEndpoint() 函数以发出需要捕获其延迟时间的另一个调用。

    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)
    	}
    }
    
  • callRemoteEndpoint() 函数使用 HTTP 库进行远程端点调用。由于对此调用的跟踪是在父级函数中处理,因此不需要使用跟踪上下文,也不需要创建更多子级 span。

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

部署应用

在本部分中,您将使用 Cloud Build 为后端和前端服务构建容器映像,并将其部署到 GKE 集群。

构建后端服务

  1. 在 Cloud Shell 中,切换到 backend 目录:

    cd $WORKDIR/backend/
    
  2. 提交构建:

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/backend:latest
    
  3. 确认容器映像已成功创建且可在 Container Registry 中使用:

    gcloud container images list
    

    如果输出内容类似如下,则表明容器映像已成功创建,其中 project-id 是您的 Cloud 项目 ID:

    NAME
    gcr.io/project-id/backend
    

部署后端服务

  1. 在 Cloud Shell 中,将 kubectx 上下文设置为 backend 集群:

    kubectx backend
    
  2. 部署 configmap 文件:

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < backend-configmap.yaml | kubectl apply -f -
    
  3. 创建 backend 部署文件:

    envsubst < backend-deployment.yaml | kubectl apply -f -
    
  4. 确认 pod 正在运行。

    kubectl get pods
    

    输出显示的 StatusRunning

    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. 使用负载平衡器公开 backend 部署:

    kubectl expose deployment backend --type=LoadBalancer
    
  6. 获取 backend 服务的 IP 地址:

    kubectl get services backend
    

    输出类似于以下内容:

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

    重复此命令,直到服务的 EXTERNAL-IP 字段从 <pending> 更改为 IP 地址。

  7. 捕获上一步中的 IP 地址作为变量:

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

构建前端服务

  1. 在 Cloud Shell 中,切换到 frontend 目录:

    cd $WORKDIR/frontend/
    
  2. 提交构建:

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/frontend:latest
    
  3. 确认容器映像已成功创建且可在 Container Registry 中使用:

    gcloud container images list
    

    如果输出内容类似如下,则表明已成功创建容器映像:

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

部署前端服务

  1. 在 Cloud Shell 中,将 kubectx 上下文设置为后端集群:

    kubectx frontend
    
  2. 部署 configmap 文件:

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < frontend-configmap.yaml | kubectl apply -f -
    
  3. 创建 frontend 部署文件:

    envsubst < frontend-deployment.yaml | kubectl apply -f -
    
  4. 确认 pod 正在运行。

    kubectl get pods
    

    输出显示的 StatusRunning

    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. 使用负载平衡器公开 frontend 部署:

    kubectl expose deployment frontend --type=LoadBalancer
    
  6. 获取 frontend 服务的 IP 地址:

    kubectl get services frontend
    

    输出类似于以下内容:

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

    重复此命令,直到服务的 EXTERNAL-IP 字段从 <pending> 更改为 IP 地址。

  7. 捕获上一步中的 IP 地址作为变量:

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

加载应用并查看跟踪记录

在本部分中,您将使用 Apache Bench 实用程序为您的应用创建请求,并在 Trace 中查看生成的跟踪记录。

  1. 在 Cloud Shell 中,使用 Apache Bench 通过 3 个并发线程来生成 1000 个请求:

    ab -c 3 -n 1000 http://${FRONTEND_IP}:8081/
    
  2. 在 Cloud Console 中,转到跟踪记录列表页面。

    转到“跟踪记录列表”页面

  3. 如需查看时间轴,请点击其中一个标有 incoming call 的 URI。

    跟踪记录的散点图。

    此跟踪记录包含三个名称如下的 span:

    • incoming call span 捕获客户端观察到的端到端延迟时间。
    • call to backend span 捕获由前端观察到的后端调用的延迟时间。
    • call remote endpoint span 捕获由后端观察到的外部端点调用的延迟时间。

    span 条形图。

清理

若要避免产生费用,最简单的方法是删除您为本教程创建的 Cloud 项目。或者,您也可以删除各个资源。

删除项目

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”页面

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

后续步骤