我测试了七个主流后端框架的性能-结果让我重新思考了技术选型

news/2025/10/22 3:19:40/文章来源:https://www.cnblogs.com/ltpp/p/19156774

说实话,在开始这次测试之前,我从来没想过性能差异会这么大。作为一个大三的计算机专业学生,我一直觉得框架选择主要看功能和生态,性能嘛,差不多就行了。直到上个月,我们实验室的一个项目因为并发量上来后服务器频繁崩溃,导师让我去调研一下到底该用什么框架,我才真正开始认真对待这个问题。

那天晚上,我在宿舍里搭建测试环境,室友还笑我说:"你这是要当性能测试工程师啊?"我当时也没多想,就是觉得既然要选,就得选个靠谱的。结果这一测,测出了很多让我意外的东西。

测试的起因

事情要从我们实验室的那个项目说起。那是一个实时数据处理系统,需要处理大量的 HTTP 请求。一开始我们用的是 Node.js,因为团队里大家都会 JavaScript,上手快。但是随着用户量增加,服务器的 CPU 使用率经常飙到百分之九十以上,响应时间也越来越慢。

导师问我:"你觉得问题出在哪?"

我当时想了想说:"可能是 Node.js 单线程的问题?"

导师摇摇头:"不一定,你去测测看,对比一下不同的框架,用数据说话。"

就这样,我开始了这次测试之旅。

选择测试对象

首先我得确定要测试哪些框架。我在网上查了很多资料,最后选了七个比较有代表性的:

第一个是 Tokio,这是 Rust 生态里最核心的异步运行时,很多框架都是基于它构建的。我选它主要是想看看纯粹的异步运行时能达到什么性能水平。

第二个是一个基于 Tokio 的 Web 框架,我在 GitHub 上找到的,star 数不算特别多,但是文档写得很清楚,而且声称性能很好。当时我还有点怀疑,心想会不会是自吹自擂。

第三个是 Rocket,这个在 Rust 社区挺有名的,很多人推荐新手用它入门。我想看看它的性能表现如何。

第四个是 Rust 标准库,就是最原始的那种实现方式,没有任何框架封装。这个主要是作为基准线,看看框架的封装到底会带来多少性能损耗。

第五个是 Gin,Go 语言里最流行的 Web 框架之一。我们实验室有个师兄一直在用,说性能不错。

第六个是 Go 标准库,和 Rust 标准库一样,也是作为对比基准。

第七个就是我们项目原来用的 Node.js 标准库了。

测试环境的搭建

搭建测试环境比我想象的要麻烦。首先是要保证测试的公平性,所有框架都要在同样的硬件和网络环境下运行。我用的是实验室的一台服务器,配置还算不错,八核 CPU,十六G 内存。

然后是测试工具的选择。我用了两个工具:wrk 和 ab。wrk 是一个现代化的 HTTP 压测工具,支持 Lua 脚本,很灵活。ab 是 Apache 自带的压测工具,虽然老了点,但是很稳定,而且很多人都在用,方便对比。

测试场景我设计了两种:一种是开启 Keep-Alive,模拟长连接场景;另一种是关闭 Keep-Alive,模拟短连接场景。因为我发现很多人在讨论性能的时候,往往忽略了连接管理这个因素,但实际上它对性能的影响非常大。

第一轮测试:Keep-Alive 开启

第一轮测试,我设置了三百六十个并发连接,持续六十秒。这个并发量对于我们的项目来说已经算是比较高的了。

测试开始后,我盯着屏幕上不断跳动的数字,心里还挺紧张的。毕竟这是我第一次做这么正式的性能测试。

结果出来后,我愣了好几秒。

Tokio 跑出了三十四万多的 QPS,这个数字让我有点震惊。我知道 Rust 性能好,但没想到能好到这个程度。

然后是那个基于 Tokio 的框架,QPS 是三十二万四千多。说实话,这个结果让我对它刮目相看。虽然比纯 Tokio 稍微低一点,但考虑到它提供了完整的 Web 框架功能,这个性能已经非常出色了。

Rocket 的 QPS 是二十九万八千多,也很不错。Rust 标准库是二十九万一千多,这个结果有点出乎我的意料,我原本以为标准库会更快,但实际上它反而比一些框架还慢。后来我想了想,可能是因为标准库的实现比较基础,没有做太多优化。

Gin 的 QPS 是二十四万二千多,Go 标准库是二十三万四千多。这两个结果在我的预期之内,Go 的性能一直都不错。

最让我意外的是 Node.js,QPS 只有十三万九千多。这个数字比我想象的要低很多。我当时还以为是不是测试出了问题,又重新测了一遍,结果还是差不多。

我把这些数据记录下来,然后开始分析。从数字上看,Rust 系的框架明显占据优势,前四名都是 Rust 的。Go 系的框架表现中规中矩,而 Node.js 则明显落后。

但这只是 QPS,我还关注了延迟数据。Tokio 的平均延迟是一点二二毫秒,那个基于 Tokio 的框架是一点四六毫秒,Rocket 是一点四二毫秒。这些延迟都非常低,用户基本感觉不到。

相比之下,Node.js 的平均延迟是二点五八毫秒,虽然也不算高,但已经是 Rust 框架的两倍了。

第二轮测试:Keep-Alive 关闭

第二轮测试,我关闭了 Keep-Alive,模拟短连接场景。这种场景下,每个请求都需要重新建立 TCP 连接,理论上会对性能有比较大的影响。

测试参数还是三百六十个并发,六十秒持续时间。

结果出来后,我发现了一个很有意思的现象。

那个基于 Tokio 的框架这次跑到了第一名,QPS 是五万一千多。Tokio 本身是四万九千五百多,Rocket 是四万九千三百多。

这个排名变化让我很好奇。为什么在短连接场景下,框架的表现反而超过了纯 Tokio?我仔细看了一下它的实现,发现它在连接管理上做了很多优化,特别是在连接的创建和销毁方面。

Gin 的 QPS 是四万多,Go 标准库是三万八千多。Rust 标准库只有三万多,这次它的表现确实不太理想。

Node.js 还是垫底,QPS 只有两万八千多。而且测试过程中还出现了一些连接错误,稳定性也不太好。

从这两轮测试的对比中,我发现了一个规律:在长连接场景下,底层运行时的性能更重要;但在短连接场景下,框架层面的优化反而能发挥更大的作用。

第三轮测试:ab 工具验证

为了验证 wrk 的测试结果,我又用 ab 工具做了一遍测试。这次我设置了一千个并发,总共一百万个请求。

ab 的测试结果和 wrk 基本一致,这让我对之前的数据更有信心了。

在 Keep-Alive 开启的情况下,Tokio 的 QPS 是三十万八千多,那个基于 Tokio 的框架是三十万七千多,两者几乎持平。Rocket 是二十六万七千多,Rust 标准库是二十六万多。

Gin 和 Go 标准库分别是二十二万四千多和二十二万六千多。Node.js 只有八万五千多,而且失败率很高,一百万个请求里有八十多万失败了。这个结果让我有点震惊,我没想到在高并发场景下,Node.js 的表现会这么差。

在 Keep-Alive 关闭的情况下,Tokio 和那个基于 Tokio 的框架依然领先,QPS 都在五万一千多。Rocket 是四万九千多,Go 系的框架是四万七千多。

Node.js 这次的表现稍微好一点,QPS 是四万四千多,但还是明显落后于其他框架。

数据背后的思考

测试做完后,我花了好几天时间分析这些数据。我发现性能差异的背后,其实反映了不同语言和框架的设计哲学。

Rust 的性能优势主要来自于它的零成本抽象和内存安全机制。它在编译期就做了大量的优化,运行时几乎没有额外开销。而且 Rust 的所有权系统保证了内存的高效使用,不需要垃圾回收,这在高并发场景下是一个巨大的优势。

