为提高 Go 应用性能而进行插桩测试(第 1 部分:跟踪记录)

1. 简介

505827108874614d.png

上次更新时间:2022 年 7 月 15 日

应用的可观测性

可观测性和 OpenTelemetry

可观测性是用于描述系统属性的术语。具有可观测性的系统可帮助团队有效调试其系统。在这种情况下,可观测性的三大支柱(日志、指标和跟踪记录)是系统获得可观测性的基本插桩。

OpenTelemetry 是一组规范、库和代理,可加速遥测数据(日志、指标和跟踪记录)的插桩和导出,从而满足可观测性要求。OpenTelemetry 是 CNCF 下的一个开放标准和社区驱动型项目。通过利用项目及其生态系统提供的库,开发者能够以与供应商无关的方式针对多种架构检测其应用。

此外,除了可观测性的三大支柱之外,持续分析也是可观测性的另一个关键组成部分,并且正在扩大行业用户群。Cloud Profiler 是性能分析器的鼻祖之一,提供了一个简单的界面,用于深入分析应用调用堆栈中的性能指标。

此 Codelab 是该系列教程的第 1 部分,介绍了如何使用 OpenTelemetry 和 Cloud Trace 在微服务中插桩分布式跟踪记录。第 2 部分将介绍如何使用 Cloud Profiler 进行持续性能剖析。

分布式轨迹

在日志、指标和轨迹中,轨迹是一种遥测数据,用于指示系统中流程特定部分的延迟时间。尤其是在微服务时代,分布式跟踪是找出整个分布式系统中的延迟瓶颈的强大驱动因素。

在分析分布式跟踪记录时,跟踪记录数据可视化是快速了解总体系统延迟的关键。在分布式跟踪中,我们会处理一组调用,以处理对系统入口点的单个请求,其形式为包含多个 span 的跟踪。

Span 表示在分布式系统中完成的单个工作单元,记录开始时间和停止时间。Span 之间通常存在层次关系 - 在下图中,所有较小的 span 都是大型 /messages span 的子 span,并组合成一个显示系统工作路径的轨迹。

轨迹

Google Cloud Trace 是分布式跟踪后端的一种选择,并且与 Google Cloud 中的其他产品集成得很好。

构建内容

在此 Codelab 中,您将对在 Google Kubernetes Engine 集群上运行的名为“莎士比亚应用”(又称 Shakesapp)的服务中的跟踪信息进行插桩。Shakesapp 的架构如下所述:

44e243182ced442f.png

  • Loadgen 通过 HTTP 将查询字符串发送到客户端
  • 客户端通过 gRPC 将查询从 loadgen 传递到服务器
  • 服务器接受来自客户端的查询,从 Google Cloud Storage 中提取所有莎士比亚作品(文本格式),搜索包含查询内容的行,并将匹配的行号返回给客户端

您将跨请求检测跟踪信息。之后,您将在服务器中嵌入分析器代理,并调查瓶颈。

学习内容

  • 如何在 Go 项目中开始使用 OpenTelemetry Trace 库
  • 如何使用库创建 span
  • 如何在应用组件之间通过网络传播 span 上下文
  • 如何将跟踪数据发送到 Cloud Trace
  • 如何在 Cloud Trace 上分析跟踪记录

本 Codelab 介绍了如何为微服务插桩。为便于理解,此示例仅包含 3 个组件(负载生成器、客户端和服务器),但您可以将此 Codelab 中介绍的相同流程应用于更复杂、更大的系统。

所需条件

  • Go 基础知识
  • 具备 Kubernetes 基础知识

2. 设置和要求

自定进度的环境设置

如果您还没有 Google 账号(Gmail 或 Google Apps),则必须创建一个。登录 Google Cloud Platform Console (console.cloud.google.com) 并创建一个新项目。

如果您已经有一个项目,请点击控制台左上方的项目选择下拉菜单:

7a32e5469db69e9.png

然后在出现的对话框中点击“新建项目”按钮以创建一个新项目:

7136b3ee36ebaf89.png

如果您还没有项目,则应该看到一个类似这样的对话框来创建您的第一个项目:

870a3cbd6541ee86.png

随后的项目创建对话框可让您输入新项目的详细信息:

affdc444517ba805.png

请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID。

接下来,如果尚未执行此操作,则需要在 Developers Console 中启用结算功能,以便使用 Google Cloud 资源并启用 Cloud Trace API

15d0ef27a8fbab27.png

在此 Codelab 中运行仅花费几美元,但是如果您决定使用更多资源或继续让它们运行,费用可能更高(请参阅本文档末尾的“清理”部分)。Google Cloud Trace、Google Kubernetes Engine 和 Google Artifact Registry 的价格已在官方文档中注明。

Google Cloud Platform 的新用户均有资格获享 $300 赠金,免费试用此 Codelab。

Google Cloud Shell 设置

虽然 Google Cloud 和 Google Cloud Trace 可以从笔记本电脑远程操作,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

基于 Debian 的这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。这意味着在本 Codelab 中,您只需要一个浏览器(没错,它适用于 Chromebook)。

如需从 Cloud 控制台激活 Cloud Shell,只需点击“激活 Cloud Shell”图标 gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A(预配和连接到环境仅需花费一些时间)。

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的 PROJECT_ID

gcloud auth list

命令输出

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

命令输出

[core]
project = <PROJECT_ID>

如果出于某种原因未设置项目,只需发出以下命令即可:

gcloud config set project <PROJECT_ID>

正在查找您的 PROJECT_ID?检查您在设置步骤中使用的 ID,或在 Cloud Console 信息中心查找该 ID:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

