为在 Go 中实现更出色的应用性能而进行插桩 (instrument)(第 2 部分:性能分析器)

1. 简介

e0509e8a07ad5537.png

上次更新日期:2022 年 7 月 14 日

应用的可观测性

可观测性和持续分析器

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

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

此 Codelab 是该系列教程的第 2 部分,介绍了如何检测持续分析器代理。第 1 部分介绍了如何使用 OpenTelemetry 和 Cloud Trace 进行分布式跟踪,您将通过第 1 部分更好地了解如何识别微服务的瓶颈。

构建内容

在本 Codelab 中,您将对在 Google Kubernetes Engine 集群上运行的“莎士比亚应用”(又称 Shakesapp)的服务器服务中的持续分析器代理进行插桩。Shakesapp 的架构如下所述:

44e243182ced442f.png

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

在第 1 部分中,您发现瓶颈存在于服务器服务中的某个位置,但无法确定确切原因。

学习内容

  • 如何嵌入 Profiler 代理
  • 如何使用 Cloud Profiler 调查瓶颈

此 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 集群

后续步骤

在下一步中,您将在服务器服务中对持续性能分析器 Agent 进行插桩。

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 中,您将更新位于 step4 文件夹下的源代码。您还可以参考 step[1-6] 文件夹中的源代码,了解从一开始所做的更改。(第 1 部分涵盖第 0 步到第 4 步,第 2 部分涵盖第 5 步和第 6 步)

运行 skaffold 命令

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

cd step4
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. Cloud Profiler 代理的插桩

持续分析的概念

在解释持续性能剖析的概念之前,我们需要先了解性能剖析的概念。性能剖析是动态分析应用(动态程序分析)的方法之一,通常在应用开发过程中进行,例如在负载测试期间进行。这是一项一次性活动,用于衡量特定时间段内的系统指标,例如 CPU 和内存用量。收集配置文件数据后,开发者会在代码之外对其进行分析。

持续性能剖析是常规性能剖析的扩展方法:它会定期针对长时间运行的应用运行短时间窗口性能剖析,并收集大量性能剖析数据。然后,它会根据应用的某个属性(例如版本号、部署区域、测量时间等)自动生成统计分析。如需详细了解此概念,请参阅我们的文档

由于目标是正在运行的应用,因此可以定期收集性能剖析数据,并将其发送到某个后端以对统计数据进行后处理。这是 Cloud Profiler 代理,您很快就会将其嵌入到服务器服务中。

嵌入 Cloud Profiler 代理

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

step4/src/server/main.go

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

        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)
        healthpb.RegisterHealthServer(srv, svc)
        if err := srv.Serve(lis); err != nil {
                log.Fatalf("error serving server: %v", err)
        }
}

main 函数中,您会看到一些针对 OpenTelemetry 和 gRPC 的设置代码,这些代码已在 Codelab 第 1 部分中完成。现在,您将在此处为 Cloud Profiler 代理添加插桩。与我们为 initTracer() 所做的一样,您可以编写一个名为 initProfiler() 的函数,以提高可读性。

step4/src/server/main.go

import (
        ...
        "cloud.google.com/go/profiler" // step5. add profiler package
        "cloud.google.com/go/storage"
        ...
)

// step5: add Profiler initializer
func initProfiler() {
        cfg := profiler.Config{
                Service:              "server",
                ServiceVersion:       "1.0.0",
                NoHeapProfiling:      true,
                NoAllocProfiling:     true,
                NoGoroutineProfiling: true,
                NoCPUProfiling:       false,
        }
        if err := profiler.Start(cfg); err != nil {
                log.Fatalf("failed to launch profiler agent: %v", err)
        }
}

我们来仔细看看 profiler.Config{} 对象中指定的选项。

  • 服务:您可以在分析器信息中心内选择和切换的服务名称
  • ServiceVersion:服务版本名称。您可以根据此值比较性能剖析数据集。
  • NoHeapProfiling:停用内存消耗分析
  • NoAllocProfiling:停用内存分配分析
  • NoGoroutineProfiling:停用 goroutine 性能分析
  • NoCPUProfiling:停用 CPU 性能分析

在此 Codelab 中,我们仅启用 CPU 分析。

现在,您只需在 main 函数中调用此函数即可。请务必在导入块中导入 Cloud Profiler 软件包。

