1. 简介
上次更新日期:2022 年 7 月 15 日
应用的可观测性
可观测性和 OpenTelemetry
可观测性是用于描述系统特性的术语。具有可观测性的系统可让团队主动调试其系统。在这种情况下,可观测性的三大支柱:日志、指标和跟踪记录是系统获取可观测性的基本插桩。
OpenTelemetry 是一组规范、库和代理,可加快可观测性所需的遥测数据(日志、指标和跟踪记录)的插桩和导出。OpenTelemetry 是 CNCF 下的开放标准和社区驱动项目。利用项目及其生态系统提供的库,开发者能够以与供应商无关的方式针对多种架构检测其应用。
此外,除了可观测性的三大支柱之外,持续剖析也是可观测性的另一个关键组成部分,并且正在扩大业内用户群。Cloud Profiler 是其发起者之一,可让您通过一个简单的界面深入了解应用调用堆栈中的性能指标。
此 Codelab 是该系列的第 1 部分,介绍了如何使用 OpenTelemetry 和 Cloud Trace 在微服务中对分布式跟踪记录进行插桩。第 2 部分将介绍使用 Cloud Profiler 进行持续分析。
分布式跟踪
在日志、指标和跟踪记录中,跟踪记录是一种遥测数据,可了解系统中进程特定部分的延迟时间。特别是在微服务时代,分布式跟踪记录是找出整个分布式系统中延迟瓶颈的有力驱动因素。
在分析分布式跟踪记录时,跟踪记录数据可视化是一目了然地了解整体系统延迟时间的关键。在分布式跟踪记录中,我们会处理一组调用,以采用包含多个 Span 的 Trace 形式处理对系统入口点的单个请求。
Span 表示在分布式系统中完成的单个工作单元,记录开始和结束时间。span 通常彼此之间存在分层关系,在下图中,所有较小的 span 都是较大 /messages span 的子 span,并组合成一个跟踪记录,显示系统的工作路径。
Google Cloud Trace 是分布式跟踪后端的可选方案之一,与 Google Cloud 中的其他产品完美集成。
构建内容
在此 Codelab 中,您将在名为“Shakespeare application”的服务中对轨迹信息进行插桩(也称为 Shakesapp)。Shakesapp 的架构如下所述:
- Loadgen 使用 HTTP 将查询字符串发送到客户端
- 客户端将查询从 loadgen 传递到 gRPC 中的服务器
- 服务器接受来自客户端的查询,从 Google Cloud Storage 提取所有 Shakespare 作品(文本格式),搜索包含该查询的行,并返回与客户端匹配的行号
您将在整个请求中检测跟踪信息。之后,您需要在服务器中嵌入一个性能分析器代理,并调查瓶颈问题。
学习内容
- 如何在 Go 项目中开始使用 OpenTelemetry Trace 库
- 如何使用库创建 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 集群,在 GKE 上运行 Shakesapp,因此需要启用 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 集群了。接下来,我们准备用于推送和部署容器的 Container Registry。对于这些步骤,我们需要设置 Artifact Registry (GAR) 和 Skaffold 才能使用它。
Artifact Registry 设置
前往“Artifact Registry”菜单并按“启用”按钮。
片刻之后,您会看到 GAR 的代码库浏览器。点击“CREATE REPOSITORY”并输入代码库的名称
在此 Codelab 中,我将新代码库命名为 trace-codelab
。工件的格式为“Docker”地理位置类型为“区域”选择靠近您为 Google Compute Engine 默认可用区设置的区域。例如,此示例选择了“us-central1-f”所以我们在这里选择“us-central1(爱荷华)”。然后点击“创建”按钮。
现在可以看到“trace-codelab”。
我们稍后将返回此处来检查注册表路径。
Skaffold 设置
在构建在 Kubernetes 上运行的微服务时,Skaffold 是一个方便的工具。它使用一小组命令来处理构建、推送和部署应用容器的工作流。默认情况下,Skaffold 使用 Docker Registry 作为 Container Registry,因此您需要配置 Skaffold 以在推送容器时识别 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
- 为 Container Registry 创建了 Artifact Registry 代码库
- 设置 Skaffold 以使用 Container Registry
- 创建一个 Kubernetes 集群并在其中运行 Codelab 微服务
后续步骤
在下一步中,您将构建、推送微服务并将其部署到集群
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
部署后,您会看到每个容器中发送到 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 的插桩
跟踪记录插桩和传播的概念
在修改源代码之前,我先用简单的图表简要说明分布式跟踪记录的工作原理。
在此示例中,我们将对代码进行插桩 (instrument),以将 Trace 和 Span 信息导出到 Cloud Trace,并在从 loadgen 服务发出的请求中将轨迹上下文传播到服务器服务。
应用需要发送跟踪元数据(如跟踪 ID 和 Span ID),以便 Cloud Trace 将具有相同跟踪 ID 的所有 span 整合到一个跟踪中。此外,应用需要在请求下游服务时传播跟踪上下文(跟踪 ID 和父 span 的 Span ID 的组合),以便应用了解其正在处理的跟踪上下文。
OpenTelemetry 可帮助您:
- 生成唯一的跟踪记录 ID 和 Span ID
- 将跟踪记录 ID 和 Span ID 导出到后端
- 将跟踪记录上下文传播到其他服务
- 嵌入有助于分析跟踪记录的额外元数据
OpenTelemetry Trace 中的组件
使用 OpenTelemetry 对应用跟踪记录进行插桩的过程如下:
- 创建导出器
- 创建一个 TracerProvider,绑定 1 中的导出器,并将其设置为全局。
- 设置 TextMapPropagaror 以设置传播方法
- 从 TracerProvider 获取跟踪器
- 通过跟踪器生成 Span
到目前为止,您无需了解每个组件中的详细属性,但要记住的最重要的一点是:
- 此处的导出器可以插入 TracerProvider
- TracerProvider 包含有关跟踪记录采样和导出的所有配置
- 所有跟踪记录都捆绑在 Tracer 对象中
了解了这一点后,我们接下来实际编码工作。
乐器第一个跨度
插桩加载生成器服务
按 Cloud Shell 右上角的 按钮,打开 Cloud Shell Editor。从左侧窗格中的 Explorer 中打开 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
导出器,以结构化格式将所有轨迹信息导出到 stdout。
然后,从 main 函数调用它。调用 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 创建自定义 span,并从自定义 HTTP 客户端自动生成 span。您需要做的是:
- 使用
otel.Tracer()
从全局TracerProvider
获取跟踪器 - 使用
Tracer.Start()
方法创建根 span - 在任意时间结束根 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
付款客户端服务
在上一部分中,我们对下图中红色矩形内包围的部分进行了插桩处理。我们在加载生成器服务中对 span 信息进行了插桩处理。与加载生成器服务类似,现在我们需要对客户端服务进行插桩。与加载生成器服务的不同之处在于,客户端服务需要在 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 )
同样,我们需要设置 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 }
现在该对跨度进行插桩了。由于客户端服务需要接受来自 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
方法开头到结尾的 span。为使 span 易于分析,请添加一个额外属性,以将匹配计数存储到查询中。在日志行之前,添加以下代码。
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。
后续步骤
在下一步中,您将对客户端服务和服务器服务进行插桩 (instrument),以确认如何通过 gRPC 传播跟踪上下文。
5. 适用于 gRPC 的插桩
在上一步中,我们在此微服务中对请求的前半部分进行了插桩测试。在此步骤中,我们将尝试对客户端服务和服务器服务之间的 gRPC 通信进行插桩。(下图中的绿色和紫色矩形框)
适用于 gRPC 客户端的预构建插桩
OpenTelemetry 的生态系统提供了许多方便开发者检测应用的库。在上一步中,我们对 net/http
软件包使用了预构建插桩。在此步骤中,由于我们要尝试通过 gRPC 传播 Trace Context,因此需要使用该库。
首先,导入名为 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 服务器调用预构建的插桩。将新软件包添加到 import 部分,例如:
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 拦截器生成了它们。
摘要
在此步骤中,您通过 OpenTelemetry 生态系统库提供的支持对基于 gRPC 的通信进行了插桩测试。
后续步骤
在下一步中,您最终会使用 Cloud Trace 直观呈现跟踪记录,并学习如何分析收集的 span。
6. 使用 Cloud Trace 直观呈现跟踪记录
您已使用 OpenTelemetry 在整个系统中进行了轨迹插桩。到目前为止,您已经学习了如何对 HTTP 和 gRPC 服务进行插桩。尽管您已经学会如何检测它们,但却没有学会如何分析它们。在本部分中,您需要将 stdout 导出器替换为 Cloud Trace 导出器,并学习如何分析您的跟踪记录。
使用 Cloud Trace 导出器
OpenTelemetry 的强大功能之一是其可插入性。如需直观呈现插桩收集的所有 span,您只需将 stdout 导出器替换为 Cloud Trace 导出器即可。
打开每项服务的 main.go
文件,并找到 initTracer()
函数。删除用于生成 stdout 导出器的行,然后改为创建 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 控制台,然后前往“跟踪记录列表”。您可通过搜索框轻松访问。否则,您可以点击左侧窗格中的菜单。
然后,您会看到延迟图上分布了许多蓝点。每个点表示一个跟踪记录。
点击其中一个,您可以在跟踪记录中看到详细信息。
即便只是简单地看一看,您已经掌握了许多数据洞见。例如,从瀑布图中可以看到,导致延迟的原因主要是名为 shakesapp.ShakespeareService/GetMatchCount
的 span。(见上图中的 1)您可以通过摘要表进行确认。(最右侧的列显示每个 span 的时长。)此外,此跟踪记录用于查询“friend”。(见上图中的 2 项)
鉴于这些简短的分析,您可能会意识到,您需要在 GetMatchCount
方法中了解更精细的 span。与 stdout 信息相比,可视化功能更强大。如需详细了解 Cloud Trace,请参阅我们的官方文档。
摘要
在此步骤中,您将 stdout 导出器替换为 Cloud Trace 导出器,并在 Cloud Trace 上直观呈现轨迹。此外,您还学习了如何开始分析跟踪记录。
后续步骤
在下一步中,您将修改服务器服务的源代码,以在 GetMatchCount 中添加子 span。
7. 添加子 span,以便更好地进行分析
在上一步中,您发现从 loadgen 观察到的往返时间主要是服务器服务中 GetMatchCount 方法(gRPC 处理程序)内的进程。但是,由于我们没有对除处理程序以外的其他内容进行插桩,因此无法从广告瀑布流图中找到更多数据洞见。当我们开始对微服务进行插桩时,这是一种常见的情况。
在本部分中,我们将对服务器调用 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 ...
以上就是添加新 span 的全部内容。让我们通过运行应用来看看效果如何。
运行微服务并确认跟踪记录
修改后,只需照常使用 skaffold 命令运行集群即可。
skaffold dev
然后,从跟踪记录列表中选择一个名为 query.request
的跟踪记录。除了 shakesapp.ShakespeareService/GetMatchCount
下的新 span 之外,您将看到类似的轨迹瀑布图。(下方红色矩形包围的跨度)
现在,您可以从此图中看出,对 Google Cloud Storage 的外部调用占用了大量延迟时间,但其他因素导致了大部分延迟时间。
仅仅从跟踪记录瀑布图中的几次查看中,您已经获得了很多数据洞见。您如何在应用中获取更多性能详情?性能分析器就派上用场了,但现在,我们先将此 Codelab 作为结尾,并将所有性能分析器教程委托给第 2 部分。
摘要
在此步骤中,您在服务器服务中对另一个 span 进行插桩,并获得了关于系统延迟时间的进一步数据分析。
8. 恭喜
您已成功使用 OpenTelemery 创建了分布式跟踪记录,并在 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 &管理员”>“设置”,然后点击“关闭”按钮。
然后在对话框的表单中输入项目 ID(而非项目名称),并确认关闭。