1.3 第一道关口:如何接住百万长连接,并正确地限流 前面两节我们做了"校准口径"和"建立地图"。这一节,我们下到最前线——接入层,直面整个架构里第一个、也最容易被轻视的难题:当百万用户同时在线对话,你的服务怎么把这一片长连接稳稳接住,又怎么在资源有限时公平地限流而不把系统打垮。 读者读完这节,应该能说清楚:为什么百万对话连接不能用传统短连接架构,以及限流到底限的是"次数"还是"并发"。 1.3.1 传统 Web 架构为什么在这里失灵 传统 Web 服务(比如一个电商 API)的经典模型是:短连接 + 连接池 + 快速释放。一次请求 50ms 完成,连接立刻归还池子,一台机器轻松扛几万 QPS。
前面两节我们做了"校准口径"和"建立地图"。这一节,我们下到最前线——接入层,直面整个架构里第一个、也最容易被轻视的难题:当百万用户同时在线对话,你的服务怎么把这一片长连接稳稳接住,又怎么在资源有限时公平地限流而不把系统打垮。
读者读完这节,应该能说清楚:为什么百万对话连接不能用传统短连接架构,以及限流到底限的是"次数"还是"并发"。
传统 Web 服务(比如一个电商 API)的经典模型是:短连接 + 连接池 + 快速释放。一次请求 50ms 完成,连接立刻归还池子,一台机器轻松扛几万 QPS。
但 AI 对话是流式长连接:用户一发消息,连接要一直保持到模型把上千 token 生成完,按 30 token/s 算就是 30 秒以上。于是出现一个反直觉的事实:
单机操作系统的文件描述符(fd)上限、内存里每个连接的开销(接收缓冲、发送缓冲、会话状态),都决定了单机撑住的并发连接数才是真正瓶颈,而不是每秒处理次数。一台 64G 内存的机器,按每连接 20KB 估算,理论上限约 300 万连接,但加上协议栈、业务状态、突发缓冲,实际能稳住的往往在 50~100 万量级——而这只是"接住",还不算处理。
结论先行:百万对话连接,必须靠"接入层集群 + 连接分散 + 状态外置"来解决,单台机器扛全局连接是不存在的。 这也是为什么 2.1 节我们会专门讲接入层的具体技术选型。
要点一:连接与业务逻辑解耦。
不要让"处理对话的业务进程"直接持有用户长连接。正确做法是:接入层(如网关、Proxy)只负责维持连接、做协议(WebSocket/SSE)和转发,把"已标准化的请求"通过内部高速 RPC 交给无状态的业务/编排层。这样业务层可以随意扩缩容、重启,而不影响用户连接。
要点二:会话状态外置。
连接对应的"用户是谁、当前对话 ID、剩余配额"等状态,不要存在接入进程内存里,而要放在外部存储(如 Redis / 分布式 KV)。这样连接可以从 A 节点 failover 到 B 节点,也方便限流做全局计数。
要点三:心跳与优雅摘流。
长连接必须心跳保活,且支持"优雅摘流":下线某节点时,先停止接收新连接,等存量连接自然结束或超时迁移,再关闭。否则一次滚动发布就会让用户集体掉线。
这是本节最关键的认知升级。限流不是限制"QPS 次数",而是限制"并发资源占用"。 在对话场景里,二者差别巨大。
传统 API 限流常写:"每个用户每秒最多 5 次请求"。但在对话场景,一个用户发起 1 次对话请求,就会占用 1 个长连接 + 1 路推理并发达 30 秒。所以更有意义的限流维度是:
这张图揭示了一个重要设计:限流要分级、分层、多维。只在最外层做"每秒次数"限流,等于没限——因为真正稀缺的是"并发推理槽位"。
工程上常见的限流算法,放到对话场景要重新评估:
| 算法 | 原理 | 对话场景适配 | 坑 |
|---|---|---|---|
| 计数器/固定窗口 | 每秒数次数 | 仅适合粗粒度 | 窗口边界突发翻倍 |
| 滑动窗口 | 平滑计数 | 适合 QPS 维度 | 仍没解决并发维度 |
| 令牌桶 | 按速率发令牌 | 适合 token 速率限制 | 令牌积压可能被突发耗尽 |
| 漏桶 | 恒定速率流出 | 适合保护下游推理 | 突发会被排队,首字延迟上升 |
我的建议是组合使用:用令牌桶控制"全局进入推理的速率"以保护模型层;用"并发槽位计数"控制单用户同时对话数;用滑动窗口做兜底防刷。没有一种算法通吃。
一个真实踩坑:某团队只用固定窗口限流 QPS,结果用户用"打开 50 个标签页"绕过(每个连接慢但并发高),直接把推理集群打满。限流必须覆盖"并发"和"连接"维度,不能只看次数。
百万级系统一定有"大客户"或"热点 Prompt"。如果限流只做全局总量,一个大客户就能把资源吃光,其他人全部无响应——这叫"吵闹邻居"问题(Noisy Neighbor)。
解法是用多租户/多优先级隔离:
把总容量切成多个"资源池",每个池独立限流。即使某个池被打爆,也只影响该池内的用户,不会蔓延到全局。这是生产级系统从"能跑"到"稳跑"的关键一跃。
限流不是银弹。当真实流量超过所有预案,你要有"最后三道防线":
我的判断:在百万级对话系统里,"优雅降级 + 有界排队"比"硬扛"重要得多。用户要的是"始终有响应",而不是"平时飞快、峰值全挂"。
前面讲了限流哲学,很多读者会问"那到底怎么配"。这里给一份贴近生产的配置思路(伪代码层级,便于你映射到任意网关/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 而流失。
限流配完不是结束,你必须能"看见"它是否在工作。接入层至少要上报这几类指标,否则限流就是盲飞:
限流的本质不是"拦住用户",而是"在系统濒临崩溃前,用可预期的方式把压力疏导掉"。监控让你知道疏导是否生效、哪里在漏。
很多产品经理一听"限流"就皱眉,觉得这是在"拒绝用户",会伤害体验。这是对限流的误读,必须澄清。
限流不是"拒绝",而是"有秩序地服务"。想象一个没有限流的系统:高峰期所有请求平等地挤进来,结果每条都慢、每条都超时、每条都失败——这是最差的体验。而有限流的系统:大部分用户正常得到服务,超限的少数用户收到清晰提示("当前繁忙,请稍候"或进入排队),系统整体稳定。
二者的体验对比:
| 情形 | 无限流 | 有限流 |
|---|---|---|
| 高峰期正常用户 | 也卡死 | 正常服务 |
| 超限用户 | 一起卡死无提示 | 清晰提示/排队 |
| 系统结局 | 雪崩全挂 | 稳过峰值 |
所以正确心智是:限流是为了让"大多数"体验好,代价是"极少数"得到有尊严的拒绝或排队,而不是所有人一起崩溃。 好的限流设计,甚至会配合"排队进度提示""预计等待时间"来把拒绝体验做得体面。
我建议把限流写进产品 SLA 的一部分,而不是当成纯技术开关。当产品和工程对"高峰期的服务预期"达成一致,限流就从"背锅侠"变成"体验保障"。
1.3 给你留下这些可复述的结论:
接入层是用户的"第一印象",也是系统的"生死线"。下一章(第 2 章)我们将正式深入接入与限流层,讲具体的技术选型、长连接网关设计、以及把限流落到代码和配置上的实战方法。
第一章到此告一段落。你已经掌握了"口径校准—全局分层—第一道关口"这条主线。带着这张地图,我们进入第 2 章,去把接入层这块最硬的骨头,真正啃下来。
用这几个问题自检接入层的准备度:
六个问题对应接入层的六块能力。建议把这份清单贴进你的接入层设计评审,逐项打勾。
小结:百万对话的瓶颈是"并发连接数"而非"QPS 次数"。限流要限"并发资源"(连接数、推理槽、token 速率),多维组合 + 资源池隔离 + 排队降级,才能在峰值下既保住系统又不丢用户体验。限流是体验保障,不是拒绝用户。