后端框架选型:Spring Boot、FastAPI、Gin 的内存、冷启动和升级成本,我实测了一遍

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

📖 摘要:不聊 hello world 压测排名,聊三件真正决定账单和维护成本的事:内存基线×副本密度、依赖注入范式带来的调试代价,以及没人愿意写进选型 PPT 的升级成本。附带可直接复现的测量步骤。

上周三凌晨两点,我把一个跑在 2C4G 小机器上的 Go 服务从 Fiber 换回了 net/http 加 chi,QPS 掉了差不多 30%,但 P99 从 210ms 降到了 88ms。不是框架的问题,是我终于把那条写错的索引补上了。这件事让我重新想了一遍:我们选框架的时候,到底在选什么。

图片

先说一下我的背景,免得你觉得我在云端说话。我做过后端五年多,前三年写 Java,后来被一个日活不到两万的项目逼着转了 Go,去年又因为客户要求硬着头皮回去写 Spring Boot。所以我既不是 Java 黑,也不是 Go 吹,我只是被账单和 oncall 电话教育过。

先说内存这笔账,因为它最实在

我在同一台机器上(阿里云 ecs.c7.xlarge,4C8G,Ubuntu 22.04)分别跑过三个框架的空服务,只挂一个 /health 接口,用 ps -o rss= -p $(pgrep -f xxx) 每隔 10 秒采一次,跑满 24 小时取 P95:

  • Spring Boot 3.2.5 + JDK 21,默认 G1,没设 -Xmx:RSS 稳定在 300-330MB。注意,你不设 -Xmx 的话它会按容器内存上限的 1/4 去要堆,容器给 1G 它就敢要 256M 堆再加一堆 Metaspace 和线程栈。
  • FastAPI 0.111 + uvicorn,单 worker:RSS 65-75MB。但你生产上不可能只起一个 worker,4 worker 大概是 230-250MB。
  • Gin 1.10,编译出来的静态二进制:RSS 15-18MB。

单个服务你看不出来什么。乘上副本数就吓人了。我手上一个小项目 12 个服务,每个 3 副本,用 Spring Boot 的话 k8s 里 requests 至少得给 512Mi,节点是 8C16G 的,一台机器撑死塞 20 个出头的 pod。换成 Go,同样的 16G 能塞 80 个左右。某云 8C16G 按量大概 1.1-1.3 元/小时,你自己乘一下 365×24。

我知道有人要说,业务服务不差这点钱。对,但我想说的不是省钱,是调度密度决定了你扩缩容的响应速度。内存基线高的服务,在流量突增时的扩容是分钟级的,因为节点得先把 pod 排上去,还得等 Spring 把 400 个 bean 初始化完。

图片

冷启动:不是只有 serverless 才在意

Spring Boot 3.2 在 JDK 21 上,我这个项目有 400 多个 bean,冷启动实测 2.8-3.5 秒。加上 CDS 和 AOT 处理之后能压到 1 秒上下,但 AOT 那套东西对反射的约束不小,MyBatis 的 mapper 扫描我是折腾了一下午才配通。

Quarkus 和 Micronaut 在 native 模式下确实能进 50ms 以内,这个数字是真的。代价是 GraalVM native image 编译一次 6-10 分钟(看机器,CI 上更久),而且反射、动态代理、资源文件全要靠 json 手写配置。我第一次给一个用到 Jackson 多态反序列化的项目配 native,光 try-catch 就试了七八轮。

Go 那边真的没什么好说的,静态二进制启动 30-80ms,time ./app 一下就知道。FastAPI 起 uvicorn 大概 300-500ms,主要是导入模块的时间,模块多了会更慢。

这个数字什么时候要命?两种情况。一是你在跑 Lambda 或者阿里云 FC 这类按调用计费的东西,冷启动直接算进用户等待时间里。二是大促前的批量扩容,你 3 秒启动和 60 毫秒启动,前 1000 个请求的失败率不是一个量级的。

图片

DI 这件事,两个社区的理解完全不同

这是我最想聊的,也是很少有人在选型文章里写的。

Spring 和 NestJS 把依赖注入当宪法写。好处大家都知道,解耦、可测试。坏处是调试链路的长度会指数级上升。我改过一个五年前的项目,一个下单接口,Controller 上干干净净没注解,ServiceImpl 上有 @Transactional,里面调了另一个 Service 的方法,那个方法上标着 @Transactional(propagation = REQUIRES_NEW)。问题是,同一个类内部的 this 调用不走代理,REQUIRES_NEW 压根没生效。这种 bug 你静态看代码看不出来,只能起 debug 一步步跟。

NestJS 相对好一些,但 @Injectable() 加 providers 数组那一套,新人第一周基本都在跟 "Nest can't resolve dependencies" 这个报错死磕,尤其是涉及到 forwardRef 循环依赖的时候。

