RustConf 2023 - Profiling async applications in Rust

https://www.youtube.com/watch?v=8FAdY_0DpkM

好的,我会按照您的要求整理原文内容,保持原有信息不遗漏,并将其翻译成中文。以下是整理和翻译后的内容:

我的名字是Vali Brki,我在JetBrains工作。我们是一家制作专门用于Rust的IDE的公司,但今天我不想谈论这个。我想讨论在Rust中分析异步应用程序,实际上是关于一般的分析。

我想从一个对我们这样为开发者制作工具的公司来说非常令人失望的事情开始。让我们看看这个图表。”你使用什么Rust分析工具?”这是我们多年来在开发者生态系统调查中问的问题,这是最新的调查结果。你可以看到,73%的调查参与者不使用分析工具,只有15%使用与IDE捆绑的分析器。重要的是,这项调查不仅针对我们的客户,而是针对所有人。在我看来,这有点令人失望。

为什么呢?我们应该进行分析。我们应该确保我们的软件确实是高性能的。我们喜欢这么说,但事实真的如此吗?你看,perf只有8%,C green和As green只有4%,这些数字基本上可以忽略不计。

当我们讨论为什么人们不进行分析时,我们通常会听到几个理由或借口:

  1. 它很难。当然,如果你使用像perf这样的工具,你需要学习数百甚至上千个命令行参数,在很多情况下解释结果非常困难。这确实很难,我可以接受这一点。

  2. 现在不需要。他们说现在已经足够好了,性能已经足够了。但是如果你不测量,你实际上并不知道你的软件中发生了什么,这是一个问题。

  3. 这是我真正讨厌的借口。他们真的说,有一些聪明的人最终会为我们分析所有内容,如果我们幸运的话。但是,我不喜欢这种说法。分析不应该那么复杂。

我相信我们应该分析我们的代码。我们喜欢说Rust非常高效,Rust程序极快,但实际上这样说是不够的。重要的是每次都要证明它确实非常高效。如果你看看其他生态系统,你会发现在分析工具方面它们要强大得多。看看JVM、.NET、C/C++或Go,这些语言和生态系统提供了许多真正有用的分析工具。如果我们说我们心爱的Rust是高效的,我们应该能够证明这一点。

我们在JetBrains看到的一个问题是,如果你有更广泛的使用,你最终会得到更好的工具。如果没有广泛使用,你永远不会得到更好的工具。因为我们是这样看的:没人用,没人需要,为什么我们要提供这样的东西?所以这就像鸡和蛋的问题。我们需要你的投入。当你使用分析工具时,无论是外部分析器还是其他工具,请告诉我们。

还有一点我想在这里提到,我想用这次演讲来证明:你知道所谓的帕累托80/20法则吗?我相信在谈论分析Rust代码时,我们实际上可以从1%的努力中获得99%的成功结果。我们不需要成为分析器方面的专家,实际上做分析很容易。这就是我要在这里做的。

目前主要的、最简单的分析方法是采样分析器。这是什么意思呢?我们运行我们的程序,每秒暂停多次,比如每秒100次,探索正在发生什么。主要是查看堆栈跟踪。这意味着我们每秒多次取样,然后查看哪个样本属于哪个函数。结果,我们可以合并每个函数执行的总时间。严格来说,这不是真正的时间,而是每个特定函数执行的样本数,但它与时间非常相似。呈现这些数据的最佳方式是使用火焰图,这是非常著名的方法。

例如,让我们看看这个。我们有这个非常小的例子,获取一个HTML网页。我使用了一个流行的库Ure。这里我在做什么,你可以看到代码,我们只是发出请求,然后将结果转换为字符串,然后计算该字符串的大小。就是这样,非常基本的代码。

让我们尝试使用分析器,在这种情况下,我使用我们自己的分析器,它构建火焰图。有时,人们在运行分析器时会遇到一些问题或问题。例如,如果你为这个特定的程序运行分析器,当然有一个main函数只调用这个Ure fetch函数,我们运行它,我们得到这样的东西,一个非常简单的图片。

你不必看到具体的细节,但它就像那样,几个矩形。然后你第二次运行它,你得到完全不同的东西。这里有问题,你运行相同的程序,你看到不同的结果。这意味着有些东西不对,不应该是这样的。这真的是一个问题,因为如果你多次运行分析器并得到不同的结果,这意味着你得到的不是实际的分析结果。

这里可能的问题之一是所谓的采样频率太小,这是默认值997,几乎每秒1000次。一个选择是调整它,你可以把它设为2000或3000,然后你会得到不同的结果。但这基本上意味着它是一个非常小的函数,运行时间只有毫秒级,这就是为什么采样频率不够。或者,你可以进行迭代,迭代并不总是可行的,在许多情况下,你没有这个机会,但假设对于这个例子我们可以这样做。

如果我们多次迭代,分析器在这种情况下会给你很好的结果。当你看这个非常复杂的图时,很容易找到Ure fetch函数,你可以看到这段代码实际上在哪里,我们在哪里连接到HTTP服务器,在哪里获得实际结果,在哪里将其转换为字符串。例如,如果你仔细看,你会立即看到大约一半的时间是用于通过get adder in four等连接到服务器。所以这不是关于将结果转换为字符串或获取结果,而是关于连接。也许在这个特定的程序中,你必须考虑如何缩短连接时间。但通过分析,你可以找到根本问题,即你程序中最慢的部分。这真的很重要。

让我们看看另一个用于获取请求的库,那就是curl。我们都知道curl,这是一个非常好的命令行实用工具,正在慢慢从C重写为Rust,至少他们已经有几个组件重写为Rust了。这只是一个相当普通的Rust代码,我们使用这个库,我们想发出一个请求,curl给我们一个能力,通过几个块接收结果。你看到中间有一个write函数,然后你也看到在这个版本中,我们将得到的一个块转换为字符串的一部分,然后我们使用连接。

顺便说一下,这个程序中有一个严重的缺陷,我不告诉你问题是什么,但你应该能够理解这里出了什么问题。我不是说这个。在这个例子中,我们只是使用字符串连接。然后还有另一个版本,几乎是一样的,但区别在于我们在一开始就有一个缓冲区,而不是连接字符串,我们是扩展那个缓冲区。这是唯一的区别,分析可以给我们一些信息,告诉我们哪种实现更快,哪种更好。

所以我们有扩展缓冲区版本2,还有字符串连接。开发人员可能会提前思考,哪个更快,是缓冲区扩展还是字符串连接?顺便说一下,我们可以在这里做个实验。请问,谁认为字符串连接比扩展缓冲区更快?请投票。字符串连接更快。好的,我看到一票。那么谁相信缓冲区扩展更快?好的,大多数人。我们开发者确实喜欢做这些预测。我们了解Rust,我们喜欢Rust,我们知道它是如何工作的。

好,让我们运行并分析,看看实际情况如何。当我们运行版本1时,我们会看到这个图片。我们会看到一件重要的事情,这里有大量的线程。我们为什么需要这些线程?它们在做什么?

有趣的是,所有这些线程的主要任务实际上是获取地址信息。如果你看这个图的右边部分,你会看到除了第一个线程外,几乎所有的线程都在做get other info。所以它们发出这个系统调用,然后获取那个URL的实际IP地址信息。我们可以看到这一点。好的,这是我们可以使用火焰图理解的东西。

让我们继续。这是分析器的实际结果。在实际程序中,我们首先运行带有字符串连接的版本,然后运行带有扩展缓冲区的版本。有趣的是,近一半的时间,大约49%,是关于带有扩展缓冲区的版本。实际上我们在这里看到的是,它实际上更慢。另一部分带有字符串连接的版本更快,这是一个问题,这实际上意味着我们的预测是错误的。字符串连接运行得更快,我们不知道为什么。这是我们可以从这张图中学到的东西。

好的,也许我说错了什么。从这个图中你可以看到,main函数的51%被这个扩展缓冲区版本占用。51%超过一半,所以它更慢,对吧?缓冲区版本更慢。

有时,如果我们尝试比较这些东西,我们会看到大部分时间都被connect函数占用,这实际上是一个curl函数,它超出了我们的范围,我们无法对此做任何事情,因为我们不控制那个实现。但是这些结果就像,这里发生了什么?为什么这个实现更快,为什么那个更慢?这只是与curl实现者对话的开始。我们可以带着这些图表去找他们,说:”嗨,这里发生了什么?我们有相同的代码,但为什么这个比那个花费更多时间?”这是一个问题。

