2.1 分层网关与连接治理:把百万洪峰挡在门外 如果只能给接入层一句话建议,我会说:这一层要解决的不是「计算」,而是「连接」和「协议」。 模型推理是计算密集,但接入层是连接密集、IO 密集。混淆了这两者,你就会用错武器——比如试图用无状态的计算节点去硬扛有状态的长连接,结果连接一多内存先爆。 读者读完这一节,应该能一句话说清楚:接入层靠「四层卸载 + 七层转换 + 连接池复用 + 背压」四件套,把不可控的洪峰变成可控的排队。 下面逐个拆。 2.1.1 为什么接入层决定了整个系统的生死 先建立一个直觉:在对话系统里,用户感知到的「系统挂了」,十次有七八次不是模型算不过来,而是接入层先撑不住。 原因有三。第一,连接是有状态的、昂贵的。
如果只能给接入层一句话建议,我会说:这一层要解决的不是「计算」,而是「连接」和「协议」。 模型推理是计算密集,但接入层是连接密集、IO 密集。混淆了这两者,你就会用错武器——比如试图用无状态的计算节点去硬扛有状态的长连接,结果连接一多内存先爆。
读者读完这一节,应该能一句话说清楚:接入层靠「四层卸载 + 七层转换 + 连接池复用 + 背压」四件套,把不可控的洪峰变成可控的排队。 下面逐个拆。
先建立一个直觉:在对话系统里,用户感知到的「系统挂了」,十次有七八次不是模型算不过来,而是接入层先撑不住。
原因有三。第一,连接是有状态的、昂贵的。一个 WebSocket 长连接从建立到释放,期间占用的不只是带宽,还有服务端的内核 fd、连接表内存、心跳定时器。百万长连接同时在线,光内存就会吃掉几十 GB。第二,协议是流式的、慢释放的。一条对话响应持续 20~40 秒,连接长时间不释放,连接池和 epoll 的压力持续累积。第三,客户端是异构、不可信的。移动端弱网重连、浏览器跨域、第三方恶意刷量,全都要在接入层消化掉。
所以接入层的第一职责是「收敛」:把海量、异构、不可控的外部连接,收敛成内部可控、高效、可治理的调用。这个收敛做得好,后面的模型集群就能在一个相对「干净」的输入下工作。
最外层用四层(L4,TCP/UDP)负载均衡,而不是七层。原因是 L4 更轻——它不解析应用层协议,只做连接的转发与卸载,单机可以维持远超 L7 的并发连接数。在百万长连接场景下,这一层的关键动作有四个:
一个常见的误判是「L4 就够了,直接打到业务机」。但 L4 不懂协议,它无法做鉴权、无法做灰度、无法终结 WebSocket。所以 L4 之下,必须再接一层七层网关。
七层(L7)网关是接入层的「前台」,它干三件 L4 干不了、也必须集中干的事:
第一,协议终结与转换。 客户端用的是 WebSocket 或 SSE 这种流式长连接协议,但你的内部服务大概率用的是 gRPC 或 HTTP/2 这种高效内部协议。网关负责把外部的 WS/SSE 终结掉,转成内部 RPC 再转发。这样内部服务不需要为每个用户维持一条 WebSocket,只需处理无状态的 RPC,水平扩展就简单了。我建议把「对外协议」和「对内协议」彻底解耦——这是接入层最重要的设计原则之一。
第二,接入治理。 鉴权(token 校验)、租户隔离、灰度发布、请求审计,全在网关统一做。好处是内部服务变「瘦」了,不用每个服务都重复实现一遍鉴权。坏处是网关成了关键路径,所以网关本身必须无状态、可水平扩展,且任何治理逻辑都不能引入显著延迟。
第三,会话亲和与故障漂移的平衡。 对话是有状态的(多轮上下文),你希望同一个用户的长连接尽量落到同一个后端实例,避免上下文在实例间反复迁移。但「亲和」不能写成「钉死」——一旦该实例故障,要能平滑漂移到其他实例。做法是:用一致性哈希做默认亲和,配合一个轻量的「会话位置表」记录用户当前会话落在哪,故障时刻按表重路由。
网关之后、模型之前,还有一个容易被忽视的杀手:下游建连开销。模型推理集群通常通过 gRPC / HTTP/2 暴露,每次新建连接都要握手、TLS、窗口协商,开销不小。如果每条用户请求都现建现连,下游会被「握手风暴」自己打垮,根本轮不到推理。
解法是连接池复用:
这是接入层最容易被忽略、却最关键的一环。限流(2.2 会讲)是「按规则提前拦」,而背压是「下游扛不住时,实时告诉上游慢点」。两者互补。
背压的核心哲学是:别无脑转发。当下游队列深度、延迟、并发超过阈值,网关不该继续把请求往里灌,而应该主动减速——返回 429、把请求转去排队、或直接拒给降级答案。这样下游能在自身容量内平稳处理,而不是被淹没在堆积的请求里。
实现背压有几个层次:
90% 的人会在这里踩坑:只做限流、不做背压。结果上游把请求全堆在内存队列里,QPS 数字没降,但首字延迟从 2 秒飙到 20 秒,用户感知比直接被拒更糟。限流是「按计划挡」,背压是「按实况挡」,缺一不可。
落到实操,有三个数字你必须心里有数,否则配置都是盲调:
把前面四块拼起来,一条请求从用户到模型的完整路径是这样的:
这条链路的每一环都在做「收敛」:L4 收敛地理入口,网关收敛协议与治理,连接池收敛下游握手,背压收敛突发。四件套到位,接入层就从「瓶颈」变成「护城河」。
光说原理不够,落到排期你必须能回答「到底要几台网关」。给一个可复用的估算框架。
目标:稳定支撑 100 万条 WebSocket 长连接在线。经验上,一台内存 64GB、文件描述符上限调到 100 万以上、内核做了 TIME_WAIT 复用与 epoll 调优的网关(无论基于 Envoy、Nginx 还是自研 epoll 服务),可稳定维持约 2030 万条空闲长连接——每条连接约占几 KB 到几十 KB,取决于心跳周期与读写缓冲配置。那么纯从「连得住」的角度,需要 100 万 ÷ 25 万 ≈ 45 台。
但这只是下限。还要叠加两件事:第一,突发新建速率。开播、大促、热点事件会在几秒内涌入十万级新建连接,TLS 握手与连接表插入是 CPU 与内核开销,需要额外吞吐;第二,冗余。任何单机故障都要能由其他节点接管,按 N+2 冗余,实际部署 8 台左右才稳。
更关键的是:上面只算了「连得住」,没算「转得动」。TLS 终结、协议转换(WS/SSE 转 gRPC)、鉴权都是 CPU 密集型。如果单台网关在峰值被转换逻辑吃满 CPU,连接虽然连住了,转发却卡住。这时要么继续横向加节点,要么把 TLS 卸载到 L4 或专用加速卡,把 CPU 留给真正的业务转换。容量估算的铁律只有一句:先用压测测出单机真实上限,再反推集群规模,绝不靠经验值拍脑袋。
接入层是系统的咽喉,一旦异常最先出事、且影响全局,所以这几条指标必须实时可观测、越线即告警:
我建议把这几条做成一块「接入层健康面板」,任何一条越线自动告警。宁可误报,不可不报——因为接入层的故障是全局性的,晚发现一秒,可能就是一次全站级抖动。
读到这里,2.1 应该给你留下这些可复述的结论:
下一节(2.2),我们进入限流算法的正题:令牌桶、漏桶、滑动窗口,各自什么脾气,又该怎么组合,才能让「堵得住」既不误杀正常用户,也不放过洪峰。