Rust比Spring快10倍?我做了实测,发现真相没那么简单

Rust比Spring快10倍?我做了实测,发现真相没那么简单
给一个.side project选运行时的时候,顺手跑了一下TechEmpower Benchmark和社区压力测试。数字出来之后,我重新想了想自己对Java性能的认知。
| 配置 | 吞吐量 | P99延迟 | 内存占用 |
|---|---|---|---|
| Spring Boot (JVM) | 4,200 req/s | 45ms | 280 MB |
| Spring Boot (Native) | 3,600 req/s | 38ms | 55 MB |
| Quarkus (Native) | 5,100 req/s | 28ms | 35 MB |
| Rust (Axum + Tokio) | 42,000 req/s | 3ms | 12 MB |
测试条件:简单JSON返回 + PostgreSQL查询,100并发连接,持续60秒压测。
Rust的数字确实漂亮——吞吐量是Spring Boot的10倍,内存只有后者的1/23。但这篇文章不是Rust吹哨文。我想聊聊这些数字到底怎么来的,以及什么时候你根本不需要Rust。
GC税:你以为的快,其实是在交保护费
性能差距最大的单点因素,就是垃圾回收。而且这个税,大多数Java程序员是不知道自己在交的。
Spring Boot的JVM里,GC在工作的时候你的程序是暂停的
Spring Boot跑在HotSpot JVM上,默认用的是G1GC。听起来高大上,但原理很简单粗暴:程序运行中不断创建对象,堆内存越用越多,满了就停下来——所有线程一起停——去做垃圾回收,回收完了再继续。
这个"一起停"就是问题。
在中等负载下,年轻代大概1到3秒就满了,触发一次年轻代GC,所有线程冻结5到15毫秒。每隔几分钟来一次混合GC,冻结时间拉到20到50毫秒。内存紧张的时候还会触发一次Full GC,整个JVM直接冻住100到500毫秒。
在你的应用代码里完全看不到GC的影子,但它会忠实地体现在尾部延迟上。更讨厌的是GC抖动——平均停顿10毫秒听起来还行,但分布曲线有个长长的尾巴。每几千个请求里就有一个撞上50毫秒以上的停顿。你的P99看起来光鲜,P99.9可能惨不忍睹。
如果你曾经觉得"我的Java服务明明挺快的,为什么总有几个请求莫名其妙地慢",大概率就是GC在作祟。
Quarkus Native Image:用编译时的工作换运行时的不打扰
Quarkus编译成GraalVM Native Image之后,默认用的是Serial GC。单线程的stop-the-world收集器,但因为整个堆只有35到60MB,停顿时间只有1到3毫秒。而且这个停顿非常稳定可预测,不存在JVM那种抖动。
更极端一点,可以用Epsilon GC——一个什么都不做的收集器。永远不回收内存,用到上限就进程重启。听着很粗暴,但对短生命周期容器来说反而完美,Kubernetes会负责处理重启逻辑。
Rust:编译时就决定了内存归属
Rust没有垃圾收集器。不是低pause GC,是根本没有GC。
内存管理在编译阶段就完成了。变量离开作用域,内存立即释放,没有运行时开销,没有后台线程扫描堆,没有"等一下"的操作。
结果就是:没有GC停顿,没有GC CPU开销,没有抖动。每个请求的延迟完全由你的代码决定,不取决于GC什么时候想跑。所以Rust的P99是3毫秒而Spring Boot是45毫秒——核心差异不在于谁跑代码更快,而在于谁从来不暂停。
编译方式的对决:JIT预热、Native编译和LLVM
第二个拉开差距的地方,是代码怎么变成机器能认识的指令。
Spring Boot的JVM模式:先慢后快
HotSpot的C2 JIT编译器在运行时分析代码,找到热点路径,编译成优化后的机器码。预热60到90秒之后,Spring Boot的吞吐量表现相当不错。
但有两个坑。
第一个是冷启动悬崖。启动后前90秒,代码以解释模式运行,性能差距巨大:
- 前30秒:P99 = 450毫秒
- 30到60秒:P99 = 120毫秒(部分JIT编译)
- 90秒以后:P99 = 45毫秒(完全优化)
同一个人在跑同一个服务,冷启动和预热后的性能相差10倍。在容器化环境里Pod频繁扩缩容,你经常要在最需要性能的时候跑在冷实例上。
第二个是启动时间。JVM模式启动要2到3秒。对Serverless函数和滚动发布来说,这个时间挺要命的。
Quarkus + GraalVM Native Image:编译时定型
Native Image在构建阶段把整个应用编译成独立的二进制文件。没有JVM,没有字节码,没有解释器。
- 启动时间:0.03到0.06秒(JVM是2到3秒)
- 没有冷启动悬崖,第一个请求就是最佳性能
- 内存减少5到8倍(没有类元数据、没有JIT编译器、没有字节码常驻)
代价:因为没有JIT运行时优化,极限吞吐量通常比完全预热后的HotSpot JVM低10%到25%。JIT编译器能做基于运行时profile的激进优化(虚函数内联、循环展开),AOT编译做不到。
Spring Boot Native(3600 req/s)比Spring Boot JVM(4200 req/s)略慢,原因就在这里——但这个比较只在JVM完全预热之后才成立。
Rust + LLVM:零成本抽象
Rust通过LLVM后端直接编译成机器码。和Native Image一样是AOT编译,但更进一步:没有运行时 substrate(GraalVM Native Image还带15到30MB的 substrate),没有GC,甚至连Serial GC都没有。LLVM在编译时做非常激进的优化,代码质量和C/C++同档次。
还有一个特点:零成本抽象。Rust的高级特性——迭代器、async/await、泛型——编译后和手写循环的机器码一样高效。没有运行时多态开销,没有隐藏的动态派发成本。
所以Rust能以12MB内存跑出42,000 req/s。它不是一个"更好的Java",而是一个完全不同的计算模型。
CPU时间都花在哪了:Spring Boot和Rust的性能消耗解剖
看完了框架,再来解剖CPU时间的分配。
Spring Boot (JVM) — 4,200 req/s
- 42%:业务逻辑和数据库查询
- 18%:JSON序列化/反序列化(Jackson)
- 15%:GC开销(G1GC后台线程和停顿)
- 12%:JVM基础设施(类加载、反射、代理生成)
- 8%:HTTP栈(Tomcat/Netty开销)
- 5%:JIT编译器后台线程
Rust (Axum + Tokio) — 42,000 req/s
- 70%:业务逻辑和数据库查询
- 12%:JSON序列化(serde,零拷贝,编译时生成)
- 0%:GC(根本不存在)
- 0%:运行时开销(没有VM,没有substrate)
- 15%:Tokio异步运行时 + Hyper HTTP
- 3%:系统调用
从数字看,CPU花在框架上的比例和性能成反比。Spring Boot JVM花42%的CPU在业务逻辑上,58%在框架上。Rust花70%在业务逻辑上,30%在HTTP/异步基础设施上。吞吐量差了10倍,但CPU干的是同样的工作——区别在于浪费了多少。
内存:一张AWS账单告诉你为什么这不只是技术指标
内存占用不只是一个技术数字,它直接换算成钱。
在AWS ECS Fargate(us-east-1,20个实例,24/7运行)上对比:
- Spring Boot JVM(0.5 vCPU,1GB/实例):每月约665美元
- Spring Boot Native(0.25 vCPU,0.5GB/实例):每月约180美元
- Quarkus Native(0.25 vCPU,0.5GB/实例):每月约180美元
- Rust(0.25 vCPU,0.5GB/实例):每月约180美元
真正的成本悬崖是JVM到Native这一步——730%的费用差距。Fargate的最低配置是0.25 vCPU/0.5GB,所以Quarkus Native和Rust落在同一个收费档。
把这个乘以10个服务,就是每月6650美元和1800美元的差别。每年省58000美元,就因为从JVM切到了Native Image。
所以真正想说的不是"Rust比Quarkus便宜",而是JVM的内存和CPU开销是AWS账单上实实在在的一行项目,GraalVM Native Image可以在不离开Java生态的情况下抹掉大部分开销。
GraalVM Native Image:Java开发者最现实的选择
如果你是Java团队,看到Rust的数字流口水,GraalVM Native Image是最务实的折中方案。
Native Image能给你:
- 启动速度提升40倍(2.5秒降到60毫秒)
- 内存减少5到8倍(280MB降到35到55MB)
- 没有JIT预热悬崖,从第一个请求开始就是最佳性能
- Serial GC下GC停顿降到2毫秒以下
- 单一二进制部署,不需要JRE
诚实的代价:
- 构建时间:3到7分钟(JVM模式10到25秒),影响开发体验
- 极限吞吐量:比完全预热的JVM低10%到25%
- 生态兼容性问题:部分Java库用反射的方式和AOT不兼容
- 调试能力受限:没有jstack、jmap、JFR、VisualVM,JVM工具链全部消失
- 闭世界假设:禁止动态类加载和运行时字节码生成
建议的使用场景:
- I/O密集型服务(REST API、CRUD服务、事件消费者)——启动速度、内存占用和延迟可预测性比极限CPU吞吐量重要,用Native Image。
- 计算密集型工作(批处理、流处理、ML推理)——JIT编译器的运行时优化真能带来15%到25%的提升,别用Native Image,继续跑JVM。
我在AWS Lambda上部署过一个Quarkus + GraalVM Native Image的REST API,走CloudFront → API Gateway。冷启动500毫秒,Lambda内存1024MB。同样的API在Spring Boot JVM Lambda上需要2048MB内存,冷启动还要10到15秒。Quarkus Native把内存砍了一半,启动速度快了20到30倍,整个Lambda月费不到50美元。
Rust:什么时候Native Image还不够
先说清楚Rust适合什么、不适合什么。
Cloudflare的边缘代理、Discord的消息系统、AWS Lambda的Firecracker虚拟机、Linkerd的服务网格代理——都是Rust,都是基于同样原因选的:P99要小于5毫秒,或者吞吐量要扛住2万req/s以上。
但Rust有一个真实的生产力差距:一个高级Java开发者2到3天能交付一个生产级REST API。同样的API一个有经验的Rust开发者要5到7天,Rust新手要2到3周。
Rust编译器出了名的严格。借用检查器在编译期捕获内存bug——这也是Rust零内存安全漏洞的原因——但学习曲线很陡。“和借用检查器搏斗"是每个Rust新人都要经历的阶段,大概持续2到4个月。
对于企业CRUD服务,10倍性能优势很少值得2到3倍的生产力成本。但对于性能关键的底层基础设施,情况完全不同。
怎么选:一个实操决策矩阵
跑完benchmark和生产部署之后,这是我的决策框架:
大多数企业API服务:选Quarkus + GraalVM Native Image。 Java生态、团队上手快、性能提升显著,三者平衡最好。
Spring主导的团队:选Spring Boot 3.x + GraalVM Native Image。 不要为了框架换框架,Native Image能弥补大部分性能差距。
性能关键路径(API网关、数据摄取管道、实时事件处理器、P99低于5毫秒是硬需求):选Rust。 为了正确的场景值得投入。
计算密集型工作(批处理、流处理、ML推理):继续用Spring Boot或Quarkus的JVM模式。 JIT编译器在这里真的有用。
最务实的做法是混合架构: API网关用Rust,核心业务服务用Quarkus/Spring Native,批处理跑在JVM上。每个服务用对的工具。
数据汇总
核心数字在这里:
| 指标 | JVM | Native | Rust |
|---|---|---|---|
| 启动时间 | 2,500ms | 25ms | 3ms |
| 内存占用 | 280MB | 35MB | 12MB |
| 吞吐量 | 4,200 req/s | 5,100 req/s | 42,000 req/s |
| P99延迟 | 45ms | 28ms | 3ms |
| GC停顿 | 5-50ms | 1-3ms | 0ms |
如果你现在在用Java,GraalVM Native Image配合Quarkus是最划算的选择——内存砍一半,启动快40倍,GC停顿降到2毫秒以下。团队不需要学新语言。
Rust留给你真正需要P99小于5毫秒的那5%的服务。剩下的,Native Image足够好了。JVM不会消亡,但2026年了还不用Native Image,确实是在自己的账单上白扔钱。
常见问题
Q: Quarkus Native Image和Spring Boot Native Image哪个更好?
如果你的团队已经用Spring,迁移框架的成本可能比性能收益还高。Spring Boot 3.x的Native Image支持已经成熟,性能差距和Quarkus Native相比不大。但如果是从零开始,Quarkus的云原生设计让它在Native Image场景下通常表现更好。
Q: 我有一个高流量API,但团队都是Java背景,该怎么过渡?
不要一次性全部重写。先把一个非核心服务用Quarkus Native Image部署上线,感受一下构建流程和生态兼容性。确认没问题之后逐步迁移。混合架构也是合理的选择——Rust处理最关键的前置网关,Java服务处理业务逻辑。
Q: GC抖动有办法缓解吗,不一定要上Native Image?
可以。用ZGC或Shenandoah这两个低暂停GC器可以把停顿降到亚毫秒级别。但它们依然有CPU开销和抖动,内存占用也比Native Image高不少。这是JVM内的最好方案,但和真正的无GC还是有本质差距。