默认情况下,Cloud Shell 还会设置一些环境变量,这对您日后运行命令可能会很有用。

echo $GOOGLE_CLOUD_PROJECT

命令输出

<PROJECT_ID>

最后,设置默认可用区和项目配置。

gcloud config set compute/zone us-central1-f

您可以选择各种不同的可用区。如需了解详情,请参阅区域和可用区

前往语言设置

在此 Codelab 中,我们使用 Go 作为所有源代码的语言。在 Cloud Shell 上运行以下命令,并确认 Go 的版本是否为 1.17 及更高版本

go version

命令输出

go version go1.18.3 linux/amd64

设置 Google Kubernetes 集群

在此 Codelab 中,您将在 Google Kubernetes Engine (GKE) 上运行微服务集群。本 Codelab 的流程如下:

  1. 将基准项目下载到 Cloud Shell 中
  2. 将微服务构建到容器中
  3. 将容器上传到 Google Artifact Registry (GAR)
  4. 将容器部署到 GKE 上
  5. 修改服务源代码以进行轨迹插桩
  6. 前往第 2 步

启用 Kubernetes Engine

首先,我们设置一个 Kubernetes 集群,其中 Shakesapp 在 GKE 上运行,因此我们需要启用 GKE。前往“Kubernetes Engine”菜单,然后按“启用”按钮。

548cfd95bc6d344d.png

现在,您可以创建 Kubernetes 集群了。

创建 Kubernetes 集群

在 Cloud Shell 中,运行以下命令以创建 Kubernetes 集群。请确认区域值位于您将用于创建 Artifact Registry 制品库的区域下。如果您的代码库区域未涵盖相应可用区,请更改可用区值 us-central1-f

gcloud container clusters create otel-trace-codelab2 \
--zone us-central1-f \
--release-channel rapid \
--preemptible \
--enable-autoscaling \
--max-nodes 8 \
--no-enable-ip-alias \
--scopes cloud-platform

命令输出

Note: Your Pod address range (`--cluster-ipv4-cidr`) can accommodate at most 1008 node(s).
Creating cluster otel-trace-codelab2 in us-central1-f... Cluster is being health-checked (master is healthy)...done.     
Created [https://container.googleapis.com/v1/projects/development-215403/zones/us-central1-f/clusters/otel-trace-codelab2].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1-f/otel-trace-codelab2?project=development-215403
kubeconfig entry generated for otel-trace-codelab2.
NAME: otel-trace-codelab2
LOCATION: us-central1-f
MASTER_VERSION: 1.23.6-gke.1501
MASTER_IP: 104.154.76.89
MACHINE_TYPE: e2-medium
NODE_VERSION: 1.23.6-gke.1501
NUM_NODES: 3
STATUS: RUNNING

Artifact Registry 和 skaffold 设置

现在,我们已准备好可供部署的 Kubernetes 集群。接下来,我们将准备一个容器注册表,用于推送和部署容器。对于这些步骤,我们需要设置 Artifact Registry (GAR) 和 skaffold 以使用它。

设置 Artifact Registry

前往“Artifact Registry”的菜单,然后按“启用”按钮。

45e384b87f7cf0db.png

稍等片刻,您将看到 GAR 的代码库浏览器。点击“CREATE REPOSITORY”按钮,然后输入代码库的名称。

d6a70f4cb4ebcbe3.png

在此 Codelab 中,我将新代码库命名为 trace-codelab。相应制品格式为“Docker”,位置类型为“区域”。选择与您为 Google Compute Engine 默认可用区设置的区域相近的区域。例如,上面的示例选择了“us-central1-f”,因此这里我们选择“us-central1 (Iowa)”。然后点击“创建”按钮。

9c2d1ce65258ef70.png

现在,您会在代码库浏览器中看到“trace-codelab”。

7a3c1f47346bea15.png

稍后我们将返回此处检查注册表路径。

Skaffold 设置

如果您要构建在 Kubernetes 上运行的微服务,Skaffold 是一款非常实用的工具。它通过一小部分命令处理构建、推送和部署应用容器的工作流。Skaffold 默认使用 Docker Registry 作为容器注册表,因此您需要配置 Skaffold,以便在将容器推送到 GAR 时识别 GAR。

再次打开 Cloud Shell,确认是否已安装 Skaffold。(Cloud Shell 默认将 Skaffold 安装到环境中。)运行以下命令,查看 Skaffold 版本。

skaffold version

命令输出

v1.38.0

现在,您可以注册供 Skaffold 使用的默认代码库。如需获取注册表路径,请前往 Artifact Registry 信息中心,然后点击您在上一步中刚刚设置的代码库的名称。

7a3c1f47346bea15.png

然后,您会在页面顶部看到面包屑导航路径。点击 e157b1359c3edc06.png 图标,将注册表路径复制到剪贴板。

e0f2ae2144880b8b.png

点击复制按钮后,您会在浏览器底部看到一个对话框,其中包含类似如下内容的消息:

已复制“us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab”

返回到 Cloud Shell。运行 skaffold config set default-repo 命令,并使用您刚刚从信息中心复制的值。

skaffold config set default-repo us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab

命令输出

set value default-repo to us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab for context gke_stackdriver-sandbox-3438851889_us-central1-b_stackdriver-sandbox

此外,您还需要将注册表配置为 Docker 配置。运行以下命令:

gcloud auth configure-docker us-central1-docker.pkg.dev --quiet

命令输出

{
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud",
    "us-central1-docker.pkg.dev": "gcloud"
  }
}
Adding credentials for: us-central1-docker.pkg.dev

