Go Profiling 教程:以优化 VictoriaMetrics 为例

VictoriaMetrics 是一个全球化的团队,我们的同事居家办公于世界各地,所以大部分技术交流都在线上进行。这篇博客是团队内一次 Go Profiling 教学的实录,来自于 Aliaksandr Valialkin(GitHub ID: valyala)。

VictoriaMetrics 开发者笔记 说到,我们调研以 VictoriaLogs 为基础构建 Distributed Tracing 存储,效果还算理想,不过它的性能还没有经过任何优化。

现在,我们以 commit 42c931b 为基础,介绍如何对它进行 Profiling 和优化。截至这个 commit,VictoriaLogs 已经提供了 /insert/opentelemetry/v1/traces 接口,接收 OTLP 请求,写入 Traces 数据。我们的目标是对这个接口的性能进行优化。

这会是一次完全可重复、可实操的课程,我们欢迎读者坐到电脑前,动起手来一起进行尝试。

1. Preparation

我们假定你已经有一台带有 Go 开发环境的电脑,并且已经有准备好了 pprof。你可以通过如下命令检查它们:

# jiekun@jiekun:~/repo$ go version
go version go1.24.4 linux/amd64
# jiekun@jiekun:~/repo$ go tool | grep pprof
pprof

接下来,Clone VictoriaMetrics 项目,切换到待优化的 commit,并尝试启动 VictoriaLogs:

# jiekun@jiekun:~/repo$ git clone https://github.com/VictoriaMetrics/VictoriaMetrics.git
# jiekun@jiekun:~/repo$ cd VictoriaMetrics
# jiekun@jiekun:~/repo/VictoriaMetrics$ git checkout 42c931b8cca12964cb8d7269c59a8756ee9b21cd

# jiekun@jiekun:~/repo/VictoriaMetrics$ go run app/victoria-logs/main.go

如果一切顺利,你将会看到一些启动的日志输出,显示它监听在 :9428 端口,并提示你它的 pprof 地址:

2025-06-22T05:47:15.818Z        info    lib/httpserver/httpserver.go:144        started server at http://0.0.0.0:9428/
2025-06-22T05:47:15.818Z        info    lib/httpserver/httpserver.go:146        pprof handlers are exposed at http://0.0.0.0:9428/debug/pprof/

最后,还需要准备一个 Traces 数据的生成器,给 VictoriaLogs 施加压力。在这一步我们有很多选择,包括:

这里我们选择使用 tracegen,它的运行需要 Docker,这个 docker-compose.yaml 将与宿主机共用网络运行 tracegen,并且启用 4 个 Worker 持续 5 分钟向 VictoriaLogs 发送 Traces 数据。

services:
  tracegen:
    network_mode: "host"
    image: jaegertracing/jaeger-tracegen:latest@sha256:5beb38414f914a1894e72997dc2da491a32590a8e4bb59a29e9c3e1e182a7bc4
    environment:
      - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:9428/insert/opentelemetry/v1/traces
    command: ["-duration", "300s", "-workers", "4", "-pause", "1ms"]

在执行 docker compose up 后,如果没有意外,你将能从 VictoriaLogs 的 UI 上观察到有数据涌入:

准备阶段到此就告一段落了,接下来我们开始采集 Profiles 进行分析。

2. Profiling & Optimization

我们需要通过 pprof 接口获取 heapprofile,其中,heap 是实时的内存分配情况,而 profile 指的是 cpu profile,是描述一段时间内 cpu 都在做些什么。

我们应该确保 VictoriaLogs 正在接收来自 tracegen 的请求,然后采集 profile,在 profile 结束时再采集瞬时的 heap:

# jiekun@jiekun:~/repo$ curl http://127.0.0.1:9428/debug/pprof/profile > cpu.profile
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10524    0 10524    0     0    350      0 --:--:--  0:00:30 --:--:--  2647

# jiekun@jiekun:~/repo$ curl http://127.0.0.1:9428/debug/pprof/heap > mem.heap
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 29896    0 29896    0     0  9581k      0 --:--:-- --:--:-- --:--:-- 9731k

接下来我们看看这些文件都能给我们什么提示,先从 cpu profile 开始。执行 pprof cpu.profile,然后输入 top 回车:

# jiekun@jiekun:~/repo$ pprof cpu.profile 
File: main
Build ID: e5619416752e0f6f0534ed55ecd5e80c2877a7a7
Type: cpu
Time: 2025-06-22 15:12:28 CST
Duration: 30.07s, Total samples = 5.05s (16.80%)
Entering interactive mode (type "help" for commands, "o" for options)

# (pprof) top
Showing nodes accounting for 2410ms, 47.72% of 5050ms total
Dropped 205 nodes (cum <= 25.25ms)
Showing top 10 nodes out of 233
      flat  flat%   sum%        cum   cum%
     560ms 11.09% 11.09%      580ms 11.49%  runtime.cgocall
     500ms  9.90% 20.99%      500ms  9.90%  runtime.memclrNoHeapPointers
     290ms  5.74% 26.73%      290ms  5.74%  runtime.memmove
     240ms  4.75% 31.49%      760ms 15.05%  runtime.scanobject
     170ms  3.37% 34.85%      230ms  4.55%  runtime.typePointers.next
     160ms  3.17% 38.02%      160ms  3.17%  github.com/cespare/xxhash/v2.Sum64
     150ms  2.97% 40.99%      200ms  3.96%  runtime.findObject
     130ms  2.57% 43.56%      130ms  2.57%  runtime.madvise
     110ms  2.18% 45.74%      110ms  2.18%  internal/runtime/syscall.Syscall6
     100ms  1.98% 47.72%      100ms  1.98%  runtime.nextFreeFast
  • flat: 在这个 function 中执行所耗费的时间。
  • cum: 在这个 function 及其调用的 function 中执行所耗费的总时间。

我们可以看到,没有占用大量时间的 low-hanging fruit(容易实现的目标),也就是想通过优化一个 function 就能大幅提升性能是不太可能的。出现最频繁的是 runtime.*,而不是应用的逻辑代码。这说明应用的逻辑已经写得很完美,无须优化。

开玩笑的,我们继续来解释怎么理解这些数字。runtime.cgocallflat% 是 11.09%,说明它的执行时间占了总样本时间(5.05s)的 11.09%。(理论上)如果我们把这个方法优化到 0%,那恭喜你,CPU 使用率最多可以降低 11%。

runtime.cgocall 之下的 4 行,是与垃圾回收相关的内容,可以看到它们一共占了 9.9% + 5.74% + 4.75% + 3.37% = 23.76% 时间,类似地,表示优化最多可以使 CPU 使用率降低 23%。

      flat  flat%   sum%        cum   cum%
     500ms  9.90% 20.99%      500ms  9.90%  runtime.memclrNoHeapPointers
     290ms  5.74% 26.73%      290ms  5.74%  runtime.memmove
     240ms  4.75% 31.49%      760ms 15.05%  runtime.scanobject
     170ms  3.37% 34.85%      230ms  4.55%  runtime.typePointers.next

// 施工中