使用 GO 时如何测量系统过载

Posted

技术标签:

【中文标题】使用 GO 时如何测量系统过载【英文标题】:How to measure system overload when using GO 【发布时间】:2013-11-12 04:28:22 【问题描述】:

我正在用 GO 重写一个旧系统,在旧系统中我正在测量系统负载平均值,以了解是否应该增加线程池中的线程数。

在 Go 中,人们不使用线程池或 goroutine 池,因为启动 goroutine 非常便宜。 但是仍然运行太多的 goroutine 效率会降低到足以将 cpu 使用率保持在 100% 附近

因此有一种方法可以知道有多少 goroutine 已准备好运行(未阻塞)但当前未运行。或者有没有办法获得预定的可运行goroutine“运行队列”的数量。

【问题讨论】:

“运行过多的 goroutine 效率会降低到足以使 cpu 使用率接近 100%”的情况。 Go 很少从正在运行的 goroutine 切换,除非它在等待 I/O 或通道操作或同步原语时被阻塞,因此启动大量 goroutine,操作系统线程计数通过runtime.GOMAXPROCS(runtime.NumCPU()) 匹配 CPU 计数,不需要创建太多额外的上下文切换开销。我们也许可以通过有关工作负载的更多信息来提供更多帮助——您的 goroutine 主要是在旋转 CPU,还是在等待数据库,或者通道操作,或者......? 感谢 user2714852 所以你是说如果 GOMAXPROCS 设置为 2 并且我启动 4 个永不停止且永不阻塞运行时的 goroutine 只会运行前 2 个并且永远不会上下文切换到另一个? 在 Go 1.1 中这是完全正确的:goroutine 调度是纯协作的,如果存在没有 I/O 等的无限循环,它将永远占用线程。这在Go bug 543 中进行了讨论。 (你总是可以调用 runtime.Gosched() 来显式地让步。)在 Go 1.2rc3 中,"The scheduler is invoked occasionally upon entry to a function.";那句话中的“偶尔”让我说“很少”强制转换。这是我知道的全部;我刚刚从 Go 源代码中获取了更多信息。 您可以添加某种程度的跟踪,以跟踪 goroutine 在运行时处理了多少作业,作为每个 goroutine 的活跃度的度量。 回答 twotwotwo 的问题,请假设 goroutine 主要执行 CPU 密集型工作。 【参考方案1】:

查看runtime/pprof package。

要打印“所有当前 goroutines 的堆栈跟踪”,请使用:

pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)

要打印“导致同步原语阻塞的堆栈跟踪”,请使用:

pprof.Lookup("block").WriteTo(os.Stdout, 1)

您可以将这些与runtime package 中的功能(例如runtime.NumGoroutine)结合起来,以获得一些基本的报告。

这个例子故意创建了许多阻塞的 goroutine 并等待它们完成。它每 5 秒打印一次 block pprof 配置文件的输出,以及仍然存在的 goroutine 的数量:

package main

import (
    "fmt"
    "math/rand"
    "os"
    "runtime"
    "runtime/pprof"
    "strconv"
    "sync"
    "time"
)

var (
    wg sync.WaitGroup
    m  sync.Mutex
)

func randWait() 
    defer wg.Done()
    m.Lock()
    defer m.Unlock()
    interval, err := time.ParseDuration(strconv.Itoa(rand.Intn(499)+1) + "ms")
    if err != nil 
        fmt.Errorf("%s\n", err)
    
    time.Sleep(interval)
    return


func blockStats() 
    for 
        pprof.Lookup("block").WriteTo(os.Stdout, 1)
        fmt.Println("# Goroutines:", runtime.NumGoroutine())
        time.Sleep(5 * time.Second)
    


func main() 
    rand.Seed(time.Now().Unix())
    runtime.SetBlockProfileRate(1)
    fmt.Println("Running...")
    for i := 0; i < 100; i++ 
        wg.Add(1)
        go randWait()
    
    go blockStats()
    wg.Wait()
    fmt.Println("Finished.")

我不确定这是否是您所追求的,但您可以修改它以满足您的需要。

Playground

【讨论】:

+1 这似乎回答了这个问题。我发现这个 SOA 信息丰富 ***.com/a/10096686/143225 工作示例程序很棒。也可能值得测试问题的前提,即动态调整 goroutine 计数是否足以提高性能以使其值得(与仅启动大量 goroutine 或其他一些简单策略相比)。答案可能取决于应用程序。但是,无论哪种方式,这似乎都回答了所提出的问题。 @user2714852 我同意,我还没有遇到过这样的情况:手动调整 goroutine 的数量比仅仅启动所需的数量并让运行时处理调度提供更好的性能。我确信有一些例子,但不是很常见,据我所知,这种情况只会在 Go 1.2 及更高版本中变得更好。 “我还没有遇到过手动调整 goroutines 的数量比只启动所需数量的 goroutines 提供更好的性能”如果你的任何 goroutines 可能正在阻塞,这是一个非常糟糕的主意文件系统或系统调用工作。假设你有一个 6 阶段的任务。最好为每个阶段创建一个具有 runtime.NumCPU * X(一些缩放因子)的工作池。然后每个阶段从 toDo 通道读取,直到它关闭并放入 nextStage 通道,直到它在 toDo 通道关闭时关闭它。这不是为了性能,而是为了避免崩溃。 @voidlogic 很好的例子。我还没有对 Go 中的 Syscall 做很多事情,而且我通常将缓冲 I/O 用于任何 FS 工作,所以还没有遇到这些问题。【参考方案2】:

有没有办法知道有多少 goroutine 准备好运行(未阻塞)但当前没有运行。?

您将能够(2014 年第四季度/2015 年第一季度)尝试和可视化这些 goroutine,正在开发新的跟踪器(2014 年第四季度):Go Execution Tracer

跟踪包含:

与 goroutine 调度相关的事件: goroutine 开始在处理器上执行, 同步原语上的 goroutine 阻塞, 一个goroutine创建或解除阻塞另一个goroutine; 网络相关事件: 网络 IO 上的 goroutine 阻塞, 一个 goroutine 在网络 IO 上被解除阻塞; 系统调用相关事件: 一个goroutine进入系统调用, 一个goroutine从系统调用返回; 垃圾收集器相关事件: GC 启动/停止, 并发扫描开始/停止;和 用户事件

“处理器”是指逻辑处理器,单位为GOMAXPROCS。 每个事件都包含事件 id、精确的时间戳、操作系统线程 id、处理器 id、goroutine id、堆栈跟踪和其他相关信息(例如未阻塞的 goroutine id)。

【讨论】:

以上是关于使用 GO 时如何测量系统过载的主要内容,如果未能解决你的问题,请参考以下文章

如何专门连接过载信号?

老是过载宕机,不试试亿级微服务秒杀系统?

go-zero高可用-自适应熔断器

网络过载时 QTcpSocket 的行为是啥?

Linux磁盘使用过载

在 Go 中分析内存时看似不一致的结果