现在,您可以继续执行下一步,在 GKE 上设置 Kubernetes 容器。

摘要

在此步骤中,您将设置 Codelab 环境:

  • 设置 Cloud Shell
  • 为容器注册表创建了 Artifact Registry 代码库
  • 设置 Skaffold 以使用容器注册表
  • 创建了运行 Codelab 微服务的 Kubernetes 集群

后续步骤

在下一步中,您将构建、推送微服务并将其部署到集群中

3. 构建、推送和部署微服务

下载 Codelab 材料

在上一步中,我们已为此 Codelab 设置了所有前提条件。现在,您可以在这些平台上运行整个微服务了。Codelab 材料托管在 GitHub 上,因此请使用以下 git 命令将其下载到 Cloud Shell 环境中。

cd ~
git clone https://github.com/ymotongpoo/opentelemetry-trace-codelab-go.git
cd opentelemetry-trace-codelab-go

项目的目录结构如下:

.
├── README.md
├── step0
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step1
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step2
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step3
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step4
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step5
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
└── step6
    ├── manifests
    ├── proto
    ├── skaffold.yaml
    └── src
  • manifests:Kubernetes 清单文件
  • proto:客户端与服务器之间通信的 proto 定义
  • src:每个服务的源代码目录
  • skaffold.yaml:Skaffold 的配置文件

在此 Codelab 中,您将更新位于 step0 文件夹下的源代码。您还可以参考 step[1-6] 文件夹中的源代码,获取以下步骤中的答案。(第 1 部分涵盖第 0 步到第 4 步,第 2 部分涵盖第 5 步和第 6 步)

运行 skaffold 命令

最后,您已准备好构建、推送和部署整个内容到刚刚创建的 Kubernetes 集群。这听起来好像包含多个步骤,但实际上 Skaffold 会为您完成所有操作。我们来尝试一下,运行以下命令:

cd step0
skaffold dev

运行命令后,您会立即看到 docker build 的日志输出,并可以确认它们已成功推送到注册表。

命令输出

...
---> Running in c39b3ea8692b
 ---> 90932a583ab6
Successfully built 90932a583ab6
Successfully tagged us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice:step1
The push refers to repository [us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice]
cc8f5a05df4a: Preparing
5bf719419ee2: Preparing
2901929ad341: Preparing
88d9943798ba: Preparing
b0fdf826a39a: Preparing
3c9c1e0b1647: Preparing
f3427ce9393d: Preparing
14a1ca976738: Preparing
f3427ce9393d: Waiting
14a1ca976738: Waiting
3c9c1e0b1647: Waiting
b0fdf826a39a: Layer already exists
88d9943798ba: Layer already exists
f3427ce9393d: Layer already exists
3c9c1e0b1647: Layer already exists
14a1ca976738: Layer already exists
2901929ad341: Pushed
5bf719419ee2: Pushed
cc8f5a05df4a: Pushed
step1: digest: sha256:8acdbe3a453001f120fb22c11c4f6d64c2451347732f4f271d746c2e4d193bbe size: 2001

推送所有服务容器后,Kubernetes 部署会自动启动。

命令输出

sha256:b71fce0a96cea08075dc20758ae561cf78c83ff656b04d211ffa00cedb77edf8 size: 1997
Tags used in deployment:
 - serverservice -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice:step4@sha256:8acdbe3a453001f120fb22c11c4f6d64c2451347732f4f271d746c2e4d193bbe
 - clientservice -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/clientservice:step4@sha256:b71fce0a96cea08075dc20758ae561cf78c83ff656b04d211ffa00cedb77edf8
 - loadgen -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/loadgen:step4@sha256:eea2e5bc8463ecf886f958a86906cab896e9e2e380a0eb143deaeaca40f7888a
Starting deploy...
 - deployment.apps/clientservice created
 - service/clientservice created
 - deployment.apps/loadgen created
 - deployment.apps/serverservice created
 - service/serverservice created

部署后,您会看到每个容器中实际应用日志输出到 stdout 的情况,如下所示:

命令输出

[client] 2022/07/14 06:33:15 {"match_count":3040}
[loadgen] 2022/07/14 06:33:15 query 'love': matched 3040
[client] 2022/07/14 06:33:15 {"match_count":3040}
[loadgen] 2022/07/14 06:33:15 query 'love': matched 3040
[client] 2022/07/14 06:33:16 {"match_count":3040}
[loadgen] 2022/07/14 06:33:16 query 'love': matched 3040
[client] 2022/07/14 06:33:19 {"match_count":463}
[loadgen] 2022/07/14 06:33:19 query 'tear': matched 463
[loadgen] 2022/07/14 06:33:20 query 'world': matched 728
[client] 2022/07/14 06:33:20 {"match_count":728}
[client] 2022/07/14 06:33:22 {"match_count":463}
[loadgen] 2022/07/14 06:33:22 query 'tear': matched 463

请注意,此时您希望看到来自服务器的任何消息。好了,现在您终于可以开始使用 OpenTelemetry 对应用进行插桩处理,以对服务进行分布式跟踪。

在开始检测服务之前,请按 Ctrl-C 关闭集群。

命令输出

...
[client] 2022/07/14 06:34:57 {"match_count":1}
[loadgen] 2022/07/14 06:34:57 query 'what's past is prologue': matched 1
^CCleaning up...
 - W0714 06:34:58.464305   28078 gcp.go:120] WARNING: the gcp auth plugin is deprecated in v1.22+, unavailable in v1.25+; use gcloud instead.
 - To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke
 - deployment.apps "clientservice" deleted
 - service "clientservice" deleted
 - deployment.apps "loadgen" deleted
 - deployment.apps "serverservice" deleted
 - service "serverservice" deleted

