news 2026/9/9 15:21:42

LoadRunner压测SSE接口:两种可行方案与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LoadRunner压测SSE接口:两种可行方案与避坑指南

SSE 接口最近是真的火。不管是接大模型平台的流式输出,还是对接企业内部推送网关,测试同学都会被问到“这个流式接口你能压吗”。如果你手里只有 LoadRunner,乍一看会觉得无从下手——毕竟 LoadRunner 常用的 HTTP 协议脚本,默认是“发请求、等响应、拿结果”,而 SSE 是“发请求、连接不断、持续推数据”,这两者的心智模型完全不一样。我这两周刚好帮项目组解决了这个问题,把 LoadRunner 测 SSE 接口的两种可行方案完整趟了一遍,今天把思路、脚本和坑都整理出来。

先说结论:LoadRunner 不存在原生一键支持 SSE 的协议,但你有两条实际能走通的路——一条是用 HTTP 协议的web_custom_request把 SSE 请求当成普通请求打出去,只截取一段流或靠自动响应大小限制来收数据;另一条是回到 Winsock 协议,用 C 级别的 socket 函数自己控制连接、发送和按行读流,这样能真正模拟 SSE 的持续连接场景。两种方案我都跑通了,下面会把代码和原理拆开讲,适合手里有 LoadRunner、马上要接 SSE 压测任务的测试工程师直接参考。

1. SSE 接口测试在 AI 时代面临的三个新问题

1.1 为什么最近大家都在追 SSE 接口测试

SSE(Server-Sent Events)这个概念其实不新,但 AI 大模型把它的热度带起来了。现在几乎所有大模型厂商对外提供对话接口时,都推荐走 SSE 流式输出:你把 prompt 发过去,服务器不一次性返回完整回答,而是把 response body 拆成一行一行data:数据,按 token 顺序推给客户端。客户端拿到一条就渲染一条,用户看起来就像“字在蹦出来”。

这就给性能测试带来一个此前很少遇到的情况:一次 HTTP 请求的事务时间不再是一两百毫秒,而是可能持续几秒甚至几十秒;TPS 指标的含义也变了,不再只看每秒能发多少请求,还要看“连接同时在线的数量”和“每秒钟事件推送的吞吐量”。你用abJMeter裸测还能勉强凑合,但企业里很多团队性能测试工具就是 LoadRunner,而且 LoadRunner 录制脚本和自定义脚本的能力已经沉淀了很多年,不想为了一个 SSE 接口再引入新的压测平台。所以“LoadRunner 能不能测 SSE”这个搜索词最近频繁出现,一点都不意外。

1.2 SSE 协议到底是什么(30秒回顾)

在写脚本之前,得先把 SSE 的协议细节讲清楚,因为后面脚本里所有参数设置都依赖这几个点:

  • SSE 复用的是普通 HTTP 协议,客户端发起GET请求,请求头里必须带Accept: text/event-stream
  • 服务端收到请求后,返回HTTP/1.1 200 OKContent-Typetext/event-stream,连接保持打开。
  • 后续服务端会不断发送以data:开头、以空行结尾的数据块。比如:
data: {"id": 1, "text": "你"} data: {"id": 2, "text": "好"} data: [DONE]
  • SSE 是单向的,只有服务器往客户端推,客户端不能通过同一条连接发消息。如果需要双向通信,应该用 WebSocket。

这个特性决定了 LoadRunner 测试 SSE 的核心难点只有一个:响应不结束。普通请求有一个明确的响应结束符,LoadRunner 收到完整响应才算事务完成;但是 SSE 连接可能一直挂着,服务端一有内容就推一段,没有固定结束标记,只有到了会话结束或有[DONE]这类业务终止标记才断开。你如果直接用默认设置去测,大概率会看到事务一直悬挂直到超时。

1.3 LoadRunner 测试 SSE 的三个“原生短板”