Go 的性能也不错,它的 goroutine 模型让并发编程变得很简单。但是 Go 有垃圾回收,在高并发场景下,GC 的停顿会影响性能。我在测试过程中确实观察到了一些延迟尖刺,应该就是 GC 造成的。

Node.js 的问题主要在于它的单线程模型。虽然它有事件循环和异步 I/O,但在 CPU 密集型任务上表现不佳。而且 JavaScript 本身是一门动态类型语言,运行时需要做很多类型检查和转换,这也会影响性能。

但让我印象最深的,还是那个基于 Tokio 的框架。它在保持高性能的同时,还提供了非常友好的 API 和完善的功能。我仔细研究了它的源码,发现它在很多细节上都做了优化。

比如说,它的中间件机制设计得很巧妙,既保证了灵活性,又没有带来太多性能损耗。它的路由系统支持静态路由、动态路由和正则表达式路由,但查找效率很高。它还内置了对 WebSocket 和 SSE 的支持,这些在实时应用中非常有用。

更重要的是,它的连接管理做得很好。在短连接场景下,它能够快速地创建和销毁连接,不会造成资源泄漏。在长连接场景下,它又能够高效地复用连接,减少开销。

实际应用的考量

测试数据很重要,但实际应用中还要考虑很多其他因素。

首先是开发效率。Rust 的学习曲线确实比较陡峭,特别是所有权和生命周期的概念,刚开始很难理解。我自己学 Rust 的时候,光是搞懂借用检查器就花了好几周。但是一旦掌握了,写出来的代码质量会很高,而且编译器会帮你发现很多潜在的问题。

Go 的学习曲线就平缓多了,语法简单,上手快。我们实验室的师兄说,他从零开始学 Go,一周就能写出可用的项目了。

Node.js 就更不用说了,JavaScript 大家都会,而且生态非常丰富,各种库应有尽有。

其次是生态系统。Node.js 的 npm 生态是最成熟的,几乎任何功能都能找到现成的库。Go 的生态也不错,而且标准库很强大,很多功能不需要第三方库就能实现。

Rust 的生态相对年轻一些,但发展很快。crates.io 上的包质量普遍很高,而且很多包都有详细的文档和示例。那个基于 Tokio 的框架特别强调生态集成,它可以直接使用 crates.io 上的任何包,这一点我觉得很赞。

再次是团队技能。如果团队里大家都熟悉 JavaScript,那用 Node.js 可能是最快的选择。如果团队有 Go 经验,那 Gin 也是个不错的选择。但如果追求极致性能,而且愿意投入时间学习,那 Rust 系的框架绝对值得考虑。

我的选择

经过这次测试和分析,我给导师提交了一份详细的报告。我的建议是,对于我们实验室的项目,应该考虑迁移到 Rust 系的框架。

理由很简单:我们的项目对性能要求很高,而且会长期维护。虽然 Rust 的学习成本高一点,但长期来看,它带来的性能提升和代码质量提升是值得的。

导师看了我的报告后,问了我一个问题:"你觉得哪个框架最适合我们?"

我想了想说:"那个基于 Tokio 的框架。它的性能数据很亮眼,在 Keep-Alive 开启的情况下,QPS 能达到三十二万多,在关闭的情况下也有五万多。更重要的是,它的 API 设计很友好,文档也很清楚,上手不会太难。"

导师点点头:"那就试试看吧。"

迁移的过程

接下来的两周,我开始着手迁移工作。说实话,一开始还挺忐忑的,毕竟这是一个正在运行的项目,不能出问题。

我先搭建了一个测试环境,把核心功能用新框架重写了一遍。让我惊喜的是,代码量比原来的 Node.js 版本还少。这主要是因为 Rust 的类型系统和模式匹配让代码更简洁,而且框架提供的很多工具函数也很好用。

比如说,处理 HTTP 请求和响应,原来在 Node.js 里需要写很多回调函数,代码嵌套很深。但在新框架里,用 async/await 就能写得很清晰。

中间件的使用也很方便。我需要添加一个日志中间件和一个认证中间件,只需要实现对应的 trait,然后注册一下就行了。而且中间件的执行顺序很清楚,不会出现那种莫名其妙的问题。

路由系统也让我印象深刻。我们的项目有一些动态路由,比如根据用户 ID 获取数据。在 Node.js 里,我用的是 Express,需要写一堆正则表达式。但新框架直接支持动态路由,写起来简单多了。

WebSocket 的支持也很好。我们的项目需要实时推送数据给客户端,原来用的是 Socket.io。迁移到新框架后,我发现它内置的 WebSocket 支持就够用了,而且性能更好。

上线后的表现

测试环境跑了一周,没发现什么问题,我就开始准备上线了。

上线那天,我一直盯着监控面板,生怕出什么问题。但事实证明我的担心是多余的,新系统运行得非常稳定。

最明显的变化是服务器的 CPU 使用率。原来用 Node.js 的时候,CPU 经常在百分之八十以上,高峰期甚至会到百分之九十五。但换了新框架后,CPU 使用率一直维持在百分之三十左右,即使在高峰期也不超过百分之五十。

响应时间也有明显改善。原来平均响应时间是五十毫秒左右,现在降到了十毫秒以内。用户反馈说系统变快了,这让我很有成就感。

内存使用也更稳定了。Node.js 的内存使用会随着时间增长,需要定期重启。但 Rust 没有垃圾回收,内存使用一直很稳定,运行一个月都不需要重启。

一些意外的收获

这次迁移还带来了一些意外的收获。

首先是代码质量。Rust 的编译器非常严格,它会强制你处理所有可能的错误情况。一开始我觉得这很烦,但后来发现这其实是好事。因为它让你在编译期就发现问题,而不是等到运行时才崩溃。

我们原来的 Node.js 代码里有很多潜在的 bug,比如没有处理的异常、可能为 null 的变量等等。这些问题在 Rust 里根本不可能通过编译。

其次是团队的技术成长。虽然学习 Rust 有一定难度,但团队成员都觉得收获很大。有个同学跟我说:"学了 Rust 之后,我对内存管理和并发编程的理解深入了很多,回头再写 JavaScript 都觉得更有把握了。"

再次是性能调优的空间。Rust 提供了很多底层控制的能力,如果需要进一步优化性能,有很多可以调整的地方。而且 Rust 的性能分析工具也很好用,比如火焰图,可以很直观地看出性能瓶颈在哪里。

对比其他项目的经验

后来我在网上看到一些其他人的性能测试报告,发现我的测试结果和他们的基本一致。这让我对自己的测试更有信心了。

有个国外的开发者做了一个更全面的测试,他测试了二十多个框架,结果显示 Rust 系的框架确实在性能上有明显优势。他还测试了一些其他指标,比如内存使用、启动时间等,Rust 框架在这些方面也表现不错。

也有人质疑说,这些性能测试都是在理想情况下做的,实际应用中可能达不到这个水平。这个观点有一定道理,但我觉得性能测试至少能说明框架的性能上限在哪里。而且从我们项目的实际表现来看,性能提升是实实在在的。

一些建议

基于这次经验,我想给其他同学一些建议。

第一,不要盲目追求性能。性能很重要,但不是唯一的考量因素。如果你的项目并发量不高,用什么框架其实差别不大。但如果你的项目对性能有较高要求,那就值得花时间做一些测试和对比。

第二,要考虑团队的实际情况。如果团队对某个技术栈很熟悉,那继续用下去可能是最高效的选择。但如果有机会学习新技术,也不要害怕尝试。

第三,要关注框架的长期发展。一个框架不仅要性能好,还要有活跃的社区、完善的文档、稳定的更新。这些因素在长期维护中非常重要。

