[AI生成] gops使用trace分析go应用为什么接口比较慢
案例背景
线上有一个订单列表接口 /api/orders,响应时间超过 5秒,而同类型的其他接口只需要几十毫秒。我们需要用 gops trace 来定位到底慢在哪里。
一、准备工作
1.1 启动慢接口服务
cd gops-trace-demo go run main.go
服务启动后会监听两个端口:
• :8080 — 业务接口
• :6060 — pprof 调试端口
1.2 验证接口确实慢
# 慢接口 curl -s http://localhost:8080/api/orders | jq '.duration' # 预期输出:5000ms 左右 # 快接口(优化后版本,用来对比) curl -s http://localhost:8080/api/orders/fast | jq '.duration' # 预期输出:几毫秒
1.3 确认 gops 可用
# 查看进程列表,找到我们的服务 gops # 输出类似: # 12345 main go1.25.0 .../gops-trace-demo
二、用 gops trace 抓trace
2.1 先压测,再抓 trace
trace 需要在有请求的情况下抓才有意义。开两个终端:
终端 1:持续压测慢接口
while true; do curl -s http://localhost:8080/api/orders > /dev/null; done
终端 2:抓取 trace
# 抓取 5 秒的 trace(生产环境建议 2~10 秒) gops trace <PID> 5s
抓完后会自动保存到一个临时文件,并输出文件路径,类似:
Trace dump saved to: /tmp/trace123456789
2.2 用 go tool trace 打开
go tool trace /tmp/trace123456789
浏览器会自动打开 trace 分析页面,主要看这几个视图:
|
View trace |
页面第一个链接 |
看时间线全貌,Goroutine 调度、GC 事件 |
|---|---|---|
|
Goroutine analysis |
第二个链接 |
看每个 Goroutine 的生命周期和耗时 |
|
Network/Sync block profile |
下方链接 |
看网络阻塞、同步阻塞的热点 |
|
Minimum mutator utilization |
MMU |
看 GC 对服务的影响 |
2.3 trace 首页概览
打开 go tool trace 后首先看到的是首页,列出了所有可用的分析视图:

图1:trace 首页概览
三、逐步分析慢接口的问题
我们的慢接口有 3 个典型的性能问题,用 trace 一步步定位。
问题 1:大量 syscall 时间 —— queryOrdersSlow
现象
打开 View trace,把时间线拉宽,会看到:
• 每个请求周期里,Goroutine 有大量时间处于 Waiting 状态
• Proc 上有很多空白(没有在跑用户代码)
• 看 Network/Syscall 时间占比很高

图2:View trace 时间线全貌
定位方法
• 在 Goroutine analysis 中,找到处理 HTTP 请求的 goroutine
• 点击进去看它的时间分布
• 找到 syscall.Sleep 占了大量时间
根因
queryOrdersSlow 函数里,每生成一条订单就 sleep 2ms,2000 条就是 4000ms+ 的纯等待:
func queryOrdersSlow(count int) []Order {
orders := make([]Order, 0, count)
for i := 0; i < count; i++ {
time.Sleep(2 * time.Millisecond) // 模拟慢查询
orders = append(orders, ...)
}
return orders
}
实际生产中,这通常对应着慢 SQL 查询、串行调用多个外部接口等问题。
优化思路
• SQL 加索引 / 改批量查询
• 串行接口调用改成并发(errgroup)
• 加缓存
问题 2:Goroutine 调度开销大 —— processOrdersSlow
现象
在 View trace 里观察到:
• 瞬间创建了大量 Goroutine(2000+)
• Proc 在不同 Goroutine 之间频繁切换
• 调度延迟(scheduling latency) 很高
• runtime.gopark / runtime.mstart 调用频繁
定位方法
• Goroutine analysis 看 goroutine 数量峰值
• Network/Sync block profile 看 sync.WaitGroup.Wait 的阻塞时间
• 看单个请求的 goroutine 生命周期,发现创建了 2000 个 goroutine 但每个只跑几微秒

