为了节省您的宝贵时间,直接上结果。
这篇文章是关于多个现代 编程语言 的并发性能对比,对比意义有助于您选择技术栈构建现代多人网络服务,我们将在其中讨论现代编程语言中的并发性。首先将构建一个并发 Web 服务器 ,然后对其进行基准测试,该服务器的灵感来自 Rust 书中 的示例,使用的是 Rust、 Go 、JavaScript ( NodeJS )、 TypeScript (Deno)、 Kotlin 和 Java 等流行语言,来比较这些语言/平台之间的并发性及其性能。(书中例子如下链接)
什么是并发
并发性是编程中最复杂的方面之一,根据您选择的语言,复杂性可以从”看起来令人困惑”到”这是什么黑魔法呀”。
并发性是指可以在重叠的时间段内以特定顺序执行多个任务而不影响最终结果的能力。并发是一个非常广泛的术语,可以通过 多线程 、并行性和/或异步处理来实现。
基准测试和比较
我们尽可能保持简单,而不是尽可能多地使用外部依赖项。首先在各种语言中保持了相似的代码。在这篇文章中,我们将比较所有这些实现的性能,以了解哪种语言为并发 Web 服务器提供了最佳性能。
如果语言同时支持异步和多线程并发,我们将尝试两者以及两者的组合,并选择表现最佳的进行比较。因此,应用程序的复杂性将取决于语言功能和语言复杂性。我们将使用语言提供的任何内容,以使并发性能尽可能好,而不会使事情过于复杂。Web 服务器将只为一个终端节点提供服务。
如果需要并且语言支持,我们将使用Promise, 线程池 和worker。我们不会在应用程序中使用任何不必要的东西 I/O。
代码实现可能不是最好的;如果您有改进建议,请在本篇文章评论。进一步的改进包括:
- 将线程池用于 Java 多线程版本
- 使用 Java Web 服务器库
- 使用 createReadStream for Node.js
- 添加了一个 Rust actix-web 示例来进行比较
免责声明 :并不是说这是一种准确的科学方法或并发的最佳基准。我们很确定不同的 用例 会产生不同的结果,并且实际的 Web服务器 将具有更高的复杂性,需要并发进程之间的通信,从而影响性能。我们只是试图为一个简单的用例提供一些简单的基础比较。此外,我对某些语言的了解比其他语言更好。因此,可能会在这里和那里错过一些优化。如果您认为可以开箱即用地改进特定语言的代码以增强并发性能,请告诉我。如果您认为此基准测试无用,那么,请建议一个更好的基准:)
基准测试条件
这些将是我将用于基准测试的一些条件。
- 使用可用语言/运行时的最新稳定发行版本,在撰写本文时:
Rust: 1.58.1-Stable
Go: 1.17.6
Java: OpenJDK 17.0.2
Node.js : 17.4.0
deno: 1.18.1
.NET: 6.0.100
- 仅当外部依赖项是该语言中的标准推荐方式时,我们才会使用外部依赖项。在撰写本文时,将使用此类依赖项的最新版本
- 我们不会考虑使用任何配置调整来提高并发性能
- 更新 :许多人指出, ApacheBench 并不是这个基准测试的最佳工具。因此,我还包括了 wrk 和drill的结果
- 我们将使用ApacheBench进行具有以下设置的基准测试:
1.100 个请求的并发factor
2.总数为 10000 个请求
3.本次基准测试将针对每种语言进行十次warmup,并采用平均值。
4. Apache Bench 版本( Fedora ): httpd-tools-2.4.52-1.fc35.x86_64
5.使用的命令: ab -c 100 -n 10000
- 所有基准测试均在运行 Fedora 35 的同一台计算机上运行,该机器采用英特尔 i9-11900H(8 核/16 线程)处理器,内存为 64GB。客户端从同一网络上的另一台类似的计算机运行,也从同一台计算机运行。结果或多或少是相同的;我使用客户端计算机的结果进行比较。 wrkdrill
比较参数
我还将比较以下与并发相关的方面。
- 性能,基于基准测试结果
- 社区共识
- 易用性和简单性,特别是对于复杂的用例
- 用于并发的外部库和生态系统
基准测试结果
更新 :我已经用wrk,drill的结果更新了基准测试结果,并在各种人建议的调整后更新了ApacheBench的先前结果。
更新 2 :现在存储库中只有一个 .NET 6 版本,这要归功于 PR 的 srollinet。基准测试更新了 .NET 结果。
更新 3 :使用 actix-web 和 Java undertow 的 Rust 现在包含在 和 基准测试中。这些实现被简化为仅返回一个 字符串 ,而不是为这些字符串执行文件 I/O,因此它们显示为一个单独的集合。我将此系列作为语言并发实验开始。现在,这感觉就像是Web服务器框架的基准;虽然并发性是其中的一个重要方面,但我不确定结果是否意味着语言方面的并发性。 wrkdrill
来自wrk 的结果
使用以下命令进行基准测试(线程 8,连接 500,持续时间 30 秒): wrk
1
wrk -t8 -c500 -d30s
Go HTTP、Rust actix-web、Java Undertow 和 .NET 6 的 更新 比较
Go,Rust和 Java Web 服务器版本在要求/秒性能方面将一切都吹得沸沸扬扬扬。如果我们删除它,我们会得到更好的图片,如下所示。
drill结果
使用并发 1000 和 100 万个请求进行基准测试 drill
Go HTTP、Rust actix-web、Java Undertow 和 .NET 6 的 更新 比较
使用并发 2000 和 100 万个请求进行基准测试 drill
Go HTTP、Rust actix-web、Java Undertow 和 .NET 6 的 更新 比较
上一篇 ApacheBench 结果与线程阻塞
在十个基准测试运行中每十个请求一次的不同指标的平均值如下所示: thread.sleep
您可以找到 GitHub 存储库中使用的所有结果
结论
根据基准测试结果,这些是我的观察结果。
基准观察
由于基于基准的建议是热门话题,我们只分享出观察结果,您可以自己做出决定。
- 对于使用的 HTTP 服务器基准测试,Go HTTP 在请求/秒、延迟和吞吐量方面获胜,但它比 Rust 使用更多的内存和 CPU 。这可能是因为Go拥有最好的内置HTTP库之一,并且它经过了极高的调整,以获得最佳性能;因此,将其与我为Java和Rust所做的简单 TCP 实现进行比较是不公平的。但是你可以将其与Node.js和Deno进行比较,因为它们也有标准的HTTP库,在这里用于基准测试。 更新 :我现在已经将Go HTTP与Rust actix-web和Java Undertow进行了比较,令人惊讶的是Undertow表现更好,actix-web排在第二位。也许像Gin这样的Go Web框架将更接近Undertow和actix-web。 wrk
- Go TCP版本与Rust和Java实现进行了公平的比较,在这种情况下,Java和Rust都优于Go,因此期望Rust和Java中的第三方HTTP库可以与Go竞争是合乎逻辑的,如果我是一个打赌的人,我会打赌有一个Rust库可以胜过Go。
- 资源使用是一个完全不同的故事,Rust似乎在所有基准测试中始终使用最少的内存和CPU,而Java使用最多的内存,而Node.js多线程版本使用最多的CPU。
- 异步 Rust 的性能似乎比多线程强 Rust 实现差。
- 在使用的基准测试中,异步Java版本优于Rust,这对我来说是一个惊喜。 drill
- Java 和 Deno 的失败请求比其他的要多。
- 当并发请求从 1000 个增加到 2000 个时,大多数实现的失败率都非常高。Go HTTP和Rust Tokio版本的故障率接近100%,而多线程Node.js的故障最少,并且在该并发级别具有良好的性能,但 CPU使用率 很高。它运行多个版本的 V8 进行多线程处理,这解释了 CPU 使用率高的原因。
- 总体而言,Node.js似乎仍然比Deno表现更好。
- 另一个重要的收获是,像ApacheBench,wrk或drill这样的基准测试工具似乎提供了非常不同的结果,因此微观基准测试不如最终的性能基准可靠。根据实际用例和特定于实现的详细信息,可能会有很多差异。感谢Eamon Nerbonne指出这一点。
- Apache基准测试在有和没有的版本上运行并不能说明什么,因为所有实现的结果都是相似的,这可能是由于ApacheBench工具的限制。因此,正如许多人指出的那样,我无视他们。 thread.sleep
有关Web框架的更全面的基准测试,我建议查看TechEmpower的Web框架基准测试
社区共识
在并发性能方面,社区共识是相当分裂的。例如,Rust 和 Go 社区都声称在并发性能方面是最好的。从个人经验来看,我发现它们在性能上相对接近,Rust略微领先于Go。Node.js生态系统建立在异步并发性能的承诺之上,并且有证据表明,在切换到 Node.js时,性能会大幅提高。Java还拥有现实世界的项目,可以毫无问题地为数百万个并发请求提供服务。因此,很难在这里站出来。
另一个普遍的观察结果是,Rust 在跨运行的性能方面非常一致,而所有其他语言都有一些差异,特别是当 GC 启动时。
单纯
虽然性能是一个重要方面,但易用性和简单性也非常重要。我认为区分异步和多线程方法也很重要。
异步 :我个人认为Node.js和Deno是最简单,最易于使用的异步并发平台。Golang将是我的第二选择,因为它也易于使用和简单,而不会影响功能或性能。Rust 遵循它,因为它有点复杂,因为它具有更多的功能和需要习惯。我认为Java排在最后,因为它需要更多的样板,并且进行异步编程比其他代码更复杂。我希望Project Loom能够为Java解决这个问题。
多线程 :对于多线程并发,我将把 Rust 放在第一位,因为它充满了功能,并且由于内存和线程安全性,在 Rust 中执行多线程操作既简单又无忧。您不必担心竞争条件等。我会把Java和Go放在第二位。Java拥有成熟的多线程生态系统,使用起来并不难。Go非常易于使用,但是您对操作系统 线程 没有太多控制,否则我会将Go评为高于Java。最后,Node.js和Deno中具有多线程功能,但它们不如其他语言灵活;因此,我将把它们放在最后。
生态系统
在我看来,Rust 拥有最好的并发生态系统,其次是 Java 和 Golang,它们已经有了成熟的选择。Node.js和Deno虽然不如其他节点好,但也提供了一个底层的生态系统。