透视图-利用性能问题的基本特征进行自动性能诊断

编者按:Perspect是一款创新的性能调试工具,由珍妮·伦和团队。它引入了一个称为关系调试的新概念。关系调试的关键思想是分析运行时事件之间的“关系”,并使用关系来解释性能问题。在她身上OSDI’23纸张珍妮将性能问题与物理学中的相对运动进行了类比。

“就像对象的速度是一个依赖于参考框架的相对度量一样,从程序执行期间的不同运行时事件来看,性能也是如此。通过将适当的运行时事件确定为参考框架,并分析与这些相关的度量的变化,可以揭示性能问题的根本原因良好运行和不良运行(性能异常)之间的事件(即关系)。

我们邀请Jenny写下她开发Perspect的故事,并解释关系调试的简单而优雅的思想。

动机:缺乏诊断性能问题的工具

在研究Linux核心功能的性能演变时,我手动诊断了许多导致性能问题的代码更改。典型的工作流程如下:(1)分析执行并查找减慢速度的代码区域,以及(2)了解是什么导致了减慢。对于易于诊断的更改,通常只需要执行步骤(1)。对于更困难的情况,我在步骤(2)上花费了大量手动工作,以便将速度减慢与代码中的根本原因联系起来。我发现没有现成的工具可以帮助我自动化诊断过程。

传统诊断技术对性能问题无效

在我决定开发一个新的工具来诊断性能问题之前,我的第一个想法是重用或扩展诊断功能故障的技术,例如统计调试或Kairux(SOSP'19)。我们可以将这些技术应用于性能诊断吗?不幸的是,答案并不是肯定的——它们在解决困难的性能问题时表现不佳。

Go-909的故事

让我们看看去-909作为一个例子。Go-909导致了严重的内存泄漏,并影响了许多工作负载。在Go-909的问题单中,一位开发人员报告说,他们发现64位Go运行时正常执行相同的工作负载,但32位Go的运行时内存不足。奇怪!Go-909被认为是诊断方面最困难的性能问题之一;直到一年后,通过试验和错误诊断,才确定其根本原因。

根本原因是Go-909中垃圾收集器(GC)的版本“不精确”——它没有区分指针和常量。因此,常量值可能会被误认为指针,如果该值恰好是指向对象的有效地址,则该对象将永远不会被回收。在32位go运行时中,由于内存布局,许多常量碰巧“指向”有效对象,导致严重的内存泄漏。64位Go运行时实际上并非没有bug,但内存泄漏发生的频率较低。

那么,为什么Go-909很难诊断?根本原因在两次执行中都被触发,但只在32位Go运行时发生的频率更高,在正常运行中没有发生在错误运行中的运行时事件,反之亦然。这一事实削弱了现有的诊断技术,这些技术假设根本原因只存在于坏的运行中,而不存在于好的运行中。因此,使用不变量或绝对的谓词。

我们在许多其他硬性能问题中观察到了这种模式——根本原因只能用相对的术语来表达。因此,Perspect的设计使用了根本原因的相对表示,称为关系一种关系,表示为`R(A|B)`,是每个元素代表执行过程中发生的每个事件B的分布,即发生的因果相关事件的数量As。

“关系”如何帮助绩效诊断?回到Go-909,Perspect在观察64位和32位执行之后,使用简单的关系捕获其根本原因。Go-909中的GC算法使用标记和扫描算法:标记阶段从堆栈和数据段扫描指针,并标记这些指针指向的对象。扫描阶段回收尚未标记的对象。Go-909的根本原因是GC算法标记了任何指针都无法访问的对象,因为它们的地址存储在常量中。通过编程,可以将这些常量与实指针区分开来,因为实指针由malloc返回,而常量则不返回。当Perspect分析执行时,它发现常量对malloc的返回值没有数据流依赖性,而实际指针则有。因此,Perspect返回以下根本原因:

64位运行时R(malloc_return|mark_object)=0.99%R(malloc_return|mark_object)=32位运行中的0.01%

它指出,在64位运行中,99%的标记对象可以从实际指针访问,而在32位运行中,只有1%的标记对象可以从实际指针访问!因此,32位运行中的大多数标记对象都是本应回收的不可访问对象。

难怪会出现严重的内存泄漏! 

将视角扩展到复杂的现实世界程序

Perspect算法的目标是确定适当的参考事件和因果相关的症状事件,其中这两个事件在良好和不良运行中的关系变化捕获了性能问题的根本原因。Perspect的直接算法只需枚举任意两条指令的事件之间的所有可能关系。但这个基本算法具有组合复杂性!Perspect聪明地使用搜索算法来解决这个组合问题。它只遍历程序一次——从程序入口点开始,以性能问题的症状结束。在每个步骤中,Perspect都会在当前指令和症状指令之间建立关系。如果某个关系没有更改,则将其丢弃,否则,Perspect将检查关系R1中的更改是否由另一个关系R2引起,如果是,则将R1细化为R2。让我们看一个例子。下面是GC算法的简化代码片段。

gc(){...标记();扫描();}标记(){如果(!对象标记)obj.marked==真;}

在Go-909的情况下,在32位运行中标记了更多对象,导致内存泄漏。在32位运行中,R(obj.marked==true|gc())增加,这意味着给定每个GC循环,标记的对象越多。Perspect将此关系细化为R(obj.markd==true|mark())因为R(标记()|gc())==1-每个GC循环总是调用mark()一次。换句话说,这意味着标记更多对象的原因与gc()和mark()之间执行的逻辑无关;根本原因在于mark()。虽然在这个例子中这看起来微不足道,但Perspect使用这个规则来消除复杂程序中许多不相关的根本原因候选者。

在上述高效算法的基础上,Perspect的实现也进行了大量优化,这使得Perspect能够在10分钟内确定我们评估的大多数实际性能问题的根本原因。一个优化是,Perspect的静态分析阶段试图最小化性能症状的静态依赖关系图:它跳过了无法影响性能症状但分析成本高昂的因果前驱,例如循环。Perspect还可以动态分析指针,因为静态指针分析既昂贵又不精确。Perspect还可以跨多个服务器进行并行化。

评估Perspect的真实性能错误

作为重点,我们使用Perspect诊断了两个打开MongoDB中的错误,其根本原因未知。Perspect成功查明了MongoDB开发人员确认的根本原因。

[Perspect的结果]将所有的部分结合在一起,形成了一个很好的解释。“--MongoDB开发人员

总之,我们将Perspect应用于12个实际性能错误,Perspect能够诊断出其中10个错误的根本原因。对于Perspect无法诊断的一个错误,其根本原因是Linux内核;在这种情况下,Perspect成功地从应用程序代码中排除了根本原因。对于另一个bug,Perspect是无效的,因为源代码在好的运行和坏的运行之间变化太多。请注意,Perspect能够容忍适度的代码更改,并在不同源代码版本之间匹配指令。例如,Perspect成功地查明了Mongodb-44991的根本原因,其中性能问题是在较新版本中发生的回归。 

作为未来的工作,我们计划将Perspect的关系调试算法应用于更多不同的用例。目前,Perspect被设计为一个在单个机器设置中工作的脱机工具。我们对应用关系调试来诊断分布式系统中的性能问题感兴趣。此外,可以通过增量运行算法来减少关系调试的开销,因此可以部署它来在线诊断性能根源。

Perspect的源代码位于:https://gitlab.dsrg.utoronto.ca/dsrg/perspect网站

关于作者向(珍妮)任丁元是多伦多大学电气与计算机工程系(ECE)的博士生。她对提高软件系统的性能和可靠性感兴趣,包括操作系统、数据库系统和分布式系统。她在学术人才市场上.

免责声明:本博客中的任何观点或观点都是个人的,仅属于博客作者,并不代表ACM SIGOPS的观点或观点。

编辑器:徐天音(伊利诺伊大学香槟分校)和董都(上海交通大学)