Go 那边就走另一个极端,构造函数显式传参:NewOrderService(db, cache, mq),谁依赖谁一眼看完,没有魔法。代价是你得自己在 main 里 new 一堆东西,或者引入 wire / fx 这类工具,而它们又成了新的学习成本。

我的看法是这样:团队人数少于 8 个、服务数少于 15 个的时候,DI 容器带来的收益是负的。你承担的抽象成本、调试成本、新人上手成本,远大于它省下来的那点耦合。超过这个规模,DI 才开始回本。这条线是我自己踩出来的,不一定适合你,但你可以拿它当个起点去验证。

图片

性能数字:TechEmpower 那个榜我每年都看,但我不按它选

我拿 wrk 在本地(Ryzen 7 5800X,16 线程,简单路由返回 JSON)自己跑过一遍,图个乐:

  • Actix-web:大约 145k req/s
  • Fiber:大约 130k req/s
  • Gin:大约 95k req/s
  • Spring Boot 3.2 + Tomcat,200 线程:大约 45k req/s
  • FastAPI + uvicorn 4 worker:大约 22k req/s
  • NestJS(Express 适配器):大约 18k req/s

这些数字在你的真实业务接口面前,基本等于零。我那个 Gin 服务换 Fiber 之后 QPS 是涨了,P99 一动不动,因为它卡在一条没走索引的 SQL 上。

真正的性能排序是这样的:SQL 和索引 > 数据库连接池配置 > N+1 查询 > 序列化方式 > 框架路由。

中间那个连接池我单独说一下,HikariCP 默认 maximumPoolSize=10,很多人根本不去改它。200 个并发打进来,190 个在等连接,你在那儿纠结 Gin 还是 Fiber,方向完全错了。顺带提一句 FastAPI,Pydantic v2 用 Rust 重写了校验核心,官方说比 v1 快 5 到 50 倍,如果你的瓶颈在请求体校验上,这个升级比换框架值。

图片

升级成本:没人写进选型 PPT 里

Spring Boot 2.7 升 3.0,javax.* 全部变成 jakarta.*。听起来是机械替换,但我手上那个项目有 14 个第三方依赖卡在 2.x 不升,最后要么找替代品,要么自己 fork 出来改。而且这种改动逼着你把所有集成测试重跑一遍,两周就没了。

Django 每年 4 月发一个 minor,LTS 三年一版(3.2 LTS 支持到 2024 年 4 月,4.2 LTS 到 2026 年 4 月)。看着挺友好,但 django.utils.timezone.utc 在 4.0 被标废弃、5.0 正式删掉,异步 ORM 接口从 4.1 才开始加,到现在还在陆续补。你如果重度用 ORM,升级前一定先把 python -W error::DeprecationWarning manage.py test 跑一遍,别等生产环境报警告。

Go 的兼容性承诺是 1.x 内不破坏 API,但行为差异还是会咬人。1.22 改了 for 循环变量的作用域,很多老代码里的闭包突然行为变了——变合理了,但也是变了,如果你的代码依赖了旧行为,那就是线上事故。

几个能直接抄走的步骤

图片

选型别拍脑袋,按这个顺序来一遍,一天能出结论:

  1. 让团队里最年轻的那个人,在 IDE 里按 F5 之后 30 分钟内跑起一个能打断点的 Hello World。跑不起来的框架直接淘汰,这比任何 benchmark 都真实。
  2. 算内存账:kubectl top pod -n your-ns 连续观察 24 小时,取 P95 值,乘你的副本数,再乘节点单价。这个数字才是框架的真实年费。
  3. 别用 hello world 压测,拿你最慢的那个真实接口来,用 k6 或者 hey,把 P50/P95/P99 全记下来。只看 QPS 是自欺欺人。
  4. 测启动:Java 用 hyperfine --warmup 3 'java -jar app.jar',Go 直接 hyperfine './app',注意关掉 warmup 缓存的影响,多跑几轮。
  5. 翻一遍这个框架近三年的 release notes,数一数有几处 breaking change,再看看你的依赖列表里有多少个是「已经半年没更新」的。
  6. 最后问自己一句:团队里有几个人能看懂这个框架的核心源码?一个都没有的话,线上出问题你只能等社区 issue,这个风险要提前认。

我现在的默认答案

新项目默认 Go,net/http 加 chi,需要更快的路由再考虑 Gin;管理后台和前端的 BFF 层用 NestJS,因为它和 TypeScript 前端的类型能打通;数据处理和 AI 相关的接口用 FastAPI,Pydantic 加异步确实顺手;只有客户明确要求 Java 生态、或者必须和企业现有中间件(比如老的 Dubbo、WebLogic)对接的时候,才上 Spring Boot。

不是说 Spring Boot 不好,它很强,只是它解决的是 2013 年那批大型企业应用的问题——模块化、声明式事务、容器化管理。你要是五个人写个日活五千的 SaaS,这些能力的成本你付不起,也用不上。

选框架说到底是在选未来两年你要还多少技术债。压测报告可以骗你,招聘 JD 和升级日志不会。

🏷️ 标签: