本内容是对知名性能评测博主 Anton Putra Go (Golang) vs. Bun: Performance (Latency - Throughput - Saturation - Availability) 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准
我对 Bun 在之前的基准测试中的出色表现感到惊讶,因此我决定将它与 Go 进行比较。在我看来,Go 与 JavaScript 相比属于不同的级别。
在以下内容中,我们将首先使用标准库来比较 Bun 和 Go。我们将使用标准库 API 的 /devices
端点,以 JSON 格式向客户端返回一个硬编码的设备信息。我们将重点关注四个黄金指标。
由于这些是面向用户的应用程序,我们的主要关注点将是延迟,特别是使用 P99 百分位(P99 percentile)。
接下来是吞吐量(Throughput),对于 Web 应用程序而言,这意味着每个应用程序每秒可以处理的请求数量。
然后,我们将通过测量服务的繁忙程度来考察饱和度(Saturation),更具体地说是应用程序相对于 Kubernetes 中定义的限制的 CPU 和内存使用情况。我们还需要测量 CPU 节流(CPU throttling),因为它在 Kubernetes 中扮演着重要角色。当一个服务被节流时,会立即增加延迟并降低整体性能。
最后,我们将通过查看特定时间段内失败请求数相对于总请求数的比例来衡量可用性(Availability)。
现在,大多数基准测试都侧重于综合测试(synthetic tests),但我也想看看这些应用程序在真实世界场景中的表现如何。我在不同的基准测试中使用了不同的用例。在这个测试中,我们将使用 MongoDB 数据库添加一个持久化层。每当应用程序收到一个 POST 请求时,它将解析请求体,生成 UUID,并将该项目保存到 MongoDB 中。
除了标准指标外,我还使用 Prometheus 指标(Prometheus Metrics)对每个应用程序进行了检测,以测量每个应用程序将设备保存到数据库所需的时间。此外,有人建议加入数据库指标,所以我还添加了 MongoDB 自身的 CPU 使用率。在这些测试中,我们将产生足够的负载以找到两个应用程序的崩溃点。
我将这两个应用程序部署到了 AWS 上的生产级 Kubernetes 集群中,为应用程序使用了大型实例,每个实例配备 2 个 CPU 和 8 GB 内存。由于 Bun 在大多数操作中使用单线程,我将每个应用程序限制为使用 1 个 CPU,这可以看作是一个无限重复的周期中 100 毫秒间隔的 100% 使用率,并由 cgroups 强制执行。此外,我分配了 256MB 的内存。我还对应用程序进行了水平扩展,在每个 EC2 实例上为每个应用程序部署了两个实例。
为了产生负载,我使用了 Graviton 实例(它们稍微便宜一些),并为每个应用程序部署了 20 个 Pod。这些 Pod 会逐渐增加虚拟客户端的数量,直到两个应用程序都失败为止。
欢迎提出任何关于如何改进任一应用程序的建议,也欢迎提交 Pull Request,我通常会在一天内合并。
好的,让我们开始吧。
在第一个测试中,我们评估每个应用程序处理 HTTP 请求并将硬编码值以 JSON 格式返回给客户端的能力。在右上方的图表中,我们将测量吞吐量,显示每个应用程序可以处理多少请求。在左侧,我们有延迟,测量响应所需的时间。从一开始你就可以看到,Bun 应用程序处理每个请求需要明显更多的时间。同样重要的是要注意,我们是从客户端测量延迟,以使其尽可能准确。
接下来,我们看饱和度,它表明服务的繁忙程度。你可以看到 CPU 使用率的趋势:Go 使用更多的 CPU 时间,并将首先经历节流。由于我为每个应用程序部署了两个副本,我们测量的是平均 CPU 使用率。在右侧,我们有内存使用率,这只在两个服务都过载时才变得关键。
接下来是可用性。在这个测试中,平均请求完成时间不到 1 毫秒,所以我将客户端超时设置为 100 毫秒。当超过该超时时间时,你会在可用性图表中看到下降。
直到 CPU 使用率达到 60% 到 70%,你不会看到任何节流。在大约每秒 23,000 个请求时,两个应用程序之间的 CPU 使用率差异变得更加明显。到这一点,Go 开始失去其在延迟方面的优势(对于这类应用程序)。一旦 CPU 使用率达到 40%,性能开始下降,延迟增加。
在大约每秒 46,000 个请求时,Go 在可处理的请求数量方面开始落后于 Bun。在每秒 61,000 个请求时,差异变得更加显著。Go 的延迟增加到 10 毫秒,Kubernetes 开始对其进行节流,这影响了性能。另一方面,Bun 保持着低延迟,尽管有些请求开始超时。数量不多,但你会在可用性图表中注意到下降。
在每秒 69,000 个请求时,情况变得很明显,Go 无法处理更多请求,并开始缓存(caching)其中一些请求。
好的,让我们继续。在大约每秒 90,000 个请求时,Bun 也达到了其极限,并被 Kubernetes 节流。
现在,让我打开每个图表以显示完整的测试持续时间。如你所见,测试耗时约 2 小时完成。
首先是每秒请求数。
接下来是客户端延迟。在 Go 于大约 40% CPU 使用率时开始性能下降后,其延迟超过了 Bun,但在此之前,它的延迟要低得多。
接下来是 CPU 使用率。
然后是内存使用率。你可以看到当 Go 无法处理所有请求并开始缓存它们时,内存使用量出现峰值。最终内存使用率达到 100%,Kubernetes 由于内存不足错误(out of memory errors)多次终止了该应用程序。
之后是可用性图表。
最后是 CPU 节流。
如果你正在构建延迟很重要的面向客户端的应用程序,你可能需要考虑 Go,因为在 CPU 使用率达到 40% 之前,它的性能要好得多。另一方面,如果你更关心吞吐量而延迟不那么关键(例如内部微服务),那么 Bun 可能是更好的选择。
现在,让我们开始第二个测试。大多数基准测试使用简单的任务和算法来衡量性能,但实际上,你会严重依赖外部库,并且你的应用程序性能将取决于这些库的开发水平。此外,几乎所有应用程序都需要某种持久化层,例如数据库。
在之前的 Bun 对比 Node 的基准测试中,我使用了 PostgreSQL 关系数据库。在这个测试中,我将其替换为 MongoDB 文档数据库。如果你对 Bun 与 PostgreSQL 的表现感兴趣,可以查看那个视频。
我使用 Prometheus 指标对两个应用程序都进行了检测,这样我们就可以测量每个函数调用的持续时间。在这个测试中,当应用程序收到一个 POST 请求时,它会生成 UUID 并将完整的对象存储在数据库中。你可以在视频描述中找到源代码的链接。我还使用 Prometheus 直方图(histogram)来测量向 MongoDB 插入数据所需的时间。
此外,根据我收到的反馈,我现在也测量 MongoDB 的 CPU 使用率以及其他标准指标。
在这个测试中,Go 的表现明显优于 Bun。客户端延迟和数据库插入延迟都几乎是 Bun 的一半。与 Bun 相比,Go 的 CPU 使用率也低得多。在大约每秒 7500 个请求时,Bun 开始性能下降并丢弃一些请求。Kubernetes 也开始对其进行节流,导致延迟增加。
让我们继续,找到 Go 的崩溃点。在大约每秒 24,000 个请求时,Go 也开始性能下降,CPU 使用率接近 100%。
如你所见,当测试涉及的不仅仅是像上一个测试那样返回静态响应时,Go 在真实世界场景中的表现要好得多。你肯定需要比静态响应更多的功能。根据我的经验和我运行的基准测试,Bun 在静态、综合基准测试中表现非常好,但是当需要执行实际工作(例如与数据库交互)时,Bun 失去了许多优势,甚至在这些情况下 Node.js 的表现都更好。所以我建议在选择 Bun(如果你的目标是提高性能)之前,先测试你的特定用例。
好的,让我展示整个测试期间的每个图表。
首先是每秒请求数图。
然后是客户端延迟。
数据库插入延迟。
MongoDB CPU 使用率。
应用程序 CPU 使用率。
内存使用率。
可用性图表。
(图略)
最后是 CPU 节流。
(图略)