在性能敏感的应用程序中,偶尔遇到程序无故卡顿是开发者们常见的挑战之一。这种现象可能会表现为突然的延迟、系统响应时间增加、吞吐量的下降。这类问题的根源可能是编程语言的垃圾回收(Garbage Collection,GC)导致,尤其是在内存管理上有着较高压力的情况下。
然而,识别和跟踪这些GC事件并非易事。GC发生时,程序线程会暂停执行(如Golang的Stop-the-World,Java的GC暂停),这一操作可能影响到应用的性能表现。在传统的监控方法中,很难准确捕捉到这些事件的开始与结束。为了有效应对这种挑战,基于eBPF的procstat软件提供了一种强大且高效的追踪机制,帮助开发者轻松掌握GC事件的执行情况,尤其是在性能敏感场景下。
背景:GC与程序卡顿
垃圾回收是现代编程语言(如Java、Go)中的核心机制,帮助开发者自动管理内存,释放不再使用的对象。然而,GC并非免费的操作,它需要消耗CPU资源并暂停正在执行的线程以清理内存,尤其是在Stop-the-World的GC模型中,所有工作线程在垃圾回收期间都会被暂停。这种暂停,虽然通常非常短暂,但在高并发、高负载的系统中,即使是几毫秒的GC暂停也可能导致显著的性能抖动。
例如,在一个高频交易系统中,每一次停顿都会影响交易的执行时间,从而造成经济损失。在这种情况下,准确定位和监控GC事件的影响就显得至关重要。然而,常见的性能分析工具往往难以做到对GC暂停的精细追踪,这时,基于eBPF的procstat软件就能够大显身手。
eBPF简介
eBPF(Extended Berkeley Packet Filter)是一种强大的内核技术,最初用于网络数据包过滤。如今,eBPF已经发展成为一种通用的内核编程平台,广泛应用于性能监控、安全审计、网络分析等领域。它的强大之处在于能够在内核中高效、灵活地运行用户定义的代码,同时对系统性能的影响极小。eBPF允许开发者编写自定义的代码,并将这些代码动态加载到内核中,以监控各种系统级和应用级事件。由于eBPF程序在内核中运行,因此它能够以极低的开销实时捕获事件并反馈给用户。
uprobe:用户态函数监控
uprobe 是eBPF提供的一种能力,专门用于监控用户态程序中的特定函数。通过设置 uprobe、uretprobe,开发者可以在目标程序的函数被调用或退出时,自动触发预先定义的eBPF程序,从而捕获该函数的执行情况。其特点有:
- 函数挂钩:uprobe 可以挂钩到任意用户态函数,无论是标准库函数还是用户自定义函数。当该函数被调用时,uprobe 会触发EBPF程序的执行。
- 精确捕获:uprobe 能够精确捕获函数的调用时间、参数、返回值等关键信息。这对性能分析和调试非常有帮助。
- 轻量级监控:与传统的调试工具相比,uprobe 和eBPF结合后的监控方案不会影响程序的功能,对性能影响也很小,尤其是监控不常运行到的异常分支,如本文介绍的垃圾回收,几乎不影响程序性能。
在本文中,eBPF被用来捕获程序的GC事件,通过与 uprobe 结合,实现对程序的垃圾回收操作的实时监控。
procstat软件简介
procstat是一款基于eBPF的监控软件,运行在Linux平台,主要用于跟踪目标程序的运行状态,并报告异常指标,是分析程序性能问题的一大利器。procstat软件能够用于追踪golang及jvm程序的垃圾回收(GC)情况,实时捕捉垃圾回收相关信息。当检测到垃圾回收的时间超过阈值时,procstat会在日志中输出详细的信息,包括回收时长、回收次数等,帮助开发者快速定位问题根源。接下来我们将通过一个小实验来展示一下procstat软件是如何追踪垃圾回收的。此软件可以在以下链接中下载到,并提供免费试用,后续还会有版本更新迭代,使用时需要能连互联网环境。
Github下载链接
GC示例代码
我们通过一个简单的 golang 程序来演示如何使用 procstat 追踪垃圾回收时间。以下是我们的示例程序:
go
package main
import (
"time"
)
func main() {
for {
allocateMemory()
time.Sleep(100 * time.Millisecond)
}
}
func allocateMemory() []byte {
mem := make([]byte, 1024*1024)
mem = nil
return mem
}
这个程序简单地在一个无限循环中每次申请1MB内存空间,然后sleep 100毫秒,之后不再保留这块内存的引用。会触发垃圾回收操作。
使用procstat追踪GC事件
编译并启动上述代码后,使用procstat软件来监控该程序的GC行为。本实验中,编译后的程序名为test。首先,将procstat软件的配置中将gc的阈值设置为0,单位是纳秒,意思是当进程发生gc就会输出日志。
配置设置
配置文件位置在procstat目前下的conf目录中,名为config.json。
cpp
"gc_stat": {
"gc_duration": 0,
"libjvm_path": ""
}
启动被监控的test小程序后,我们可以通过以下命令启动procstat进行监控:sh start.sh -p 进程号。其中,<进程号>是正在运行的test程序的进程ID。
运行程序
cpp
[root@VM-8-2-centos go_gc]# ls
test test.go
[root@VM-8-2-centos go_gc]# ./test &
[1] 3983572
[root@VM-8-2-centos go_gc]#
[root@VM-8-2-centos go_gc]# cd /root/work_dir/procstat/bin
[root@VM-8-2-centos bin]# sh start.sh -p 3983572
Start Loading...!
Start Stating...!
启动监控后(输出"Start Stating...!"后就已开始监控了),procstat会持续监控该程序的运行状态,并在日志中记录时间超过配置文件中设定的阈值的GC操作(时长可配置)。
procstat软件日志
一、接下来我们查询procstat的日志信息,并搜索[GC]关键字。
从上图的日志中可以看出已经成功地捕获到了test进程的GC操作,我们对第1行日志分析一下:
(1)GC发生的时间是12:19:47.664795232,精确到纳秒级别;
(2)发生GC的进程名称是test,进程id是3983572,线程id是3983574;
(3)发生GC的时长是79479纳秒。
二、搜索[GC STAT]关键字。
上图是每秒输出一次的GC统计信息,可以看出已经成功地捕获到了test进程的GC操作,我们对第1行日志分析一下:
(1)发生GC的进程名称是test,进程id是3983572,线程id是3983572;
(2)当秒该线程发生GC的总时长是63115纳秒;
(3)当秒该线程每次的平均GC时长是63115纳秒;
(4)当秒该线程发生的最长的GC时长是63115纳秒;
(5)当秒该线程共发生1次GC;
(6)当秒该线程发生的最长的GC是第1次GC。
总结
procstat 是一个功能强大的工具,通过 eBPF 技术实现了对程序中GC时间的全面追踪。通过本文的介绍和示例,相信你已经对 procstat 有了基本的了解。希望你能在实际工作中充分利用这个软件,提高程序的性能和稳定性。
procstat软件还可以监测很多的程序异常状态,随着eBPF技术的不断发展和procstat软件不断的迭代,希望能够帮助大家定位程序的性能问题和异常问题,进一步提升对软件和操作系统的监控能力,欢迎大家试用,有问题请私信我,共同学习、交流,共同进步!
推荐文章:
基于eBPF的procstat软件追踪程序Offcpu时间
基于eBPF的procstat软件追踪等待锁和持有锁的时间
基于eBPF的procstat软件定位软件死锁
基于eBPF的procstat软件追踪C++ STL容器扩容