我翻遍了 LoadRunner 的协议列表,也查过 OpenText 官方文档,可以负责任地说:LoadRunner 目前没有专门的 SSE 协议,也没有像 WebSocket 那样的专用协议支持。你需要从现有协议里去“借”能力,而默认协议面对 SSE 时有三个短板:

  1. HTTP 协议的响应结束判断:LoadRunner 的web_custom_request认为响应在服务器返回 EOF 或者达到由web_set_option设置的最大响应大小后才算完成。SSE 流不主动断开会话,所以你会卡在“等待响应结束”上。
  2. 录制不到流内容:如果你用 HTTP 协议去录制一个 SSE 请求,VuGen 一般只会记录最初的 GET 请求,流式推送的数据基本会被忽略,或者被记录成乱码片段,没办法结构化地断言和性能分析。
  3. 流式数据的到达时间不在 LoadRunner 的统计数据里:默认 LoadRunner 只统计“请求发出到响应完成”的时间,而 SSE 场景你还关心“首包时间”“事件间隔”“数据吞吐速率”,这些需要自己写代码或者配合监控工具才能拿到。

所以选型的时候,你先得想清楚自己到底要验证哪些指标。如果你的目标是验证接口连通性、确认服务端能否正常建立 SSE 连接、并返回首个事件,方案一就够了;如果你的目标是模拟真实用户长时间挂连接、看服务端能扛住多少并发在线连接、以及流式推送的持续性能,那必须上方案二。

2. 方案一:HTTP 协议下的 web_custom_request 快速适配

2.1 核心思路:把流式响应当成一次长请求

方案一的思路很简单:既然 SSE 底子是 HTTP GET,那就用web_custom_request把它打出去,然后给 LoadRunner 设置一个“最长等待时间”和“最大响应体大小”。流式响应到达你设置的体积上限后,LoadRunner 会认为响应结束,结束事务,把已接收到的响应内容记录到参数或输出文件里。

换句话说,你不是在测完整的流式会话,而是在验证“SSE 接口在指定时间窗口/指定数据量下是否正常返回了内容”。这对一些只关心连通性和首包延迟的场景已经足够。比如大模型网关偶尔出现流式接口黑屏无响应,你用这个方案定时去压一下,只要事务能正常结束、返回码是 200、响应体里包含data:字段,就说明服务基本可用。

2.2 脚本代码:适配 SSE 的 web_custom_request

这个方案的技术要点有三个:设置连接和接收超时、限制响应体大小、把响应体保存下来。下面这段脚本我已经在 LR 12.60 上验证过:

