1. 简介
上次更新时间: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 集群上运行的“Shakespeare 应用”(也称为 Shakesapp)服务中插桩跟踪信息。Shakesapp 的架构如下所述:
- 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) 并创建一个新项目。
如果您已经有一个项目,请点击控制台左上方的项目选择下拉菜单:
然后在出现的对话框中点击“新建项目”按钮以创建一个新项目:
如果您还没有项目,则应该看到一个类似这样的对话框来创建您的第一个项目:
随后的项目创建对话框可让您输入新项目的详细信息:
请记住项目 ID,它在所有 Google Cloud 项目中都是唯一名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID。
接下来,如果尚未执行此操作,则需要在 Developers Console 中启用结算功能,以便使用 Google Cloud 资源并启用 Cloud Trace API。
在此 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”图标 (预配和连接到环境仅需花费一些时间)。
在连接到 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:
默认情况下,Cloud Shell 还会设置一些环境变量,这对您日后运行命令可能会很有用。
echo $GOOGLE_CLOUD_PROJECT
命令输出
<PROJECT_ID>
最后,设置默认可用区和项目配置。
gcloud config set compute/zone us-central1-f
您可以选择各种不同的可用区。如需了解详情,请参阅区域和可用区。
Go 语言设置
在此 Codelab 中,我们将使用 Go 编写所有源代码。在 Cloud Shell 中运行以下命令,并确认 Go 的版本是否为 1.17 或更高版本
go version
命令输出
go version go1.18.3 linux/amd64
设置 Google Kubernetes 集群
在本 Codelab 中,您将在 Google Kubernetes Engine (GKE) 上运行一组微服务集群。此 Codelab 的流程如下:
- 将基准项目下载到 Cloud Shell
- 将微服务构建到容器中
- 将容器上传到 Google Artifact Registry (GAR)
- 将容器部署到 GKE
- 修改服务的源代码以进行轨迹插桩
- 前往第 2 步
启用 Kubernetes Engine
首先,我们将设置一个 Kubernetes 集群,其中 Shakesapp 将在 GKE 上运行,因此我们需要启用 GKE。前往“Kubernetes Engine”菜单,然后按“启用”按钮。
现在,您可以创建 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”(工件注册库)菜单,然后按“ENABLE”(启用)按钮。
片刻后,您将看到 GAR 的代码库浏览器。点击“CREATE REPOSITORY”(创建代码库)按钮,然后输入代码库的名称。
在此 Codelab 中,我将新代码库命名为 trace-codelab
。工件的格式为“Docker”,位置类型为“区域”。选择与您为 Google Compute Engine 设置的默认可用区相近的区域。例如,本示例在上文中选择了“us-central1-f”,因此我们在此处选择“us-central1(爱荷华州)”。然后,点击“创建”按钮。
现在,您会在代码库浏览器中看到“trace-codelab”。
我们稍后会返回此处检查注册表路径。
Skaffold 设置
在构建在 Kubernetes 上运行的微服务时,Skaffold 是一款非常实用的工具。它可以通过一小组命令处理应用容器的构建、推送和部署工作流。Skaffold 默认使用 Docker Registry 作为容器注册表,因此您需要配置 Skaffold,以便在将容器推送到 GAR 时识别 GAR。
再次打开 Cloud Shell,并确认是否已安装 skaffold。(Cloud Shell 会默认将 skaffold 安装到环境中。)运行以下命令,查看 skaffold 版本。
skaffold version
命令输出
v1.38.0
现在,您可以注册默认代码库以供 skaffold 使用。如需获取注册表路径,请前往 Artifact Registry 信息中心,然后点击您在上一步中刚刚设置的代码库的名称。
然后,您会在页面顶部看到面包屑导航路径。点击 图标将注册表路径复制到剪贴板。
点击“复制”按钮后,您会在浏览器底部看到一个对话框,其中显示如下消息:
已复制“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 以使用容器注册表
- 创建了用于运行本教程微服务的 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
- 清单: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
部署完成后,您会看到每个容器中发送到标准输出的实际应用日志,如下所示:
命令输出
[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 插桩
轨迹插桩和传播的概念
在修改源代码之前,我先通过一个简单的图表简要介绍一下分布式轨迹的运作方式。
在此示例中,我们将对代码进行插桩,以将轨迹和跨度信息导出到 Cloud Trace,并在从 loadgen 服务到服务器服务的请求中传播轨迹上下文。
应用需要发送跟踪元数据(例如跟踪 ID 和 Span ID),以便 Cloud Trace 将具有相同跟踪 ID 的所有 span 组装到一个跟踪记录中。此外,应用还需要在请求下游服务时传播跟踪上下文(父级 span 的跟踪 ID 和 span ID 的组合),以便下游服务知道自己正在处理哪个跟踪上下文。
OpenTelemetry 可帮助您:
- 以生成唯一的轨迹 ID 和 Span ID
- 将轨迹 ID 和 Span ID 导出到后端
- 将跟踪上下文传播到其他服务
- 嵌入有助于分析轨迹的额外元数据
OpenTelemetry Trace 中的组件
使用 OpenTelemetry 对应用轨迹进行插桩的流程如下:
- 创建导出工具
- 创建一个 TracerProvider,将导出器绑定到 1,并将其设为全局。
- 设置 TextMapPropagaror 以设置传播方法
- 从 TracerProvider 获取 Tracer
- 通过跟踪器生成 Span
目前,您无需了解每个组件的详细属性,但最重要的是记住以下几点:
- 此处的导出器可插接到 TracerProvider
- TracerProvider 包含与轨迹抽样和导出相关的所有配置
- 所有轨迹都捆绑在 Tracer 对象中
了解了这一点,我们接下来进行实际编码工作。
为第一个 span 添加指标
插桩负载生成器服务
按 Cloud Shell 右上角的 按钮,打开 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
函数中使用 net/http
在 HTTP 中调用客户端服务,因此我们为 net/http
使用 contrib 软件包,并使用 httptrace
和 otelhttp
软件包的扩展启用插桩。
首先添加一个软件包全局变量 httpClient,以便通过插桩客户端调用 HTTP 请求。
step0/src/loadgen/main.go
var httpClient = http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport) }
接下来,在 runQuery
函数中添加插桩,以使用 OpenTelemetry 和自定义 HTTP 客户端自动生成的 span 创建自定义 span。您需要执行以下操作:
- 使用
otel.Tracer()
从全局TracerProvider
获取跟踪器 - 使用
Tracer.Start()
方法创建根 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.mod
和 go.sum
。
go mod tidy
插桩客户服务
在上一部分中,我们对下图中红色矩形框中的部分进行了插桩。我们在负载生成器服务中插桩了跨度信息。与负载生成器服务类似,现在我们需要对客户端服务进行插桩。与负载生成器服务的不同之处在于,客户端服务需要从 HTTP 标头中提取从负载生成器服务传播的轨迹 ID 信息,并使用该 ID 生成 Span。
打开 Cloud Shell Editor,然后添加所需的软件包,就像为负载生成器服务添加软件包一样。
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 )
再次强调,我们需要设置 OpenTelemetry。只需复制并粘贴 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 }
现在,该对跨度进行插桩了。由于客户端服务需要接受来自 loadgen 服务的 HTTP 请求,因此需要插桩处理程序。客户端服务中的 HTTP 服务器是使用 net/http 实现的,您可以像在 loadgen 中所做的那样使用 otelhttp
软件包。
首先,我们将处理程序注册替换为 otelhttp
处理程序。在 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
方法开头到结尾的跨度。为了便于分析这些跨度,请向查询添加一个用于存储匹配计数的额外属性。在日志行前面,添加以下代码。
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 信息导出到标准输出。
后续步骤
在下一步中,您将插桩客户端服务和服务器服务,以确认如何通过 gRPC 传播轨迹上下文。
5. gRPC 插桩
在上一步中,我们在此微服务中插桩了请求的前半部分。在此步骤中,我们尝试对客户端服务和服务器服务之间的 gRPC 通信进行插桩。(下图中的绿色和紫色矩形)
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 拦截器,以便在客户端每次向服务器发出请求时插桩新的跨度。
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 和客户端服务设置 OpenTelemetry 类似。
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
现在,您再次在标准输出上看到了大量的 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 直观呈现轨迹,并了解如何分析收集的跨度。
6. 使用 Cloud Trace 可视化跟踪记录
您已使用 OpenTelemetry 在整个系统中插桩了跟踪记录。到目前为止,您已经学习了如何插桩 HTTP 和 gRPC 服务。虽然您已经了解了如何插桩,但尚未了解如何分析插桩数据。在本部分中,您将 stdout 导出器替换为 Cloud Trace 导出器,并了解如何分析轨迹。
使用 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
现在,您在标准输出上不会看到太多采用结构化日志格式的 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 控制台,然后前往“跟踪记录列表”。您可以通过搜索框轻松访问它。或者,您也可以点击左侧窗格中的菜单。
然后,您会看到延迟时间图表中分布着许多蓝色圆点。每个点代表一条轨迹。
点击其中一个,您就可以查看轨迹中的详细信息。
即使是通过这简单的快速浏览,您已经获得了许多数据洞见。例如,从瀑布图中,您可以看到延迟时间的主要原因是名为 shakesapp.ShakespeareService/GetMatchCount
的跨度。(请参阅上图中的 1)。您可以从摘要表格中确认这一点。(最右侧的列显示了每个时间段的持续时间。)此外,此轨迹是对“friend”查询的轨迹。(请参阅上图中的 2)
通过这些简短的分析,您可能意识到,需要了解 GetMatchCount
方法中更精细的范围。与标准输出信息相比,可视化功能非常强大。如需详细了解 Cloud Trace,请参阅我们的官方文档。
摘要
在此步骤中,您将标准输出导出器替换成了 Cloud Trace 导出器,并在 Cloud Trace 上直观呈现跟踪记录。此外,您还学习了如何开始分析轨迹。
后续步骤
在下一步中,您将修改服务器服务的源代码,以在 GetMatchCount 中添加子跨度。
7. 添加了子跨度,以便更好地进行分析
在前面的步骤中,您发现导致 loadgen 观察到的往返时间较长的原因主要是服务器服务中 GetMatchCount 方法(gRPC 处理程序)内的进程。不过,由于我们未对处理程序以外的任何内容进行插桩,因此无法从瀑布图中获取更多数据分析。这是我们开始插桩微服务时常见的情况。
在本部分中,我们将对服务器调用 Google Cloud Storage 的子跨度进行插桩,因为在该过程中,一些外部网络 I/O 通常会花费很长时间,因此请务必确定调用是否是原因。
在服务器中插桩子跨度
在服务器中打开 main.go
,然后找到 readFiles
函数。此函数会向 Google Cloud Storage 发出请求,以提取莎士比亚作品的所有文本文件。在此函数中,您可以创建子跨度,就像在客户端服务中为 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 ...
以上就是添加新 span 的全部内容。我们来运行一下应用,看看效果如何。
运行微服务并确认轨迹
修改完成后,只需使用 skaffold 命令照常运行集群即可。
skaffold dev
然后从轨迹列表中选择一个名为 query.request
的轨迹。您会看到类似的轨迹瀑布图,但 shakesapp.ShakespeareService/GetMatchCount
下会显示一个新的跨度。(下方以红色矩形标记的 span)
现在,您可以从此图表中看出,对 Google Cloud Storage 的外部调用占用了大量延迟时间,但仍有其他因素占据了大部分延迟时间。
只需查看几次跟踪记录瀑布图,您就已经获得了很多有价值的洞见。如何在应用中获取更多性能详情?这时就需要用到性能分析器了,但目前,我们先结束此 Codelab,并将所有性能分析器教程交给第 2 部分。
摘要
在此步骤中,您在服务器服务中插桩了另一个跨度,并获得了有关系统延迟时间的进一步数据分析。
8. 恭喜
您已成功使用 OpenTelemetry 创建分布式跟踪记录,并在 Google Cloud Trace 中确认了微服务中的请求延迟时间。
如需进行更深入的练习,您可以自行尝试以下主题。
- 当前实现会发送健康检查生成的所有跨度。(
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 和管理”>“设置”,然后点击“关闭”按钮。
然后,在对话框中的表单中输入项目 ID(而非项目名称),并确认关停。