想象你经营一家 AI 流水席饭店:大模型是后厨,用户是坐在桌边的客人。一次提问不是端上一道菜就结束,而是从一条长长的传送带上,持续不断地把汤、主菜、甜点一份份送到客人面前——这就是流式输出。WebSocket 是为每桌客人架设的“双向传菜电梯”,客人可以随时催菜、加菜;SSE 则是一扇“只出不进”的单向出菜窗口。长连接测试,就是检验这些传送带在几百上千桌同时开饭时,会不会卡住、断掉、堆盘,甚至把后厨的仓库撑爆。 你要关心的事很具体:传送带怎么架起来?多久没人端菜就要撤走?后厨出菜太快、客人来不及吃怎么办?不同传送带会占多少仓库空间?怎么调参数让整条流水线稳如老狗?这篇文章就用这套“传送带”思维,把 WebSocket/SSE 长连接压测的核心方法讲清楚。
对 AI 从业者而言,掌握长连接测试意味着你能在模型上线前发现流式服务的连接瓶颈、内存泄漏和背压风险,真正把“能跑”变成“能扛住生产流量”。
可视化演示
展示WebSocket全双工与SSE单向流协议差异、Ping/Pong心跳保活机制,以及流式输出场景的背压与缓冲处理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
WebSocket全双工通信 客户端与服务端双向随时发送 full-duplex SSE为单向流式输出,仅服务端推送。
连接建立管理
传送带不是一开机就凭空出现的,它需要“按下启动按钮、从出菜口拉到客人桌边”。WebSocket 的连接建立就像先拨通电话,再在通话中升级成专线;SSE 则简单一些,客人只需要打开一扇“单向出菜窗”,后厨就能把菜源源不断送出来。这段从无到有的过程,决定了第一道菜的等待时间,也决定了系统能不能快速接纳大量新客人。
技术上看,一条长连接会经历 Connecting → Open → Closing → Closed 的生命周期。压测时不能只看“连上了没有”,还要统计握手耗时、升级成功率、被动关闭比例。服务端管理连接的方式也分两种:传统线程模型是一个服务员盯一条传送带,连接一多就累垮;而 epoll 事件驱动模型是中央控制台同时盯着成千上万条传送带,只有真正有菜进出时才处理。长连接压测要验证的就是:中央控制台到底能扛住多少条“同时在线”的传送带。
规划连接数时,我们不能凭感觉拍脑袋,而要参考到达率和连接时长:
搭建压测脚本后,要记录“已建立连接数 / 尝试连接数”,如果成功率低于 99.9%,说明大门开启的速度跟不上客流,需要排查握手超时或服务端 accept 队列溢出。传送带架设得快,宴会才能按时开席。
- λ(连接到达率)每秒由客户端发起的新连接数量,可以从业务统计或历史日志中得到。
- T(平均连接时长)所有连接从建立到关闭的平均存活秒数,体现业务会话的长短。
- C(并发连接数)用到达率乘以连接时长,就得到稳定状态下同时挂着的传送带数量,是容量规划的基础。
超时与心跳
一条传送带如果长时间没有菜经过,你分不清它是“暂时没单”还是“卡在某个角落没人管”。为了避免死连接越积越多,我们需要安排巡线机器人:每隔一段时间敲一下传送带,看看有没有回应。这个“敲一下”就是心跳 PING/PONG;如果敲了多次都没回音,机器人就会把这条传送带撤掉,回收占用的人力与空间。
在实际系统中,空闲超时和心跳必须成对配置。如果空闲超时设得太短,活连接会被误杀;如果心跳太频繁,又会浪费带宽和 CPU。业界常见的经验是让心跳间隔小于空闲超时的一半,给网络往返留足余量:
压测时还要专门验证断线重连:把服务端或负载均衡器的空闲超时调小,观察客户端是否能在规定时间内重新建立传送带,并且不丢状态。真实 AI 流式服务中,长连接常跨多个网关节点,每一层都有自己的超时,任何一层“悄悄拆带子”,用户都会看到“输出中断”。所以超时与心跳的配置,必须放在全链路里一起测。
- T_timeout(空闲超时)连接在没有任何业务消息时,服务端允许它保留的最长秒数。
- T_hb(心跳间隔)每隔这么多秒发送一次 PING 帧,用来探活并续期。
- 不等式约束心跳间隔小于超时时间的一半,保证在超时回收前至少有一次心跳能完成“敲一下-回一声”的往返。
流式背压处理
后厨出菜速度再快,也要看客人能不能吃得过来。如果传送带不停地上菜,客人来不及端走,出锅口的托盘就会越堆越高,最终把桌面和地面全部占满。这种“供大于求”导致的拥堵,在流式系统里就是背压(Backpressure)。AI 流式场景尤其常见:模型生成 token 的速度远高于前端 UI 渲染或调用方消费的速度。
处理背压最优雅的方式不是硬扛,而是让上游感知下游的速度。实现上常用滑动窗口:客户端告诉服务端“我最多能同时接收 N 条未确认消息”,服务端发满 N 条后就必须等待 ACK,窗口滑动了才能继续发。这样队列长度就不会无限膨胀。背压的严重程度可以用一个非常经典的公式来量化:
压测时,我们要故意制造“慢消费者”—让客户端每隔几十毫秒才读一条消息,观察服务端发送队列是否上涨、内存是否飙升、是否会丢弃最旧消息。合理的背压策略是在队列满时让生产者降速,或丢弃过期消息并记录告警,而不是无限扩大缓冲区。传送带不是仓库,能即时流动才是健康的。
- λ(消息到达率)单位时间内服务端生成并投递给客户端的消息数量。
- W(平均等待时间)一条消息从进入发送队列到被客户端取走的平均秒数。
- L(队列长度)λ 乘 W 得到平均在途/排队消息数,是背压的直接指标;L 持续上涨就说明需要限流。
内存消耗评估
每条传送带旁边都会摆几个中转台,用来临时放菜。中转台越多、堆的菜越多,占用的仓库空间就越大。长连接服务的内存消耗,本质上就是“每连接固定开销 + 每连接缓冲/队列开销 + 全局共享开销”的总和。压测时如果只盯着整体内存,很容易漏掉某一条连接悄悄吃掉的隐形空间。
评估内存时必须逐层拆解:内核层面的 Socket 收发缓冲区、用户态的编解码器与连接对象、等待发送的业务消息队列、以及事件循环和连接池等共享结构。当背压出现时,消息队列长度 L 会增大,内存也会随之线性上涨。由此我们可以做容量估算:
在长时间压测中,内存曲线如果是“锯齿形”——涨一点又因 GC 回落一点,属于正常;如果整体斜率持续向上且不回落,就要怀疑连接对象泄漏或队列积压无法释放。仓库面积是有限的,压测的最终目标就是算清楚:最多能同时开多少条传送带,才不会把仓库挤爆。
- C(连接数)当前同时在线长连接的数量,来自第一小节的容量规划。
- m_fix(固定开销)每个连接对象、Socket 通道、事件注册等不随消息增长的内存。
- B_buf(读写缓冲)内核与用户态为每条连接预留的收发缓冲区大小。
- L × s_msg(排队消息内存)在途消息数量乘以单条消息大小,是背压造成内存上涨的部分。
- M_shared(共享内存)连接管理器、定时器、监控等所有连接共享的全局结构。
参数调优实践
光会测还不够,还得会调。就像传送带运行一段时间后,我们要调整它的转速、中转台大小和巡检频率。长连接服务的性能调优,核心是对超时、心跳、缓冲区和连接池做组合配置。调优的目标不是某个参数最大,而是“资源占用和稳定性”之间的平衡。
这里分享几个真实场景里的调优方向:空闲超时不要设太大,否则死连接会像没人管的传送带一样占着位置;心跳间隔按超时的一半来配;读写缓冲不要盲目调大,OS 层 SO_SNDBUF/SO_RCVBUF 翻倍会立刻体现在每连接内存上;连接池则要根据连接建立速率和握手耗时来估算。连接池大小可参考下面的经验公式:
还要强调资源隔离:不要让长连接和普通短请求共用同一个线程池或连接池。AI 流式推理的一次请求可能持续几十秒,如果它把线程池占满,普通健康检查接口也会跟着超时。给长连接单独划分线程、队列和连接池,就像给传送带开设独立通道,避免“一条带子卡死,整条流水线瘫痪”。
- Q(每秒建连数)峰值时可容忍的连接重建速率,例如断线重连风暴时的 QPS。
- T_setup(单次建连耗时)从发起握手到连接可用的平均毫秒数,包含 TCP 握手和协议升级。
- Q × T_setup / 1000把毫秒换算成秒,得到在途建连并发量。
- 向上取整并加 1保证连接池在峰值波动时仍有富余能力。
压测方法验证
最后一步是“全链路演习”:先让少量传送带跑起来,再逐步加压,接着持续运行数小时,看传送带会不会在长时间运转中慢慢卡死。压测方法的核心不是把并发数调到最大,而是用科学的场景设计暴露问题。常用场景包括:逐步加压找出连接数拐点、峰值冲击测试极限、以及长时间稳定测试验证无泄漏。
压测过程中需要监控的指标也要遵循长连接特点:连接建立成功率与耗时、PING/PONG 往返时延、消息推送延迟、断线重连次数、发送队列长度、GC 频率和堆内存曲线。稳定性是否合格,可以用错误率来量化:
真正的大规模验证,要在接近生产环境的网络上进行:模拟真实客户端数量、真实消息大小、真实心跳周期。比如某 AI 客服系统上线前,用 10 万条 WebSocket 连接连续压测 24 小时,把发现的内存锯齿和队列堆积问题提前修复。传送带只有在真实客流中连续转一天不断线,才配得上“稳定”二字。
- N_timeout(超时错误)空闲超时或心跳超时导致的连接中断次数。
- N_reset(重置错误)对端 RST 导致的连接被强制断开次数。
- N_reject(拒绝错误)服务端因连接数满、限流等原因拒绝握手的次数。
- N_total(总请求数)压测期间所有尝试建立的长连接总数。
- 百分比判据错误总数除以总连接数,必须小于 0.1% 才具备生产上线条件。
AI 应用场景
大模型流式对话服务压测
阶段:模型推理服务上线前
解决的问题:LLM 对话服务通过 SSE 把 token 流式推送给前端,传统短连接压测只看单次响应时间,无法发现“连接长时间挂起、心跳超时、队列积压”等问题。长连接测试能模拟真实用户持续监听流式输出的行为,提前暴露网关超时配置不当和内存泄漏。
具体案例:某智能助手平台在 GPT 类模型上线前,用 5000 条 SSE 连接同时模拟用户提问。压测发现当模型思考时间超过 30 秒时,网关会主动断开空闲连接,导致前端反复重连。通过把心跳间隔设为 15 秒、空闲超时改为 90 秒,问题消失。
实时 Agent 任务流推送网关
阶段:Agent 架构运行监控
解决的问题:Agent 在规划、调用工具、生成结果时会持续向客户端推送状态,WebSocket 承担大量双向消息。压测需要验证高并发下是否存在背压:当客户端消费速度跟不上 Agent 事件流时,消息队列是否堆积、内存是否飙升、是否触发丢弃策略。
具体案例:某 AI Agent 平台用 WebSocket 推送“正在思考、正在调用工具”等中间状态。压测脚本故意把客户端消费间隔拉到 100ms,结果发现服务端发送队列从 100 涨到 5 万条,内存直接翻倍。随后为事件流增加滑动窗口和丢弃过旧事件策略,内存恢复平稳。
多租户实时推送服务容量规划
阶段:容量规划与成本控制
解决的问题:云上 AI 基础设施需要为多个租户提供实时推送能力,长连接数量直接影响 CPU、内存和带宽成本。通过连接数规划公式和内存评估模型,可以在采购资源前算清“单机最多支撑多少连接”。
具体案例:某云服务商为 20 个租户规划 WebSocket 推送集群,先用 λ=200 每秒新建连接、T=30 分钟平均时长算出并发连接约 36 万,再按每连接 50KB 内存估算需要约 18GB。据此提前扩容并设置单机连接上限,上线后没有出现 OOM。
Python 演示
import asyncioimport time class LongConnectionProbe: """A small simulator for long-lived streaming connection load tests.""" def __init__(self, total, concurrent, heartbeat=5.0, idle_timeout=10.0): self.total = total self.concurrent = concurrent self.heartbeat = heartbeat self.idle_timeout = idle_timeout self.done_count = 0 self.error_count = 0 self.backpressure_count = 0 self.max_queue_len = 0 self.started_at = time.time() async def simulate_connection(self, cid): q = asyncio.Queue(maxsize=4) async def producer(): for seq in range(6): if q.full(): self.backpressure_count += 1 await q.put((cid, seq)) if seq == 0: await asyncio.sleep(self.heartbeat / 2) else: await asyncio.sleep(0.02) async def consumer(): while True: try: _, seq = await asyncio.wait_for(q.get(), timeout=self.idle_timeout) self.max_queue_len = max(self.max_queue_len, q.qsize()) except asyncio.TimeoutError: self.error_count += 1 break if seq == 5: break await asyncio.sleep(0.06) await asyncio.gather(producer(), consumer()) self.done_count += 1 async def run(self): sem = asyncio.Semaphore(self.concurrent) async def guarded(cid): async with sem: await self.simulate_connection(cid) tasks = [asyncio.create_task(guarded(i)) for i in range(self.total)] await asyncio.gather(*tasks) def report(self): print("total connections:", self.done_count) print("idle timeout errors:", self.error_count) print("backpressure events:", self.backpressure_count) print("max queue length:", self.max_queue_len) print("elapsed seconds:", round(time.time() - self.started_at, 2)) if __name__ == "__main__": probe = LongConnectionProbe(total=20, concurrent=5) asyncio.run(probe.run()) probe.report()
q = asyncio.Queue(maxsize=4)用容量为 4 的队列模拟服务端发送缓冲,队列满就产生背压。if q.full(): self.backpressure_count += 1生产者发现队列已满时记录背压事件,相当于“传送带托盘堆满”。await q.put((cid, seq))模拟服务端向长连接中写入一条业务消息。await asyncio.wait_for(q.get(), timeout=self.idle_timeout)模拟客户端等待消息,同时检验空闲超时。await asyncio.sleep(0.06)人为让消费变慢,制造供大于求的背压场景。asyncio.Semaphore(self.concurrent)限制同时存在的连接探测数,模拟服务端的并发上限。tasks = [asyncio.create_task(guarded(i)) for i in range(self.total)]并发创建 20 条长连接,模拟压测流量。self.max_queue_len = max(self.max_queue_len, q.qsize())记录队列最大深度,作为内存压力的关键指标。
常见误区
误区反正都是长连接,SSE 压测脚本直接照抄 WebSocket 的脚本,消息来回发,结果也一样。
正解WebSocket 是全双工,客户端可以随时上行数据;SSE 是单工,只有服务端能持续推送。背压处理上,WebSocket 需要同时关注“上行窗口”和“下行队列”,SSE 主要关注下行缓冲与断线重连。压测脚本必须按协议方向设计,否则会高估或低估真实负载。
为何常见:因为两者都属于“长连接”,看上去都是挂着的连接,容易忽略传输方向的本质差异。
误区长连接压测就是不断建立连接,把连接数推到 10 万就算成功。
正解连接建立只是开始,长期稳定才是关键。心跳超时、空闲回收、断线重连这些“维持阶段”的行为,才是长连接最常见的故障来源。压测必须包含长时间运行脚本,并主动配置心跳周期来触发探活逻辑。
为何常见:短连接压测的惯性思维让人只关注“打满吞吐”,忘记了长连接真正考验的是时间维度上的稳定性。
误区既然队列会堆满,那把服务端发送队列调大,不就不会丢消息了吗?
正解增大缓冲区只是把问题往后推。队列越大,单条连接占用的内存越高,背压严重时反而更容易触发 OOM。正确的做法是让生产者感知消费者速度,通过滑动窗口、限速、丢弃过期消息来控制队列长度。
为何常见:因为“加内存”是运维里最直观的应急手段,但它没有解决“上游太快、下游太慢”的根本矛盾。
误区压测几小时后,进程 RSS 没有持续上涨,就说明内存没问题。
正解堆内对象会被 GC 回收,RSS 平稳可能只是“锯齿形”的表面结果。长连接的内存风险常发生在堆外缓冲区、Netty ByteBuf、连接对象池等地方,必须同时监控堆内存、直接内存、发送队列长度和 GC 日志。
为何常见:监控面板默认只展示进程总内存,容易让人漏掉隐藏在堆外和队列里的真实消耗。
误区把 wrk 或 ab 的并发数调大、持续时间拉长,就能测试 WebSocket/SSE 服务。
正解短连接工具关注的是“请求-响应”闭环,无法模拟长连接的握手升级、心跳维持、流式消息分片、断线重连等状态机。长连接压测需要专门的客户端工具或自研脚本,能够保持连接、监听主动推送、记录协议级事件。
为何常见:因为这些工具使用简单、报告丰富,初学者容易忽略协议状态与生命周期管理之间的巨大差异。
面试题
Q1. WebSocket 与 SSE 在传输方向上的核心区别是什么?
- A.WebSocket 支持全双工,SSE 只能服务端推送← 正确答案
- B.WebSocket 只能客户端推送,SSE 支持全双工
- C.两者都属于短连接轮询,没有本质区别
- D.两者都依赖 UDP 报文,不能可靠传输
答案:A。WebSocket 是全双工协议,客户端和服务端可以同时互发消息;SSE 是基于 HTTP 的单工流,只能由服务端持续推送给客户端。理解了这一点,压测脚本的设计方向才不会出错。
Q2. 在长连接压测中,心跳机制最主要的作用是什么?
- A.用于探测死连接并触发及时回收← 正确答案
- B.用于压缩业务数据以减少带宽
- C.用于替代 TLS 完成身份认证
- D.用于把消息拆成更小的分片
答案:A。心跳 PING/PONG 是探活手段,能发现那些已经断掉但未被操作系统感知的连接。心跳超时后服务端会回收死连接,避免连接数被僵尸连接占满。它不是加密、认证或数据压缩机制。
Q3. 若服务端空闲超时设为 60 秒,心跳间隔最合理的是?
- A.每隔 90 秒发一次心跳
- B.每隔 120 秒发一次心跳
- C.每隔 20 秒发一次心跳← 正确答案
- D.每隔 61 秒发一次心跳
答案:C。心跳间隔应小于空闲超时的一半,即小于 30 秒。20 秒能保证在超时前至少完成一次心跳往返;而 90 秒、120 秒或 61 秒都会让连接在第二次心跳前超时断开。
Q4. 流式场景中,以下哪个现象最能说明背压已经产生?
- A.发送端待确认队列长度不断上升← 正确答案
- B.服务端 CPU 使用率降为零
- C.磁盘 IO 长时间处于空闲
- D.网络丢包率始终保持为零
答案:A。背压的本质是生产者速度大于消费者速度,消息在队列中堆积。因此“待确认队列长度上升”是最直接的信号。CPU 下降、磁盘空闲和零丢包都不代表背压,反而可能是流量太低。
Q5. 在长连接系统中,Little 定律 L = λ × W 中的 L 表示什么?
- A.平均在途排队消息数量← 正确答案
- B.每秒新建连接总数
- C.单个连接的最大带宽
- D.服务器磁盘总容量
答案:A。Little 定律把“消息到达率 λ”和“平均等待时间 W”相乘,得到系统内平均停留的消息数量 L。它常被用来评估发送队列或在途消息的堆积程度,是背压和内存分析的理论基础。
Q6. 长连接压测时,应当持续观察哪些指标?
- A.连接建立成功率和耗时← 正确答案
- B.心跳超时次数← 正确答案
- C.消息推送延迟和队列长度← 正确答案
- D.SQL 索引碎片率
答案:ABC。长连接压测关注连接生命周期、探活能力和消息流是否顺畅,因此 A、B、C 都是核心指标。SQL 索引碎片率与长连接稳定性没有直接关系,属于数据库调优范畴。
Q7. 单条长连接的内存评估中,固定开销通常不包括什么?
- A.内核 Socket 收发缓冲
- B.连接对象与解码器状态
- C.已排队业务消息实体← 正确答案
- D.epoll 事件的上下文信息
答案:C。固定开销指不随消息数量变化的每连接成本,包括 Socket 缓冲、连接对象、解码器状态和事件上下文。已排队的业务消息属于随队列长度变化的动态内存,应单独用“消息数 × 单条消息大小”估算。
Q8. 大规模长连接稳定性验证,以下做法正确的是?
- A.逐步加压并观察连接数拐点← 正确答案
- B.压测期间动态开关心跳观察恢复← 正确答案
- C.持续数小时检查内存锯齿曲线← 正确答案
- D.只压测一秒然后推算峰值
答案:ABC。逐步加压能找出服务端能撑住的连接数上限,动态开关心跳验证探活恢复,长时间观察内存曲线能发现泄漏。只压测一秒无法覆盖长连接真正的风险点,不能作为稳定性依据。
Q9. 与 WebSocket 相比,SSE 的主要优势是什么?
- A.基于 HTTP,兼容性更好且免自定义协议← 正确答案
- B.支持客户端任意双向发送二进制
- C.可以在 UDP 上实现组播和广播
- D.无需心跳也能永不超时断线
答案:A。SSE 直接跑在 HTTP 上,复用现有网关、代理和鉴权设施,实现简单,还自带断线重连机制。它不支持双向通信和任意二进制帧,也不能避免超时断线,只是协议实现更轻量。
Q10. 当 AI 推理输出速度远超客户端消费速度时,合理的背压策略有哪些?
- A.客户端按窗口大小确认并降低在途消息← 正确答案
- B.服务端检测队列堆积后主动降速← 正确答案
- C.丢弃过旧消息并记录丢弃告警← 正确答案
- D.无限扩大内存队列直到写满
答案:ABC。滑动窗口、生产者降速和丢弃过期消息都是成熟的背压控制手段,能保护服务端内存稳定。无限扩大内存队列只会把压力后移,最终导致 OOM,是典型的错误做法。