摘要

在此步骤中,您已在环境中准备好 Codelab 材料,并确认 Skaffold 运行正常。

后续步骤

在下一步中,您将修改 loadgen 服务的源代码,以插桩跟踪信息。

4. HTTP 的插桩

跟踪插桩和传播的概念

在编辑源代码之前,我先用一个简单的图表简要说明一下分布式轨迹的工作原理。

6be42e353b9bfd1d.png

在此示例中,我们对代码进行插桩,以将跟踪记录和 span 信息导出到 Cloud Trace,并在从 loadgen 服务到服务器服务的请求中传播跟踪上下文。

应用需要发送跟踪元数据(例如跟踪 ID 和 span ID),以便 Cloud Trace 将具有相同跟踪 ID 的所有 span 组装成一条跟踪记录。此外,应用还需要在请求下游服务时传播跟踪上下文(父 span 的跟踪 ID 和 span ID 的组合),以便下游服务了解它们正在处理哪个跟踪上下文。

OpenTelemetry 可帮助您:

  • 以生成唯一的轨迹 ID 和 span ID
  • 将 Trace ID 和 Span ID 导出到后端
  • 将跟踪上下文传播到其他服务
  • 嵌入有助于分析轨迹的额外元数据

OpenTelemetry Trace 中的组件

b01f7bb90188db0d.png

使用 OpenTelemetry 对应用轨迹进行插桩的流程如下:

  1. 创建导出工具
  2. 创建将导出器绑定到 1 的 TracerProvider 并将其设置为全局。
  3. 设置 TextMapPropagaror 以设置传播方法
  4. 从 TracerProvider 获取 Tracer
  5. 从 Tracer 生成 Span

目前,您无需了解每个组件中的详细属性,但最重要的一点是,请记住:

  • 此处的导出器可插入到 TracerProvider 中
  • TracerProvider 包含有关轨迹采样和导出的所有配置
  • 所有轨迹都捆绑在 Tracer 对象中

了解了这一点后,我们接下来开始实际的编码工作。

插桩第一个 span

检测负载生成器服务

按 Cloud Shell 右上角的按钮 776a11bfb2122549.png 打开 Cloud Shell 编辑器。从左侧窗格中的探索器打开 step0/src/loadgen/main.go,然后找到 main 函数。

step0/src/loadgen/main.go

func main() {
        ...
        for range t.C {
                log.Printf("simulating client requests, round %d", i)
                if err := run(numWorkers, numConcurrency); err != nil {
                        log.Printf("aborted round with error: %v", err)
                }
                log.Printf("simulated %d requests", numWorkers)
                if numRounds != 0 && i > numRounds {
                        break
                }
                i++
        }
}

在主函数中,您会看到循环调用其中的函数 run。在当前实现中,该部分包含 2 个记录函数调用开始和结束的日志行。现在,我们来对 Span 信息进行插桩,以跟踪函数调用的延迟时间。

首先,如上一部分中所述,我们来设置 OpenTelemetry 的完整配置。添加 OpenTelemetry 软件包,如下所示:

step0/src/loadgen/main.go

import (
        "context" // step1. add packages
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net/http"
        "net/url"
        "time"
        // step1. add packages
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
        "go.opentelemetry.io/otel/trace"
        // step1. end add packages
)

为了提高可读性,我们创建了一个名为 initTracer 的设置函数,并在 main 函数中调用该函数。

step0/src/loadgen/main.go

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

您可能会发现,设置 OpenTelemetry 的过程与上一部分中所述的相同。在此实现中,我们使用 stdout 将所有轨迹信息以结构化格式导出到标准输出。

然后,从主函数中调用该函数。调用 initTracer(),并确保在关闭应用时调用 TracerProvider.Shutdown()

step0/src/loadgen/main.go

func main() {
        // step1. setup OpenTelemetry
        tp, err := initTracer()
        if err != nil {
                log.Fatalf("failed to initialize TracerProvider: %v", err)
        }
        defer func() {
                if err := tp.Shutdown(context.Background()); err != nil {
                        log.Fatalf("error shutting down TracerProvider: %v", err)
                }
        }()
        // step1. end setup

        log.Printf("starting worder with %d workers in %d concurrency", numWorkers, numConcurrency)
        log.Printf("number of rounds: %d (0 is inifinite)", numRounds)
        ...

完成设置后,您需要创建一个具有唯一轨迹 ID 和 span ID 的 span。OpenTelemetry 为此提供了一个便捷的库。向插桩 HTTP 客户端添加了额外的软件包。

step0/src/loadgen/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net/http"
        "net/http/httptrace" // step1. add packages
        "net/url"
        "time"
        // step1. add packages
        "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        // step1. end add packages
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
        "go.opentelemetry.io/otel/trace"
)

由于负载生成器在 runQuery 函数中通过 HTTP 调用客户端服务,因此我们使用 net/http 的 contrib 软件包,并通过 httptraceotelhttp 软件包的扩展程序启用插桩。net/http

首先,添加一个软件包全局变量 httpClient,以通过插桩客户端调用 HTTP 请求。

step0/src/loadgen/main.go

var httpClient = http.Client{
        Transport: otelhttp.NewTransport(http.DefaultTransport)
}

