导航菜单

使用 pprof 排查性能问题

🔴 困难

题目描述

以下代码存在性能问题,请使用 pprof 定位瓶颈并优化。

func ProcessItems(items []string) []string {
    var results []string
    for _, item := range items {
        result := processItem(item)
        results = append(results, result)
    }
    return results
}

func processItem(item string) string {
    // 模拟复杂处理
    var result string
    for i := 0; i < 100; i++ {
        result += fmt.Sprintf("%s-%d", item, i)
    }
    return result
}

func main() {
    items := make([]string, 1000)
    for i := range items {
        items[i] = fmt.Sprintf("item-%d", i)
    }
    
    results := ProcessItems(items)
    fmt.Println(len(results))
}

提示

  • 添加 pprof HTTP 服务
  • 使用 go tool pprof 分析
  • 查找 CPU 和内存热点
  • 优化热点代码

解法

参考答案 (3 个标签)
pprof 性能优化 Benchmark

步骤 1:添加 pprof 服务

import (
    _ "net/http/pprof"  // 自动注册 pprof handler
    "net/http"
)

func main() {
    // 启动 pprof HTTP 服务
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    
    // 原有代码
    items := make([]string, 1000)
    for i := range items {
        items[i] = fmt.Sprintf("item-%d", i)
    }
    
    results := ProcessItems(items)
    fmt.Println(len(results))
}

步骤 2:采集 CPU profile

# 采集 30 秒 CPU 数据
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof

# 或使用 go tool
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

步骤 3:分析 CPU profile

# 启动 pprof 交互界面
go tool pprof cpu.prof

# 查看热点函数
(pprof) top

# 输出示例:
# Showing nodes accounting for 1.5s, 50% of 3s total
#       flat  flat%   sum%        cum   cum%
#     1.20s 40.00% 40.00%      1.50s 50.00%  runtime.mallocgc
#     0.30s 10.00% 50.00%      0.80s 26.67%  strings.Join
#     0.20s  6.67% 56.67%      0.50s 16.67%  fmt.Sprintf

# 查看函数代码
(pprof) list processItem

# 生成火焰图
(pprof) web

步骤 4:采集内存 profile

# 采集堆内存
curl http://localhost:6060/debug/pprof/heap > heap.prof

# 分析内存分配
go tool pprof heap.prof

# 查看内存分配热点
(pprof) top

# 输出示例:
#       flat  flat%   sum%        cum   cum%
#    512MB 50.00% 50.00%      1024MB 100.00%  processItem
#    256MB 25.00% 75.00%       512MB 50.00%  ProcessItems

步骤 5:优化代码

// ❌ 原代码:多次字符串拼接和格式化
func processItem(item string) string {
    var result string
    for i := 0; i < 100; i++ {
        result += fmt.Sprintf("%s-%d", item, i)  // 每次都创建新字符串
    }
    return result
}

// ✅ 优化 1:使用 strings.Builder
func processItemOptimized(item string) string {
    var builder strings.Builder
    builder.Grow(100 * (len(item) + 10))  // 预分配
    
    for i := 0; i < 100; i++ {
        builder.WriteString(item)
        builder.WriteByte('-')
        builder.WriteString(strconv.Itoa(i))
    }
    
    return builder.String()
}

// ✅ 优化 2:预分配切片
func ProcessItemsOptimized(items []string) []string {
    results := make([]string, 0, len(items))  // 预分配
    
    for _, item := range items {
        result := processItemOptimized(item)
        results = append(results, result)
    }
    
    return results
}

步骤 6:验证优化效果

// 添加 Benchmark
func BenchmarkProcessItems(b *testing.B) {
    items := make([]string, 1000)
    for i := range items {
        items[i] = fmt.Sprintf("item-%d", i)
    }
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ProcessItems(items)
    }
}

func BenchmarkProcessItemsOptimized(b *testing.B) {
    items := make([]string, 1000)
    for i := range items {
        items[i] = fmt.Sprintf("item-%d", i)
    }
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ProcessItemsOptimized(items)
    }
}
# 运行 Benchmark
go test -bench=. -benchmem

# 输出:
# BenchmarkProcessItems-8             100    10245678 ns/op    5120000 B/op    10000 allocs/op
# BenchmarkProcessItemsOptimized-8   10000     102456 ns/op      256000 B/op      1000 allocs/op

pprof 常用命令

交互命令

# 进入交互界面
go tool pprof cpu.prof

# 查看热点
(pprof) top
(pprof) top10
(pprof) top -cum

# 查看函数代码
(pprof) list functionName
(pprof) list -lines functionName

# 查看调用图
(pprof) web
(pprof) web functionName
(pprof) pdf
(pprof) png

# 比较两个 profile
(pprof) base old.prof
(pprof) difference new.prof

# 保存报告
(pprof) png > output.png
(pprof) pdf > output.pdf

命令行选项

# 采集 CPU profile(默认 30s)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60

# 采集内存 profile
go tool pprof http://localhost:6060/debug/pprof/heap

# 采集分配对象
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap

# 采集使用内存
go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap

# 采集 goroutine
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 采集锁竞争
go tool pprof http://localhost:6060/debug/pprof/mutex

# 采集阻塞操作
go tool pprof http://localhost:6060/debug/pprof/block

pprof 在代码中集成

import (
    "os"
    "runtime/pprof"
)

func main() {
    // CPU profiling
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    
    // 执行代码
    // ...
    
    // 内存 profiling
    f2, _ := os.Create("mem.prof")
    pprof.WriteHeapProfile(f2)
    f2.Close()
}

常见性能问题模式

1. 高 CPU 使用

// ❌ 低效算法
func FindDuplicate(nums []int) int {
    for i := 0; i < len(nums); i++ {
        for j := i + 1; j < len(nums); j++ {
            if nums[i] == nums[j] {
                return nums[i]
            }
        }
    }
    return -1
}

// ✅ 使用 Map
func FindDuplicateOptimized(nums []int) int {
    seen := make(map[int]bool)
    for _, num := range nums {
        if seen[num] {
            return num
        }
        seen[num] = true
    }
    return -1
}

2. 高内存分配

// ❌ 频繁分配
func Concat(items []string) string {
    var result string
    for _, item := range items {
        result += item  // 每次都分配
    }
    return result
}

// ✅ 使用 Builder
func ConcatOptimized(items []string) string {
    var builder strings.Builder
    builder.Grow(estimateSize(items))
    for _, item := range items {
        builder.WriteString(item)
    }
    return builder.String()
}

3. 锁竞争

// ❌ 粗粒度锁
type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    time.Sleep(time.Millisecond)  // 持锁时间过长
    c.count++
    c.mu.Unlock()
}

// ✅ 减小临界区
func (c *Counter) IncrementOptimized() {
    // 准备工作
    time.Sleep(time.Millisecond)
    
    // 只在必要时加锁
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}

搜索