大模型SSE流式接口性能压测实践:指标、并发模型与工具应用
做性能测试这么多年我一直有个体会压测工具和被压系统的通信模型必须保持一致否则结果再漂亮到了线上也是两张皮。很多团队给大模型应用做上线前压测习惯性打开通用压测工具挂一批线程对着对话接口直接打。结果通常很好看QPS上千平均耗时几十毫秒错误率接近于零。可真上了线用户反馈清一色是卡。请求发得出去但字出得太慢——这里缺的正是一套针对 AI 流式输出场景的性能测试手段。7DGroup 开源的这款 AI SSE 流式输出性能测试工具就是专门补这个缺口的。这篇文章我会把它拆开来讲SSE 协议在底层给压测带来了什么差异这个工具的度量模型和并发模型是怎么设计的从部署到跑完一场压测的完整流程以及流式指标到底应该怎么解读。无论你是性能测试工程师、运维还是做大模型应用的后端开发看完应该能把这套工具用起来并且知道压测结果里的每个数字在说什么。1. 为什么流式接口的压测不能照搬传统方案1.1 传统压测工具测的是一次请求用户看到的是一个流拿餐厅打个比方。传统 HTTP 接口像点外卖你下单骑手把整份餐送到整个过程结束。压测工具记录下单到送达总时长这个时间很直观。但大模型对话接口不是送外卖它是后厨做好一道菜就端一道菜鱼香肉丝上来之后你还在等第二道中间甚至还有厨师停下来想菜谱的时间。用户感受到的是第一道菜多久上来两道菜之间的间隔稳不稳定一整套菜最终齐不齐。SSE 流式输出就是这种模式。客户端发一个请求服务端建立的是一条长连接然后在这条连接上持续推送数据块。从浏览器角度看就是那个一个字一个字蹦出来的打字机效果。这段体验不是一次瞬时的请求-响应而是一个有开始、有中间过程、有明确结束标志的完整事件流。通用压测工具最大的问题恰恰在于它把发出请求、收到完整响应作为一个事务的终点。遇到 SSE 接口很多工具在收到第一个数据块时就已经认为响应完成了后面那一大段流式数据根本没有被纳入计时和统计。你拿这样的工具去做大模型压测等于只测了服务员把第一道菜端上桌的时间后面的菜上得再慢、断没断你完全不知道。1.2 SSE协议的关键机制连接、分块与心跳要把 SSE 场景的压测做好得先理解 SSE 协议的几个基本事实。SSE 全称 Server-Sent Events它是基于普通 HTTP 的单向流式通信。服务端在响应头里声明Content-Type: text/event-stream然后连接保持打开服务端可以持续向客户端发送多行事件。每一帧的格式很朴素核心字段就那么几个以data:开头的数据行、以event:命名的事件类型还有可选的id:。一条消息可以由多行 data 组成消息之间用空行分隔。很多大模型推理服务在流结束时会发送一行类似data: [DONE]的特殊数据业务上把这个当结束信号。还有一个容易忽视的细节心跳。SSE 连接如果长期没有真实数据推送中间经过的代理或网关可能会因为连接空闲把它掐掉。所以很多服务端会定时发一个注释行以冒号开头或者发个空行纯粹为了保活。做压测时如果傻傻按超时无数据去断连很容易误伤那些还在正常思考中的会话。SSE 和常见的 WebSocket 有点像但本质不同WebSocket 是双向全双工数据库可以双向实时推SSE 是服务端到客户端的单向流客户端想发消息得另外发 HTTP 请求。单看方向性好像 WebSocket 更厉害但对大模型对话这种场景SSE 反而更合适——简单、基于 HTTP、浏览器原生支持尤其适合用户问一句、服务端持续答一堆的交互。1.3 通用压测工具面对SSE的三大失真通用工具测 SSE 接口我总结下来有三层失真做性能工作的人如果只看汇总报告很容易被误导。第一层失真在吞吐量统计。通用工具习惯用每秒请求数衡量服务能力但 SSE 场景里一个用户一分钟可能只发一两个请求大部分时间都挂在一条连接上等着收数据。用 QPS 衡量这种服务高并发可能只代表几十个请求在轰炸和真实用户规模差着数量级。第二层失真是事务边界。前面说了收到第一个 chunk 就算完成导致首字之后的流式阶段全是盲区。最典型的表现压测报告显示响应时间几十毫秒但首字之后服务端每隔一两秒才吐一个 token累计下来用户等了几分钟。这种畸形的长尾体验在通用工具的聚合结果里根本看不出来。第三层失真是错误分类。传统工具把连接失败读超时当成通用错误但 SSE 场景里服务端中途断开和连接建立失败是完全不同的问题。前者往往意味着推理引擎异常、内存溢出、并发容量饱和后者可能只是负载均衡或网关配置问题。错误类型不细分定位问题就得靠日志一点点扒。所以不是说通用工具不能用而是它的模型决定了它不适合测这类接口。要测准 SSE就得用按 SSE 语义设计的专用工具。7DGroup 这款工具正是因为绑定了 SSE 的语义才能把这几个盲区逐个补上。2. 7DGroup流式压测工具的内核设计2.1 以流式会话为单位的质量模型7DGroup 这款工具在度量模型上换了基准单位。它不把一次 HTTP 往返当作最小事务而是把从建立连接、持续接收事件、到最后收到结束标记的完整过程做成一个流式会话。所有统计都基于会话展开。围绕流式会话工具记录的几个关键指标我列个表说明它们各自在回答什么问题指标含义对应的是什么用户体验首事件耗时从发出请求到收到第一个数据块的时间用户点了发送之后屏幕上第一个字出来的速度事件到达间隔相邻两个数据块之间的时间差打字机蹦字的节奏卡不卡、稳不稳流总耗时从连接建立到流结束的完整时长用户等完整段回答的总时间完整率按结束标记或事件数判断流是否完整结束有没有出现说一半就停住的情况异常分类连接失败、等待超时、中途断开等细分错误问题出在哪个环节这里最值得说的是事件到达间隔。传统压测里没有这个指标它的分布形态对用户体感影响极大。如果一个 SSE 流的输出时间轴是前 3 秒极其流畅、中间突然停顿 8 秒、后面又恢复平均间隔可能并不难看但用户会觉得卡了一下甚至是不是死掉了。工具如果把到达间隔的 P50、P95、P99 都算出来这种服务端生成速度抖动就无所遁形。2.2 以活跃连接数为单位的并发模型并发模型是这款工具另一个和传统压测明显不同的地方。通用工具控制的是每秒请求速率它控制的是同一时刻活跃的流式连接数。为什么这么做两个原因。一方面更贴近真实用户。真实场景里用户的对话是发一句、等一段流、看完、思考一下、再发一句发请求的频率远低于连接长期存活的概率。压测里维持 100 个活跃连接就相当于有 100 个用户正在盯着界面等输出。另一方面更贴近服务端的资源瓶颈。大模型推理服务每个流式会话都要占用模型推理资源一个用户占一份服务端的真实压力取决于同时有多少个会话在推理而不是每秒涌入多少个请求。用活跃连接数做并发单位压测脚本的表达也变得很自然维持 N 个连接每个连接循环执行发起一轮流式请求 → 收完整个流 → 模拟用户思考 → 发起下一轮。这种模型下并发数、连接保持时间、每轮流的长度都跟线上真实行为一一对应。配置出来的压测场景不再是抽象的数字而是20 个人同时用对话助手这样可以被产品经理和开发团队共同理解的语言。2.3 为自动化而生的设计取向从我实际使用这几周的感受看这个工具在自动化方面的投入是刻意为之的。它支持全配置文件驱动压测用的目标地址、请求头、请求体、并发数、时长、断言条件都能写进 YAML。没有图形界面也能跑适合放在服务器上批量执行。压测结束后的产物也考虑得很细。除了控制台能看实时指标结果会落盘成结构化文件包括完整率和错误分类。更关键的是它支持按断言条件决定退出码——比如首字 P95 超过 3 秒就判定失败返回非零。这意味着它可以被接进发布流水线当门禁每次发版前自动跑一轮不合格直接挡掉不再依赖人肉盯着报表做判断。这种可自动化、可观测、可判定的设计让性能测试从一次性的排查动作变成持续的回归动作。对应用迭代频繁的团队来说价值比单次压测结果高得多。3. 实操从部署到跑完第一场流式压测3.1 部署与启动先让工具跑起来先说部署。这个工具对机器没有特殊要求我建议压测机至少 8 核 16G因为大并发下工具自身也要消耗 CPU 处理数据帧。可以从发布页下载对应平台的压缩包解压后直接启动服务端。启动命令大致长这样./7d-sse-server --listen 0.0.0.0:9000服务起来之后浏览器访问http://localhost:9000就能看到控制台界面会显示工具本身的状态比如当前有没有压测任务在跑。端口可以按需要改9000 只是示例。如果要跑大规模压测建议用多台压测机同时执行每台机器控制一个并发子集最后再合并汇总结果。启动完成后先别急着压先在控制台或配置里确认工具能连上被测服务。这一步看起来多余但能省掉后面排查环境问题的不少时间。拿一个已知正常的流式接口做一次 1 并发、10 秒的冒烟测试通了再上规模这是我会反复强调的基本习惯。3.2 配置一场真实的对话流压测场景设计是压测的灵魂。我拿一个常见的案例来说明被测服务是某个基于开源推理引擎搭建的对话服务接口路径是/v1/chat/stream。业务方的要求是验证20 个用户同时使用对话功能输出流不断、首字不要让人等太久、整体体验可接受。对应到配置上我一般会写成这样global: concurrency: 20 duration: 10m warmup: 30s target: url: http://your-service/v1/chat/stream method: POST headers: Content-Type: application/json body: | { model: chat-model, stream: true, messages: [ {role: user, content: 请用一个长句说明性能测试的意义} ] } stream: end_marker: data: [DONE] first_event_timeout: 5s idle_timeout: 30s assertions: first_event_p95: 3s complete_rate: 99%逐项说几个关键参数。concurrency: 20是目标活跃连接数20 个虚拟用户全程在线。warmup: 30s是预热期这 30 秒的结果会单独标记不计入最终断言避免冷启动拉低指标。end_marker告诉工具什么样的数据帧代表流结束这是判定完整率的基础。first_event_timeout是首字超时超过这个时间还没有任何数据帧计一次异常。idle_timeout是相邻两个数据帧之间的最大间隔超过则认为流中断。这里特别说明一下idle_timeout。大模型想答案的时候可能很长时间不说话但这个沉默和真断了是两个概念。我建议你根据被压服务的实际行为来调初始可以从 30 秒起步跑一轮看日志里有没有大量超时误报再决定收紧还是放宽。宁可先放宽也别一轮压测下来全是假异常白忙活。3.3 执行压测与实时观察配置文件准备好之后直接命令行拉起任务./7d-sse-runner --config chat-stream.yaml --output result/执行过程中控制台会实时展示当前的活动连接数、已完成会话数、首字耗时趋势等指标。可以看到随着并发逐步到 20首字耗时如何变化。预热期内指标波动大属于正常现象真正看趋势要等预热结束、并发顶满之后再观察。跑完之后result/目录下会有聚合结果和明细日志。压测期间如果发现首字 P95 已经远远超出断言阈值可以直接停下来先看服务端监控——常见的情况是推理引擎的显存被打满或者中间网关的并发连接数到了上限。一次压测的价值往往就体现在这种压出来的问题上而不是跑完一个绿油油的报告。3.4 结果解读从数字判断服务健康度压测结束后的结果表大致长这样活动流式连接成功会话数首字 P50首字 P95Token间隔 P50Token间隔 P95完整率20/20387612ms2480ms82ms390ms99.2%怎么判断好不好先看首字耗时。P50 六百毫秒算正常P95 接近 2.5 秒说明有部分请求排队明显可能是并发上来之后服务端调度出现了拐点。再看 Token 间隔P50 在 80ms 上下说明大部分时间生成节奏很好P95 到 390ms意味着有 5% 的时刻会出现肉眼可感知的停顿。最后看完整率99.2% 意味着有 3 个会话没有正常走完需要去看错误分类里是超时、断开还是连接失败定位是哪一层的问题。这套读法和我做传统压测的习惯很不一样。传统压测我会先看错误率和响应时间均值面对流式压测顺序变成了首字决定第一印象Token 间隔决定整体流畅度完整率决定有多少会话被腰斩最后才是错误分类定位。面向用户体验的指标优先级天然就比面向服务端的指标高。4. 流式指标阈值与门禁配置建议4.1 不同场景下的合理阈值参考流式指标没有放之四海皆准的硬标准不同业务的用户耐受度完全不同。我根据自己的实测经验给一个阈值参考范围应用场景首字 P95Token间隔 P95完整率通用对话助手3s500ms99%代码补全/翻译1s200ms99.5%长文档总结/离线任务5s1000ms98%对话助手场景对首字更敏感因为用户等着第一句话开始读;代码补全对首字要求极高光标旁边迟迟不出内容人会立刻切走长文档总结则相反用户预期就是等得久一点反而对稳定性的耐受度更高。这里要提醒一点阈值是给断言用的不是给分析用的。压测跑出来的真实指标哪怕全部低于阈值也要看分布趋势。比如首字 P95 连续三次压测从 2s 涨到 2.9s说明服务在劣化只是还没爆雷。把趋势变化纳入关注范围比单次卡阈值更有价值。4.2 把质量模型接进发布流程前面说工具支持退出码判定真正的用法是把它变成流水线里的一个门禁阶段。每次发布前自动执行一轮固定场景的流式压测只要结果不满足断言流水线就挂掉。这样做的效果比让测试同学每周手动跑一次压测再写报告要直接得多。我的建议是先做一个基线版本。选一次服务状态良好的时候跑通压测把首字 P95、Token 间隔 P95、完整率这些指标存下来。之后的每次回归都拿新结果和基线对比。出现明显劣化就自动告警不用等用户线上反馈。这个思路其实也是性能回归自动化的基本玩法只不过把事务模型从普通请求换成了 SSE 流。配置断言的时候要记得留一点冗余。网络抖动、压测机自身波动都会带来小幅波动把断言卡得太死流水线天天亮红灯团队就会对门禁脱敏。一般建议阈值比业务可接受值放宽 20% 左右比如业务要求首字 P95 不超过 3 秒断言就设 3 秒以内但标准偏差容忍稍大或者在连续两次失败时才阻断发布。4.3 用用户思考时间让压测更真实流式压测最容易犯的错误是让虚拟用户像机器人一样一个流刚结束立刻发起下一个请求。真实用户不会这样——他要读完上一段回答可能还要编辑一下输入框才发下一句。这个间隔在性能测试里叫思考时间SSE 场景同样需要。配置里可以设置两轮流之间的平均间隔和随机波动。我的一个常用做法是模拟用户阅读完整回答的时间与回答长度挂钩平均每 100 个 token 阅读 3 到 5 秒再加上 5 到 15 秒的随机思考时间。这样压出来的吞吐量更接近真实线上不会因为请求过于密集把服务端压出一个线上不存在的极端形态。反过来如果你是刻意要压出服务的容量上限那可以故意把思考时间设为零所有虚拟用户都在收到完立刻再发。这种压法测的是系统极限但你要清楚它的结果不代表线上真实体验。两种压法各有用途别搞混。5. 踩过的坑与排查路径5.1 压测机自己先被压垮第一次跑 200 并发的时候我发现压测结果里出现大量连接失败第一反应被测服务出问题了查了半天服务端负载很低回头一看压测机CPU 接近打满进程的文件描述符数量顶到了上限。这个坑很典型。SSE 长连接比短连接更消耗压测机资源每个活跃连接都要维持一个 TCP 连接、一个接收缓冲区、一套事件解析状态。beast.连接数一高先倒下的未必是被测服务很可能是压测机自己。宿主机还要注意 ulimit 限制默认的 1024 文件描述符上限几百个连接就容易撞枪口。ulimit -n 65535超过单机能力之后最稳妥的办法是多台压测机分摊并发。每台机器连一部分用户统一汇总结果。你也会慢慢发现压测系统本身的容量规划本身就值得花时间不然指标失真都不知道是谁的锅。5.2 思考期长间隙与误报超时的冲突有一次压一个带思维链提示的模型任务的差错列表里全是等待数据超时完整率低得吓人。我看服务端日志发现推理引擎一切正常只是模型进入了一段十几秒的长考一个 token 都没吐。我们设置的 idle_timeout 只有 5 秒于是全部误报。解决思路不是无脑把超时调大。你需要先区分两种情况一种是连接还在、数据没来比如服务端用注释行或空行做心跳连接层面是活的另一种是连接已经被关闭数据彻底不来了。前者应该继续等后者才是真异常。做法上可以结合压测详单和网络抓包确认连接状态再把 idle_timeout 调到能够覆盖正常情况下的最长思考时间同时保留对连接断开的独立错误统计。如果你压的服务有成本审计需求也可以在压测配置里把等待数据超时和连接中断分开计数这样定位问题时就清楚得多不会被一个笼统的超时错误带偏。5.3 流结束标记不规范导致完整率失真另一个真实案例是被测服务在部分会话里没有按约定发送data: [DONE]而是靠关闭连接来表示结束。结果压测统计的完整率只有 93%但业务说自己没问题。工具按结束标记判断流是否完整这本身没错错的是服务端实现没有遵守 SSE 的结束语义。这类问题的处理思路是让完整率的判定条件和业务端对齐。比如配置里指定一个业务字段作为结束条件或者校验事件数量是否符合预期。服务端实现如果不统一优先推动业务方规范流结束语义因为压测工具不可能替你修正被压服务的所有历史债。压测报告出现完整率异常时第一件事不是怀疑工具而是确认业务端认为怎样才算完整。这个定义对齐了后续所有指标才有意义。5.4 详细日志过大拉低效率长时压测会把每个事件的内容都记下来方便事后分析但代价是日志文件急剧膨胀。一次 10 分钟、50 并发、每个流几百 token 的压测明细日志可能就超过几个 GB。磁盘一满压测又会踩出新坑。我的做法是分场景记录小规模、需要定位问题时开完整日志常规回归压测只保留聚合结果和错误样本明细数据按比例抽样。工具一般会提供采样率配置比如只记录 1% 的详细流数据。聚合指标足够完成门禁判断明细数据只在指标异常时才需要。这不是妥协是性能测试的基本效率管理。现象可能原因处理方式大量连接失败但服务端负载低压测机文件描述符或带宽耗尽调大 ulimit、换多机压测完整率低且全是等待数据超时idle_timeout 小于模型思考时间调大超时并分开统计连接断开完整率低但没有超时错误服务端流结束标记不统一和业务对齐结束条件定义结果文件过大详细事件日志全量落盘开启日志采样只留聚合数据做流式压测这段时间我最深的体会是工具解决了能不能测准的问题剩下的怎么用准还是压在测试工程师身上。7DGroup 这套工具的指标体系和并发模型正好把我们对 SSE 场景的关注点从请求成功与否拉到了流完整性和输出节奏上。建议你先把 10 并发的小规模压测跑通把首字耗时、Token 间隔这些概念在自己的服务上建立直观认知再慢慢加压。等把它接进流水线连续跑几轮回归之后你会发现很多线上体验问题在上线前就提前暴露出来了。这个价值比单独跑一次压测大得多。