1.3 百万长连接难题与限流哲学


文档摘要

1.3 第一道关口:如何接住百万长连接,并正确地限流 前面两节我们做了"校准口径"和"建立地图"。这一节,我们下到最前线——接入层,直面整个架构里第一个、也最容易被轻视的难题:当百万用户同时在线对话,你的服务怎么把这一片长连接稳稳接住,又怎么在资源有限时公平地限流而不把系统打垮。 读者读完这节,应该能说清楚:为什么百万对话连接不能用传统短连接架构,以及限流到底限的是"次数"还是"并发"。 1.3.1 传统 Web 架构为什么在这里失灵 传统 Web 服务(比如一个电商 API)的经典模型是:短连接 + 连接池 + 快速释放。一次请求 50ms 完成,连接立刻归还池子,一台机器轻松扛几万 QPS。

1.3 第一道关口:如何接住百万长连接,并正确地限流

前面两节我们做了"校准口径"和"建立地图"。这一节,我们下到最前线——接入层,直面整个架构里第一个、也最容易被轻视的难题:当百万用户同时在线对话,你的服务怎么把这一片长连接稳稳接住,又怎么在资源有限时公平地限流而不把系统打垮。

读者读完这节,应该能说清楚:为什么百万对话连接不能用传统短连接架构,以及限流到底限的是"次数"还是"并发"。

1.3.1 传统 Web 架构为什么在这里失灵

传统 Web 服务(比如一个电商 API)的经典模型是:短连接 + 连接池 + 快速释放。一次请求 50ms 完成,连接立刻归还池子,一台机器轻松扛几万 QPS。

但 AI 对话是流式长连接:用户一发消息,连接要一直保持到模型把上千 token 生成完,按 30 token/s 算就是 30 秒以上。于是出现一个反直觉的事实:

```mermaid flowchart LR subgraph 传统短连接 A1[请求 50ms] --> A2[连接释放] A2 --> A3[下一请求复用] end subgraph 对话长连接 B1[请求发起到来] --> B2[保持连接 30s+] B2 --> B3[持续收 token 流] B3 --> B4[连接关闭] end A1 -.每连接占用 ms 级.-> R1[单机撑数万连接] B1 -.每连接占用 30s 级.-> R2[单机连接数受内存/文件描述符限制] ```

单机操作系统的文件描述符(fd)上限、内存里每个连接的开销(接收缓冲、发送缓冲、会话状态),都决定了单机撑住的并发连接数才是真正瓶颈,而不是每秒处理次数。一台 64G 内存的机器,按每连接 20KB 估算,理论上限约 300 万连接,但加上协议栈、业务状态、突发缓冲,实际能稳住的往往在 50~100 万量级——而这只是"接住",还不算处理。

结论先行:百万对话连接,必须靠"接入层集群 + 连接分散 + 状态外置"来解决,单台机器扛全局连接是不存在的。 这也是为什么 2.1 节我们会专门讲接入层的具体技术选型。

1.3.2 连接管理的三个工程要点

要点一:连接与业务逻辑解耦。

不要让"处理对话的业务进程"直接持有用户长连接。正确做法是:接入层(如网关、Proxy)只负责维持连接、做协议(WebSocket/SSE)和转发,把"已标准化的请求"通过内部高速 RPC 交给无状态的业务/编排层。这样业务层可以随意扩缩容、重启,而不影响用户连接。

要点二:会话状态外置。

连接对应的"用户是谁、当前对话 ID、剩余配额"等状态,不要存在接入进程内存里,而要放在外部存储(如 Redis / 分布式 KV)。这样连接可以从 A 节点 failover 到 B 节点,也方便限流做全局计数。

要点三:心跳与优雅摘流。

长连接必须心跳保活,且支持"优雅摘流":下线某节点时,先停止接收新连接,等存量连接自然结束或超时迁移,再关闭。否则一次滚动发布就会让用户集体掉线。

1.3.3 限流的哲学:你到底在限制什么

这是本节最关键的认知升级。限流不是限制"QPS 次数",而是限制"并发资源占用"。 在对话场景里,二者差别巨大。

传统 API 限流常写:"每个用户每秒最多 5 次请求"。但在对话场景,一个用户发起 1 次对话请求,就会占用 1 个长连接 + 1 路推理并发达 30 秒。所以更有意义的限流维度是:

  • 并发连接数限制:单用户最多同时 3 路对话。
  • 并发推理限制:单用户最多占用 2 路模型生成。
  • token 速率限制:单用户每分钟最多消耗 N 千 token(防止刷量把 GPU 吃光)。
  • 全局容量保护:系统整体推理并发到达阈值时,新请求进入排队或快速失败。
```mermaid flowchart TD REQ[新对话请求] --> L1{单用户并发连接
超限?} L1 -->|是| REJ1[拒绝 429] L1 -->|否| L2{单用户 token 速率
超限?} L2 -->|是| REJ2[拒绝 429] L2 -->|否| L3{全局推理并发
达阈值?} L3 -->|否| PASS[放行 进入推理] L3 -->|是| QUE[进入优先级队列
或快速失败] ```

这张图揭示了一个重要设计:限流要分级、分层、多维。只在最外层做"每秒次数"限流,等于没限——因为真正稀缺的是"并发推理槽位"。

1.3.4 四种限流算法,以及它们在对话场景的适配度

工程上常见的限流算法,放到对话场景要重新评估:

算法 原理 对话场景适配
计数器/固定窗口 每秒数次数 仅适合粗粒度 窗口边界突发翻倍
滑动窗口 平滑计数 适合 QPS 维度 仍没解决并发维度
令牌桶 按速率发令牌 适合 token 速率限制 令牌积压可能被突发耗尽
漏桶 恒定速率流出 适合保护下游推理 突发会被排队,首字延迟上升

我的建议是组合使用:用令牌桶控制"全局进入推理的速率"以保护模型层;用"并发槽位计数"控制单用户同时对话数;用滑动窗口做兜底防刷。没有一种算法通吃。

一个真实踩坑:某团队只用固定窗口限流 QPS,结果用户用"打开 50 个标签页"绕过(每个连接慢但并发高),直接把推理集群打满。限流必须覆盖"并发"和"连接"维度,不能只看次数。

1.3.5 公平性:限流如何避免"一个大客户拖垮所有人"

百万级系统一定有"大客户"或"热点 Prompt"。如果限流只做全局总量,一个大客户就能把资源吃光,其他人全部无响应——这叫"吵闹邻居"问题(Noisy Neighbor)。

解法是用多租户/多优先级隔离

```mermaid flowchart TD IN[全部流量] --> TEN{租户/优先级分类} TEN --> P1[高优租户池 配额 40%] TEN --> P2[普通用户池 配额 50%] TEN --> P3[匿名/突发池 配额 10%] P1 --> GUARD[各池独立限流
互不挤占] P2 --> GUARD P3 --> GUARD ```

把总容量切成多个"资源池",每个池独立限流。即使某个池被打爆,也只影响该池内的用户,不会蔓延到全局。这是生产级系统从"能跑"到"稳跑"的关键一跃。

1.3.6 当限流拦不住时:排队、降级与快速失败

限流不是银弹。当真实流量超过所有预案,你要有"最后三道防线":

  1. 排队:非核心请求进入有界队列(带超时),先告诉用户"正在排队",而不是直接失败。对话场景里,用户愿意为"稍等几秒拿到好答案"排队,但不接受"瞬间失败"。
  2. 降级:推理资源紧张时,自动把部分流量切到更小、更快的模型,或返回"缓存的近似答案",牺牲一点质量换可用性。
  3. 快速失败:当系统已濒临崩溃(如排队长度超阈值),果断返回 429/503,附上"稍后重试"提示,保护系统不雪崩。

我的判断:在百万级对话系统里,"优雅降级 + 有界排队"比"硬扛"重要得多。用户要的是"始终有响应",而不是"平时飞快、峰值全挂"。

1.3.8 一个落地案例:限流配置长什么样

前面讲了限流哲学,很多读者会问"那到底怎么配"。这里给一份贴近生产的配置思路(伪代码层级,便于你映射到任意网关/Nginx/自研组件),把 1.3.3~1.3.5 的多个维度落到一个统一策略上:

# 第一道防线:单用户并发连接数(保护接入层 fd 与内存) limit.user.max_concurrent_connections = 3 # 第二道防线:单用户 token 速率(保护推理算力不被刷爆) limit.user.token_rate_per_min = 8000 # 约 8k token/分钟 # 第三道防线:全局推理并发槽位(保护模型层不雪崩) limit.global.inference_slots = 200000 # 第四道防线:资源池隔离(避免吵闹邻居) pool.high_priority.quota = 0.40 # 高优租户 40% pool.normal_user.quota = 0.50 # 普通用户 50% pool.anonymous_burst.quota = 0.10 # 匿名/突发 10% # 超限后的行为策略 on_limit.user = reject_429 # 单用户超限:直接拒绝并提示 on_limit.global_slot = enqueue_or_503 # 全局槽满:有界排队,超时则 503

这份配置的关键,是把"四个维度"分开陈述、各管一摊,再叠加"资源池隔离"。你会发现它没有任何一处是"每秒最多 N 次请求"——因为那在对话场景里是最弱的一道口。

经验值:并发连接数设 3、token 速率按"单用户每分钟不超过其套餐额度的 1.2 倍"来设,是高优与体验的平衡点。设太松挡不住刷量,设太紧正常用户会频繁 429 而流失。

1.3.9 监控什么:限流是否生效,靠指标说话

限流配完不是结束,你必须能"看见"它是否在工作。接入层至少要上报这几类指标,否则限流就是盲飞:

  • 被拒请求数 / 原因分布:429 是因为单用户并发?token 速率?还是全局槽满?原因分布直接告诉你哪道口在扛压。
  • 各资源池占用率:高优池、普通池、突发池各自用了多少配额,有没有某个池长期打满而别的池空闲(说明配额切分不合理)。
  • 全局推理槽位排队长度与等待时间:这是"用户感知到的卡顿"的直接前兆,一旦排队超时率上升,要立刻触发降级而非硬扛。
  • 长连接数 / 单机 fd 使用率:提前预警"哪台接入机快撑不住了",用于滚动扩容决策。
```mermaid flowchart LR M1[被拒请求分布] --> DASH[限流监控面板] M2[资源池占用率] --> DASH M3[排队长度/等待] --> DASH M4[单机 fd 使用率] --> DASH DASH --> ACT{决策} ACT -->|槽满| DEG[触发降级] ACT -->|池不均| REBAL[调整配额] ACT -->|fd 高| SCALE[接入机扩容] ```

限流的本质不是"拦住用户",而是"在系统濒临崩溃前,用可预期的方式把压力疏导掉"。监控让你知道疏导是否生效、哪里在漏。

1.3.11 一个常见认知误区:限流 = 用户体验差

很多产品经理一听"限流"就皱眉,觉得这是在"拒绝用户",会伤害体验。这是对限流的误读,必须澄清。

限流不是"拒绝",而是"有秩序地服务"。想象一个没有限流的系统:高峰期所有请求平等地挤进来,结果每条都慢、每条都超时、每条都失败——这是最差的体验。而有限流的系统:大部分用户正常得到服务,超限的少数用户收到清晰提示("当前繁忙,请稍候"或进入排队),系统整体稳定。

二者的体验对比:

情形 无限流 有限流
高峰期正常用户 也卡死 正常服务
超限用户 一起卡死无提示 清晰提示/排队
系统结局 雪崩全挂 稳过峰值

所以正确心智是:限流是为了让"大多数"体验好,代价是"极少数"得到有尊严的拒绝或排队,而不是所有人一起崩溃。 好的限流设计,甚至会配合"排队进度提示""预计等待时间"来把拒绝体验做得体面。

我建议把限流写进产品 SLA 的一部分,而不是当成纯技术开关。当产品和工程对"高峰期的服务预期"达成一致,限流就从"背锅侠"变成"体验保障"。

1.3.13 本节要点回顾

1.3 给你留下这些可复述的结论:

  1. 瓶颈是连接数不是次数:对话是流式长连接,单机瓶颈在 fd 与内存,而非每秒处理次数。
  2. 连接管理三要点:连接与业务解耦、会话状态外置、心跳 + 优雅摘流。
  3. 限流限的是并发资源:连接数、推理槽、token 速率,多维组合,而非只看"每秒次数"。
  4. 资源池隔离防吵闹邻居:把容量切成高优/普通/突发池,各池独立限流,避免一个大客户拖垮全局。
  5. 三道防线兜底:有界排队、优雅降级、快速失败,在限流拦不住时保系统不雪崩。
  6. 限流是体验保障:目的是让大多数人正常服务,少数得到有尊严的拒绝或排队,而非一起崩溃。

接入层是用户的"第一印象",也是系统的"生死线"。下一章(第 2 章)我们将正式深入接入与限流层,讲具体的技术选型、长连接网关设计、以及把限流落到代码和配置上的实战方法。

第一章到此告一段落。你已经掌握了"口径校准—全局分层—第一道关口"这条主线。带着这张地图,我们进入第 2 章,去把接入层这块最硬的骨头,真正啃下来。

1.3.14 快速自测:你的接入层扛得住吗

用这几个问题自检接入层的准备度:

  • 单机 fd 上限和内存,按每连接 20KB 估算,能否支撑你规划的并发连接数?是否需要调整内核参数或加节点?(测连接容量)
  • 用户长连接是否由专门的接入组件持有,业务重启时用户无感知?(测解耦)
  • 会话状态是否外置到 Redis 等存储,支持连接 failover?(测状态外置)
  • 限流是否覆盖了"并发连接数、推理槽、token 速率"三个维度,而不只是每秒次数?(测限流维度)
  • 是否做了资源池隔离,避免单个大客户或热点把全局资源吃光?(测隔离)
  • 限流超限时,是粗暴 429 还是有排队/降级/清晰提示?(测体验)

六个问题对应接入层的六块能力。建议把这份清单贴进你的接入层设计评审,逐项打勾。

小结:百万对话的瓶颈是"并发连接数"而非"QPS 次数"。限流要限"并发资源"(连接数、推理槽、token 速率),多维组合 + 资源池隔离 + 排队降级,才能在峰值下既保住系统又不丢用户体验。限流是体验保障,不是拒绝用户。


发布者: 作者: 转发
评论区 (0)
U