在其他情况下,如果我们仔细观察,我们可以看到我们的代码中存在一些问题。例如,在这里我们可以看到,在那个缓冲区版本中,我们花了很多时间来扩展缓冲区。这意味着我们提供给R的原始信息不够。例如,我们可以做这样的事情:如果我们为那个实现保留足够的缓冲区,足够的缓冲区大小,它会更快。简单的解决方案就是设置缓冲区的大小,这使得这个实现更快。

好的,但我答应过要谈论异步代码。在进入异步之前,我实际上会尝试使用第三个库,第三个crate,叫做request。它在技术上是异步的,但它有这个阻塞接口,所以我们可以像这样运行它:使用阻塞接口为request,我们使用get,然后我们可以将所有内容转换为文本,然后计算结果的大小。

如果你这样做,你会立即看到问题。你看到右边那个小矩形了吗?那是我们的函数request blocking Fetch,问题是我们不知道在这个火焰图的所有其他部分发生了什么。我们从未为那些部分写过代码,我们实际上在那里看到的是我们的运行时机器。技术上它是Tokyo,被request在幕后使用。

分析这个异步代码的问题是,Tokyo本身发生了一些事情,我们不知道,我们不控制它。我们只调用了一个函数request blocking get,结果是这个函数真的很小,它占整个执行时间的177%,其他所有东西我们都不知道。

所以在分析异步代码时,我们确实遇到了一些问题。一个问题是堆栈跟踪通常要长得多,因为你有所有这些异步的东西。我们不知道真正的问题在哪里,因为我们不为Tokyo本身写代码,它只是在幕后的某个地方。我们不知道执行的顺序,因为异步运行时对我们隐藏了一切,所以即使使用火焰图我们也看不到。而且有很多线程,这和curl的情况不一样,那时所有线程都在做同样的事情。这里情况不同,那些线程只是在做一些Tokyo的工作,但我们不知道是哪一个。这就是问题所在。

一个好消息是,如果你的程序完全是异步的,所以你有一个主函数,它是Tokyo main,是异步的,你使用几乎相同的版本,只是有几个await调用,那么你就会看到事情是如何发生的。如果你分析这个特定的异步main版本,你会看到大约一半的时间 - 再次看右边那个矩形 - 是我们的异步request fetch函数,但仍然有一半的时间只是一些Tokyo的工作。再次强调,我们不知道那里发生了什么。

这种情况使得分析异步应用程序变得更加困难。我们可以使用传统的分析方法,如火焰图,它可以给我们一些信息,但这不是全部信息。所以我们需要一些不同的东西。

在进入真正不同的东西之前,我会向你展示一个我非常喜欢的非常小的库,它叫做diagnosing futures。它真的很简单,想法是将每个对异步的调用,对future的调用,对await的调用,都包装在这个diagnos函数中。我们有futures,但在实际执行它们之前,你运行这个包装器。这不是你在Rust代码中经常做的事情,像用其他调用包装,但你可以明白其中的要点。

当我们开始分析这个例子时,我们在这段代码中有两个await部分。这意味着我们有两个futures,我们可以实际分析这些futures。当你运行这个带有diagnos调用的程序时,你会得到一个特殊的JSON文件,包含分析信息。你可以在Chrome中查看该分析信息,还有一个特殊的推荐UI,叫做perfecta。从那个JSON文件中,你可以得到类似这样的东意图:

有几个线程,如果你看到那个三角形,意味着唤醒一个future,而那些垂直线是实际执行这些futures。通常在异步代码中,我们多次执行,直到future的结果准备好被处理。所以我们唤醒future,然后我们多次执行它,直到它准备好。

如果你把它放大一点,你会看到类似这样的东西:那些矩形,它们在前一张幻灯片中是垂直线,实际上是在执行futures。所以你基本上会看到一个矩形,然后箭头指向另一个矩形,这意味着执行,再次执行,再次执行,一旦准备好,我们就转到另一个future。所以你可以看到这些futures之间的特定信息流。

这真的很重要。实际上,我们在这里看到的是一种范式转变。对于异步代码,我们不再进行采样分析,而是我们做的是跟踪。我们跟踪我们程序中发生的事情,然后我们在运行时生成这些跟踪,然后我们分析它们。所以这就像是对火焰图的一个很好的补充,这就是我们在这里得到的。

future diagnos这个crate给了我们一个非常简单的例子。我不确定它是否应该在生产环境中使用,但我们还有更多。例如,我们有所谓的Tokyo console工具,这是另一个例子。

如果你想使用它,你需要在你的程序中进行插桩以收集诊断数据。你基本上只需要一堆属性宏,说明这个应该在运行时被跟踪。然后你运行你的程序,交互式地显示和探索数据。如果你为插桩后的程序使用这个工具,你可以得到类似这样的东西。

Tokyo console给你提供了关于Tokyo任务的信息。这种方法的一个问题是,它真的是交互式的,交互式程序中的事物变化非常快。所以在许多情况下,你可能无法从看这个工具中看出发生了什么。但当然,它非常强大,你可以探索这些任务,你也可以查看特定的资源。例如,在这里你可以看到这些futures被执行了多少次。通常,执行次数越少越好,因为没有得到结果的执行实际上会消耗大量的CPU时间。所以我们不希望有那种情况。

这是一种分析方法,但问题是,当你交互式地做这个时,并不总是可行的很快就获得实际信息。我们不够快,无法在运行时处理所有那些信息。所以我们用不同的方式做一些事情。

我们实际上使用相同的跟踪数据,我们可以通过例如OpenTelemetry将它发送到某个地方。所以我们不是在运行时交互式地查看,而是将数据发送到某个地方,然后我们也在运行时使用一些OpenTelemetry收集器收集数据,比如在我的经验中,honeycomb很不错,或者Jaeger也很不错。你只需收集你的数据,你可以和你的程序一起运行它,例如用一个Docker容器。所以它收集所有东西,然后你可以稍后处理所有那些数据。根据特定OpenTelemetry收集器的能力,你可以获得一些指标,你可以获得图表,你可以获得很多东西。

所以这就是想法。不是做采样分析,而是在运行时收集所有信息,然后将该信息发送到收集器,然后稍后处理它。这种方法在Web应用程序中已经广为人知多年了,但在你想做分析时并不那么为人所知。但技术上,这是同一件事。你不必编写Web应用程序就可以以这种方式进行分析,通过跟踪运行时程序中发生的事情。Tokyo正好给你提供了你需要了解发生了什么的信息。

所以当我们分析异步应用程序时,我们应该记住,它们通常用于负载不稳定的情况。比如这一刻你有很多请求,一分钟后你有少得多的请求。所以这会给我们关于程序在运行时实际行为的一些错误信息。在某些服务器机器上运行该程序是有用的,使其更接近真实的生产环境。拥有一些稳定的负载也是有用的。所以当你分析代码时,你只是生成一些稳定的负载,以确保一切都类似于某些生产环境。

例如,为了生成那个负载,你可以使用Goose。Goose是一个用Rust实现的非常好的工具。所以我非常喜欢它。它只是给你一个机会来描述你的负载,然后它生成它。它可以以稳定的速率做这个。所以你只是创造了一个非常好的、几乎像生产环境的环境。它当然有自己的报告系统,但我们可能对此不感兴趣,因为在此期间我们生成了那些我们稍后会分析的跟踪。

在这个设置中使用IDE分析器也是有用的。如果你有一些远程开发能力,例如,你只是从服务器机器启动你的代码,你在那里运行IDE,这是可能的,比如在我们的IDE中。你可以分析火焰图,同时你生成所有那些用于分布式跟踪的跟踪,并将数据传输到收集器。然后作为结果,你得到关于你程序的完整信息。

所以再次强调,我们可以分析火焰图,这是我们应该经常做的事情,因为这给我们提供了关于我们代码的正确信息。但然后我们也应该使用这种跟踪方法,生态系统正在慢慢向这个方向发展。

不幸的是,这种方法有一个问题:没有真正的工具来非交互式地分析性能数据。Tokyo console的交互式方法没问题,但没有分析器来分析我们运行程序后得到的信息。这是整个社区和生态系统应该考虑的问题。也许我们在JetBrains会考虑这个问题,我不能保证,我不知道,但这对整个社区来说是一个问题,我们应该如何分析这些数据以使其更容易。

我的主要观点是,我们的共同目标是让每个人都更容易进行分析。这样我们就不会有那种不幸的情况,只有15%的人进行分析。不,我们所有人都应该这样做,因为我们足够聪明,可以分析我们的代码,并向外面的所有人证明我们实际上是一个成熟的生态系统,产生可证明高效的代码。

今天我就说到这里。非常感谢。请来我们的展位,我们在4:15,一个小时后,有一个测验。有许多有趣的Rust问题,其中一些实际上相当困难,但你可以得到书籍。所以请来参加,从我们这里获得一些奖品。再次非常感谢。

文章目录