Go Wiki:调试 Go 程序中的性能问题

– 最初由 Dmitry Vyukov 编写

假设您有一个 Go 程序并希望提高其性能。有几个工具可以帮助完成此任务。这些工具可以帮助您识别各种类型的热点(CPU、IO、内存),热点是您需要重点关注的地方,以便显著提高性能。但是,另一种可能的结果是 - 这些工具可以帮助您识别程序中明显的性能缺陷。例如,您在每次查询之前准备一条 SQL 语句,而您可以在程序启动时准备一次。另一个示例是,如果 O(N^2) 算法以某种方式进入明显存在且预期为 O(N) 的地方。为了识别此类情况,您需要对您在配置文件中看到的内容进行健全性检查。例如,对于第一个案例,在 SQL 语句准备中花费大量时间将是一个危险信号。

了解性能的各种限制因素也很重要。例如,如果程序通过 100 Mbps 网络链路通信,并且已经利用了 >90Mbps,您无法对程序执行太多操作来提高其性能。磁盘 IO、内存消耗和计算任务也有类似的限制因素。考虑到这一点,我们可以查看可用的工具。

注意:这些工具可能会相互干扰。例如,精确的内存分析会影响 CPU 分析,goroutine 阻塞分析会影响调度程序跟踪,等等。单独使用工具以获取更精确的信息。

CPU 分析器

