4. CPU Profiling 基础:如何开启 CPU Profiling?

CPU Profiling 是性能调优的起点。在 Go 中,标准库 runtime/pprof 提供了最底层的 CPU 数据采集能力。理解 StartCPUProfile 与 StopCPUProfile 的用法,是掌握后续所有 pprof 分析工具的前提。

4.1 核心 API 与调用契约

两个函数构成了 CPU Profiling 的开关:

函数 签名 行为
StartCPUProfile func StartCPUProfile(w io.Writer) error 启动 CPU 采样,将 profile 数据写入 w。默认采样频率为 100 Hz(每 10 ms 一次)。
StopCPUProfile func StopCPUProfile() 停止采样,将缓冲区数据 flush 到 writer。必须在 StartCPUProfile 成功后调用。
⚠️ 关键约束: 同一时刻只能有一个活跃的 CPU profile。重复调用 StartCPUProfile 会返回 error: "runtime: cannot start CPU profiling: CPU profiling already in use"。务必保证 StopCPUProfile 在函数退出路径上被调用。

4.2 标准用法模板

以下是最常见的“文件输出”模式,适用于一次性性能分析:

package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // 1. 创建输出文件
    f, err := os.Create("cpu.pprof")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    // 2. 启动 CPU Profiling
    if err := pprof.StartCPUProfile(f); err != nil {
        panic(err)
    }
    defer pprof.StopCPUProfile()

    // 3. 业务逻辑(被采样的代码段)
    doHeavyWork()
}

func doHeavyWork() {
    sum := 0
    for i := 0; i < 1e9; i++ {
        sum += i
    }
}
💡 最佳实践: 将 StartCPUProfile 与 StopCPUProfile 放在 main 函数最外层,使用 defer 保证配对。不要将采样范围缩得太小(如只包裹一个循环),否则采样点过少会导致统计失真。

4.3 采样原理与时间窗口

CPU Profiling 基于 定时信号中断(SIGPROF)。Go runtime 注册一个定时器,每隔固定间隔(默认 10 ms)向当前运行的 goroutine 发送信号,记录其调用栈。下图展示了采样循环的完整流程:

CPU Profiling 采样循环流程 应用启动 StartCPUProfile 注册 SIGPROF 定时器 定时中断(每10ms) 记录当前调用栈 写入 profile 缓冲区 StopCPUProfile Flush 到输出文件 采样期间业务逻辑持续运行 Stop 前不要退出进程

4.4 采样频率与数据量估算

默认 100 Hz 的采样频率在大多数场景下足够。调整频率可通过 runtime.SetCPUProfileRate 实现,但官方文档明确建议:不要在生产环境中修改采样频率。过高的频率会导致 runtime 开销剧增,过低的频率则丢失热点细节。

采样频率 间隔 10 秒采样点数 适用场景
100 Hz(默认) 10 ms ~1,000 通用分析,CPU 密集型服务
250 Hz 4 ms ~2,500 短时间高精度分析(需谨慎)
50 Hz 20 ms ~500 低开销监控,长时间采样
📊 数据量参考: 一个 30 秒的 CPU profile(默认 100 Hz)大约产生 3,000 个采样点,每个采样点包含完整的调用栈信息。生成的 .pprof 文件大小通常在 200 KB ~ 2 MB 之间,取决于调用栈深度和 goroutine 数量。

4.5 常见错误与避坑指南

  • 忘记调用 StopCPUProfile: 导致 profile 文件不完整(缺少尾部数据),go tool pprof 解析时会报 "profile is truncated"。始终使用 defer pprof.StopCPUProfile()。
  • 在子 goroutine 中启动 profile: StartCPUProfile 是全局操作,在任意 goroutine 中调用都会影响整个进程。不要在多个 goroutine 中重复调用。
  • 采样时间过短: 少于 1 秒的采样几乎无意义。建议至少采样 10~30 秒,确保覆盖完整的业务周期。
  • 写入网络或管道 writer: 如果 io.Writer 阻塞(如网络延迟),会导致采样中断。优先写入本地文件或 bytes.Buffer。

4.6 进阶:在 HTTP 服务中动态开启

生产环境中,通常通过 HTTP 端点动态控制 CPU Profiling 的启停,避免修改代码重启进程。以下是一个安全实现:

var cpuProfileMu sync.Mutex

func startProfileHandler(w http.ResponseWriter, r *http.Request) {
    cpuProfileMu.Lock()
    defer cpuProfileMu.Unlock()

    f, err := os.Create("/tmp/cpu.pprof")
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := pprof.StartCPUProfile(f); err != nil {
        f.Close()
        http.Error(w, err.Error(), 500)
        return
    }
    // 注意:这里不能 defer StopCPUProfile,因为 handler 返回后采样仍需继续
    // 需要另一个端点来触发 Stop
    w.Write([]byte("CPU profiling started"))
}

func stopProfileHandler(w http.ResponseWriter, r *http.Request) {
    cpuProfileMu.Lock()
    defer cpuProfileMu.Unlock()

    pprof.StopCPUProfile()
    w.Write([]byte("CPU profiling stopped"))
}
⚠️ 生产安全: 上述端点必须通过鉴权或内网访问保护,防止恶意用户频繁启停导致性能抖动。建议配合 pprof.Profile 的 Lookup("cpu") 方法做幂等性检查。