Golang性能分析:如何基于挂钟时间而非CPU时间分析程序性能?
Golang性能分析:如何基于挂钟时间而非CPU时间分析程序性能? 据我所知,Go性能分析使用SIGPROF信号在CPU时间内每秒采样N次调用堆栈。由于空闲时间、等待时间不会消耗CPU,这些时间将被忽略。
对于混合负载的Go程序性能分析,我希望基于挂钟时间进行分析:在挂钟时间内每秒采样N次调用堆栈。结果将在与CPU时间相同的UI界面中显示。
例如,简易性能分析器(https://poormansprofiler.org/)通过每秒(或每分钟)运行gdb来采样程序。
如何使用当前的Go性能分析工具实现?或者是否有其他工具可用?
开始测量相关的部分。我认为了解程序中每个函数的执行时间并不重要。
更多关于Golang性能分析:如何基于挂钟时间而非CPU时间分析程序性能?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
虽然程序可以运行,但我的程序中函数实在太多了。
我正在尝试通过以下方式使其工作:
- 将CPU采样率设置为1Hz
- 启动一个时间定时器(100Hz)向自身发送SIGPROF信号
也许你在寻找 time.Since 🤔
package main
import (
"time"
)
func main() {
start := time.Now()
time.Sleep(3)
println(time.Since(start))
}
可以创建一个这样的包。这里我直接放在主包中,用于统计每个函数的调用次数和耗时
package main
import (
"fmt"
"io"
"time"
)
type called struct {
count int
totalTime time.Duration
}
var func2called map[string]*called
func init() {
func2called = make(map[string]*called, 100)
}
func Measure(funcName string, start time.Time) {
timeTaken := time.Since(start)
c, ok := func2called[funcName]
if ok {
c.count++
c.totalTime += timeTaken
} else {
func2called[funcName] = &called{count: 1, totalTime: timeTaken}
}
}
func WriteCalled(w io.Writer) {
for k, v := range func2called {
fmt.Fprintf(w, "%s: called %d times, %s total time, %s average\n",
k, v.count, v.totalTime, v.totalTime/time.Duration(v.count))
}
}
然后可以这样调用。唯一无法使用defer测量的是main函数
package main
import (
"os"
"time"
)
func f() {
defer Measure("f", time.Now())
time.Sleep(3 * time.Second)
}
func g() {
defer Measure("g", time.Now())
time.Sleep(time.Second)
}
func main() {
start := time.Now()
f()
for i := 0; i < 5; i++ {
g()
}
Measure("main", start)
WriteCalled(os.Stdout)
}
我将留给你作为练习:使函数能够在多个goroutine中安全调用,根据某些有用的变量对输出进行排序,并编写一个程序在你的项目中的每个函数体内添加defer语句
在Go中,标准性能分析工具确实基于CPU时间采样,但可以通过几种方式实现基于挂钟时间的性能分析。
方法1:使用runtime.SetBlockProfileRate
通过设置阻塞分析,可以捕获在等待系统调用、通道操作、互斥锁等时的堆栈跟踪:
package main
import (
"os"
"runtime"
"runtime/pprof"
"time"
)
func main() {
// 启用阻塞分析(基于挂钟时间)
runtime.SetBlockProfileRate(1) // 每纳秒记录一次阻塞事件
// 你的应用程序代码
runWorkload()
// 写入阻塞分析文件
f, _ := os.Create("block.pprof")
pprof.Lookup("block").WriteTo(f, 0)
f.Close()
}
func runWorkload() {
// 模拟混合负载
for i := 0; i < 1000; i++ {
// CPU密集型工作
for j := 0; j < 1000000; j++ {
_ = j * j
}
// I/O等待(挂钟时间)
time.Sleep(10 * time.Millisecond)
}
}
方法2:使用自定义采样器
实现一个基于挂钟时间的自定义采样器:
package main
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"time"
"sync"
)
type WallClockProfiler struct {
samples map[string]int
mu sync.Mutex
stop chan bool
}
func NewWallClockProfiler() *WallClockProfiler {
return &WallClockProfiler{
samples: make(map[string]int),
stop: make(chan bool),
}
}
func (p *WallClockProfiler) Start(interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ticker.C:
p.takeSample()
case <-p.stop:
ticker.Stop()
return
}
}
}()
}
func (p *WallClockProfiler) Stop() {
close(p.stop)
}
func (p *WallClockProfiler) takeSample() {
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true)
stack := string(buf[:n])
p.mu.Lock()
p.samples[stack]++
p.mu.Unlock()
}
func (p *WallClockProfiler) WriteProfile(filename string) error {
p.mu.Lock()
defer p.mu.Unlock()
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
// 写入简单的分析格式
for stack, count := range p.samples {
fmt.Fprintf(file, "Count: %d\n%s\n\n", count, stack)
}
return nil
}
func main() {
profiler := NewWallClockProfiler()
profiler.Start(100 * time.Millisecond) // 每100ms采样一次
// 运行混合负载
runMixedWorkload()
profiler.Stop()
profiler.WriteProfile("wallclock.prof")
}
func runMixedWorkload() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 100; j++ {
// CPU工作
sum := 0
for k := 0; k < 100000; k++ {
sum += k * k
}
// 模拟I/O等待
time.Sleep(50 * time.Millisecond)
// 通道操作(会被阻塞分析捕获)
ch := make(chan int, 1)
ch <- sum
<-ch
}
}(i)
}
wg.Wait()
}
方法3:结合多种分析类型
同时启用多种分析来获得更全面的视图:
package main
import (
"os"
"runtime"
"runtime/pprof"
"time"
)
func startComprehensiveProfiling() {
// CPU分析(传统方式)
cpuFile, _ := os.Create("cpu.pprof")
pprof.StartCPUProfile(cpuFile)
// 阻塞分析
runtime.SetBlockProfileRate(1000000) // 1ms
// 互斥锁分析
runtime.SetMutexProfileFraction(1)
}
func stopComprehensiveProfiling() {
pprof.StopCPUProfile()
// 写入阻塞分析
blockFile, _ := os.Create("block.pprof")
pprof.Lookup("block").WriteTo(blockFile, 0)
blockFile.Close()
// 写入互斥锁分析
mutexFile, _ := os.Create("mutex.pprof")
pprof.Lookup("mutex").WriteTo(mutexFile, 0)
mutexFile.Close()
}
func main() {
startComprehensiveProfiling()
defer stopComprehensiveProfiling()
runMixedWorkload()
}
分析结果查看
使用go tool pprof查看结果:
# 查看阻塞分析
go tool pprof block.pprof
# 查看互斥锁分析
go tool pprof mutex.pprof
# 文本格式输出
go tool pprof -text block.pprof
阻塞分析和互斥锁分析能够捕获程序在等待时的状态,这更接近挂钟时间的分析视角。虽然这不是完全等同于挂钟时间采样,但在实践中对于识别I/O等待、锁竞争等问题非常有效。

