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 发送信号,记录其调用栈。下图展示了采样循环的完整流程:
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") 方法做幂等性检查。