Go 运行时包含内置的 CPU 分析器,它显示哪些函数消耗了多少 CPU 时间。有 3 种方法可以访问它

  1. 最简单的一种是 'go test'(https://pkg.go.dev/cmd/go/#hdr-Description_of_testing_flags)命令的 -cpuprofile 标志。例如,以下命令

    $ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
    

    将分析给定的基准并把 CPU 分析写入 'cprof' 文件。然后

    $ go tool pprof --text http.test cprof
    

    将打印最热门函数的列表。

    有几种输出类型可用,最有用的是:--text--web--list。运行 go tool pprof 以获取完整列表。此选项的明显缺点是它仅适用于测试。

  2. net/http/pprof:这是网络服务器的理想解决方案。您只需导入 net/http/pprof,并使用

    $ go tool pprof --text mybin  http://myserver:6060:/debug/pprof/profile
    
  3. 手动收集分析。您需要导入 runtime/pprof 并将以下代码添加到主函数

    if *flagCpuprofile != "" {
        f, err := os.Create(*flagCpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }
    

    该配置文件将写入指定的文件,以与第一个选项相同的方式对其进行可视化。

以下是使用 --web 选项可视化的配置文件示例:[cpu_profile.png]

您可以使用 --list=funcname 选项调查单个函数。例如,以下配置文件显示时间花费在 append 函数中

 .      .   93: func (bp *buffer) WriteRune(r rune) error {
 .      .   94:     if r < utf8.RuneSelf {
 5      5   95:         *bp = append(*bp, byte(r))
 .      .   96:         return nil
 .      .   97:     }
 .      .   98:
 .      .   99:     b := *bp
 .      .  100:     n := len(b)
 .      .  101:     for n+utf8.UTFMax > cap(b) {
 .      .  102:         b = append(b, 0)
 .      .  103:     }
 .      .  104:     w := utf8.EncodeRune(b[n:n+utf8.UTFMax], r)
 .      .  105:     *bp = b[:n+w]
 .      .  106:     return nil
 .      .  107: }

当分析器无法展开堆栈时,它还会使用 3 个特殊条目:GC、System 和 ExternalCode。GC 表示垃圾回收期间花费的时间,请参阅下面的内存分析器和垃圾回收器跟踪部分以获取优化建议。System 表示在协程调度程序、堆栈管理代码和其他辅助运行时代码中花费的时间。ExternalCode 表示在原生动态库中花费的时间。

以下是一些关于如何解释配置文件中所见内容的提示。

如果您看到在 runtime.mallocgc 函数中花费了大量时间,则该程序可能会进行大量小内存分配。该配置文件将告诉您分配的来源。请参阅内存分析器部分以获取有关如何优化此案例的建议。

如果在通道操作、sync.Mutex 代码和其他同步基元或 System 组件中花费了大量时间,则该程序可能存在争用。考虑重新构建程序以消除频繁访问的共享资源。为此,常用的技术包括分片/分区、本地缓冲/批处理和写时复制技术。

如果在 syscall.Read/Write 中花费了大量时间,则该程序可能会进行大量小读写操作。在这种情况下,os.File 或 net.Conn 周围的 Bufio 包装器可能会有所帮助。

如果在 GC 组件中花费了大量时间,则该程序要么分配了太多瞬态对象,要么堆大小非常小,因此垃圾回收发生得太频繁。请参阅垃圾回收器跟踪器和内存分析器部分以获取优化建议。

注意:对于 darwin CPU 分析器,目前仅 适用于 El Capitan 或更高版本

注意:在 windows 上,您需要安装 Cygwin、Perl 和 Graphviz 才能生成 svg/web 配置文件。

内存分析器

内存分析器显示哪些函数分配堆内存。您可以以类似于 CPU 配置文件的方式收集它:使用 go test --memprofile (https://pkg.go.dev/cmd/go/#hdr-Description_of_testing_flags),使用 net/http/pprof 通过 http://myserver:6060:/debug/pprof/heap 或通过调用 runtime/pprof.WriteHeapProfile

您只能可视化在配置文件收集时存在的分配(--inuse_space 标志为 pprof,默认),或自程序启动以来发生的所有分配(--alloc_space 标志为 pprof)。前者适用于在实时应用程序上使用 net/http/pprof 收集的配置文件,后者适用于在程序结束时收集的配置文件(否则您将看到几乎为空的配置文件)。

注意:内存分析器是采样的,也就是说,它只收集部分内存分配的信息。对对象进行采样的概率与对象的大小成正比。您可以使用 go test --memprofilerate 标志或在程序启动时设置 runtime.MemProfileRate 变量来更改采样率。速率为 1 将导致收集所有分配的信息,但这可能会降低执行速度。默认采样率为每 512KB 分配内存进行 1 次采样。

您还可以可视化已分配的字节数或已分配的对象数(分别为 --inuse/alloc_space--inuse/alloc_objects 标志)。分析器在分析期间往往会更多地对较大对象进行采样。但重要的是要了解,大对象会影响内存消耗和 GC 时间,而大量微小分配会影响执行速度(以及在一定程度上影响 GC 时间)。因此,同时查看两者可能很有用。

对象可以是持久的或暂时的。如果您在程序启动时分配了几个大型持久对象,则分析器很可能会对它们进行采样(因为它们很大)。此类对象确实会影响内存消耗和 GC 时间,但它们不会影响正常的执行速度(不会对它们执行内存管理操作)。另一方面,如果您有大量生命周期非常短的对象,则它们几乎不会出现在分析中(如果您使用默认的 --inuse_space 模式)。但它们确实会显著影响执行速度,因为它们会不断地被分配和释放。因此,再次强调,同时查看两种类型的对象可能很有用。因此,通常,如果您想减少内存消耗,则需要查看在正常程序操作期间收集的 --inuse_space 分析。如果您想提高执行速度,请查看在运行时间较长后或程序结束时收集的 --alloc_objects 分析。

有几个标志控制报告粒度。--functions 使 pprof 在函数级别报告(默认)。--lines 使 pprof 在源代码行级别报告,如果热点函数在不同的行上分配,这将很有用。还有 --addresses--files 分别用于精确的指令地址和文件级别。

内存分析有一个有用的选项——您可以在浏览器中直接查看它(前提是您导入了 net/http/pprof)。如果您打开 http://myserver:6060/debug/pprof/heap?debug=1,您必须看到类似以下内容的堆分析

heap profile: 4: 266528 [123: 11284472] @ heap/1048576
1: 262144 [4: 376832] @ 0x28d9f 0x2a201 0x2a28a 0x2624d 0x26188 0x94ca3 0x94a0b 0x17add6 0x17ae9f 0x1069d3 0xfe911 0xf0a3e 0xf0d22 0x21a70
#   0x2a201 cnew+0xc1   runtime/malloc.goc:718
#   0x2a28a runtime.cnewarray+0x3a          runtime/malloc.goc:731
#   0x2624d makeslice1+0x4d             runtime/slice.c:57
#   0x26188 runtime.makeslice+0x98          runtime/slice.c:38
#   0x94ca3 bytes.makeSlice+0x63            bytes/buffer.go:191
#   0x94a0b bytes.(*Buffer).ReadFrom+0xcb       bytes/buffer.go:163
#   0x17add6    io/ioutil.readAll+0x156         io/ioutil/ioutil.go:32
#   0x17ae9f    io/ioutil.ReadAll+0x3f          io/ioutil/ioutil.go:41
#   0x1069d3    godoc/vfs.ReadFile+0x133            godoc/vfs/vfs.go:44
#   0xfe911 godoc.func·023+0x471            godoc/meta.go:80
#   0xf0a3e godoc.(*Corpus).updateMetadata+0x9e     godoc/meta.go:101
#   0xf0d22 godoc.(*Corpus).refreshMetadataLoop+0x42    godoc/meta.go:141

2: 4096 [2: 4096] @ 0x28d9f 0x29059 0x1d252 0x1d450 0x106993 0xf1225 0xe1489 0xfbcad 0x21a70
#   0x1d252 newdefer+0x112              runtime/panic.c:49
#   0x1d450 runtime.deferproc+0x10          runtime/panic.c:132
#   0x106993    godoc/vfs.ReadFile+0xf3         godoc/vfs/vfs.go:43
#   0xf1225 godoc.(*Corpus).parseFile+0x75      godoc/parser.go:20
#   0xe1489 godoc.(*treeBuilder).newDirTree+0x8e9   godoc/dirtrees.go:108
#   0xfbcad godoc.func·002+0x15d            godoc/dirtrees.go:100

每个条目开头(("1: 262144 [4: 376832]"))中的数字分别表示当前活动对象数、活动对象占用的内存量、分配总数和所有分配占用的内存量。

优化通常是针对特定应用程序的,但这里有一些常见的建议。

  1. 将对象合并成更大的对象。例如,将 *bytes.Buffer 结构成员替换为 bytes.Buffer(您可以通过稍后调用 bytes.Buffer.Grow 为写入预分配缓冲区)。这将减少内存分配的数量(更快)并减轻垃圾收集器的压力(更快的垃圾收集)。

  2. 超出其声明范围的局部变量会被提升为堆分配。编译器通常无法证明多个变量具有相同生命周期,因此它会单独分配每个此类变量。因此,您也可以对局部变量使用上述建议。例如,替换

    for k, v := range m {
        k, v := k, v   // copy for capturing by the goroutine
        go func() {
            // use k and v
        }()
    }
    

    for k, v := range m {
        x := struct{ k, v string }{k, v}   // copy for capturing by the goroutine
        go func() {
            // use x.k and x.v
        }()
    }
    

    这将两个内存分配替换为一个分配。但是,此优化通常会对代码可读性产生负面影响,因此请合理使用。

  3. 分配合并的一个特例是切片数组预分配。如果您知道切片的典型大小,则可以按如下方式为其预分配一个支持数组

    type X struct {
        buf      []byte
        bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
    }
    
    func MakeX() *X {
        x := &X{}
        // Preinitialize buf with the backing array.
        x.buf = x.bufArray[:0]
        return x
    }
    
  4. 如果可能,请使用较小的数据类型。例如,使用 int8 而不是 int

  5. 不包含任何指针的对象(请注意,字符串、切片、映射和通道包含隐式指针)不会被垃圾收集器扫描。例如,1GB 字节切片实际上不会影响垃圾收集时间。因此,如果您从活动使用的对象中删除指针,则可以对垃圾收集时间产生积极影响。一些可能性包括:用索引替换指针,将对象拆分为两部分,其中一部分不包含指针。

  6. 使用 freelist 重用瞬态对象并减少分配数量。标准库包含 sync.Pool 类型,该类型允许在垃圾收集之间多次重用同一对象。但是,请注意,与任何手动内存管理方案一样,不正确使用 sync.Pool 可能导致使用后释放错误。

您还可以使用垃圾收集器跟踪(见下文)来深入了解内存问题。

TODO(dvyukov):提及统计信息以延迟方式更新:“Memprof 统计信息以延迟方式更新。在分配连续进行而释放随后批量进行的情况下,这是为了呈现一致的画面。几个连续的 GC 推动更新管道向前。这就是您观察到的。因此,如果您对实时服务器进行分析,那么任何样本都会为您提供一致的快照。但是,如果程序完成了一些活动,并且您希望在此活动后收集快照,那么您需要在收集之前执行 2 或 3 个 GC。”

阻塞分析器

阻塞分析器显示协程在等待同步基元(包括计时器通道)时阻塞的位置。您可以使用与 CPU 分析类似的方式收集它:使用 go test --blockprofile (https://pkg.go.dev/cmd/go/#hdr-Description_of_testing_flags),使用 net/http/pprof 通过 http://myserver:6060:/debug/pprof/block 或通过调用 runtime/pprof.Lookup(“block”).WriteTo

但有一个重要的警告——默认情况下不会启用阻塞分析器。go test --blockprofile 会自动为您启用它。但是,如果您使用 net/http/pprofruntime/pprof,则需要手动启用它(否则该分析文件将为空)。要启用阻塞分析器,请调用 runtime.SetBlockProfileRate。SetBlockProfileRate 控制阻塞分析文件中报告的协程阻塞事件的分数。该分析器旨在对每指定纳秒阻塞时间平均采样一个阻塞事件。要将每个阻塞事件包含在分析文件中,请将比率设置为 1。

如果一个函数包含多个阻塞操作,并且不清楚哪一个导致阻塞,请对 pprof 使用 --lines 标志。

请注意,并非所有阻塞都是不好的。当一个协程阻塞时,底层工作线程会简单地切换到另一个协程。因此,在协作式 Go 环境中阻塞与在非协作式系统中对互斥锁进行阻塞非常不同(例如,典型的 C++ 或 Java 线程库,其中阻塞会导致线程空闲和昂贵的线程上下文切换)。为了让您有所了解,我们来看一些示例。

对 time.Ticker 进行阻塞通常是可以的。如果一个协程对 Ticker 阻塞 10 秒,您将在分析文件中看到 10 秒的阻塞,这是完全正常的。对 sync.WaitGroup 进行阻塞通常是可以的。例如,如果一个任务需要 10 秒,那么在 WaitGroup 上等待完成的协程将在分析文件中计为 10 秒的阻塞。对 sync.Cond 进行阻塞可能可以,也可能不可以,具体取决于情况。消费者对通道进行阻塞表明生产者速度较慢或缺乏工作。生产者对通道进行阻塞表明消费者速度较慢,但这通常是可以的。对基于通道的信号量进行阻塞显示了协程在信号量上受到多少限制。对 sync.Mutex 或 sync.RWMutex 进行阻塞通常是不好的。您可以在可视化过程中使用 --ignore 标志对 pprof 进行排除,以从分析文件中排除已知的无趣阻塞。

协程的阻塞可能导致两个负面后果

  1. 由于缺乏工作,程序无法根据处理器进行扩展。调度程序跟踪有助于识别这种情况。

  2. 过度的 goroutine 阻塞/解除阻塞会消耗 CPU 时间。CPU Profiler 有助于识别这种情况(查看系统组件)。

以下是一些有助于减少 goroutine 阻塞的常见建议

  1. 在生产者-消费者场景中使用足够缓冲的通道。无缓冲通道极大地限制了程序中可用的并行性。

  2. 对于大多数读取的工作负载,使用 sync.RWMutex 代替 sync.Mutex。在 sync.RWMutex 中,即使在实现级别,读取器也永远不会阻塞其他读取器。

  3. 在某些情况下,可以通过使用写时复制技术完全移除互斥锁。如果受保护的数据结构修改不频繁,并且可以复制它,那么可以按如下方式更新它

    type Config struct {
        Routes   map[string]net.Addr
        Backends []net.Addr
    }
    
    var config atomic.Value  // actual type is *Config
    
    // Worker goroutines use this function to obtain the current config.
      // UpdateConfig must be called at least once before this func.
    func CurrentConfig() *Config {
        return config.Load().(*Config)
    }
    
    // Background goroutine periodically creates a new Config object
    // as sets it as current using this function.
    func UpdateConfig(cfg *Config) {
      config.Store(cfg)
    }
    

    此模式可防止写入器在更新期间阻塞读取器。

  4. 分区是减少对共享可变数据结构的争用/阻塞的另一种通用技术。以下是分区哈希映射的示例

    type Partition struct {
        sync.RWMutex
        m map[string]string
    }
    
    const partCount = 64
    var m [partCount]Partition
    
    func Find(k string) string {
        idx := hash(k) % partCount
        part := &m[idx]
        part.RLock()
        v := part.m[k]
        part.RUnlock()
        return v
    }
    
  5. 本地缓存和批量更新有助于减少对不可分区数据结构的争用。以下是如何批量发送到通道

    const CacheSize = 16
    
    type Cache struct {
        buf [CacheSize]int
        pos int
    }
    
    func Send(c chan [CacheSize]int, cache *Cache, value int) {
        cache.buf[cache.pos] = value
        cache.pos++
        if cache.pos == CacheSize {
            c <- cache.buf
            cache.pos = 0
        }
    }
    

    此技术不限于通道。它可用于批量更新映射、批量分配等。

  6. 使用 sync.Pool 而不是基于 chan 或受互斥锁保护的 freelist 来获取 freelist。sync.Pool 在内部使用智能技术来减少阻塞。

Goroutine Profiler

goroutine profiler 只是为您提供进程中所有活动 goroutine 的当前堆栈。它可以方便地调试负载平衡问题(请参阅下面的调度程序跟踪部分),或调试死锁。该配置文件仅对正在运行的应用程序有意义,因此 go test 不会显示它。您可以使用 net/http/pprof 通过 http://myserver:6060:/debug/pprof/goroutine 收集配置文件,并将其可视化为 svg/pdf 或通过调用 runtime/pprof.Lookup(“goroutine”).WriteTo。但最实用的方法是在浏览器中输入 http://myserver:6060:/debug/pprof/goroutine?debug=2,这将为您提供类似于程序崩溃时看到的符号化堆栈。请注意,“syscall”状态中的 goroutine 会消耗一个操作系统线程,而其他 goroutine 不会(除了调用 runtime.LockOSThread 的 goroutine,不幸的是,该 goroutine 在配置文件中不可见)。请注意,“IO 等待”状态中的 goroutine 也不会消耗线程,它们停放在非阻塞网络轮询器上(该轮询器使用 epoll/kqueue/GetQueuedCompletionStatus 在稍后解除 goroutine 停放)。

垃圾收集器跟踪

除了分析工具之外,还有另一种可用的工具——跟踪器。它们允许跟踪垃圾回收、内存分配器和 goroutine 调度程序状态。要启用垃圾收集器 (GC) 跟踪,请使用 GODEBUG=gctrace=1 环境变量运行程序

$ GODEBUG=gctrace=1 ./myserver

然后,程序将在执行期间打印类似于以下内容的输出

gc9(2): 12+1+744+8 us, 2 -> 10 MB, 108615 (593983-485368) objects, 4825/3620/0 sweeps, 0(0) handoff, 6(91) steal, 16/1/0 yields
gc10(2): 12+6769+767+3 us, 1 -> 1 MB, 4222 (593983-589761) objects, 4825/0/1898 sweeps, 0(0) handoff, 6(93) steal, 16/10/2 yields
gc11(2): 799+3+2050+3 us, 1 -> 69 MB, 831819 (1484009-652190) objects, 4825/691/0 sweeps, 0(0) handoff, 5(105) steal, 16/1/0 yields

让我们考虑这些数字的含义。每行打印一个 GC。第一个数字(“gc9”)是 GC 的数量(这是自程序启动以来的第 9 个 GC)。括号中的数字(“(2)”)是参与 GC 的工作线程的数量。接下来的 4 个数字(“12+1+744+8 us”)分别表示停止世界、扫描、标记和等待工作线程完成,单位为微秒。接下来的 2 个数字(“2 -> 10 MB”)表示上一次 GC 后的活动堆大小和当前 GC 前的完整堆大小(包括垃圾)。接下来的 3 个数字(“108615 (593983-485368) 个对象”)是堆中对象的总数(包括垃圾)以及内存分配和释放操作的总数。接下来的 3 个数字(“4825/3620/0 次扫描”)描述了扫描阶段(上一次 GC):共有 4825 个内存跨度,3620 个按需或在后台扫描,0 个在停止世界阶段扫描(其余为未使用跨度)。接下来的 4 个数字(“0(0) 移交,6(91) 窃取”)描述了并行标记阶段的负载平衡:有 0 个对象移交操作(0 个对象被移交),6 个窃取操作(91 个对象被窃取)。最后 3 个数字(“16/1/0 让步”)描述了并行标记阶段的有效性:在等待另一个线程期间,总共有 17 个让步操作。

GC 是 标记和扫描类型。总 GC 可表示为

Tgc = Tseq + Tmark + Tsweep

其中 Tseq 是停止用户 goroutine 和一些准备活动的时间(通常很小);Tmark 是堆标记时间,标记发生在所有用户 goroutine 停止时,因此会显著影响处理延迟;Tsweep 是堆扫描时间,扫描通常与正常程序执行同时发生,因此对延迟不太关键。

标记时间可以近似表示为

Tmark = C1*Nlive + C2*MEMlive_ptr + C3*Nlive_ptr

其中 Nlive 是 GC 期间堆中活动对象的数量,MEMlive_ptr 是具有指针的活动对象占用的内存量,Nlive_ptr 是活动对象中的指针数量。

扫描时间可以近似表示为

Tsweep = C4*MEMtotal + C5*MEMgarbage

其中 MEMtotal 是堆内存的总量,MEMgarbage 是堆中垃圾的量。

程序在分配额外内存量(与已用内存量成正比)后,将执行下一次 GC。该比例由 GOGC 环境变量控制(默认值为 100)。如果 GOGC=100 且程序正在使用 4M 堆内存,则当程序达到 8M 时,运行时将再次触发 GC。这使 GC 成本与分配成本成线性比例。调整 GOGC 会更改线性常量以及所用额外内存量。

只有扫描取决于堆的总大小,并且扫描与正常程序执行同时进行。因此,如果你负担得起额外的内存消耗,则将 GOGC 设置为较高值(200、300、500 等)是有意义的。例如,GOGC=300 可以将垃圾回收开销减少多达 2 倍,同时保持延迟不变(代价是堆增大 2 倍)。

GC 是并行的,并且通常与硬件并行性很好地扩展。因此,即使对于顺序程序,将 GOMAXPROCS 设置为较高值也可能是有意义的,仅仅是为了加速垃圾回收。但是,请注意,垃圾回收器线程的数量目前限制为 8。

内存分配器跟踪

内存分配器跟踪只是将所有内存分配和释放操作转储到控制台。它通过 GODEBUG=allocfreetrace=1 环境变量启用。输出类似于

tracealloc(0xc208062500, 0x100, array of parse.Node)
goroutine 16 [running]:
runtime.mallocgc(0x100, 0x3eb7c1, 0x0)
    runtime/malloc.goc:190 +0x145 fp=0xc2080b39f8
runtime.growslice(0x31f840, 0xc208060700, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0)
    runtime/slice.goc:76 +0xbb fp=0xc2080b3a90
text/template/parse.(*Tree).parse(0xc2080820e0, 0xc208023620, 0x0, 0x0)
    text/template/parse/parse.go:289 +0x549 fp=0xc2080b3c50
...

tracefree(0xc208002d80, 0x120)
goroutine 16 [running]:
runtime.MSpan_Sweep(0x73b080)
        runtime/mgc0.c:1880 +0x514 fp=0xc20804b8f0
runtime.MCentral_CacheSpan(0x69c858)
        runtime/mcentral.c:48 +0x2b5 fp=0xc20804b920
runtime.MCache_Refill(0x737000, 0xc200000012)
        runtime/mcache.c:78 +0x119 fp=0xc20804b950
...

跟踪包含内存块的地址、大小、类型、goroutine ID 和堆栈跟踪。它可能对调试更有用,但也可以为分配优化提供非常细粒度的信息。

调度程序跟踪

调度程序跟踪可以深入了解 goroutine 调度程序的动态行为,并允许调试负载平衡和可伸缩性问题。要启用调度程序跟踪跟踪,请使用 GODEBUG=schedtrace=1000 环境变量运行程序(该值表示输出周期,以毫秒为单位,在本例中为每秒一次)

$ GODEBUG=schedtrace=1000 ./myserver

然后,程序将在执行期间打印类似于以下内容的输出

SCHED 1004ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=8 [0 1 0 3]
SCHED 2005ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=5 runqueue=6 [1 5 4 0]
SCHED 3008ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=10 [2 2 2 1]

第一个数字(“1004ms”)是自程序启动以来的时间。Gomaxprocs 是 GOMAXPROCS 的当前值。Idleprocs 是空闲处理器的数量(其余正在执行 Go 代码)。Threads 是调度程序创建的工作线程的总数(线程可以处于 3 种状态:执行 Go 代码(gomaxprocs-idleprocs)、执行系统调用/cgocall 或空闲)。Idlethreads 是空闲工作线程的数量。Runqueue 是具有可运行 goroutine 的全局队列的长度。方括号中的数字(“[0 1 0 3]”)是具有可运行 goroutine 的每个处理器队列的长度。全局和本地队列长度的总和表示可用于执行的 goroutine 的总数。

注意:你可以将任何跟踪器组合为 GODEBUG=gctrace=1,allocfreetrace=1,schedtrace=1000。

注意:还有详细的调度程序跟踪,你可以使用 GODEBUG=schedtrace=1000,scheddetail=1 启用它。它打印有关每个 goroutine、工作线程和处理器的详细信息。我们不会在此处描述其格式,因为它主要对调度程序开发人员有用;但你可以在 src/pkg/runtime/proc.c 中找到详细信息。

当程序不能随 GOMAXPROCS 线性扩展和/或不消耗 100% 的 CPU 时间时,调度程序跟踪很有用。理想的情况是所有处理器都忙于执行 Go 代码,线程数量合理,所有队列中都有大量工作,并且工作分布合理均匀

gomaxprocs=8 idleprocs=0 threads=40 idlethreads=5 runqueue=10 [20 20 20 20 20 20 20 20]

当上述情况不成立时,就会出现糟糕的情况。例如,以下示例演示了工作不足以让所有处理器保持忙碌的情况

gomaxprocs=8 idleprocs=6 threads=40 idlethreads=30 runqueue=0 [0 2 0 0 0 1 0 0]

注意:使用操作系统提供的工具来测量实际 CPU 利用率作为最终特征。在 Unix 系列操作系统中,是 top 命令;在 Windows 中,是任务管理器。

在工作不足的情况下,可以使用 goroutine 分析器来了解 goroutine 在何处阻塞。注意,只要所有处理器都处于忙碌状态,负载不平衡最终并不是一件坏事,它只会造成一些适度的负载平衡开销。

内存统计信息

Go 运行时通过 runtime.ReadMemStats 函数公开粗粒度的内存统计信息。这些统计信息还通过 net/http/pprof 公开在 http://myserver:6060/debug/pprof/heap?debug=1 的底部。统计信息在此处 描述。一些有趣的字段包括

  1. HeapAlloc - 当前堆大小。
  2. HeapSys - 总堆大小。
  3. HeapObjects - 堆中的对象总数。
  4. HeapReleased - 释放给操作系统的内存量;运行时将 5 分钟内未使用的内存释放给操作系统,可以使用 runtime/debug.FreeOSMemory 强制执行此进程。
  5. Sys - 从操作系统分配的总内存量。
  6. Sys-HeapReleased - 程序的有效内存消耗。
  7. StackSys - 为 goroutine 堆栈消耗的内存(注意,一些堆栈是从堆中分配的,并且此处未计算在内,遗憾的是,没有办法获取堆栈的总大小 (https://code.google.com/p/go/issues/detail?id=7468))
  8. MSpanSys/MCacheSys/BuckHashSys/GCSys/OtherSys - 运行时为各种辅助目的分配的内存量;它们通常并不有趣,除非它们太高。
  9. PauseNs - 上次垃圾回收的持续时间。

堆转储器

最后一个可用工具是堆转储器,它可以将整个堆的状态写入文件以供将来探索。它对于识别内存泄漏和深入了解程序内存消耗非常有用。

首先,您需要使用 runtime/debug.WriteHeapDump 函数编写转储

    f, err := os.Create("heapdump")
    if err != nil { ... }
    debug.WriteHeapDump(f.Fd())

然后,您可以将其呈现为具有堆的图形表示形式的 dot 文件,或将其转换为 hprof 格式。要将其呈现为 dot 文件

$ go get github.com/randall77/hprof/dumptodot
$ dumptodot heapdump mybinary > heap.dot

并使用 Graphviz 打开 heap.dot

要将其转换为 hprof 格式

$ go get github.com/randall77/hprof/dumptohprof
$ dumptohprof heapdump heap.hprof
$ jhat heap.hprof

并将浏览器导航到 http://myserver:7000。

总结性评论

优化是一个开放的问题,有一些简单的秘诀可以用来提高性能。有时优化需要对程序进行彻底的重新架构。但我们希望这些工具能成为您工具箱中宝贵的补充,您可以使用它们至少分析和了解发生了什么。 分析 Go 程序 是一个关于使用 CPU 和内存分析器优化简单程序的优秀教程。


此内容是 Go Wiki 的一部分。