前阵子帮一个团队做性能复盘他们的核心接口在压测跑到两千并发时响应时间突然从几十毫秒跳到几秒数据库没被打满链路里各组件看起来也正常最后定位到瓶颈恰好是框架默认线程池被阻塞 IO 任务占满了。这件事给我的触动挺大高并发场景里的技术问题很多时候不是代码写得不够好而是框架选的调度模型与业务流量长相反了。这些年我每次做框架选型都会把候选框架拉到同一套压测方案下跑一遍。倒不是为了证明谁最强而是为了看它在极端情况下怎么“崩”。崩的位置比峰值数据更能暴露框架的脾性。下面这些思路来自我个人的项目复盘性能数据只能说明特定环境下的一种相对趋势不能当通用基准拿去写报告但它背后的选型逻辑是通用的。1. 高并发场景到底在考验框架的什么1.1 业务流量不等于技术并发很多团队在讨论高并发时习惯用“我们系统日活多少万”来定义问题这其实属于业务流量指标技术侧真正关心的往往是另一个东西某个瞬间同时有多少请求在途以及每个请求会占用多久的底层资源。举个最简单的例子一个接口平均耗时 20ms如果要支撑每秒一万次调用数学上同时需要的并发处理能力大概在 200 左右。但如果同样的接口做的是长连接推送或 WebSocket 消息转发一个连接可能要存活几分钟甚至几小时那“并发在线连接数”和“每秒请求数”就完全是两码事了。框架选型的难点就在这里。不同框架的线程模型、IO 模型、内存管理方式决定了它擅长应对哪一种“并发形态”。有的框架擅长处理大量短而快的请求有的则更适合少量但长时间活着的连接。不分清这一点很容易被一份看似漂亮的 RPS 测试报告带偏。1.2 框架实际上在管理你的稀缺资源从底层视角看框架做的事情并不神秘它帮我们把网络请求接入、协议解析、业务逻辑调度、响应写出串成一条流水线。一张 CPU 核心里能同时运行的线程数是很有限的框架的核心职责之一就是把并不充裕的线程和内存分配给最应该被处理的请求。阻塞式模型很像餐厅里一个服务员从头负责一桌客人的所有需求。客人少的时候体验很好逻辑也简单一旦客人多起来服务员数量成了天花板。非阻塞模型则像流水线分工有人专门接单、有人专门传菜、有人专门结账每个人做完自己那步就去服务下一位不需要等服务结束。高并发场景里框架选型实际上就是在选“服务员的调度方式”。同步阻塞框架思路清晰但遇到大量请求互相等待 IO 时线程被白白占住。反应式或事件循环框架通过回调、异步任务把等待阶段腾出来让线程去干别的活。想清楚这个底层逻辑再回头看性能数据就会有完全不同的理解。2. 几类主流框架的性能画像把数据摆到台面上2.1 JVM 系框架的线程模型差异先说 Java 生态。最常用的 Spring Boot默认用的还是 Servlet 容器加 Thread-per-request 模型。Idea 很直接每个请求进来容器从线程池里取一个线程一直跟到请求结束。这个方案对大多数业务系统没问题也是团队技术栈最顺手的选择。但到了高并发场景阻塞模型会有两个代价。一是线程池大小必须手工调调太小了排队调太大了上下文切换和锁竞争反而把吞吐拉下来。二是如果业务里有大量远程调用、数据库访问、消息发送线程会在等待 IO 的时间里空转。很多人以为加机器能解决一切结果成本涨了延迟曲线依然很难看。Spring WebFlux 换成了 Reactor 加 Netty 的响应式模型线程数量少但利用率高。Vert.x 更彻底它允许你在一个 JVM 里开多个事件循环实例把不同业务模块部署在不同的 verticle 上隔离性更强。Netty 则是更底层的网络框架相当于给你一套积木自己搭服务端。这三者的基准性能通常是从高到低排列但同时开发成本和维护复杂度也是递增的。2.2 非 JVM 系框架凭什么后来居上Go 语言这几年的崛起很大程度上是因为 goroutine 这种轻量级并发模型。一个 goroutine 初始栈只有几 KB可以轻松创建几十万个配合 netpoll 网络轮询让开发者用接近同步的代码风格获得接近异步的高并发能力。Gin 或 Fiber 这类 Web 框架在压测里表现亮眼不是框架本身多神而是底层调度模型占了便宜。Node.js 则走的是单线程事件循环路线。它能支撑很高并发的原因在于异步非阻塞几乎所有 IO 操作都会立刻交出去事件循环不会被阻塞。但这个模型对业务代码要求极其严格只要有一个 CPU 密集型的同步任务卡在事件循环里整个进程的吞吐都会塌方。Rust 系框架如 Axum 性能很好因为它没有运行时垃圾回收内存控制精确但这意味着学习曲线陡峭。做技术决策时如果只按性能数据挑Rust 往往会胜出可团队能否在一两周内写出可维护的业务代码才是最大的不确定性。2.3 同场景同参数下的压测参考有次我在测试环境做过一组相对公平的对比。机器规格是 4 核 8GJDK 17压测工具 wrk参数-t4 -c1000 -d60s请求是一个简单的 JSON 返回接口数据量很小。方案RPS 参考P99 延迟参考Spring Boot 3.xTomcat 同步线程池约 1.1 万约 90msSpring WebFluxNetty约 1.6 万约 45msVert.x 4.x约 1.9 万约 30msNetty 手写最小 HTTP 服务约 2.4 万约 18msGo GinGOMAXPROCS4约 2.2 万约 20msNode.js Express约 0.8 万约 120ms这个数据不是要证明某种语言最强。我是想说从单机极限吞吐看框架之间的差距有量级意义但在真实业务里如果下游数据库或第三方接口只能支撑每秒几千次调用框架多出的那几千 RPS 根本体现不出来。因此性能数据首先帮助判断“天花板在哪”其次才谈得上选择。3. 从性能数据到决策别只盯着 RPS3.1 延迟分位数比平均值诚实得多刚开始做压测的人最喜欢看平均响应时间。平均值被极端值抬高以后往往会掩盖真实体验。一个接口的平均响应时间是 50ms听上去不错可如果 P99 是 800ms意味着每一百个请求里就有一个响应慢到用户能明显感知。高并发系统的尾延迟通常才是口碑崩坏的起点。框架对尾延迟的影响非常明显。阻塞线程池在负载超过临界点后请求会进入队列等待等待时间一长P99 会快速抬头。即使吞吐量还没有下降延迟曲线已经露出险情。反而是一些基于事件循环的框架在接近极限时会先拒绝新请求已经接受的请求还能保持相对稳定的延迟。这两种表现没有绝对的好坏取决于业务能不能容忍排队。我在看压测报告时会同时记录 P50、P90、P99 和 P999。如果 P50 和 P999 差距超过 10 倍就必须查是不是有锁竞争、GC 暂停或者连接池等待。很多性能数据不是框架本身输赢而是这些“隐藏噪音”导致的不排查清楚就做决策很容易误判。3.2 资源消耗GC、CPU 和内存的联动只看 RPS 还不够还得看达到这个数据时机器资源是什么状态。Java 响应式框架在大压力下往往会创建大量短生命周期对象这些对象会加剧 GC 频率。GC 暂停会影响延迟尤其在大堆场景下停顿可能达到几十甚至上百毫秒。Go 的 GC 虽然也在持续优化但大规模 goroutine 调度对内存占用并不算省。Node.js 单线程模型里如果出现未捕获的异步回调异常可能直接让进程退出。判断框架适不适合不能只看它峰值吞吐还要看资源使用是否符合你的成本预算和运维能力。实践里我会用同样的接口逻辑分别把 CPU、内存、GC 耗时、网络重传这些指标打点出来再对比框架之间的差异。通常你会看到高性能框架用资源换性能但到底“换”得值不值要结合业务增长评估。如果服务部署在云上每提升一万 RPS 需要吃掉多少核直接决定了长期成本这本身就是技术决策的一部分。3.3 背压能力和“不可杀性”高并发场景不只有请求响应型业务还有大量消息处理型任务。比如从 Kafka 拉取消息做实时计算消费速度不稳定时如果框架不能优雅处理堆积可能导致消费组持续重平衡消息延迟迅速增大。“背压”这个概念用大白话说就是上游倒水太快时下游能不能把阀门拧小一点而不是被水淹掉。很多流处理框架、响应式框架和消息队列客户端都提供了不同层面的背压机制。选型时我会问一个问题当系统一瞬间收到十倍于预期的流量候选框架是会保护自身稳定、快速失败还是一味接收请求导致内存被打爆。处理 KafKa 这类高并发消息场景时有人执着于对比消费框架大小其实更重要的是线程模型和提交偏移量的时机。消费线程如果直接在里面做耗时 IO再好的框架也一样堆积。反过来把消费和数据处理拆成不同线程池加上批量拉取和限流很多框架都能跑出不错的效果。这个经验同样适用于智能体编排框架和任务调度框架它们的高并发瓶颈常常在状态同步上而不是在计算本身。4. 一次真实选型复盘核心推送接口的升级之路4.1 初始设计暴露的问题之前有个项目是做实时消息推送场景比较典型保持在线状态的连接数量高单条消息体积小但对延迟敏感。第一个版本为了快速上线用了团队最熟悉的 Spring Boot加 WebSocket 长连接原计划先跑通业务后面再优化。结果在线连接数到两万左右时服务器就开始频繁报警。诊断后发现 Tomcat 的工作线程已经全部被 WebSocket 长连接占住了。长连接的特点就是连接建立后不会频繁发消息但连接本身一直存在Thread-per-request 模式为每条长时间存活连接都配了一个线程线程池自然很快耗光。后续新连接全部阻塞服务整体进入半瘫痪状态。这个阶段我们学到的最重要一课是不要直接用一个“以为顺手”的框架去套所有业务形态。Spring Boot 本身不是问题问题在于 Tomcat 的线程模型和长连接业务完全错配。即使强行把线程池调大两万线程带来的内存和上下文切换开销也是不可接受的。4.2 两次切换和关键压测数据我们把方案拆成了三个候选Spring WebFlux、Vert.x 和 Netty 手写网关。设计初期没有直接选 Netty 手写是因为业务里涉及鉴权、消息路由、离线存储和用户状态同步纯手写网络层会让业务代码和 IO 逻辑耦合得很深。先测了 Spring WebFlux。推进到一半发现反应式编程要求整个调用链都返回 Publisher 类型已有的同步 JDBC 和第三方 SDK 都要做额外适配。代码改造量超出预期团队里大部分人还没有完全习惯响应式思维。压测数据虽然不错但开发效率明显下滑。后来转测 Vert.x它在底层同样基于 Netty 的事件循环但提供了更贴合传统开发的 verticle 模型。我们可以把推送服务拆成多个 verticle每个 verticle 在独立事件循环里运行通过 EventBus 通信。这个模型保留了异步高并发能力却不像纯响应式流那么侵入业务代码。压测时把真实参数抛进去在线连接数从两万升到十万消息吞吐和 P99 都还在可接受范围。4.3 最终选择背后的综合判断最终落地选择里Vert.x 只是底层基础框架外围依然保留了 Spring 的依赖注入和配置管理。很多中间件和工具类层面的能力并不需要连框架一起推倒重写。这样的组合让团队既拿到了事件循环的高并发优势又没有牺牲太大的开发效率。这个案例里真正起关键作用的不是某个框架单点性能多么出色而是它和业务并发形态的匹配度。我们花了大量时间看性能数据最终做决策时引入了一个很重要的评估维度当请求特征发生改变时这个框架是不是有足够的调节空间。例如 Vert.x 可以通过调整线程池、心跳参数、Backlog 大小来应对突发连接这种可调性在业务快速变化期比纯粹的数字优势更有价值。5. 框架落地时别把容量评估和团队成本晾在一边5.1 把业务指标翻译成技术参数很多框架选型失败败在容量预估拍脑袋。上线前没有想清楚峰值流量是多少也没有把业务量换算成并发请求直到大促或活动流量真的进来才发现服务器数量不够。我在定容量时会先用一个很朴素的公式计算单机预估吞吐 CPU 核心数 × 单核每秒能处理的请求数再乘一个安全系数。安全系数通常取 0.4 到 0.6因为现实系统里还有日志、GC、网络抖动这些额外开销。通过这个公式可以快速推算出需要几台机器也能反过来判断框架提供的压测数据能不能覆盖业务预期。压测基线也要留好。把“哪个框架在多少并发下达到多少 QPS、P99 是多少、机器规格是什么”记录下来后续版本迭代后重新跑一遍看性能有没有退化。没有基线的性能优化全是空谈上线后出现性能问题连是不是框架引入的都说不清。5.2 团队熟悉度和运维可观测性算成本性能数据再漂亮如果团队需要三个月才能熟练那这三个月内的业务风险也是选型成本的一部分。有些团队盲目跟随热搜里的流行框架看到别人用得好就引入却忽略了自己的团队有没有对应的排查能力。框架只是个工具真正出问题时能快速定位的是熟悉底层模型的开发者。可观测性也容易被低估。框架自带的 Metrics 能不能接入 Prometheus链路追踪方不方便线程池、连接数、消息队列积压量这些指标能不能暴露出来都属于技术决策的一环。一个性能稍弱但指标完善、排障路径清晰的框架往往比“裸性能”极高但黑盒严重的框架更适合生产环境。我遇到过因为框架内部线程池命名不规范导致线上 jstack 打出来的线程名全是“pool-x-thread-y”根本分不清是哪个组件在吃资源。后来选型时专门加了一条硬性要求框架内部线程必须有明确含义的名字。这种细节看似不起眼等出了故障就知道多重要。5.3 当热门框架“出圈”时怎么避坑高并发框架选择这件事特别容易被热搜词牵着走。比如“若依框架”这类后台管理脚手架确实能把权限和 CRUD 快速搭起来但它默认的线程模型和架构设计并不是为超高并发准备的。把核心高并发链路塞进后台管理框架后续做性能调优时会非常难受。Spring Boot 也类似它是生态级框架不代表它默认配置就是高并发最优解。真正做决策时要分清“业务开发框架”和“高并发底座的网络框架”是两种层次的东西。前者给你提供 MVC、依赖注入和大量现成组件后者决定请求从网卡进来到业务代码执行之间资源是怎么被调度的。还有 AI Agent、RAG 这类应用前几个月也出现在热搜里。流量入口如果走 Spring Boot 同步模型后面接模型服务时如果响应很慢线程就会被长时间占住。这种场景里更需要把入口层与任务处理层分开选框架入口层要扛住连接和并发任务处理层要能管理超时、重试和状态。说得更直白点不存在一个“全功能框架”能解决所有层次的高并发问题组合使用比单选更接近答案。6. 从压测到上线我踩过的坑和排查技巧6.1 压测环境做不好数据全是噪音不少团队第一次做框架压测时连压测机器本身都没管好。wrk 或 JMeter 所在的客户端先达到 CPU 上限导致被测框架还没到真正压力点数据就已经失真。做对比测试时我会让压测机与被测服务分开部署至少保证客户端 CPU 使用率不超过 70%否则结果没有任何参考价值。压测脚本里也要注意连接模型。默认短连接请求和真实用户的长连接行为差距很大框架的连接处理能力容易被高估或低估。我会根据线上流量特征写压测场景比如 80% 的请求集中在三个热点接口上剩余的均匀散落这样得到的性能数据才能说明问题。并发数并不是越高越好。压测最有价值的产出是找到“拐点”也就是服务吞吐开始不再上升、延迟急剧抬头的那个位置。很多框架在低并发时表现都差不多一旦接近拐点就能看出谁先扛不住。与其用五千并发压到全部超时不如用两百、四百、八百这么逐级摸上去。6.2 线上一旦劣化按这几个信号排查框架上线后并不是万事大吉。最常遇到的三个劣化信号是响应时间呈阶梯状上升、线程池拒绝异常增多、GC 时间明显变长。信号出现时不要急着重启服务先看监控面板上的活跃线程数和队列长度这两个指标能快速判断是任务堆积还是处理能力降低。如果是 Java 服务jstack 打几次线程快照对比不同时间点线程状态。大量线程处于 BLOCKED 或 WAITING说明锁竞争或线程池耗尽大量线程 RUNNABLE 且 CPU 占比高说明业务计算或 GC 线程出了问题。Go 服务则先看 goroutine 数量如果平白多出几十万个多半是某个阻塞调用没设置超时。热词里的“pytest 框架”“Agent 框架”这类工具在高并发排查时也会遇到类似的资源模型问题。测试框架跑并发用例时线程数开了多少、Agent 任务编排时状态锁粒度是不是太粗都会造成吞吐下降。排查思路是通用的不迷信框架内置的隐藏调优先找出资源到底被谁占住。6.3 一个小习惯给业务代码和框架之间留隔离层如果让我只分享一个选型建议那就是不要在业务代码里直接散落大量框架专用 API。通过一个薄薄的适配层把网络协议、路由策略和业务处理器隔开后续要换框架时核心业务代码的改动量会小很多。举个例子推送服务内部定义了一个 MessageHandler 接口Vert.x 的 HTTP 接收器只负责把请求体解析出来然后调用这个接口。框架负责连接和协议业务只面对普通的方法调用和返回值。这样即使将来想迁移到其他异步框架也不会伤筋动骨。从压测到上线这个周期里我越来越觉得框架选型不是一锤子买卖。高并发场景下的最佳选择会随着业务形态、团队能力和基础设施变化而漂移。与其争论哪个框架性能最强不如认真积累自己系统的性能基线在特定负载模型下用数据做验证。毕竟真正支撑起高并发系统的是清晰的技术决策逻辑和能吃苦的排查能力而不是某个框架的名字本身。