接下来,在 runQuery 函数中添加插桩,以使用 OpenTelemetry 和自定义 HTTP 客户端自动生成的 span 创建自定义 span。您需要执行以下操作:

  1. 从全球 TracerProvider 获取 otel.Tracer() 的 Tracer
  2. 使用 Tracer.Start() 方法创建根 span
  3. 在任意时间结束根 span(在本例中,在 runQuery 函数结束时结束)

step0/src/loadgen/main.go

        reqURL.RawQuery = v.Encode()
        // step1. replace http.Get() with custom client call
        // resp, err := http.Get(reqURL.String())

        // step1. instrument trace
        ctx := context.Background()
        tr := otel.Tracer("loadgen")
        ctx, span := tr.Start(ctx, "query.request", trace.WithAttributes(
                semconv.TelemetrySDKLanguageGo,
                semconv.ServiceNameKey.String("loadgen.runQuery"),
                attribute.Key("query").String(s),
        ))
        defer span.End()
        ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
        req, err := http.NewRequestWithContext(ctx, "GET", reqURL.String(), nil)
        if err != nil {
                return -1, fmt.Errorf("error creating HTTP request object: %v", err)
        }
        resp, err := httpClient.Do(req)
        // step1. end instrumentation
        if err != nil {
                return -1, fmt.Errorf("error sending request to %v: %v", reqURL.String(), err)
        }

现在,您已完成 loadgen(HTTP 客户端应用)中的插桩。请务必使用 go mod 命令更新 go.modgo.sum

go mod tidy

器械客户服务

在上一部分中,我们对下图中的红色矩形框内的部分进行了插桩。我们在负载生成器服务中实现了 span 信息。与负载生成器服务类似,现在我们需要对客户端服务进行插桩。与负载生成器服务的不同之处在于,客户端服务需要提取 HTTP 标头中从负载生成器服务传播的跟踪 ID 信息,并使用该 ID 生成 Span。

bcaccd06691269f8.png

打开 Cloud Shell 编辑器,然后添加所需的软件包,就像我们为负载生成器服务所做的那样。

step0/src/client/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "net/url"
        "os"
        "time"

        "opentelemetry-trace-codelab-go/client/shakesapp"
        // step1. add new import
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "go.opentelemetry.io/otel/trace"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        // step1. end new import
)

同样,我们需要设置 OpenTelemtry。只需从 loadgen 复制并粘贴 initTracer 函数,然后在客户端服务的 main 函数中调用该函数即可。

step0/src/client/main.go

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

现在可以对 span 进行插桩了。由于客户端服务需要接受来自 loadgen 服务的 HTTP 请求,因此需要对处理程序进行插桩。客户端服务中的 HTTP 服务器使用 net/http 实现,您可以像在 loadgen 中一样使用 otelhttp 软件包。

首先,我们将处理程序注册替换为 otelhttp Handler。在 main 函数中,找到使用 http.HandleFunc() 注册 HTTP 处理程序的行。

step0/src/client/main.go

        // step1. change handler to intercept OpenTelemetry related headers
        // http.HandleFunc("/", svc.handler)
        otelHandler := otelhttp.NewHandler(http.HandlerFunc(svc.handler), "client.handler")
        http.Handle("/", otelHandler)
        // step1. end intercepter setting
        http.HandleFunc("/_genki", svc.health)

然后,我们在处理程序中检测实际的 span。找到 func (*clientService) handler(),并使用 trace.SpanFromContext() 添加 span 插桩。

step0/src/client/main.go