step4/src/server/main.go

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

        // step5. start profiler
        go initProfiler()
        // step5. end

        svc := NewServerService()
        // step2: add interceptor
        ...
}

请注意,您正在使用 go 关键字调用 initProfiler() 函数。由于 profiler.Start() 会阻塞,因此您需要在另一个 goroutine 中运行它。现在,它已准备好进行构建。请务必在部署之前运行 go mod tidy

go mod tidy

现在,使用新的服务器服务部署集群。

skaffold dev

通常,您只需等待几分钟,即可在 Cloud Profiler 上看到火焰图。在顶部的搜索框中输入“分析器”,然后点击分析器的图标。

3d8ca8a64b267a40.png

然后,您将看到以下火焰图。

7f80797dddc0128d.png

摘要

在此步骤中,您将 Cloud Profiler 代理嵌入到服务器服务中,并确认它会生成火焰图。

后续步骤

在下一步中,您将使用火焰图调查应用中出现瓶颈的原因。

5. 分析 Cloud Profiler 火焰图

什么是火焰图?

火焰图是可视化性能剖析数据的一种方式。如需详细说明,请参阅我们的文档,但简要总结如下:

  • 每个条形都表示应用中的方法/函数调用
  • 纵向是调用堆栈;调用堆栈从上到下增长
  • 横向表示资源使用情况;越长表示情况越糟糕。

鉴于此,我们来看看获得的火焰图。

7f80797dddc0128d.png

分析火焰图

在上一部分中,您了解到火焰图中的每个条形都表示函数/方法调用,而条形的长度则表示函数/方法中的资源用量。Cloud Profiler 的火焰图会按降序或从左到右的长度对条形进行排序,您可以先查看图表的左上角。

6d90760c6c1183cd.png

在本例中,很明显 grpc.(*Server).serveStreams.func1.2 消耗了大部分 CPU 时间,并且通过从上到下查看调用堆栈,发现大部分时间都花费在 main.(*serverService).GetMatchCount 中,这是服务器服务中的 gRPC 服务器处理程序。

在 GetMatchCount 下,您会看到一系列 regexp 函数:regexp.MatchStringregexp.Compile。它们来自标准软件包,也就是说,它们应该从多个角度(包括性能)经过了充分的测试。但此结果显示,CPU 时间资源使用率在 regexp.MatchStringregexp.Compile 中较高。鉴于上述事实,我们在此假设使用 regexp.MatchString 与性能问题有关。因此,我们来读取使用该函数的源代码。

step4/src/server/main.go

func (s *serverService) GetMatchCount(ctx context.Context, req *shakesapp.ShakespeareRequest) (*shakesapp.ShakespeareResponse, error) {
        resp := &shakesapp.ShakespeareResponse{}
        texts, err := readFiles(ctx, bucketName, bucketPrefix)
        if err != nil {
                return resp, fmt.Errorf("fails to read files: %s", err)
        }
        for _, text := range texts {
                for _, line := range strings.Split(text, "\n") {
                        line, query := strings.ToLower(line), strings.ToLower(req.Query)
                        isMatch, err := regexp.MatchString(query, line)
                        if err != nil {
                                return resp, err
                        }
                        if isMatch {
                                resp.MatchCount++
                        }
                }
        }
        return resp, nil
}

这是调用 regexp.MatchString 的位置。通过阅读源代码,您可能会注意到该函数是在嵌套的 for 循环内调用的。因此,此函数的使用可能不正确。我们来查找 regexp 的 GoDoc。

80b8a4ba1931ff7b.png

根据该文档,regexp.MatchString 会在每次调用时编译正则表达式模式。因此,资源消耗量过大的原因如下。

摘要

在此步骤中,您通过分析火焰图,对资源消耗的原因做出了假设。

后续步骤

在下一步中,您将更新服务器服务的源代码,并确认版本已从 1.0.0 更改。

6. 更新源代码并比较火焰图

更新源代码

在上一步中,您假设 regexp.MatchString 的使用与大量资源消耗有关。因此,我们来解决这个问题。打开代码并稍微更改该部分。

step4/src/server/main.go

