后端框架怎么选?我在 4C8G 上测了 Axum/Gin/Fastify/NestJS/Spring Boot,发现该看的根本不是 QPS

🔑 关键词:后端框架选型,Spring Boot,NestJS,Axum,框架性能对比

📖 摘要:一篇不吹 QPS 排行榜的后端框架对比。我用 wrk 在 4C8G 云主机上实测了 Axum、Gin、Fastify、NestJS、Spring Boot 的吞吐/内存/冷启动,给出一套「逃生舱测试」评估法,并附上 5 个真实踩坑和排查命令,帮你算清框架做「难事」时的真实成本。

先说个凌晨两点的故事

图片

前年 11 月我接了个电商后台的活,技术栈是我自己拍的:NestJS + TypeORM + PostgreSQL。理由很正当——团队三个人都是前端出身,一套 TypeScript 写到底,学习成本低。前三个月确实爽,标准 CRUD 一天出五个接口。

转折出现在「用户列表,要带每个人最后一次下单时间」这个需求上。SQL 本身六行就完了:

SELECT u.id, u.name, o.created_at
FROM users u
LEFT JOIN LATERAL (
  SELECT created_at FROM orders
  WHERE user_id = u.id ORDER BY created_at DESC LIMIT 1
) o ON true;

TypeORM 的 QueryBuilder 表达不了 LATERAL,用 relations 加载会把该用户历史上所有订单全拉回来——有个大客户下了 3 万多单,那个接口直接把 P95 从 120ms 干到 1.6 秒。最后我退到 dataSource.query(),返回 any[],TypeScript 在这里彻底断了链。

这件事让我想明白一个之前没认真想过的问题:评估一个后端框架,不该看它做容易的事有多轻松,要看它做难的事有多贵。

我到底测了什么

图片

网上那些排行(TechEmpower 之类)我不是不信,是跟我的场景对不上。所以我自己跑了一轮,环境写清楚,你要复现也就半小时:

  • 机器:阿里云 ecs.c7.xlarge,4 核 8G,Ubuntu 22.04,Docker 24.0
  • 压测:wrk -t4 -c256 -d60s,每个跑三轮取中位数
  • 接口:GET /ping 返回 {"ok":true}不碰数据库,纯框架开销

结果(req/s 和 P99):

框架 吞吐 P99 空闲 RSS 冷启动到首个响应
Axum 0.7 / Rust 1.76 ~118,000 4.2ms 9MB 0.04s
Gin 1.9 / Go 1.22 ~96,000 6.8ms 14MB 0.03s
Fastify 4 / Node 20 ~44,000 11ms 78MB 0.18s
NestJS 10 + Fastify 适配器 ~31,000 15ms 112MB 0.9s
Spring Boot 3.2 / Java 21 平台线程 ~22,000 19ms 310MB 1.8s
Spring Boot 3.2 / Java 21 虚拟线程 ~26,000 17ms 320MB 1.9s
NestJS 10 + 默认 Express ~8,700 34ms 105MB 0.8s

几个我自己也没想到的点:

第一,NestJS 默认用 Express,和换成 Fastify 适配器差了 3.5 倍。很多人吐槽 NestJS 慢,其实慢的是 Express 那一层,换适配器只改两行代码(main.ts 里 NestFactory.create 换成 FastifyAdapter),但这一改很多基于 Express 的中间件就得重写。

图片

第二,Spring Boot 开虚拟线程只涨了 18%,没有传说中那么神。因为我的测试接口根本没阻塞操作,虚拟线程解决的是 IO 等待,不是 CPU 计算。你要是压测里塞个 Thread.sleep(50),数字会完全反过来。

第三,Axum 和 Gin 差了 20% 左右,但这个差距在你接上数据库之后基本就消失了——一个 Postgres 查询轻松就是 2-5ms,抵得上几十万 QPS 的框架开销。

两个被严重低估的指标:内存和冷启动

上面表格里我最在意的其实是后两列。

内存:Spring Boot 一个空服务 310MB,NestJS 112MB,Go 14MB,Rust 9MB。你如果上 Kubernetes,按 8 个副本算,Java 那套光空闲就要吃掉 2.5GB,Node 那套 900MB,Go/Rust 加起来不到 200MB。按 4C8G 每台 300 块一个月算,这个差别一年是几千到上万块。对小团队来说这比 QPS 实在。

冷启动:Serverless 场景这是生死线。AWS Lambda 上,Java 冷启动常态 800ms-3s,Node 100-400ms,Go 通常 <50ms。Spring Boot 3.2 之后可以用 GraalVM native image 把启动压到 90ms 左右,但代价是构建时间从 40 秒涨到 3 分 20 秒,而且反射、动态代理这些要一个个写配置——我那个项目有 6 个 starter 需要额外处理,折腾了整整两天。

