Golang性能分析:如何基于挂钟时间而非CPU时间分析程序性能?

Golang性能分析:如何基于挂钟时间而非CPU时间分析程序性能? 据我所知,Go性能分析使用SIGPROF信号在CPU时间内每秒采样N次调用堆栈。由于空闲时间、等待时间不会消耗CPU,这些时间将被忽略。

对于混合负载的Go程序性能分析,我希望基于挂钟时间进行分析:在挂钟时间内每秒采样N次调用堆栈。结果将在与CPU时间相同的UI界面中显示。

例如,简易性能分析器(https://poormansprofiler.org/)通过每秒(或每分钟)运行gdb来采样程序。

如何使用当前的Go性能分析工具实现?或者是否有其他工具可用?

6 回复

开始测量相关的部分。我认为了解程序中每个函数的执行时间并不重要。

更多关于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等待、锁竞争等问题非常有效。

回到顶部