2.1 分层网关与连接治理


文档摘要

2.1 分层网关与连接治理:把百万洪峰挡在门外 如果只能给接入层一句话建议,我会说:这一层要解决的不是「计算」,而是「连接」和「协议」。 模型推理是计算密集,但接入层是连接密集、IO 密集。混淆了这两者,你就会用错武器——比如试图用无状态的计算节点去硬扛有状态的长连接,结果连接一多内存先爆。 读者读完这一节,应该能一句话说清楚:接入层靠「四层卸载 + 七层转换 + 连接池复用 + 背压」四件套,把不可控的洪峰变成可控的排队。 下面逐个拆。 2.1.1 为什么接入层决定了整个系统的生死 先建立一个直觉:在对话系统里,用户感知到的「系统挂了」,十次有七八次不是模型算不过来,而是接入层先撑不住。 原因有三。第一,连接是有状态的、昂贵的。

2.1 分层网关与连接治理:把百万洪峰挡在门外

如果只能给接入层一句话建议,我会说:这一层要解决的不是「计算」,而是「连接」和「协议」。 模型推理是计算密集,但接入层是连接密集、IO 密集。混淆了这两者,你就会用错武器——比如试图用无状态的计算节点去硬扛有状态的长连接,结果连接一多内存先爆。

读者读完这一节,应该能一句话说清楚:接入层靠「四层卸载 + 七层转换 + 连接池复用 + 背压」四件套,把不可控的洪峰变成可控的排队。 下面逐个拆。

2.1.1 为什么接入层决定了整个系统的生死

先建立一个直觉:在对话系统里,用户感知到的「系统挂了」,十次有七八次不是模型算不过来,而是接入层先撑不住。

原因有三。第一,连接是有状态的、昂贵的。一个 WebSocket 长连接从建立到释放,期间占用的不只是带宽,还有服务端的内核 fd、连接表内存、心跳定时器。百万长连接同时在线,光内存就会吃掉几十 GB。第二,协议是流式的、慢释放的。一条对话响应持续 20~40 秒,连接长时间不释放,连接池和 epoll 的压力持续累积。第三,客户端是异构、不可信的。移动端弱网重连、浏览器跨域、第三方恶意刷量,全都要在接入层消化掉。

所以接入层的第一职责是「收敛」:把海量、异构、不可控的外部连接,收敛成内部可控、高效、可治理的调用。这个收敛做得好,后面的模型集群就能在一个相对「干净」的输入下工作。

```mermaid flowchart LR subgraph 外部混沌 A1[移动端弱网] A2[浏览器跨域] A3[第三方刷量] A4[突发洪峰] end subgraph 接入层收敛 B[四层卸载] --> C[七层转换] C --> D[连接池复用] D --> E[背压整形] end subgraph 内部干净 F[模型推理集群
稳定输入] end A1 --> B A2 --> B A3 --> B A4 --> B E --> F ```

2.1.2 四层 LB:连接卸载与入口收敛

最外层用四层(L4,TCP/UDP)负载均衡,而不是七层。原因是 L4 更轻——它不解析应用层协议,只做连接的转发与卸载,单机可以维持远超 L7 的并发连接数。在百万长连接场景下,这一层的关键动作有四个:

  • 连接卸载(offload):把 TCP 握手、TLS 握手这些开销从后端业务机移到 LB 上集中处理。后端业务机不再直接面对海量客户端握手,专心干自己的事。
  • 入口收敛与就近接入:通过 Anycast + 区域 LB,让用户的连接落在地理上最近的接入点,既降低首字延迟,也把全局流量先切碎到区域维度,避免单点。
  • 连接表与 fd 管理:L4 自身要能维持巨量 conntrack / fd。这要求 LB 节点本身做内核参数调优(文件描述符上限、TIME_WAIT 复用等),否则它反而成了瓶颈。
  • 健康检查与故障剔除:后端实例挂了,L4 要在秒级把流量切走,且切走时要区分「实例真死」和「实例假死(GC 停顿)」,避免误剔导致雪崩。

一个常见的误判是「L4 就够了,直接打到业务机」。但 L4 不懂协议,它无法做鉴权、无法做灰度、无法终结 WebSocket。所以 L4 之下,必须再接一层七层网关。

2.1.3 七层网关:协议终结与接入治理

七层(L7)网关是接入层的「前台」,它干三件 L4 干不了、也必须集中干的事:

第一,协议终结与转换。 客户端用的是 WebSocket 或 SSE 这种流式长连接协议,但你的内部服务大概率用的是 gRPC 或 HTTP/2 这种高效内部协议。网关负责把外部的 WS/SSE 终结掉,转成内部 RPC 再转发。这样内部服务不需要为每个用户维持一条 WebSocket,只需处理无状态的 RPC,水平扩展就简单了。我建议把「对外协议」和「对内协议」彻底解耦——这是接入层最重要的设计原则之一。

第二,接入治理。 鉴权(token 校验)、租户隔离、灰度发布、请求审计,全在网关统一做。好处是内部服务变「瘦」了,不用每个服务都重复实现一遍鉴权。坏处是网关成了关键路径,所以网关本身必须无状态、可水平扩展,且任何治理逻辑都不能引入显著延迟。

第三,会话亲和与故障漂移的平衡。 对话是有状态的(多轮上下文),你希望同一个用户的长连接尽量落到同一个后端实例,避免上下文在实例间反复迁移。但「亲和」不能写成「钉死」——一旦该实例故障,要能平滑漂移到其他实例。做法是:用一致性哈希做默认亲和,配合一个轻量的「会话位置表」记录用户当前会话落在哪,故障时刻按表重路由。

```mermaid flowchart LR WS[客户端 WebSocket] --> GW[七层网关] SSE[客户端 SSE] --> GW GW -->|鉴权/灰度/审计| CONV[协议转换
WS/SSE → gRPC] CONV --> HASH[一致性哈希
会话亲和] HASH --> S1[后端实例 A] HASH --> S2[后端实例 B] HASH --> S3[后端实例 C] S1 -.故障.-> RE[会话位置表
重路由] RE --> S2 ```

2.1.4 连接池:复用下游,避免每次握手

网关之后、模型之前,还有一个容易被忽视的杀手:下游建连开销。模型推理集群通常通过 gRPC / HTTP/2 暴露,每次新建连接都要握手、TLS、窗口协商,开销不小。如果每条用户请求都现建现连,下游会被「握手风暴」自己打垮,根本轮不到推理。

解法是连接池复用

  • gRPC Channel 复用:一条底层 HTTP/2 连接上可以多路复用成百上千个逻辑流,一个 Channel 就能服务海量并发请求,不每请求建连。
  • 池大小调优:池太小,请求排队等连接;池太大,下游被过量连接压垮。经验上,池的目标并发应约等于「下游可承受并发 ÷ 单连接复用度」,再留 20% 余量。不要拍脑袋设一个无限大的池。
  • 连接健康检测与驱逐:池里的连接会过期、会被对端关闭(尤其是经过 LB 的 idle timeout)。要有心跳探测和懒重建,避免把请求塞给一条已死连接然后超时。
  • 按下游分池:不同模型、不同规格的推理实例,要分不同的连接池隔离。否则慢模型会占满共享池,把快模型也拖死。
```mermaid flowchart TD GW[七层网关] --> POOL[下游连接池] POOL -->|HTTP2 多路复用| CH1[Channel 1 → 大模型集群] POOL -->|HTTP2 多路复用| CH2[Channel 2 → 小模型集群] POOL -->|HTTP2 多路复用| CH3[Channel 3 → 检索服务] CH1 --> M1[大模型 实例组] CH2 --> M2[小模型 实例组] CH3 --> R[检索 实例组] ```

2.1.5 背压:下游慢,上游主动减速

这是接入层最容易被忽略、却最关键的一环。限流(2.2 会讲)是「按规则提前拦」,而背压是「下游扛不住时,实时告诉上游慢点」。两者互补。

背压的核心哲学是:别无脑转发。当下游队列深度、延迟、并发超过阈值,网关不该继续把请求往里灌,而应该主动减速——返回 429、把请求转去排队、或直接拒给降级答案。这样下游能在自身容量内平稳处理,而不是被淹没在堆积的请求里。

实现背压有几个层次:

  • TCP 层:利用滑动窗口天然地让发送方在接收方处理不过来时减速。这是最底层的、自适应的背压。
  • 应用层信号量:网关对每个下游维护一个「在途请求数」计数器,超过上限就不再发,先排队或拒。
  • 消息队列的 pull 模式:下游按自己的节奏去拉取,而不是被上游 push。pull 模式天然带背压——下游慢,就少拉。

90% 的人会在这里踩坑:只做限流、不做背压。结果上游把请求全堆在内存队列里,QPS 数字没降,但首字延迟从 2 秒飙到 20 秒,用户感知比直接被拒更糟。限流是「按计划挡」,背压是「按实况挡」,缺一不可。