func (s *serverService) GetMatchCount(ctx context.Context, req *shakesapp.ShakespeareRequest) (*shakesapp.ShakespeareResponse, error) {
        resp := &shakesapp.ShakespeareResponse{}
        texts, err := readFiles(ctx, bucketName, bucketPrefix)
        if err != nil {
                return resp, fmt.Errorf("fails to read files: %s", err)
        }

        // step6. considered the process carefully and naively tuned up by extracting
        // regexp pattern compile process out of for loop.
        query := strings.ToLower(req.Query)
        re := regexp.MustCompile(query)
        for _, text := range texts {
                for _, line := range strings.Split(text, "\n") {
                        line = strings.ToLower(line)
                        isMatch := re.MatchString(line)
                        // step6. done replacing regexp with strings
                        if isMatch {
                                resp.MatchCount++
                        }
                }
        }
        return resp, nil
}

如您所见,现在,正则表达式模式编译过程已从 regexp.MatchString 中提取出来,并移出了嵌套的 for 循环。

在部署此代码之前,请务必更新 initProfiler() 函数中的版本字符串。

step4/src/server/main.go

func initProfiler() {
        cfg := profiler.Config{
                Service:              "server",
                ServiceVersion:       "1.1.0", // step6. update version
                NoHeapProfiling:      true,
                NoAllocProfiling:     true,
                NoGoroutineProfiling: true,
                NoCPUProfiling:       false,
        }
        if err := profiler.Start(cfg); err != nil {
                log.Fatalf("failed to launch profiler agent: %v", err)
        }
}

现在,我们来看看它的运作方式。使用 skaffold 命令部署集群。

skaffold dev

过一段时间后,重新加载 Cloud Profiler 信息中心,看看效果如何。

283cfcd4c13716ad.png

请务必将版本更改为 "1.1.0",以便仅查看 1.1.0 版中的配置文件。如图所示,GetMatchCount 的条形图长度缩短了,CPU 时间的使用率(即条形图)也降低了。

e3a1456b4aada9a5.png

您不仅可以查看单个版本的火焰图,还可以比较两个版本之间的差异。

841dec77d8ba5595.png

将“比较对象”下拉列表的值更改为“版本”,并将“比较版本”的值更改为“1.0.0”(原始版本)。

5553844292d6a537.png

您将看到如下所示的火焰图。图表的形状与 1.1.0 相同,但颜色不同。在比较模式下,颜色的含义如下:

  • 蓝色:减少的值(资源消耗)
  • 橙色:获得的价值(资源消耗)
  • 灰色:中性

根据图例,我们来详细了解一下该函数。点击要放大的条形,即可查看堆叠中的更多详细信息。请点击 main.(*serverService).GetMatchCount 栏。此外,将光标悬停在柱状图上,您还可以查看比较详情。

ca08d942dc1e2502.png

它表示总 CPU 时间从 5.26 秒减少到 2.88 秒(总时间为 10 秒,即采样窗口)。这方面有了巨大改进!

现在,您可以根据分析的配置文件数据来提高应用性能。

摘要

在此步骤中,您在服务器服务中进行了修改,并确认了 Cloud Profiler 比较模式的改进。

后续步骤

在下一步中,您将更新服务器服务的源代码,并确认版本已从 1.0.0 更改。

7. 额外步骤:确认轨迹瀑布图中的改进

分布式跟踪与持续分析之间的区别

在 Codelab 的第 1 部分中,您确认了自己可以找出请求路径中各个微服务的瓶颈服务,但无法找出特定服务中瓶颈的确切原因。在此 Codelab 的第 2 部分中,您了解到持续分析可让您通过调用堆栈识别单个服务中的瓶颈。

在此步骤中,我们来查看分布式跟踪记录 (Cloud Trace) 中的瀑布图,并了解它与持续分析的不同之处。

此瀑布图显示的是包含查询“love”的轨迹。总共耗时约 6.7 秒(6700 毫秒)。

e2b7dec25926ee51.png

这是同一查询在改进后的结果。如您所见,总延迟时间现在为 1.5 秒(1500 毫秒),与之前的实现相比有了巨大改进。

feeb7207f36c7e5e.png

这里的重要一点是,在分布式轨迹瀑布图中,除非您在各处进行插桩 span,否则无法获取调用堆栈信息。此外,分布式轨迹仅关注服务之间的延迟,而持续分析则侧重于单个服务的计算机资源(CPU、内存、操作系统线程)。

另一方面,分布式轨迹是基于事件的,而持续分析是基于统计的。每条轨迹都有不同的延迟时间图,您需要使用其他格式(例如分布图)来了解延迟时间变化的趋势。

摘要

在此步骤中,您检查了分布式轨迹与持续分析之间的区别。

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(而非项目名称),并确认关停。