func (cs *clientService) handler(w http.ResponseWriter, r *http.Request) {
        ...
        ctx := r.Context()
        ctx, cancel := context.WithCancel(ctx)
        defer cancel()
        // step1. instrument trace
        span := trace.SpanFromContext(ctx)
        defer span.End()
        // step1. end instrument
        ...

通过这种插桩,您可以获得从 handler 方法开始到结束的 span。为了便于分析 span,请向查询添加一个用于存储匹配次数的额外属性。在日志行之前,添加以下代码。

step0/src/client/main.go

func (cs *clientService) handler(w http.ResponseWriter, r *http.Request) {
        ...
        // step1. add span specific attribute
        span.SetAttributes(attribute.Key("matched").Int64(resp.MatchCount))
        // step1. end adding attribute
        log.Println(string(ret))
        ...

完成上述所有插桩后,您便完成了 loadgen 和客户端之间的轨迹插桩。我们来看看具体要如何操作。再次使用 Skaffold 运行代码。

skaffold dev

在 GKE 集群上运行服务一段时间后,您会看到大量如下所示的日志消息:

命令输出

[loadgen] {
[loadgen]       "Name": "query.request",
[loadgen]       "SpanContext": {
[loadgen]               "TraceID": "cfa22247a542beeb55a3434392d46b89",
[loadgen]               "SpanID": "18b06404b10c418b",
[loadgen]               "TraceFlags": "01",
[loadgen]               "TraceState": "",
[loadgen]               "Remote": false
[loadgen]       },
[loadgen]       "Parent": {
[loadgen]               "TraceID": "00000000000000000000000000000000",
[loadgen]               "SpanID": "0000000000000000",
[loadgen]               "TraceFlags": "00",
[loadgen]               "TraceState": "",
[loadgen]               "Remote": false
[loadgen]       },
[loadgen]       "SpanKind": 1,
[loadgen]       "StartTime": "2022-07-14T13:13:36.686751087Z",
[loadgen]       "EndTime": "2022-07-14T13:14:31.849601964Z",
[loadgen]       "Attributes": [
[loadgen]               {
[loadgen]                       "Key": "telemetry.sdk.language",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "go"
[loadgen]                       }
[loadgen]               },
[loadgen]               {
[loadgen]                       "Key": "service.name",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "loadgen.runQuery"
[loadgen]                       }
[loadgen]               },
[loadgen]               {
[loadgen]                       "Key": "query",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "faith"
[loadgen]                       }
[loadgen]               }
[loadgen]       ],
[loadgen]       "Events": null,
[loadgen]       "Links": null,
[loadgen]       "Status": {
[loadgen]               "Code": "Unset",
[loadgen]               "Description": ""
[loadgen]       },
[loadgen]       "DroppedAttributes": 0,
[loadgen]       "DroppedEvents": 0,
[loadgen]       "DroppedLinks": 0,
[loadgen]       "ChildSpanCount": 5,
[loadgen]       "Resource": [
[loadgen]               {
[loadgen]                       "Key": "service.name",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "unknown_service:loadgen"
...

stdout 导出器会发出这些消息。您会注意到,loadgen 生成的所有 span 的父级都有 TraceID: 00000000000000000000000000000000,因为这是根 span,即轨迹中的第一个 span。您还会发现,嵌入属性 "query" 包含传递给客户端服务的查询字符串。

摘要

在此步骤中,您已对以 HTTP 方式通信的负载生成器服务和客户端服务进行了插桩,并确认您能够成功跨服务传播跟踪上下文,以及将这两个服务的 span 信息导出到 stdout。

后续步骤

在下一步中,您将对客户端服务和服务器服务进行插桩,以确认如何通过 gRPC 传播跟踪上下文。

5. gRPC 的插桩

在上一步中,我们检测了此微服务中的前半部分请求。在此步骤中,我们尝试对客户端服务和服务器服务之间的 gRPC 通信进行插桩。(下图中的绿色和紫色矩形)

75310d8e0e3b1a30.png

针对 gRPC 客户端的预构建插桩

OpenTelemetry 的生态系统提供了许多实用的库,可帮助开发者对应用进行插桩。在上一步中,我们为 net/http 软件包使用了预构建插桩。在此步骤中,由于我们尝试通过 gRPC 传播跟踪记录上下文,因此我们使用该库。

首先,您需要导入名为 otelgrpc 的预构建 gRPC 软件包。

step0/src/client/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "net/url"
        "os"
        "time"

        "opentelemetry-trace-codelab-go/client/shakesapp"
        // step2. add prebuilt gRPC package (otelgrpc) 
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "go.opentelemetry.io/otel/trace"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

这次,客户端服务是针对服务器服务的 gRPC 客户端,因此您需要对 gRPC 客户端进行插桩。找到 mustConnGRPC 函数,并添加 gRPC 拦截器,以便在每次客户端向服务器发出请求时检测新的 span。

step0/src/client/main.go

// Helper function for gRPC connections: Dial and create client once, reuse.
func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) {
        var err error
        // step2. add gRPC interceptor
        interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
        *conn, err = grpc.DialContext(ctx, addr,
                grpc.WithTransportCredentials(insecure.NewCredentials()),
                grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor(interceptorOpt)),
                grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(interceptorOpt)),
                grpc.WithTimeout(time.Second*3),
        )
        // step2: end adding interceptor
        if err != nil {
                panic(fmt.Sprintf("Error %s grpc: failed to connect %s", err, addr))
        }
}

由于您已在上一部分中设置了 OpenTelemetry,因此无需再执行此操作。

针对 gRPC 服务器的预构建插桩

与我们为 gRPC 客户端所做的类似,我们为 gRPC 服务器调用了预构建的插桩。将新软件包添加到导入部分,如下所示:

step0/src/server/main.go