图片

「逃生舱测试」:我现在评估框架只用这一套

踩坑踩多了,我总结了一个土办法。核心就一句话:别测框架帮你做的事,测框架不让你做的事。具体三步:

第一步,查询层测试。拿你业务里最变态的那条 SQL(带 CTE、窗口函数、LATERAL 的那种),分别用框架推荐的 ORM 路径写一遍。记录三件事:写了几行、丢没丢类型、能不能进单元测试。

Spring Data JPA 我给的评分是 6 分:JPA 表达不了的时候有 @Query 和 nativeQuery 兜底,但分页 + 动态条件拼起来很难看。TypeORM 5 分,queryRaw 一用类型就没了。Prisma 7 分,$queryRaw 有类型但那套 Tagged Template 得重新学。Go 的 sqlc 我给 9 分——SQL 即源码,生成类型,比任何 ORM 都诚实。

第二步,事务层测试。设计一个场景:写两张表 + 一次外部 HTTP 调用,要求要么全成功要么全回滚。看框架给你几条路。这里 Spring 的 @Transactional 要注意自调用失效(同类内部方法调用不走代理),我见过至少三个项目栽在这上面,而且不报错,静默不回滚。

图片

第三步,协议层测试。想象一个不符合 REST 规范的接口:SSE 长连接 + 自定义 header + 分块响应。Go 和 Rust 直接写,Node 要用 res.raw,Spring 要返回 SseEmitter 或者干脆上 WebFlux。这一步最能看出框架的边界在哪。

三步跑完,你手里就有了一张「抽象泄漏成本表」,比看一百篇对比文章都管用。

一个反直觉的结论

我的结论可能跟你想的相反:框架的技术债不是负债,是期权。

你选 Spring Boot,买的不是它好写,是「以后遇到任何问题都能在 StackOverflow 上找到 2013 年的答案」这个权利。你选 Axum,是拿生态成熟度去换运行时确定性。你自研框架,相当于卖了一张行权价无穷大的期权。

所以选型的关键问题不是「哪个快」,是「半年后那个接手代码的人,第一次提交生产代码要多久」。这个问题有个操作性很强的近似答案:看这个框架的官方文档,能不能在 20 分钟内让你写出一条带事务、带校验、带错误处理的完整接口。Spring Boot 我用了两年,写第一条接口花了 40 分钟(光搞清楚 starter 依赖就够呛)。Fastify 我没用过,照着文档 12 分钟跑通了带 JSON Schema 校验的接口。

图片

那到底怎么选

写个决策流程,你可以直接抄:

  • 团队 ≤5 人,业务规则复杂,交付期紧 → Spring Boot 或 Django。慢没关系,业务复杂度才是主要矛盾,别在基础设施上省时间。
  • 做 BFF、边缘聚合、或者要上 Serverless → Fastify 或 Hono。冷启动和内存优势明显,Node 生态的东西拿来就用。
  • 团队有 3 年以上 Go 经验,服务边界清晰 → Gin / Echo。部署简单到离谱,一个二进制扔上去就跑,内存占用低到可以忽略。
  • 需要极低尾延迟(金融、实时);或者团队能接受编译期心智负担 → Axum。但要提前想清楚:你会为 trait bound 报错过至少两周。

附:我踩过的 5 个坑和排查命令

  1. Spring Boot 启动 4 秒以上java -jar app.jar --debug,看 auto-configuration report,把没用到的 starter 一个个删。我删掉 spring-boot-starter-data-redis 之后启动从 4.2s 降到 2.1s。
  2. NestJS 内存涨到 1G 不降node --inspect 挂 chrome://inspect,做两次 heap snapshot 对比。我上次是 EventEmitter 上的 listener 没解绑。
  3. N+1 查询:Django 用 select_related / prefetch_related,Spring 用 @EntityGraph 或 join fetch。别凭感觉,开 spring.jpa.show-sql=true 数 SQL 条数。
  4. Fastify 启动慢:JSON Schema 是 JDK 式按需编译的,schema 多的时候首启动会慢,可以预热一次请求。
  5. Go 里 goroutine 泄漏go tool pprof http://localhost:6060/debug/pprof/goroutine,看哪些协程卡在 channel 上。

最后说句实话:性能测试看看就行,别当真。真正让项目失败的从来不是 p99 多了 5ms,是三个月后没人敢改那段代码。选框架的时候,多想想那个人。

🏷️ 标签: