ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 接口测试SSEServer-Sent Events这个概念其实不新但 AI 大模型把它的热度带起来了。现在几乎所有大模型厂商对外提供对话接口时都推荐走 SSE 流式输出你把 prompt 发过去服务器不一次性返回完整回答而是把 response body 拆成一行一行data:数据按 token 顺序推给客户端。客户端拿到一条就渲染一条用户看起来就像“字在蹦出来”。这就给性能测试带来一个此前很少遇到的情况一次 HTTP 请求的事务时间不再是一两百毫秒而是可能持续几秒甚至几十秒TPS 指标的含义也变了不再只看每秒能发多少请求还要看“连接同时在线的数量”和“每秒钟事件推送的吞吐量”。你用ab、JMeter裸测还能勉强凑合但企业里很多团队性能测试工具就是 LoadRunner而且 LoadRunner 录制脚本和自定义脚本的能力已经沉淀了很多年不想为了一个 SSE 接口再引入新的压测平台。所以“LoadRunner 能不能测 SSE”这个搜索词最近频繁出现一点都不意外。1.2 SSE 协议到底是什么30秒回顾在写脚本之前得先把 SSE 的协议细节讲清楚因为后面脚本里所有参数设置都依赖这几个点SSE 复用的是普通 HTTP 协议客户端发起GET请求请求头里必须带Accept: text/event-stream。服务端收到请求后返回HTTP/1.1 200 OKContent-Type为text/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 时有三个短板HTTP 协议的响应结束判断LoadRunner 的web_custom_request认为响应在服务器返回 EOF 或者达到由web_set_option设置的最大响应大小后才算完成。SSE 流不主动断开会话所以你会卡在“等待响应结束”上。录制不到流内容如果你用 HTTP 协议去录制一个 SSE 请求VuGen 一般只会记录最初的 GET 请求流式推送的数据基本会被忽略或者被记录成乱码片段没办法结构化地断言和性能分析。流式数据的到达时间不在 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, SearchBody, RelFrameID1, LAST); lr_start_transaction(SSE_QuickTest); web_custom_request(sse_stream_test, URLhttp://your-ai-gateway.example.com/v1/chat/completions/stream, MethodGET, Resource0, EncTypetext/event-stream, HeadersAccept: 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不加LB和RB时匹配整个响应体。响应体如果超过 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, Timeout5000); // 单次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 里默认返回字符串如果返回的内容含二进制或特殊字符建议加Encoded0或者用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 秒你设置了MaxResponseSize1MB可能在第 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 个虚拟用户各持一条连接很快就出现cant bind socket之类的错误。这不是脚本问题是系统参数问题。使用 Winsock 方案时建议上压测前先调大注册表MaxUserPort和TcpTimedWaitDelay并且尽量用多台压测机分摊连接压力。另外虚拟用户跑完后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_options和lr_recv的组合参数踩了不少次如果你在调脚本过程中遇到“连接建立成功但接收不到任何数据”先别急着怀疑服务端打印一下完整请求头看看Accept: text/event-stream和Connection: keep-alive是否真的发出去了。SSE 能不能压得准很多时候就卡在这些看似不起眼的细节上。
返回列表