建议:runtime/trace:改进Go诊断/跟踪

bqujaahr  于 6个月前  发布在  Go
关注(0)|答案(6)|浏览(53)

概述
Go 提供了大量的诊断工具,但在大型、高度并发的现实应用中,使用100+个 Go 协程时,它们的用处有限。为了这个提案的目的,我正在使用一个真实的应用。这个应用使用 gRPC,有一个活动的 RPC 流。它还支持通过标准 TCP 套接字(没有活动连接)的“修复金融协议”。它也发出多播数据包。它有一个网页界面,还有网页套接字接口(两者都没有活动连接)。所以有很多网络端点在使用中。尽管这是一个真实的应用,但与大多数企业应用相比,它还是非常小的。
以下是一张性能分析图和操作系统 CPU 利用率的截图:

这里有一个树状图提供了更多的上下文:

10ms   runtime.libcCall
             runtime.pthread_mutex_lock
             runtime.semawakeup
             runtime.notewakeup
             runtime.startm
             runtime.wakep
             runtime.ready
             runtime.goready.func1
             runtime.systemstack
             runtime.goready
             runtime.send
             runtime.chansend
             runtime.selectnbsend
             google.golang.org/grpc/internal/transport.(*controlBuffer).executeAndPut
             google.golang.org/grpc/internal/transport.(*controlBuffer).put
             google.golang.org/grpc/internal/transport.(*http2Server).handleData
             google.golang.org/grpc/internal/transport.(*http2Server).HandleStreams
             google.golang.org/grpc.(*Server).serveStreams
             google.golang.org/grpc.(*Server).handleRawConn.func1

这里是操作系统总 CPU % 的分解:

这里是性能分析图的“web”视图:

这里是一个带有 scheddetail=1 的 GODEBUG 采样。

SCHED 87116ms: gomaxprocs=4 idleprocs=1 threads=14 spinningthreads=1 idlethreads=4 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=580546 syscalltick=798347 m=-1 runqsize=0 gfreecnt=0
  P1: status=2 schedtick=570346 syscalltick=805620 m=-1 runqsize=0 gfreecnt=0
  P2: status=1 schedtick=571461 syscalltick=801749 m=4 runqsize=0 gfreecnt=0
  P3: status=1 schedtick=567930 syscalltick=814616 m=3 runqsize=0 gfreecnt=0
  M13: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M12: p=-1 curg=50 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M11: p=-1 curg=19 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M9: p=1 curg=16 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M8: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M7: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M5: p=-1 curg=34 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M4: p=2 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M3: p=3 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=true blocked=false lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=17
  M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  G1: status=3(chan receive) m=0 lockedm=-1
  G17: status=6() m=1 lockedm=1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1
  G18: status=4(GC sweep wait) m=-1 lockedm=-1
  G3: status=4(finalizer wait) m=-1 lockedm=-1
  G5: status=4(chan receive) m=-1 lockedm=-1
  G6: status=4(IO wait) m=-1 lockedm=-1
  G7: status=4(select) m=-1 lockedm=-1
  G8: status=4(select) m=-1 lockedm=-1
  G9: status=4(select) m=-1 lockedm=-1
  G10: status=4(select) m=-1 lockedm=-1
  G11: status=4(select) m=-1 lockedm=-1
  G12: status=4(select) m=-1 lockedm=-1

... 20 entries exactly like above except Go routine number...

  G28: status=4(GC worker (idle)) m=-1 lockedm=-1
  G71: status=4(IO wait) m=-1 lockedm=-1
  G72: status=4(IO wait) m=-1 lockedm=-1
  G73: status=4(sleep) m=-1 lockedm=-1
  G74: status=4(timer goroutine (idle)) m=-1 lockedm=-1
  G41: status=4(select) m=-1 lockedm=-1
  G42: status=4(IO wait) m=-1 lockedm=-1
  G75: status=4(GC worker (idle)) m=-1 lockedm=-1
  G29: status=4(GC worker (idle)) m=-1 lockedm=-1
  G44: status=4(GC worker (idle)) m=-1 lockedm=-1

当使用“trace”时,更容易发现问题,因为“goroutine 分析”显示:

然后进入 startMarketData (示例 A) 函数的详细信息,我们可以看到:

检查同步块时间,我们可以看到:

但是检查调度器等待时间,我们可以看到:

困境

虽然性能分析图清楚地显示了许多操作系统活动,操作系统监视器显示了高 CPU 利用率,但没有简单的方法来分析导致系统 CPU% 高的原因。最大的问题是性能分析图没有按“go routine”分区。使用 trace 工具情况会好一些,但没有汇总视图。理想情况下,详细视图(上面的示例 A)应该显示在所有 go routines 的“分析页面”上——获取这些信息唯一的方法是逐个检查每个 routine。类似这样(作为 issue #29103 提交):

但更重要的是,需要进行汇总分析以了解“为什么”——而不是查看图表,例如,如果我打开上面 start market data 函数的详细信息,它应该显示(仅作示例):

startmarketdata.func1
   network wait time 102 ms:
      5% send on socket
      70% poll
      25% read on socket
   scheduler wait 1 us
      57% wait for GC cycle
      43% wait for CPU
   sync time 50 ms
      67% wait on channel
               100% sent by go routine xxxx
      23% wait on mutex filename:line#
   etc...

用户还可以使用“trace viewer”,它是完全手动的——非常强大,但很难看清森林中的树木。

具体建议

  1. 能够给 Go 协程添加一个“名称”,类似于:
go "myname" func(){...}
      go stringexp func(){...}

与现有命名相比,更难做到这一点——你需要成为所有代码库的所有库的Maven才能对正在发生的事情有一个基本的理解。名称将允许库创建者提供关于其目的的重要信息。这需要附加到现有诊断工具中的现有“名称”。

  1. 作为替代方案,至少应将当前提供的 go 协程名称更改为包括源文件和行号。通常一个函数会生成多个 go 协程,而惯用的方法是匿名函数,因此你最终会得到 func1、func2 等。源/行号会使跟踪这些协程变得更容易。如果程序没有在第1点中提供名称,这也是同样的情况。
  2. trace 输出应该有一个选项来进行每个 go 协程堆栈采样,在采样间隔内将“堆栈跟踪事件”放置在 trace 文件中。可能可以基于时间戳同步捕获性能分析与同步捕获,但简化很多的东西是在单个捕获文件中包含所有内容。
  3. 应该有 Go 协程示例的 trace 报告。目前,使用 trace 时,同一类型的所有协程都被归类在一起。这不允许轻松检测处理瓶颈。例如,如果有4个消耗了15%的协程,是1个消耗了15%,还是3个消耗了1%和1个消耗了14%?
    1.最重要的是,没有公共 API 可以读取 trace 文件以生成自定义分析报告,尤其是当存在一个 API 可以向 trace 添加“用户事件”时。内部/trace 包应该是公开的。目前,信息可能是从“工具 trace webserver”中以 json 形式获得的,但这是未记录的,可能对于某些分析需求来说效率太低。目前的解决方案涉及复制粘贴该包。
yyyllmsg

yyyllmsg1#

这可能需要分成多个问题,但除了在文本中引用“父”事件之外,我没有看到一个简单的方法来将它们分组?

3bygqnnd

3bygqnnd2#

感谢您的报告/问题 @robaho!
请转告 @hyangah@rakyll@dvyukov@aclements

ykejflvf

ykejflvf3#

问题4是错误的。当你追踪一个Go协程时,每个协程都有条目(图表等)。

问题在于这些层次太低了。通过库和应用程序将用户事件放在各个地方可能会有所帮助,但即使这样,分析仍然是手动的。

我感兴趣的是开发一种更宏观的东西,能够理解事件依赖关系并提供更容易理解的性能分析。

hkmswyz6

hkmswyz64#

跟踪器仍在开发中。最近进展缓慢。我们还不清楚何时能实现运行时/跟踪的改进。

我们确实已经记录了创建goroutine的位置(例如,它会出现在panic崩溃输出中)。如果可以很容易地将其添加到跟踪格式中,那么这是值得的。关于良好摘要和摘要呈现的较大问题是困难的。

ipakzgxi

ipakzgxi5#

如果将跟踪解析功能从内部包移到公共包,那么用户就可以开发更高级分析工具。现在你需要复制粘贴代码,随着事情的发展,这会变得脆弱。至少这个问题可以很容易地解决。这对于用户事件(注解)尤其重要,因为几乎肯定需要自定义分析

k2fxgqgv

k2fxgqgv6#

@rsc顺便说一下,我已经在#29103中添加了一些聚合工作,通过查看代码,我可以想到许多非常有用的用于处理这些数据的演示文稿,甚至可以为分析创建一个实时流事件处理器。跟踪系统设计得非常好。

相关问题