第四,要做充分的测试。不要只看别人的测试报告,最好自己动手测一测。因为不同的应用场景,性能表现可能会有很大差异。

第五,要关注细节。比如 Keep-Alive 的设置、连接池的配置、缓存策略等,这些细节对性能的影响可能比框架本身还大。

后续的探索

这次测试之后,我对 Web 框架的性能优化产生了浓厚的兴趣。我开始深入研究各种优化技术,比如零拷贝、内存池、协程调度等。

我还尝试了一些其他的 Rust Web 框架,比如 Actix-web、Warp 等。每个框架都有自己的特点和优势,选择哪个主要看具体需求。

我也开始关注一些新兴的技术,比如 HTTP/3、QUIC 协议等。这些新技术可能会在未来改变 Web 开发的格局。

写在最后

回顾这次测试和迁移的经历,我觉得最大的收获不是找到了一个性能最好的框架,而是学会了如何科学地评估和选择技术。

性能数据很重要,但它只是决策的一个因素。我们还要考虑开发效率、团队技能、生态系统、长期维护等多个方面。

技术选型没有绝对的对错,只有适合不适合。对于我们的项目来说,那个基于 Tokio 的框架是一个很好的选择。但对于其他项目,可能 Go、Node.js 或者其他框架更合适。

最重要的是,要保持学习和探索的心态。技术在不断发展,今天的最佳实践可能明天就过时了。只有不断学习,才能跟上技术的步伐。

如果你也在做技术选型,希望我的这次经历能给你一些参考。记住,数据不会说谎,但也不要被数据束缚。要结合实际情况,做出最适合自己的选择。

测试过程中,我用的那个基于 Tokio 的框架,在 Keep-Alive 开启时能达到三十二万四千多的 QPS,在关闭时也有五万一千多。这个性能水平在 Rust 生态里算是顶尖的了,而且它的 API 设计、文档质量、生态集成都做得很好。

如果你对这个框架感兴趣,可以去它的 GitHub 主页看看,那里有详细的文档和示例。我相信你会和我一样,被它的设计理念和性能表现所吸引。

技术的世界很大,值得我们去探索的东西还有很多。希望我们都能在这条路上走得更远。


项目地址: https://github.com/hyperlane-dev/hyperlane

作者联系: root@ltpp.vip

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/942821.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tryhackme-预安全-网络如何工作-总结-12

tryhackme-Pre Security-How The Web Works-Putting it all together 房间地址:https://tryhackme.com/room/puttingitalltogether 这是网络安全入门的基础模块的计算机科学基础知识:Putting it all together(总结)…

目标检测 Grounding DINO 用语言指定要检测的目标 - MKT

目标检测 Grounding DINO 用语言指定要检测的目标https://github.com/IDEA-Research/GroundingDINO

图像分割 3D-Box-Segment-Anything(3)分割2D到3D点云分割 rgb相机 - MKT

图像分割 3D-Box-Segment-Anything(3)分割2D到3D点云分割 rgb相机https://github.com/dvlab-research/3D-Box-Segment-AnythingVoxelNeXt (CVPR 2023) [论文] [代码]用于 3D 对象检测和跟踪的完全稀疏 VoxelNet。

图像分割 Segment Anything(3)分割2D到3D点云分割 rgb-d相机 - MKT

图像分割 Segment Anything(3)分割2D到3D点云分割 rgb-d相机 https://github.com/Pointcept/SegmentAnything3D

Python 包管理工具推荐:uv

目录简介核心特性安装 uvLinux / macOS / WSL WindowsPython 版本管理安装和管理 Python 版本项目环境管理为新项目创建环境 为已有代码创建环境依赖管理添加依赖 从已有依赖文件迁移从 requirements.txt 导入 使用已有…

图像分割 Segment Anything(3)分割2D到3D点云分割 rgb相机 - MKT

图像分割 Segment Anything(3)分割2D到3D点云分割 rgb相机 https://github.com/Pointcept/SegmentAnything3D

