跳转至

CPU 调度与负载

学习目标

学完本章后,学习者应该能够:

  1. 理解 CPU 调度、时间片、优先级和负载的含义。
  2. 区分 CPU 密集型任务和 IO 密集型任务。
  3. 理解上下文切换、锁竞争和容器 CPU limit 对 Go 服务的影响。
  4. 能初步定位 Go 服务 CPU 飙高问题。

CPU 调度是什么

机器上的可运行任务通常比 CPU 核心多。操作系统调度器负责决定哪个线程在什么时候使用 CPU。

调度器关注:

  • 公平性:不能让某个任务长期饿死。
  • 响应性:交互或服务请求要及时响应。
  • 吞吐量:单位时间完成更多工作。
  • 优先级:重要任务可以获得更多机会。

后端工程师不需要实现调度器,但要理解 CPU 不是无限资源。

时间片与优先级

时间片是任务连续使用 CPU 的一小段时间。时间片用完后,调度器可能切换到其他任务。

优先级决定任务获得 CPU 的倾向。Linux 中可以通过 nice 值、cgroups、容器 CPU limit 等方式影响调度。

在 Kubernetes 中,容器的 CPU request 和 limit 会影响调度与运行:

  • request:调度时声明需要多少 CPU。
  • limit:运行时最多能用多少 CPU。

CPU limit 设置过低时,服务可能被节流,表现为延迟周期性升高。

CPU 密集型与 IO 密集型

CPU 密集型任务主要消耗计算资源:

  • 加解密。
  • 压缩。
  • JSON 大量编解码。
  • 图片或音视频处理。
  • 大量正则匹配。

IO 密集型任务主要等待外部资源:

  • 数据库查询。
  • Redis 请求。
  • HTTP 调用。
  • 文件读写。
  • 网络传输。

优化方向不同。CPU 密集型要减少计算、并行化或优化算法;IO 密集型要设置超时、连接池、批处理、缓存和异步化。

Go 后端实际应用例子

例子一:用 pprof 定位 CPU 热点

Go 服务可以开启 pprof:

import _ "net/http/pprof"

go func() {
    _ = http.ListenAndServe("127.0.0.1:6060", nil)
}()

采集 CPU profile:

go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30
go tool pprof 'http://127.0.0.1:6060/debug/pprof/profile?seconds=30'

重点看哪些函数占用 CPU 时间最多,再判断是算法问题、序列化问题、日志问题,还是 GC 压力。

例子二:控制 CPU 密集型任务并发

CPU 密集型任务不适合无限并发。并发数超过 CPU 能力后,可能只是增加上下文切换。

func workerPool(tasks <-chan Task, workers int) {
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for task := range tasks {
                task.Run()
            }
        }()
    }
    wg.Wait()
}

workers 通常要结合 CPU 核数、任务耗时、容器 CPU limit 和压测结果调整。

常见误区

  • 误区一:CPU 高一定是坏事。

CPU 高可能说明机器资源被充分利用,也可能说明计算异常。要结合吞吐、延迟和错误率判断。

  • 误区二:加 goroutine 就能提高 CPU 密集型任务性能。

并发超过 CPU 能力后,更多 goroutine 可能只带来调度开销和锁竞争。

  • 误区三:容器里看到的 CPU 核数一定等于可用 CPU。

容器可能被 cgroups 限制。Go 服务需要关注 GOMAXPROCS、CPU request、limit 和实际节流指标。

线上问题案例

某服务上线新日志字段后 CPU 飙高。排查 pprof 发现大量 CPU 消耗在 JSON 序列化和字符串拼接上。日志量随请求量线性增长,且每条日志都包含大对象。

修复方式包括减少热路径日志、避免打印大对象、使用结构化日志字段、抽样记录,以及把详细日志放到错误分支。

实战任务

给一个 Go HTTP 服务接入 pprof,并完成一次 CPU profile 分析:

  1. 增加 pprof 监听端口。
  2. 构造一段会消耗 CPU 的接口。
  3. 采集 30 秒 CPU profile。
  4. 找出最耗 CPU 的函数。
  5. 写出一个优化方向。
参考答案

可以在服务启动时单独开启 127.0.0.1:6060 的 pprof 端口,然后用 go tool pprof 采集 /debug/pprof/profile?seconds=30。如果热点函数是 JSON 编解码,可以考虑减少字段、避免重复序列化、缓存结果或换更合适的数据格式。

pprof 分析时不要只看函数名,要结合请求路径、流量变化和最近代码变更。CPU profile 说明“CPU 时间花在哪里”,不直接说明“为什么业务变慢”,还需要和延迟、QPS、GC、外部依赖指标一起看。

面试题

1. CPU 密集型和 IO 密集型任务有什么区别?

参考答案

CPU 密集型任务主要时间花在计算上,例如加密、压缩、编解码、复杂算法。IO 密集型任务主要时间花在等待外部资源上,例如数据库、网络、文件、缓存。

CPU 密集型优化重点是减少计算、优化算法、控制并发和利用多核;IO 密集型优化重点是超时、连接池、缓存、批处理、异步化和依赖治理。

2. 为什么上下文切换过多会导致性能下降?

参考答案

上下文切换需要保存和恢复执行现场,本身有开销。同时切换会破坏 CPU 缓存局部性,让后续执行更容易访问慢速内存。

当线程过多、锁竞争严重或任务频繁阻塞唤醒时,CPU 可能花太多时间在调度上,业务代码实际执行时间反而变少。

3. Go 服务 CPU 飙高怎么排查?

参考答案

先确认 CPU 高是否伴随延迟升高、错误率上升或吞吐下降。然后采集 pprof CPU profile,查看热点函数;同时观察 GC 指标、goroutine 数量、最近发布、日志量、流量变化和容器 CPU 节流。

常见原因包括死循环、低效算法、大量序列化、正则匹配、日志格式化、GC 压力和锁竞争。