import (
        "context"
        "fmt"
        "io/ioutil"
        "log"
        "net"
        "os"
        "regexp"
        "strings"

        "opentelemetry-trace-codelab-go/server/shakesapp"

        "cloud.google.com/go/storage"
        // step2. add OpenTelemetry packages including otelgrpc
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
        "go.opentelemetry.io/otel"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "google.golang.org/api/iterator"
        "google.golang.org/api/option"
        "google.golang.org/grpc"
        healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

由于这是首次对服务器进行插桩,因此您需要先设置 OpenTelemetry,这与我们为 loadgen 和客户端服务所做的操作类似。

step0/src/server/main.go

// step2. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }
        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

func main() {
        ...

        // step2. setup OpenTelemetry
        tp, err := initTracer()
        if err != nil {
                log.Fatalf("failed to initialize TracerProvider: %v", err)
        }
        defer func() {
                if err := tp.Shutdown(context.Background()); err != nil {
                        log.Fatalf("error shutting down TracerProvider: %v", err)
                }
        }()
        // step2. end setup
        ...

接下来,您需要添加服务器拦截器。在 main 函数中,找到调用 grpc.NewServer() 的位置,并向该函数添加拦截器。

step0/src/server/main.go

func main() {
        ...
        svc := NewServerService()
        // step2: add interceptor
        interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
        srv := grpc.NewServer(
                grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(interceptorOpt)),
                grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor(interceptorOpt)),
        )
        // step2: end adding interceptor
        shakesapp.RegisterShakespeareServiceServer(srv, svc)
        ...

运行微服务并确认跟踪记录

然后,使用 skaffold 命令运行修改后的代码。

skaffold dev

现在,您再次在 stdout 上看到了一堆 span 信息。

命令输出

...
[server] {
[server]        "Name": "shakesapp.ShakespeareService/GetMatchCount",
[server]        "SpanContext": {
[server]                "TraceID": "89b472f213a400cf975e0a0041649667",
[server]                "SpanID": "96030dbad0061b3f",
[server]                "TraceFlags": "01",
[server]                "TraceState": "",
[server]                "Remote": false
[server]        },
[server]        "Parent": {
[server]                "TraceID": "89b472f213a400cf975e0a0041649667",
[server]                "SpanID": "cd90cc3859b73890",
[server]                "TraceFlags": "01",
[server]                "TraceState": "",
[server]                "Remote": true
[server]        },
[server]        "SpanKind": 2,
[server]        "StartTime": "2022-07-14T14:05:55.74822525Z",
[server]        "EndTime": "2022-07-14T14:06:03.449258891Z",
[server]        "Attributes": [
...
[server]        ],
[server]        "Events": [
[server]                {
[server]                        "Name": "message",
[server]                        "Attributes": [
...
[server]                        ],
[server]                        "DroppedAttributeCount": 0,
[server]                        "Time": "2022-07-14T14:05:55.748235489Z"
[server]                },
[server]                {
[server]                        "Name": "message",
[server]                        "Attributes": [
...
[server]                        ],
[server]                        "DroppedAttributeCount": 0,
[server]                        "Time": "2022-07-14T14:06:03.449255889Z"
[server]                }
[server]        ],
[server]        "Links": null,
[server]        "Status": {
[server]                "Code": "Unset",
[server]                "Description": ""
[server]        },
[server]        "DroppedAttributes": 0,
[server]        "DroppedEvents": 0,
[server]        "DroppedLinks": 0,
[server]        "ChildSpanCount": 0,
[server]        "Resource": [
[server]                {
...
[server]        ],
[server]        "InstrumentationLibrary": {
[server]                "Name": "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc",
[server]                "Version": "semver:0.33.0",
[server]                "SchemaURL": ""
[server]        }
[server] }
...

您会发现,您没有嵌入任何 span 名称,而是使用 trace.Start()span.SpanFromContext() 手动创建了 span。不过,您仍然会获得大量 span,因为 gRPC 拦截器生成了这些 span。

摘要

在此步骤中,您在 OpenTelemetry 生态系统库的支持下,对基于 gRPC 的通信进行了插桩。

后续步骤

在下一步中,您将最终使用 Cloud Trace 可视化跟踪记录,并了解如何分析收集的 span。

6. 使用 Cloud Trace 直观呈现跟踪记录

您已使用 OpenTelemetry 对整个系统中的轨迹进行了插桩。到目前为止,您已经了解了如何对 HTTP 和 gRPC 服务进行插桩。虽然您已了解如何插桩,但尚未了解如何分析这些指标。在本部分中,您将使用 Cloud Trace 导出器替换 stdout 导出器,并了解如何分析跟踪记录。

使用 Cloud Trace 导出器

OpenTelemetry 的一个强大特性是其可插拔性。如需直观呈现插桩收集的所有 span,您只需将标准输出导出器替换为 Cloud Trace 导出器。

打开每项服务的 main.go 文件,然后找到 initTracer() 函数。删除该行以生成标准输出导出器,并改为创建 Cloud Trace 导出器。

step0/src/loadgen/main.go

import (
        ...
        // step3. add OpenTelemetry for Cloud Trace package
        cloudtrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
)

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // step3. replace stdout exporter with Cloud Trace exporter
        // cloudtrace.New() finds the credentials to Cloud Trace automatically following the
        // rules defined by golang.org/x/oauth2/google.findDefaultCredentailsWithParams.
        // https://pkg.go.dev/golang.org/x/oauth2/google#FindDefaultCredentialsWithParams
        exporter, err := cloudtrace.New()
        // step3. end replacing exporter
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

您还需要在客户端和服务端服务中修改同一函数。

运行微服务并确认跟踪记录

修改完成后,只需使用 skaffold 命令照常运行集群即可。

skaffold dev

然后,您现在不会在 stdout 上看到结构化日志格式的太多 span 信息,因为您已将导出器替换为 Cloud Trace 导出器。

命令输出

[loadgen] 2022/07/14 15:01:07 simulated 20 requests
[loadgen] 2022/07/14 15:01:07 simulating client requests, round 37
[loadgen] 2022/07/14 15:01:14 query 'sweet': matched 958
[client] 2022/07/14 15:01:14 {"match_count":958}
[client] 2022/07/14 15:01:14 {"match_count":3040}
[loadgen] 2022/07/14 15:01:14 query 'love': matched 3040
[client] 2022/07/14 15:01:15 {"match_count":349}
[loadgen] 2022/07/14 15:01:15 query 'hello': matched 349
[client] 2022/07/14 15:01:15 {"match_count":484}
[loadgen] 2022/07/14 15:01:15 query 'faith': matched 484
[loadgen] 2022/07/14 15:01:15 query 'insolence': matched 14
[client] 2022/07/14 15:01:15 {"match_count":14}
[client] 2022/07/14 15:01:21 {"match_count":484}
[loadgen] 2022/07/14 15:01:21 query 'faith': matched 484
[client] 2022/07/14 15:01:21 {"match_count":728}
[loadgen] 2022/07/14 15:01:21 query 'world': matched 728
[client] 2022/07/14 15:01:22 {"match_count":484}
[loadgen] 2022/07/14 15:01:22 query 'faith': matched 484
[loadgen] 2022/07/14 15:01:22 query 'hello': matched 349
[client] 2022/07/14 15:01:22 {"match_count":349}
[client] 2022/07/14 15:01:23 {"match_count":1036}
[loadgen] 2022/07/14 15:01:23 query 'friend': matched 1036
[loadgen] 2022/07/14 15:01:28 query 'tear': matched 463
...

现在,我们来确认所有 span 是否已正确发送到 Cloud Trace。访问 Cloud 控制台,然后前往“跟踪记录列表”。您可以轻松地从搜索框中访问该功能。否则,您可以点击左侧窗格中的菜单。8b3f8411bd737e06.png

然后,您会看到许多蓝点分布在延迟时间图上。每个点代表一条轨迹。

3ecf131423fc4c40.png

点击其中一个,即可查看轨迹中的详细信息。4fd10960c6648a03.png

即使只是简单地快速浏览一下,您也能获得许多数据洞见。例如,从瀑布图中,您可以看到延迟时间主要是由名为 shakesapp.ShakespeareService/GetMatchCount 的 span 造成的。(请参阅上图中的 1)您可以在摘要表格中确认这一点。(最右侧的列显示了每个时间段的持续时间。)此外,此轨迹是针对查询“朋友”的。(请参见上图中的 2)

通过这些简短的分析,您可能会意识到,您需要了解 GetMatchCount 方法中更精细的 span。与 stdout 信息相比,可视化功能非常强大。如需详细了解 Cloud Trace 跟踪记录详情,请参阅我们的官方文档

摘要

在此步骤中,您将 stdout 导出器替换为 Cloud Trace 导出器,并在 Cloud Trace 上直观呈现了跟踪记录。此外,您还学习了如何开始分析轨迹。

后续步骤

在下一步中,您将修改服务器服务的源代码,以在 GetMatchCount 中添加子 span。

7. 添加了子跨度,以便更好地进行分析

在上一步中,您发现从 loadgen 观察到的往返时间大部分是服务器服务中 GetMatchCount 方法(即 gRPC 处理程序)内部的进程。不过,由于我们除了处理程序之外没有检测任何其他内容,因此无法从瀑布图中获得更多分析洞见。当我们开始对微服务进行插桩时,这是一种常见情况。

3b63a1e471dddb8c.png

在本部分中,我们将对服务器调用 Google Cloud Storage 的子 span 进行插桩,因为在进程中,某些外部网络 I/O 往往需要很长时间,因此务必要确定该调用是否是造成这种情况的原因。

在服务器中对子 span 进行插桩

在服务器中打开 main.go,然后找到函数 readFiles。此函数正在调用对 Google Cloud Storage 的请求,以提取莎士比亚作品的所有文本文件。在此函数中,您可以创建一个子 span,就像您在客户端服务中为 HTTP 服务器插桩所做的那样。

step0/src/server/main.go

func readFiles(ctx context.Context, bucketName, prefix string) ([]string, error) {
        type resp struct {
                s   string
                err error
        }

        // step4: add an extra span
        span := trace.SpanFromContext(ctx)
        span.SetName("server.readFiles")
        span.SetAttributes(attribute.Key("bucketname").String(bucketName))
        defer span.End()
        // step4: end add span
        ...

以上就是添加新范围的全部内容。让我们运行应用,看看效果如何。

运行微服务并确认跟踪记录

修改完成后,只需使用 skaffold 命令照常运行集群即可。

skaffold dev

然后从轨迹列表中选择一个名为 query.request 的轨迹。您会看到类似的跟踪记录瀑布图,只是在 shakesapp.ShakespeareService/GetMatchCount 下多了一个新的 span。(下方红色矩形框内的范围)

3d4a891aa30d7a32.png

从该图表中,您现在可以了解到,对 Google Cloud Storage 的外部调用占用了大量延迟时间,但仍有其他因素导致了大部分延迟时间。

仅从跟踪记录瀑布图中,您就已经获得了许多数据洞见。如何在应用中获取更多效果详情?此时,性能剖析器就派上用场了,但现在,我们先结束此 Codelab,并将所有性能剖析器教程都放在第 2 部分中。

摘要

在此步骤中,您在服务器服务中检测了另一个 span,并进一步了解了系统延迟时间。

8. 恭喜

您已成功使用 OpenTelemetry 创建分布式跟踪记录,并在 Google Cloud Trace 上确认了微服务中的请求延迟时间。

如需进行扩展练习,您可以自行尝试以下主题。

  • 当前实现会发送健康检查生成的所有 span。(grpc.health.v1.Health/Check) 如何从 Cloud Trace 中过滤掉这些 span?提示位于此处
  • 将事件日志与 span 相关联,并了解其在 Google Cloud Trace 和 Google Cloud Logging 中的运作方式。提示位于此处
  • 将某些服务替换为其他语言的服务,并尝试使用相应语言的 OpenTelemetry 对其进行插桩。

此外,如果您想在完成本教程后了解分析器,请继续学习第 2 部分。在这种情况下,您可以跳过下方的清理部分。

清理

完成本 Codelab 后,请停止 Kubernetes 集群并务必删除项目,以免在 Google Kubernetes Engine、Google Cloud Trace 和 Google Artifact Registry 上产生意外费用。

首先,删除集群。如果您使用 skaffold dev 运行集群,只需按 Ctrl-C 即可。如果您使用 skaffold run 运行集群,请运行以下命令:

skaffold delete

命令输出

Cleaning up...
 - deployment.apps "clientservice" deleted
 - service "clientservice" deleted
 - deployment.apps "loadgen" deleted
 - deployment.apps "serverservice" deleted
 - service "serverservice" deleted

删除集群后,在菜单窗格中依次选择“IAM 和管理”>“设置”,然后点击“关停”按钮。

45aa37b7d5e1ddd1.png

然后在对话框中的表单中输入项目 ID(而非项目名称),并确认关停。