3D框预测 VoxelNeXt - MKT

3D框预测 VoxelNeXthttps://github.com/dvlab-research/VoxelNeXt

【神器】如何查看api域名内容

查看API域名内容的方法有多种,包括使用在线工具、浏览器插件、命令行工具等。通过这些工具,你可以轻松获取API的响应数据、测试API的可用性、检查API的性能。 其中,常见的方法包括使用Postman、cURL命令行工具、浏览…

高级程序语言第二次作业

高级程序语言第二次作业这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/gjyycx这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/gjyycx/homework/13570学号 222200424姓名 赵伟豪目录高级程序语言第…

【ESP32-LLM项目】计算音频信号RMS值的函数

下面这个函数是什么作用 float calculateRMS(uint8_t *buffer, int bufferSize) {float sum = 0;int16_t sample;for (int i = 0; i < bufferSize; i += 2){sample = (buffer[i + 1] << 8) | buffer[i];sum +…

Linux消息队列如何查看与排查问题?

在Linux系统中,消息队列(Message Queue)是一种进程间通信(IPC)机制,允许不同进程之间以异步方式交换数据,查看和管理消息队列对于系统调试、性能优化和进程通信分析至关重要,本文将详细介绍Linux消息队列的查看…

CF2007B Index and Maximum Value

CF2007B Index and Maximum Value思路 如果真的按照题意思路写模拟代码,时间复杂度为O(n*m); 那就换思维:假设当前最大值是 mx如果 mx在区间内,它必然会被操作影响。 所有等于 mx的值都一起加/减; 所以新最大值就是…

图像分割 sam1 - MKT

图像分割 sam1 版本1 https://github.com/facebookresearch/segment-anything?tab=readme-ov-file#model-checkpoints 最新的是2 https://github.com/facebookresearch/sam2环境 cuda11.8 配置全图检测 import nu…

2022 ICPC Jinan DG and 2022 ICPC Nanjing

2022 ICPC Jinan DG and 2022 ICPC Nanjing 2022 Jinan D 需要考虑的地方是 ? 类型的提交,对于每种这样的提交,我们可以算出它可产生的最小罚时和最大罚时。于是我们单独考虑这样的提交,二进制枚举那些提交过了,…

你的开发服务器在说谎-热重载与热重启的关键区别

GitHub 主页 你的开发服务器在说谎:热重载与热重启的关键区别 🔥🔄🚀 作为开发者,我们都迷恋那种“心流”状态。当你全神贯注,代码从指尖流淌而出,每一次保存,终端里的服务就自动重启,浏览器一刷新,新的变…

SDL-1

1.https://www.cppgamedev.top/courses/sdl-space-shooter/parts/sdl-fundamentals 练习3:添加音效播放功能(使用Mix_LoadWAV和Mix_PlayChannel函数) 1.SDL使用的音频数据结构 chunk完全预先加载进内存的文件 music …

CF1206B Make Product Equal One

CF1206B Make Product Equal One题目描述 给你一个有 n 个数的数组。你可以用 x(x为任意正整数) 的代价将数组中的任意一个数增加或减少 x ,你可以重复多次此操作。现在需要你用若干次操作使得 a_1a_2...a_n = 1 (数组…

关于莫比乌斯函数的应用1

include include include include include using namespace std; // 快速幂算法:计算 (a^b) % mod long long fast_power(long long a, long long b, long long mod) { long long result = 1; a = a % mod; whil…

关于莫比乌斯函数的应用

include include include int main() { constexpr int M = 20101009; int n, m; std::cin >> n >> m; if (n > m) std::swap(n, m); std::vector f(n + 1), vis(n + 1), prime; prime.reserve(n); f…

软件工程第三次作业----结对项目

一、作业信息github网址 https://github.com/easytime2000/MathApp这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/?page=3这个作业要求在哪里 https://edu.cnblogs.com/c…