遇事不慌,先问ChatGPT
小王提问“如何定位性能问题”,ChatGPT 给出了3个答案。
第二个与第三个答案是本质同样使用时间模块记录程序函数的运行时时间,而这对于开发者来说,这可能是最为熟悉的方式,如下所示,使用4条Log 来定位性能瓶颈出现的性能范围,但这对于线上服务来说,不断地Log定位方式也意味着不断地重启,风险是可能带来不断地业务损失,但是更坏的一个问题是如果线上产生死锁等问题,重启也意味着”案发现场“的丢失,这也为下次问题的再次出现埋下隐患。
2023-03-17 15:55:01 [INF] outer function start 2023-03-17 15:55:10 [INF] inner function start 2023-03-17 15:55:13 [INF] inner function end 2023-03-17 15:55:30 [INF] outer function end
而ChatGPT 的首推答案也指出可以使用性能分析工具来帮助开发者定位瓶颈问题,随着软件工程的演进,性能分析工具的地位也愈加上升,对于一些新出现的编程语言,如Golang,性能诊断已经成为语言标准库的一部分。
什么是性能采集?
对于前端开发者来说,Chrome 浏览器中的性能分析可能是最为直观的性能采集,下图中Chrom记录加载网页每个事件的时间戳和持续时间,包括JavaScript函数执行、网络请求和渲染过程等。而这些数据也为前端开发者寻找网页性能瓶颈与定位优化点提供了依据。
以下内容将介绍常见的CPU 与内存 性能采集的基本原理,了解为什么性能采集是“统计值”而不是精确值。
CPU 性能采集基本原理
以CPU 性能Perf采集为例,如下图所示,实线代表某单核CPU 的持续运行所经过的时间,而pid/func1、pid/func2 以及pid/func3 表示在此段运行时间内此单核所经过的函数,当每隔一段时间产生一个中断(箭头处),此单核CPU必然执行于某一具体的函数。对于下图来说,可以产生如下统计结果,根据命中率计算可知,在采集时间段内pid/fun3花费了此核CPU的62.5%。对于一般的程序CPU 中断的间隔一般为100ns,从上述基本原理可知,当CPU 采样时间越大,中断间隔忽略的函数越多,因此CPU 性能采集是统计结果,可能存在误差。
函数 |
中断命中次数 |
占用CPU比率 |
pid/fun1 |
1 |
6.25% |
pid/fun2 |
5 |
31.25% |
pid/fun3 |
10 |
62.5% |
内存性能采集基本原理
与 CPU 性能采集原理不同的是,内存性能数据因为要拿到具体的分配数量,因此内存性能采集的通常做法是将工具集成到内存的分配器中,当语言进行内存分配时,拿到当前的函数调用栈,最终将样本聚合,便可推算出函数的内存分配了。以下代码为GO语言的内置Profiling行为,当开启Profling后,每次功能mallocgc 分配512K大小内存,便触发一次profilealloc函数记录分配行为。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if rate := MemProfileRate; rate > 0 { // Note cache c only valid while m acquired; see #47302 if rate != 1 && size < c.nextSample { c.nextSample -= size } else { profilealloc(mp, x, size) } } ... } func profilealloc(mp *m, x unsafe.Pointer, size uintptr) { c := getMCache() if c == nil { throw("profilealloc called without a P or outside bootstrapping") } c.nextSample = nextSample() mProf_Malloc(x, size) }
如下图所示,实线代表某单核CPU 的持续运行所经过的时间,而pid/func1、pid/func2 以及pid/func3 表示在此段运行时间内此单核所经过的函数,每个箭头处表示一次内存分配,但由于默认512K记录一次分配行为,因此可以产生如下统计结果,第一次与第二次malloc行为因为分配不满足512K记录大小规则,因此跳该过记录,如将MemProfileRate设置为1,可以完全精确的记录每一个函数的内存分配,但是性能开销带来的成本是巨大的,因此内存性能采集也同样是统计结果,可能存在误差,因此Profling程序一般并不是采用上述512K的固定值进行,而是以512K为平均值的指数分布中随机取一个值,降低误差影响。
函数 |
分配次数 |
pid/fun1 |
1 |
pid/fun2 |
1 |
pid/fun3 |
2 |
为什么需要持续性能分析?
性能采集带给开发者分析程序性能的工具,开发者可以清楚的了解性能采集时刻的CPU 或内存的分布,通过分析这些数据,开发者可以快速定位程序性能瓶颈问题。但现实情况可能更加复杂,应用的部署结构已经从单体结构走向微服务结构,并且随着云化浪潮,部署也存在着多云、多Region、多架构机器等诸多行为。因此宏观的角度分析服务的性能变得更加重要,这也就意味着性能采集多了一个新的维度“时间”,当有了时间维度,性能采集便成为了持续的性能分析,这也意味着分析从单体分析变成了高纬度的对比分析,也让以下场景的性能分析成为了可能。
场景 |
性能采集 |
持续性能分析 |
对比某服务测试环境与生产环境的性能差异 |
N |
Y |
对比某服务版本1.0 与版本2.0 性能差异 |
N |
Y |
对比某服务在Arm 服务器与Amd 服务器性能差异 |
N |
Y |
对比某服务在北京地域与杭州地域服务的性能差异 |
N |
Y |
...... |
SLS 持续性能数据采集与分析
SLS 持续性能分析为开发者提供开放、一站式性能持续分析平台,目前已经对于Pyroscope 开源SDK 或Agent 完全兼容,后续SLS 持续性能分析会持续拓展接入兼容性,如支持对原生Golang Profile接口的采集或对Datadog Profling SDK 等接入的兼容。
性能数据存在着不同的协议,如pprof、JFR、Tire 等,不同协议对性能数据的承载能力不同,如Tire 协议一般仅承载CPU 性能数据,而pprof 可承载cpu、memory、mutex、goroutines 等多种性能数据,下文将使用基于pprof 协议的Go 语言样例介绍SLS持续分析的功能。
类型 |
Go |
Java |
NodeJs |
Python |
CPU时间 |
是 |
是 |
是 |
|
堆 |
是 |
是 |
是 |
|
分配堆 |
是 |
|||
争用 |
是 |
|||
线程 |
是 |
|||
实际时间 |
是 |
是 |
是 |
数据探索
在SLS 持续性能分析的数据查询界面,开发者可以选择关注服务性能类别与时间后,整个数据查询探索界面会通过火焰图与表格两种方式显示性能数据,下图展示了Go的一个demo 程序在CPU性能分析下的样例。但对于持续性能分析更重要的是价值是更高维度的数据探索,因此SLS 持续性能分析支持通过动态标签组合满足开发者对性能数据的高纬度探索需求,如下图所示的性能查询条件为进行sls-mall 集群下、test环境、version1版本的性能分析。
对于持续性能分析,时间维度的加入也就意味着可以进行更多的性能数据的对比,在在SLS 持续性能分析探索界面,开发者可以轻松的选择对比性能的时间范围,通过火焰图或者表格方式进行数据探索,如下图为对比Go Demo 程序过去一小时性能与当前性能的差异,通过灰色颜色可以快速得知,服务性能没有明显变化。
- 灰色: 代表性能性能相近
- 红色:代表性能下降,颜色越深代表性能下降越严重
- 蓝色:代表性能提升,颜色约深代表性能上升越多
当然性能对比界面也同样支持利用动态标签组合的方式满足满足开发者对性能数据更高的对比需求,如下图展示了相同服务不同版本的性能差异。
优化案例
小王公司待优化的服务为一个gRPC服务,在限定资源(CPU限制为250m核,内存限制为50M),小王使用日志方式打印了压测下此服务待优化接口性能,发现QPS 仅仅在30左右。
CPU 优化
- 定位问题
小王将此服务接入了SLS 持续性能分析发现有一个内部库函数regexp.MatchString消耗了应用程序的大量CPU性能。
- 分析问题
小王发现此程序的业务逻辑为计算存储文件行包含输入字符串的数量,使用regexp.MatchString进行匹配,但此行为并未涉及复杂的正则提取,字符串包含操作已经可以满足业务需求,且相比于正则匹配,字符串匹配的算法复杂度更低,因此小王进行了下面的优化。
- 性能对比
小王将此优化版本标记版本为version2,通过SLS 持续性能对比界面分析此次"小" 改动的带来的性能差异,下图展示了 version :1与 version:2(CPU 优化后)的差别,浅蓝色代表部分优化,深蓝色代表全部优化,小王发现经过对正则表达式的优化,上层函数耗时竟然下降77.29%。
- 验证优化
小王再次打开自己最初打印的QPS 统计日志,发现同等资源条件下,程序10次平均QPS 提升到1399左右,接口性能得到接近40倍的性能提升,小王出色的完成了公司提出的降本增效任务。
内存优化
- 定位问题
小王出色的完成了公司降本增效的认为后,公司领导很高兴,提出能不能把程序的内存也优化一下。小王公司的程序为Go程序,GO的内存性能指标分为alloc_objects、alloc_space、inuse_objects以及inuse_space。alloc_*指标为从程序启动到目前分配的大小或数量,可以帮助定位频繁分配对象的方法,降低Go程序GC 压力,inuse_* 方法是内存中存在的对象大小或数量,可以帮助定位内存泄漏等问题。小王继续通过SLS 持续性能分析来分析上述4类指标。小王通过对不同指标的查看,发现服务alloc_space指标存在异常,大量内存分配行为都存在于标准库strings.split函数。
- 分析问题
小王通过对业务逻辑的梳理,基本逻辑为从本地文件读取文件全量数据,之后按行进行切割,接着与输入进行字符串进行匹配比较,按行切割时存在大量小字符串分配,通过slice 偏移量截取字符串对比,可以避免大量小字符串的分配与释放,因此小王进行了如下优化。
- 性能对比
小王将此优化版本标记版本为version3,通过SLS 持续性能对比界面分析此次改动的带来的内存性能差异,下图展示了 version :3与 version:2(CPU 优化后)的差别,浅蓝色代表部分优化,深蓝色代表全部优化,小王发现经过对本次字符串优化,核心函数空间分配下降59.46%。
小王推测内存分配空间的减少也就意味对象分配时间与GC 时间的减少,因此推测本次优化可能还会带来性能的提升,因此继续使用CPU性能对比查看CPU 性能的变化,小王发现CPU 性能确实得到了19.71%的性能提升。
- 验证优化
小王再次打开自己的QPS 统计日志,发现同等资源条件下,验证第二次性能优化的结果,程序10次平均QPS 提升到1840左右,性能再次获得了30%的提升。
优化回顾
上述案例中小王的2次优化,第一次是对正则的优化,第二次是对字符串使用的优化,都是对标准库的使用优化,这样的问题可能也潜藏在每一位开发者的程序中,通过SLS持续性能分析的查询、聚合、对比能力,可以快速的定位应用程序潜藏的顽疾。
展望
SLS性能持续分析基于开放的接入生态与持续性能分析的理念所构建(开放接入部分已在iLogtail开源),基于SLS 性能持续分析,将为广大开发者提供开箱即用、一站式的的性能观测体验,助力开发者轻松面对多云、多Region、多版本、微服务等场景下的性能分析需求,目前已经发布部分地域, 欢迎大家试用与交流。在后续的版本中,我们将为开发者提供更为便捷、丰富的性能监控接入渠道,更强大的性能分析体验。
- 更多开源Agent与SDK 兼容,如Datadog 等。
- 更多无需集成SDK 的接入方式,如Golang下的主动抓取采集等。
- 在交互分析上,提供更便捷、性能更高的迷你图划线对比。
- ...
参考文档
Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers