CPU 飙高排查
问题
Go 服务 CPU 使用率突然飙高或持续居高不下,如何排查定位根因?
答案
排查流程
第一步:系统级定位
# 找到 CPU 最高的进程
top -c
# 查看进程内的线程 CPU
top -H -p <pid>
# Go 程序的 Goroutine 运行在线程上,具体对应关系需要 pprof 分析
第二步:pprof CPU Profile
# 采集 30 秒的 CPU Profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 交互模式
(pprof) top 20 # 按 CPU 消耗排序
(pprof) list hotFunction # 查看具体函数的逐行消耗
(pprof) web # 生成调用图(需 graphviz)
# 也可以直接导出火焰图
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# 浏览器打开 http://localhost:8081,切换到 Flame Graph 视图
提示
CPU profile 采样原理:每 10ms 中断一次,记录当前所有 goroutine 的调用栈。采样 30 秒后统计各函数被采到的次数。
第三步:分析火焰图
火焰图中:
- 宽度:函数占用 CPU 时间比例,越宽越耗 CPU
- 深度:调用栈深度
- 看最宽的"平顶":那就是 CPU 热点
常见 CPU 飙高场景
场景 1:死循环 / 忙等待
// ❌ CPU 100%:空转忙等待
func waitForReady() {
for !isReady() {
// 没有 sleep 或 channel 等待,CPU 空转
}
}
// ✅ 修复:用 channel 或 time.Sleep
func waitForReady(ctx context.Context) error {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if isReady() {
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
}
场景 2:正则表达式热点
// ❌ 每次调用都编译正则,CPU 密集
func validate(email string) bool {
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
return matched
}
// ✅ 预编译正则
var emailRegexp = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func validate(email string) bool {
return emailRegexp.MatchString(email)
}
场景 3:JSON 序列化/反序列化
// encoding/json 使用反射,高并发下是 CPU 热点
// ✅ 使用更快的 JSON 库
import jsoniter "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibrary
// 或使用 sonic(字节出品,SIMD 加速)
import "github.com/bytedance/sonic"
场景 4:GC 压力大
# 查看 GC 频率
GODEBUG=gctrace=1 ./myapp
# 输出示例:gc 1 @0.012s 5%: ...
# 5% 表示 GC 占用了 5% 的 CPU 时间
# 如果 > 25% 就需要优化
优化方向:
- 减少内存分配(sync.Pool、预分配 slice)
- 调整
GOGC降低 GC 频率 - 使用
GOMEMLIMIT控制内存上限
场景 5:锁竞争
# 互斥锁竞争 profile
go tool pprof http://localhost:6060/debug/pprof/mutex
# 阻塞 profile
go tool pprof http://localhost:6060/debug/pprof/block
// ❌ 大锁:所有操作共用一把锁
var mu sync.Mutex
var data = make(map[string]int)
// ✅ 分片锁,降低竞争
type ShardedMap struct {
shards [256]struct {
mu sync.RWMutex
data map[string]int
}
}
func (m *ShardedMap) getShard(key string) int {
h := fnv.New32a()
h.Write([]byte(key))
return int(h.Sum32()) % len(m.shards)
}
场景 6:加密/压缩计算
// TLS 握手、bcrypt 哈希、gzip 压缩都是 CPU 密集型
// 优化方向:
// 1. TLS Session Ticket 复用,减少握手
// 2. bcrypt cost 不要设太高(10~12 合理)
// 3. 压缩用更快的算法(zstd 替代 gzip)
// 4. 加密操作限流,避免并发过高
排查工具总结
| 工具 | 用途 |
|---|---|
top -H -p <pid> | 系统级线程 CPU |
pprof/profile | Go CPU 采样,火焰图 |
pprof/mutex | 互斥锁竞争分析 |
pprof/block | 阻塞分析 |
GODEBUG=gctrace=1 | GC 频率监控 |
trace | 精细调度追踪(go tool trace) |
常见面试问题
Q1: Go 的 CPU Profile 采样原理是什么?
答案:
- 通过
SIGPROF信号每 10ms 中断一次(可配置) - 中断时记录所有 goroutine 当前的调用栈
- 统计各函数出现在栈顶的次数 → CPU 消耗比例
- 采样是统计近似,不是精确值,但足够定位热点
Q2: 线上服务 CPU 飙高,你的排查顺序是什么?
答案:
top确认是哪个进程- 看是否有近期部署(有则回滚止血)
pprof/profile?seconds=30抓 CPU Profile- 火焰图看热点函数
- 根据热点类型对症处理(正则预编译、换 JSON 库、减少 GC 等)
- 如果是突发的,对比正常时期的 Profile
Q3: Go 的 runtime.GOMAXPROCS 设多少合适?
答案:
- 默认等于 CPU 核心数,一般不需调整
- 容器环境注意:Go 可能读到宿主机 CPU 核数,需要用
uber-go/automaxprocs自动适配 - CPU 密集型不要超过核心数,IO 密集型可以适当调高
Q4: go tool trace 和 pprof 有什么区别?
答案:
- pprof:统计型分析,告诉你"哪个函数消耗最多 CPU"
- trace:事件型追踪,告诉你"每个 goroutine 在什么时间做了什么"
- trace 能看到调度延迟、GC STW、系统调用等精细信息
- pprof 用于定位热点函数,trace 用于分析调度和并发问题