图3:Goroutine analysis 页面
根因
processOrdersSlow 对每条订单起一个 goroutine,但每个 goroutine 做的事情极少(只是转大写),创建和调度 goroutine 的开销远大于实际工作量:
func processOrdersSlow(orders []Order) []Order {
var wg sync.WaitGroup
results := make([]Order, len(orders))
for i, order := range orders {
wg.Add(1)
go func(idx int, o Order) { // 2000个goroutine
defer wg.Done()
runtime.Gosched() // 主动让出CPU,增加调度开销
runtime.Gosched()
runtime.Gosched()
_ = strings.ToUpper(o.Status)
results[idx] = o
}(i, order)
}
wg.Wait()
return results
}
优化思路
• 任务量小的时候不要用 goroutine,串行执行更快
• 真需要并发,用 worker pool 控制并发数,而不是每个任务起一个 goroutine
• 2000 条订单的状态转换,直接 for 循环做,比 goroutine 快几个数量级
优化后的代码(快 100 倍以上):
func processOrdersFast(orders []Order) []Order {
results := make([]Order, len(orders))
for i, order := range orders {
order.Status = strings.ToUpper(order.Status)
results[i] = order
}
return results
}
问题 3:O(n²) 的内存分配 —— buildResponseSlow
现象
trace 中观察到:
• GC 事件非常频繁(几毫秒一次)
• 每次 GC 都有 STW(虽然很短,但频率高了也会影响延迟)
• Heap profile 显示大量的 slice 分配
• runtime.mallocgc 占 CPU 比例高
定位方法
• 在 View trace 中看 GC events(黄色竖线),是否过于密集
• Minimum mutator utilization 图如果有明显低谷,说明 GC 影响较大
• 结合 pprof heap profile 看分配热点

图4:Minimum Mutator Utilization 图
根因
buildResponseSlow 每次 append 都重新分配一个更大的 slice 并 copy,时间复杂度 O(n²),产生大量临时对象:
func buildResponseSlow(orders []Order) []Order {
var result []Order
for _, o := range orders {
temp := make([]Order, len(result)+1) // 每次都重新分配
copy(temp, result)
temp[len(result)] = o
result = temp
}
return result
}
2000 条数据就要分配 2000 次、copy 2000 次,总 copy 量是 1+2+3+...+2000 ≈ 200 万条。
优化思路
• 预分配 slice 容量:make([]Order, 0, len(orders))
• 直接用原 slice 或返回原 slice(如果不需要 copy)
优化后的代码:
func buildResponseFast(orders []Order) []Order {
return orders
}
四、trace 分析的核心技巧
4.1 看 trace 的正确姿势
先宏观 → 再微观
│ │
│ └─ 选中一个 Goroutine,看它在干嘛
│
└─ 整个时间线:CPU利用率?GC频率?Goroutine数量?
4.2 常见性能问题的 trace 特征
|
CPU 密集 |
Proc 全程满负荷,Goroutine Running 状态多 |
|---|---|
|
I/O 等待 |
Goroutine 大量时间在 Waiting,Proc 空闲 |
|
锁竞争 |
Sync block profile 有热点,很多 Goroutine 在等锁 |
|
GC 频繁 |
黄色 GC 竖线密集,MMU 图有低谷 |
|
goroutine 泄漏 |
Goroutine 数量只增不减 |
|
调度开销大 |
大量短生命周期 goroutine,gopark/mstart 频繁 |
4.3 配合其他工具使用
trace 擅长看时间维度的全貌,但不擅长看具体的 CPU 热点:
• CPU 热点 → gops pprof-cpu 更精准
• 内存分配 → gops pprof-heap
• 锁竞争 → go tool pprof 的 mutex profile
• 调用链路耗时 → trace 的 Goroutine analysis
生产环境排查建议:先用 trace 看全局,再用 pprof 挖细节。
五、优化前后对比
|
接口耗时 |
~5000ms |
~1ms |
5000x |
|---|---|---|---|
|
每秒请求数 |
~0.2 QPS |
~1000 QPS |
5000x |
|
Goroutine 数/请求 |
2000+ 个 |
1 个 |
减少 99.9% |
|
GC 频率 |
每请求多次(非常频繁) |
很低 |
大幅减少 |
|
CPU 利用率 |
低(很多等待) |
高 |
提升效率 |
六、生产环境使用注意事项
• trace 有一定开销(约 5-10% CPU),生产环境抓取时间不要太长(2-10 秒)
• 不要抓太长的 trace,文件会很大,浏览器打开会卡死
• 抓 trace 期间要有流量,最好用压测工具(wrk/ab)打一波
• 建议在低峰期抓,或者先在测试环境复现
• trace 文件包含详细的运行时信息,不要上传到公开渠道
七、附录:完整服务代码
package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
_ "net/http/pprof"
"runtime"
"strings"
"sync"
"time"
)
type Order struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
Created string `json:"created"`
Detail string `json:"-"`
}
func main() {
go func() {
log.Println("pprof server started on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", listOrders)
mux.HandleFunc("/api/orders/fast", listOrdersFast)
log.Println("server started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func listOrders(w http.ResponseWriter, r *http.Request) {
start := time.Now()
orders := queryOrdersSlow(2000)
processed := processOrdersSlow(orders)
result := buildResponseSlow(processed)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"total": len(result),
"list": result,
"duration": time.Since(start).Milliseconds(),
})
}
func listOrdersFast(w http.ResponseWriter, r *http.Request) {
start := time.Now()
orders := queryOrdersFast(2000)
processed := processOrdersFast(orders)
result := buildResponseFast(processed)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"total": len(result),
"list": result,
"duration": time.Since(start).Milliseconds(),
})
}
// --- 慢版本 ---
func queryOrdersSlow(count int) []Order {
orders := make([]Order, 0, count)
for i := 0; i < count; i++ {
time.Sleep(2 * time.Millisecond) // 模拟慢查询
detail := strings.Repeat("x", 4096)
orders = append(orders, Order{
ID: fmt.Sprintf("order-%d", i),
UserID: fmt.Sprintf("user-%d", i%10),
Amount: rand.Float64() * 1000,
Status: []string{"pending", "paid", "shipped"}[i%3],
Created: time.Now().Add(-time.Duration(i) * time.Hour).Format(time.RFC3339),
Detail: detail,
})
}
return orders
}
func processOrdersSlow(orders []Order) []Order {
var wg sync.WaitGroup
results := make([]Order, len(orders))
for i, order := range orders {
wg.Add(1)
go func(idx int, o Order) {
defer wg.Done()
runtime.Gosched() // 主动让出CPU,模拟调度开销
runtime.Gosched()
runtime.Gosched()
_ = strings.ToUpper(o.Status)
results[idx] = o
}(i, order)
}
wg.Wait()
return results
}
func buildResponseSlow(orders []Order) []Order {
var result []Order
for _, o := range orders {
temp := make([]Order, len(result)+1) // 每次重新分配,O(n²)
copy(temp, result)
temp[len(result)] = o
result = temp
}
return result
}
// --- 快版本(优化后)---
func queryOrdersFast(count int) []Order {
orders := make([]Order, 0, count)
for i := 0; i < count; i++ {
orders = append(orders, Order{
ID: fmt.Sprintf("order-%d", i),
UserID: fmt.Sprintf("user-%d", i%10),
Amount: rand.Float64() * 1000,
Status: []string{"pending", "paid", "shipped"}[i%3],
Created: time.Now().Add(-time.Duration(i) * time.Hour).Format(time.RFC3339),
})
}
return orders
}
func processOrdersFast(orders []Order) []Order {
results := make([]Order, len(orders))
for i, order := range orders {
order.Status = strings.ToUpper(order.Status)
results[i] = order
}
return results
}
func buildResponseFast(orders []Order) []Order {
return orders
}
浙公网安备 33010602011771号