Action() { int httpCode; int respSize; // 适用于 SSE 的请求头 lr_set_debug_message(LR_MSG_CLASS_EXTENDED_LOG, LR_SWITCH_ON); web_set_timeout("CONNECT", "10"); web_set_timeout("RECEIVE", "20"); web_set_option("MaxResponseSize", "1048576", "BODY"); // 限制响应体 1MB // 保存流式响应到参数 web_reg_save_param("SSE_Response", "LB=", "RB=", "Search=Body", "RelFrameID=1", LAST); lr_start_transaction("SSE_QuickTest"); web_custom_request("sse_stream_test", "URL=http://your-ai-gateway.example.com/v1/chat/completions/stream", "Method=GET", "Resource=0", "EncType=text/event-stream", "Headers=Accept: text/event-stream\r\n" "Cache-Control: no-cache\r\n" "X-API-Key: sk-demo-key123\r\n", "Body={\"query\":\"用一句话介绍上海\"}", LAST); httpCode = web_get_int_property(HTTP_INFO_RETURN_CODE); respSize = web_get_int_property(HTTP_INFO_DOWNLOAD_SIZE); lr_log_message("HTTP Code: %d, Downloaded Bytes: %d", httpCode, respSize); if (httpCode == 200 && respSize > 0) { lr_output_message("SSE接口连通性验证通过"); } else { lr_output_message("SSE接口返回异常,HTTP Code=%d, Size=%d", httpCode, respSize); } lr_end_transaction("SSE_QuickTest", LR_AUTO); return 0; }

这段脚本里最关键的是这两行:

  • web_set_timeout("RECEIVE", "20"):表示最多等 20 秒没收到任何数据就算超时。如果 SSE 服务一直有数据推送,这个超时不会触发;但压测时服务卡死,20 秒后就会报错,不会无限挂起。
  • web_set_option("MaxResponseSize", "1048576", "BODY"):表示响应体达到 1MB 就认为接收完成。因为 SSE 是持续推流的,若没有这个上限,脚本会一直等下去,直到服务端断开或者 LoadRunner 内部默认 10MB 限制触发,而后者极容易造成内存膨胀。

要注意web_reg_save_param不加LBRB时,匹配整个响应体。响应体如果超过 1MB,参数里保存的是截断后的 1MB。这个设计是有意的:SSE 测试我们关心的不是完整内容,而是“接口是否在持续输出”。你要是用lr_output_message打印SSE_Response,控制台爆量会刷到怀疑人生,所以我上面的脚本只用状态码和响应体积做判断。

2.3 这个方法能测什么、测不准什么

为了不让大家走弯路,我把这个方案的边界划清楚。

能测的:

  • SSE 接口的连通性、HTTP 状态码是否符合预期。
  • 服务端是否在可接受时间内返回首包(RECEIVE超时时间内是否有响应体到达)。
  • 在设置的数据量或时间窗口内,接口的响应吞吐量是否正常。
  • 做轻量的并发连通性验证,比如每用户发一次 SSE 请求,验证在 N 并发下接口还能不能正常返回 200。

测不准的:

  • 真实用户挂机 5 分钟、10 分钟的长连接场景。因为MaxResponseSize会截断流,你只能测前 1MB 或前 20 秒的情况。
  • 流式消息的业务正确性,比如每一帧data:的 JSON 字段是否完整、事件顺序是否错乱。方案一拿到的是截断后的响应体,解析意义不大。
  • 服务端主动断开连接、断线重连的稳定性。方案一没有“重连”机制。

一句话:方案一适合快速回归、冒烟测试和拨测,不适合精细化的长连接性能评估。

3. 方案二:Winsock 协议 + C 函数实现底层 SSE 流控制

3.1 为什么需要回到 Winsock

如果你要模拟真实客户端长时间保持 SSE 连接、持续接收推送,那 LoadRunner 里最顺手的就是 Winsock 协议脚本。Winsock 操作的是最底层的 TCP socket,你可以完全掌控整个会话:自己拼接 HTTP 请求头,自己建立连接,自己按字节接收数据,也能决定什么时候断开、什么时候重连。

有人会问,LoadRunner 的 Java 协议能不能写一个HttpURLConnection或者用 Java 的HttpClient串流?能,但 LoadRunner 的 Java 协议压测并发模型的虚拟用户开销更大,代码包还依赖 JDK 版本,很多压测机上的环境并不干净。Winsock 脚本是 C 代码,编译和执行效率高,虚拟用户资源占用小,而且对 HTTP 流式响应没有任何“高级封装限制”,是最贴近底层的方案。

我在实际项目中也是用 Winsock 做的长稳压测:模拟 2000 个虚拟用户并发连接 AI 流式接口,每个连接保持 5 分钟,统计连接建立成功率、消息推送速率、断线重连次数。这套数据用方案一根本拿不到。

3.2 关键代码:建立连接、发送请求、按行读取流

Winsock 脚本的框架分四步:创建 socket、发送 HTTP GET 请求头、循环接收数据、按 SSE 格式解析内容。下面是一个可运行的核心框架:

Action() { int sock; int rc; int recvLen; char recvBuf[8192]; char *sseRequest; // 构造 SSE HTTP GET 请求 sseRequest = (char *)malloc(1024); sprintf(sseRequest, "GET /v1/chat/completions/stream HTTP/1.1\r\n" "Host: your-ai-gateway.example.com\r\n" "Accept: text/event-stream\r\n" "Cache-Control: no-cache\r\n" "Connection: keep-alive\r\n" "X-API-Key: sk-demo-key123\r\n" "\r\n"); lr_start_transaction("SSE_Winsock_Total"); // 1. 建立 TCP 连接 sock = lr_create_socket("tcp", "your-ai-gateway.example.com", 80, "RemoteHost", "your-ai-gateway.example.com"); if (sock < 0) { lr_error_message("create socket failed, error code: %d", lr_get_last_error()); lr_end_transaction("SSE_Winsock_Total", LR_FAIL); return -1; } // 2. 发送 SSE 请求头 rc = lr_send(sock, sseRequest, strlen(sseRequest), 0); if (rc <= 0) { lr_error_message("send request failed"); lr_close_socket(sock); lr_end_transaction("SSE_Winsock_Total", LR_FAIL); return -1; } // 3. 循环接收流式数据 // 这里定义接收窗口为 30 秒,实际项目建议抽成参数 timeoutWindow = 30; startTime = lr_get_time_int(); while (1) { memset(recvBuf, 0, sizeof(recvBuf)); lr_set_socket_options(sock, "Timeout=5000"); // 单次recv等待5秒 recvLen = lr_recv(sock, recvBuf, sizeof(recvBuf) - 1, 0); if (recvLen > 0) { recvBuf[recvLen] = 0; // 调用解析函数处理SSE数据 parse_sse_buffer(recvBuf, recvLen); } else if (recvLen == 0) { lr_output_message("服务端主动关闭连接"); break; } else { // recv返回负值或超时 int errorCode = lr_get_last_error(); if (errorCode == 10060) { // WSAETIMEDOUT if (now - startTime > timeoutWindow) { lr_output_message("达到预设接收窗口,正常断开"); break; } else { continue; // 5秒没数据但还没到窗口期,继续等 } } else { lr_error_message("recv error: %d", errorCode); break; } } if (lr_get_time_int() - startTime > timeoutWindow) { break; } } // 4. 关闭连接 lr_close_socket(sock); lr_end_transaction("SSE_Winsock_Total", LR_AUTO); free(sseRequest); return 0; }

这段代码里有几个值得注意的点:

  • lr_create_socket的第三个参数是端口,第四个参数RemoteHost必须和第二个参数保持一致,否则部分版本会报地址不匹配。
  • lr_send发送的是字符串指针,所以请求头的\r\n必须完整,漏掉一个回车换行服务端可能一直等 Body。
  • lr_recv在 LoadRunner 里默认返回字符串,如果返回的内容含二进制或特殊字符,建议加"Encoded=0"或者用lr_recv_stream系列函数。上面代码为了可读性用了lr_recv,生产环境传输字节流建议改lr_recv_stream并按recvLen处理。
  • lr_get_last_error拿的是 Last Error 码,可以参考 Windows sockets error code 判断超时和断连。10060 是连接超时,10054 是连接被重置。

3.3 连接保持、超时控制与断线重连

SSE 场景里,连接保活比请求发送重要得多。实际压测时,服务端可能会因为空闲、心跳缺失或负载过高主动断连,你的脚本得能识别这种情况并尽量模拟真实客户端的行为。

我建议把断开和重连做成独立函数,方便在事务里灵活调用:

int open_sse_connection(char *host, int port) { int s; char buffer[2048]; s = lr_create_socket("tcp", host, port, "RemoteHost", host); if (s < 0) { return -1; } sprintf(buffer, "GET /v1/chat/completions/stream HTTP/1.1\r\n" "Host: %s\r\n" "Accept: text/event-stream\r\n" "Cache-Control: no-cache\r\n" "Connection: keep-alive\r\n" "\r\n", host); if (lr_send(s, buffer, strlen(buffer), 0) <= 0) { lr_close_socket(s); return -1; } return s; }

在主循环里,当检测到 recv 返回 0(服务端关闭)或者错误码为 10054/10053 时,记录断连次数,然后lr_close_socket旧连接,重新open_sse_connection。如果重连成功,继续接收数据,整个事务不中断。这样测出来的“连接稳定性”才是真实用户感知的稳定性,而不是脚本崩了就报错。

超时控制方面,lr_set_socket_options可以设置单次recv的等待时间,但是要注意这个时间不是整条事务的持续时间。如果你想压“每用户挂 2 分钟连接”,就在收流循环里用lr_get_time_int()做总时长判断,到点主动断开。我见过有人把Timeout设置成 120 秒然后等服务端推完,那个根本不是长连接压测,是等接口超时,方向错了。

4. 两种方案的压测效果对比与选型建议

4.1 实测对比

我拿一个模拟的大模型流式接口做了对比,服务端每秒向每个连接推送 10 条事件,每条事件约 200 字节,连续推送 60 秒。分别用方案一和方案二加压,结果如下:

对比项方案一:web_custom_request方案二:Winsock 底层控制
支持的 SSE 连接时长RECEIVE超时和MaxResponseSize限制,通常只能测前几秒/前 1MB可任意控制连接持续时间,满足长连接场景
事务结束判断响应体达到阈值或超时即结束由脚本自定义,适合长时间稳定流
首包时间统计可以由web_get_int_property间接推导自定义代码准确记录到毫秒
每事件内容断言弱,只能对截断响应做简单判断可按data:逐行解析,做业务断言
断线重连模拟不支持支持
压测机资源占用较低(常用 HTTP 协议模块)适中(C 代码,socket 开销)
脚本编写难度低,几行代码即可中高,需要处理 TCP 细节
适合场景拨测、冒烟测试、连通性回归长稳压测、并发连接数压测、流式吞吐测试

补充一个细节:方案一里web_custom_request的并发用户数做得上去,因为它不维护额外状态,虚拟用户发完请求就收数据;但它的接收时间截断会导致事务时间偏低,例如服务端推 30 秒,你设置了MaxResponseSize=1MB,可能在第 5 秒就提前结束了,TPS 会虚高。方案二虽然可以完整测满 60 秒,但每个虚拟用户会持续占用 60 秒连接时间,体现在报告里就是“在线并发数”和“累计连接数”两个指标,压测前评估连接数时不要搞混。

4.2 什么时候用方案一、什么时候用方案二

我发现很多团队一开始都纠结用哪个,其实只要看你的测试目标:

  • 目标是“这个 SSE 地址通不通”、接口每次改动后能否快速验证、或者定期拨测系统可用性,选方案一。脚本稳定、不挑环境、发送请求失败可以快速报警,适合集成到持续测试流水线。
  • 目标是“模拟真实用户挂机场景、评估流式服务在高并发下的推送稳定性、验证断线重连是否影响体验”,直接选方案二。方案一长连接根本扛不住,硬用只会给你错误的数据。

如果团队里 LoadRunner 脚本维护水平参差不齐,可以考虑两条腿走路:冒烟用方案一,长稳压测用方案二,各司其职。我自己维护的这套脚本就是按这个思路分开管理的。

5. 我在 LoadRunner 测 SSE 时踩过的几个坑

5.1 痛点一:响应永远不结束,事务永远不结束

刚接触 SSE 时,我第一次用web_custom_request直接压,场景跑到一半发现大量事务显示为“进行中”,跑了一晚上还是“进行中”。查了半天,原因是 SSE 服务端不会主动断开连接,LoadRunner 一直等 EOF。后来加了web_set_option("MaxResponseSize",...)才好。

这里有个细节:MaxResponseSize的单位是字节,但有些版本里数值写小会被内部默认值覆盖。建议先设一个很小的值,比如 1024,跑一次看是否在首包到达后立刻结束事务,再逐步调大。这样可以避免你误以为脚本没生效。

5.2 痛点二:连接数被压测机耗尽

方案二持久连接并发压测时,最容易踩的坑是压测机本身端口/连接数不够。一台 Windows 压测机默认的 TCP 连接端口范围有限,2000 个虚拟用户各持一条连接,很快就出现can't bind socket之类的错误。这不是脚本问题,是系统参数问题。

使用 Winsock 方案时,建议上压测前先调大注册表MaxUserPortTcpTimedWaitDelay,并且尽量用多台压测机分摊连接压力。另外,虚拟用户跑完后,lr_close_socket一定要放到事务结束后,否则断开前的缓冲数据没读完,服务器日志里会看到一堆半截请求。

5.3 痛点三:流内容被 LoadRunner 的 buffer 截断

方案一里web_reg_save_param保存的响应体如果超过 1MB,默认会被截断。这个问题在普通 HTTP 接口测试里很少见,因为响应都小;但 SSE 接口一旦流式输出长文本,比如大模型生成几千字,很容易超过限制。如果你需要用方案一做内容完整性校验,建议把MaxResponseSize调得足够大,搭配web_save_timestamp_param记录保存时间,或者干脆把响应体写入本地文件再解析。

Winsock 方案同样要注意recvBuf缓冲区的大小。我遇到过lr_recv返回的某个事件跨了两个 recv 包,导致data:被切断的情况。解决办法是不要按“包”来解析,而是维护一个动态累积缓冲区,按\n\n(空行)切分事件。简单说,SSE 的事件边界是空行,不是 TCP 包边界。

5.4 痛点四:代理配置和中间网关对流式响应的影响

公司网络环境里,LoadRunner 脚本有时会被要求走代理。普通 HTTP 请求走代理没问题,但 SSE 的长连接如果中间经过代理,代理可能会缓冲整个响应,导致你收到的是“到达代理时”的数据,而不是真实服务端流式推送的数据。这会让首包时间、事件间隔这些指标完全失真。

如果你的目标接口前面有 API 网关、负载均衡、或者安全代理,压测之前一定要确认它们是否支持text/event-stream透传,是否对连接空闲时间有限制。我曾经遇到网关默认 60 秒无数据就断开连接,导致长连接压测 61 秒后全部断连,而服务端本身没有任何问题。这不是 LoadRunner 的锅,但排查起来很容易怀疑脚本写错了。

还有一点:LoadRunner 场景设置里的Run Logic如果设置了“每次迭代前 reset”,某些协议模式下 socket 会被重建,SSE 长连接维持不了。方案二写完后,建议先跑单用户 5 分钟,确认连接没有被 LoadRunner 自身机制重置,再上并发。


如果只是验证接口通断,方案一 20 分钟就能搞定;一旦要压真实流式场景,建议直接投入方案二,前期多花点时间把底层连接和事件解析的骨架写好,后面所有 SSE 接口复用成本很低。我在实操中对lr_set_socket_optionslr_recv的组合参数踩了不少次,如果你在调脚本过程中遇到“连接建立成功但接收不到任何数据”,先别急着怀疑服务端,打印一下完整请求头,看看Accept: text/event-streamConnection: keep-alive是否真的发出去了。SSE 能不能压得准,很多时候就卡在这些看似不起眼的细节上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:19:32

AI论文网站实测:千笔智能体与灵感AI写文献综述全对比

综述不会写&#xff1f;AI论文网站 千笔专业学术智能体 VS 灵感ai&#xff0c;研究生必备&#xff01;研究生阶段&#xff0c;写文献综述几乎是每个人躲不过去的坎。我见过太多人从研一开始就对综述发怵&#xff1a;文献读了一堆&#xff0c;读的时候觉得都有用&#xff0c;放下…

作者头像 李华
网站建设 2026/9/9 15:18:46

深度学习环境配置指南:从 CUDA 到 PaddlePaddle 安装的完整实操

说来也怪&#xff0c;我最早学人工智能的时候&#xff0c;第一个劝退我的不是反向传播&#xff0c;也不是梯度消失&#xff0c;而是装环境。当时照着教程敲pip install paddlepaddle&#xff0c;以为接下来就能愉快地跑模型了。结果一运行&#xff0c;先是报ModuleNotFoundErro…

作者头像 李华
网站建设 2026/9/9 15:18:46

AI代码代理工具选型与常见CLI错误排查指南

我无法根据“ruflo”这一标题生成符合要求的博文内容。原因如下&#xff1a;“ruflo”在当前公开技术生态、主流AI开发工具链&#xff08;如Claude Code、Codex、npx、Agent框架等&#xff09;中&#xff0c;无明确、可验证的对应项目、工具、库或产品。经交叉核查GitHub、npm …

作者头像 李华
网站建设 2026/9/9 15:18:09

Java对接华视CVR-100身份证阅读器:JNA调用与数据解析全攻略

简介&#xff1a;一份面向Java开发者的华视CVR-100系列设备集成开发资源&#xff0c;共33个文件、约2.12MB。内容以lib目录下的jar依赖包&#xff08;含jna.jar、JNative.jar&#xff09;、cvr_100正式业务代码和test测试代码为核心&#xff0c;辅以dll动态库、class文件及项目…

作者头像 李华
网站建设 2026/9/9 15:17:58

Opencode:开源本地化AI编程代理范式解析

1. 项目概述&#xff1a;Opencode 不是工具&#xff0c;而是一类新型 AI 编程协作范式的代号 “Opencode”这个词最近在开发者社区里频繁刷屏&#xff0c;但它既不是某个具体软件的官方名称&#xff0c;也不是 npm 上可直接 npm install opencode 的标准包——它本质上是一个…

作者头像 李华