在 profiling 方面,Go 是独一无二的。在 runtime 里面它包含强大的,有自主意识的 profilers。其他编程语言像 Ruby,Python,或者 Node.js,包含了 profilers 或者一些 APIs 接口用来写 profiler。但与 Go 提供的开箱即用的服务相比,它们的范围还是有限的。如果你想学习更多关于 Go 提供的可观测性工具,我强烈建议 Felix Geisendörfer 的 忙碌的开发人员指南 - Go 性能分析,追踪和可观测性
作为一个好奇的工程师,我喜欢钻研底层的实现原理,并且我一直希望能学习 Go CPU profiler 是如何实现的。这个博客就是这个学习的过程。毫无例外的,我总是能在我阅读 Go runtime 的时候找到并学习新的知识点。
基础知识
有两种类型的 profiler :
追踪型:任何时候触发提前设定的事件就会做测量,例如:函数调用,函数退出,等等
采样型:常规时间间隔做一次测量
Go CPU profiler 是一个采样型的 profiler。也有一个追踪型的 profiler,Go 执行追踪器,用来追踪特定事件像请求锁,GC 相关的事件,等等。
采样型 profiler 通常包含两个主要部分:
采样器:一个在时间间隔触发的回调,一个堆栈信息一般会被收集成 profiling data。不同的 profiler 用不同的策略去触发回调。
数据收集:这个是 profiler 收集数据的地方:它可能是内存占用或者是调用统计,基本上跟堆栈追踪相关的数据
其他 profiler 如何工作的小调研
Linux perf
使用 PMU
(Performance Monitor Unit)计数器进行采样。你指示 PMU
在某些事件发生 N 次后产生一个中断。一个例子,可能是每 1000 个 CPU 时钟周期进行一次采样。Denis Bakhvalov 写了一篇详细的文章,解释了像 perf
和 VTune
这样的工具如何使用 PMU 计数器来实现。一旦数据收集回调被定期触发,剩下的就是收集堆栈痕迹并适当地汇总。为了完整起见,Linux perf
使用 perf_event_open(PERF_SAMPLE_STACK_USER,...)
来获取堆栈追踪信息。捕获的堆栈痕迹通过 mmap'd
环形缓冲区写到用户空间。
pyspy
和 rbspy
是 Python 和 Ruby 著名的采样分析器。它们都作为外部进程运行,定期读取目标应用程序的内存,以捕获运行线程的堆栈追踪。在Linux中,它们使用 process_vm_readv
,如果我没记错的话,这个 API 在读取内存时,会让目标程序暂停几毫秒。然后他们在读取的内存中跟踪指针,来找到当前运行的线程结构和堆栈追