```mermaid flowchart TD REQ[用户请求] --> GW[网关] GW -->|在途计数| SEM[信号量
在途上限 N] SEM -->|未满| DOWN[下游模型] SEM -->|已满| Q{决策} Q -->|排队| QUEUE[有限队列
超时则降级] Q -->|直达| DEG[降级答案] DOWN -->|延迟升高| FB[背压信号回传] FB --> GW ```

2.1.6 连接治理的三个实战数字

落到实操,有三个数字你必须心里有数,否则配置都是盲调:

  • 单机可维持长连接数:受 fd 上限和内存限制。一个空闲 WebSocket 连接大约占几 KB 到几十 KB(取决于心跳与缓冲),但你得为峰值预留内存。建议压测出单机连接上限,再按「目标并发 ÷ 单机上限 × 冗余系数」定网关节点数。
  • 连接空闲超时与心跳间隔:心跳太频浪费带宽,太疏会误杀活连接。一般心跳 3060 秒、空闲超时 510 分钟,并和 LB 的 idle timeout 对齐,避免「网关以为活、LB 已切断」的半死连接。
  • 优雅下线时间:发布网关节点时,要先摘流量(drain),等存量连接自然结束或迁移,再停进程。粗暴 kill 会瞬间断开大量用户长连接,造成可见的「闪断」。

2.1.7 把四件套串成一条完整接入链路

把前面四块拼起来,一条请求从用户到模型的完整路径是这样的:

```mermaid sequenceDiagram participant U as 用户设备 participant L4 as 四层 LB participant GW as 七层网关 participant P as 连接池 participant M as 模型集群 U->>L4: 建 WebSocket 长连接 L4->>GW: 卸载后转发 GW->>GW: 鉴权/灰度/协议转换 GW->>P: 取复用连接 P->>M: gRPC 多路复用转发 M-->>P: token 流 P-->>GW: 流式回传 GW-->>U: SSE/WS 推回 M-)GW: 延迟升高(背压) GW-)U: 必要时 429/排队 ```

这条链路的每一环都在做「收敛」:L4 收敛地理入口,网关收敛协议与治理,连接池收敛下游握手,背压收敛突发。四件套到位,接入层就从「瓶颈」变成「护城河」。

2.1.9 接入层容量估算:从「百万连接」反推机器数

光说原理不够,落到排期你必须能回答「到底要几台网关」。给一个可复用的估算框架。

目标:稳定支撑 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.10 接入层必须盯住的可观测性指标

接入层是系统的咽喉,一旦异常最先出事、且影响全局,所以这几条指标必须实时可观测、越线即告警:

  • 活跃连接数与新建/关闭速率:异常飙升往往是刷量攻击或客户端重连风暴的前兆。
  • 握手失败率与 TLS 重协商率:证书过期、握手超时、密码套件不匹配,都会先在这里露出苗头。
  • 各下游连接池的「在途数 / 池占用率」:逼近上限就要预警,避免背压被误触发、把正常流量错杀。
  • 背压触发次数与 429 返回量:直接反映限流是否过紧、下游是否悄悄变慢。
  • 网关自身 P99 延迟:接入层自己不应成为延迟大头,一旦它变慢,后面全链路都会跟着抖。

我建议把这几条做成一块「接入层健康面板」,任何一条越线自动告警。宁可误报,不可不报——因为接入层的故障是全局性的,晚发现一秒,可能就是一次全站级抖动。

2.1.8 本节要点回顾

读到这里,2.1 应该给你留下这些可复述的结论:

  1. 接入层解决的是连接和协议,不是计算。混淆两者会用错武器。
  2. 四层卸载 + 七层转换 + 连接池复用 + 背压,是接入层四件套。L4 做连接卸载与就近接入,L7 做协议终结与治理,连接池复用下游避免握手风暴,背压在下游慢时主动减速。
  3. 对外协议与对内协议必须解耦,内部服务才能保持无状态、易扩展。
  4. 限流 + 背压缺一不可:限流按计划挡,背压按实况挡;只做限流不做背压,延迟会爆而 QPS 不降。
  5. 三个实战数字要压测出来:单机长连接上限、心跳/超时配置、优雅下线时间。

下一节(2.2),我们进入限流算法的正题:令牌桶、漏桶、滑动窗口,各自什么脾气,又该怎么组合,才能让「堵得住」既不误杀正常用户,也不放过洪峰。


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