编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 体系建设优化

  • 代码品质工坊

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

    • README
    • 通用架构设计方案
    • 组件化方案的设计
    • SDK设计与发布方案
    • 缓存架构设计思想
    • 数据库SQL设计思想
    • 分库分表方案设计
    • 分布式ID生成方案
    • 消息队列方案选型
    • 09.长链接方案的设计
    • 认证授权方案设计
    • API网关设计方案
      • 一、案例引入:Spring4Shell 打穿网关的 9 个小时
        • 1.1 事故现场:8 分钟从 0 到"内网 root"
        • 1.2 顺藤摸根因:漏洞只是导火索,架构才是弹药
        • 1.3 七个"为什么":疑惑清单
      • 二、架构决策三角:性能 × 可用 × 安全
      • 三、网关的"存在本质":把横切关注点从业务里抽出来
        • 3.1 没有网关时的"N × M 复杂度地狱"
        • 3.2 网关的三大存在价值
        • 3.3 什么时候不该用网关
      • 四、五大网关横向对比:选型不能拍脑袋
        • 4.1 5 类主流网关的"人设"
        • 4.2 性能级横向对比
        • 4.3 选型决策:先看语言栈,再看规模
        • 4.4 为什么本文事故用了 SCG 却出事
      • 五、两层网关架构:为什么大厂都拆成"流量层 + 业务层"
        • 5.1 双层网关的分工
        • 5.2 拆两层的三个真正原因
        • 5.3 两层架构的部署形态
      • 六、单机百万 QPS 的秘密:网关的性能内核
        • 6.1 I/O 模型是命脉:epoll + 事件驱动
        • 6.2 内存管理:池化 + 零拷贝
        • 6.3 连接复用:对后端建"连接池"
        • 6.4 缓存所有能缓存的
        • 6.5 性能基线:什么样是"优秀"
      • 七、网关五大核心能力的落地细节
        • 7.1 路由:匹配算法决定 QPS 上限
        • 7.2 鉴权:不能同步阻塞
        • 7.3 限流:多维度组合拳
        • 7.4 协议转换:外部标准化、内部灵活
        • 7.5 灰度路由:从 1% 到 100% 的科学
      • 八、网关自身的高可用:单点即末日
        • 8.1 多层冗余
        • 8.2 健康检查:不是"能 ping 就行"
        • 8.3 优雅启停:不能中断已有请求
        • 8.4 配置回滚:秒级止损
      • 九、反例合集:三个"典型翻车"
        • 9.1 反例一:网关单点 + 假高可用
        • 9.2 反例二:业务下沉网关(本文事故的间接根因)
        • 9.3 反例三:同步阻塞导致雪崩
        • 9.4 从事故版到最终版:网关演进
      • 十、综合案例串讲:把 7 问全部回扣
        • 10.1 逐条回答一开始的 7 问
        • 10.2 事故重演:如果当时用了本文方案
        • 10.3 一次请求的"网关一生"完整时序
        • 10.4 四条网关设计哲学
        • 10.5 网关设计速查表
        • 10.6 上线检查清单(22 项)
      • 十一、写在最后
    • 路由库设计思想
    • 网络检测方案设计
    • 幂等性设计方案
    • 分布式锁方案设计
    • 限流熔断方案设计
    • 移动端防抓包实践
    • 通用轮训方案设计
    • 状态机设计的思想
    • 20.实时通信设计原理
  • 性能优化实践

  • 真经
  • 方案设计思想
杨充
2026-05-21
目录

API网关设计方案

# 11.API 网关设计方案

本篇定位:API 网关是微服务的"门面"——所有外部流量的第一道关卡。鉴权、限流、路由、监控、协议转换全在它身上。做好了后端服务只关心业务;做差了它就是全站的单点故障 + 攻击面 + 性能瓶颈。本文从一次 Spring4Shell 打穿网关横向拖库 80 万条的真实事故讲起,回答七个刺入骨髓的问题——没有网关会怎样?流量层与业务层为啥要拆两层?插件链怎么设计不打架?灰度路由怎么做才安全?

# 一、案例引入:Spring4Shell 打穿网关的 9 个小时

# 1.1 事故现场:8 分钟从 0 到"内网 root"

某中型 SaaS 公司,1000+ 微服务,全部走 Spring Cloud Gateway 一层网关对外暴露。2022 年 3 月,安全社区披露 Spring4Shell(CVE-2022-22965)——一个 Spring MVC 的 RCE 零日漏洞。团队周会讨论过,但**"网关不是普通 Spring MVC 应用,应该不受影响"**,没有列为紧急补丁。

03:00:00 某境外 IP 开始对网关做漏洞探测。 03:02:15 探测到网关某个上传型接口存在 Class Loader 参数注入漏洞,触发点确认。 03:05:30 攻击者构造 class.module.classLoader.resources.context.parent.pipeline.first.pattern= 一系列参数,在网关进程内部写入了一个 JSP webshell——因为 Tomcat 目录可写。 03:08:00 webshell 触达 → 反向 shell 建立 → 攻击者拿到网关容器的 root 权限(当年 K8s 部署没做 non-root)。 03:08:15 攻击者开始在网关容器内往内网横向扫端口——因为网关是"内网 hub",防火墙对它开了所有内网权限。 03:15:00 发现内网 MySQL 5.7、Redis、Kafka、ClickHouse 全部无认证或弱密码。 03:15:00 ~ 12:00:00 9 小时里,攻击者从数据库拖走了 80 万条客户数据,包括合同扫描件、员工薪资、客户联系方式。 12:00:00 客户在暗网看到自家数据被叫卖,紧急报警。公司紧急下架网关,全站 4 小时不可用。

# 1.2 顺藤摸根因:漏洞只是导火索,架构才是弹药

复盘会锁定 7 处设计缺陷(每一处都是网关系统的经典雷区):

# 缺陷 直接后果
① 网关 = 一个巨型 Spring Boot 应用,业务逻辑、鉴权、路由都塞进去了 攻击面等于普通 web 应用
② 网关容器以 root 运行,且挂载了 K8s ServiceAccount 全权限 RCE 一次 = 整个集群
③ 网关对内网可达所有服务,没做服务级 NetworkPolicy 一台被攻破 = 全网通吃
④ 没有 WAF 前置——所有请求直接打到 Spring Cloud Gateway 应用层漏洞第一时间打进来
⑤ 补丁流程平均 SLA 14 天,零日漏洞不例外 攻击窗口过大
⑥ 网关是单层架构——SSL、限流、鉴权、协议转换全在一个进程 单点故障、性能瓶颈、变更风险高
⑦ 流量异常(凌晨 3 点大量 4xx)没告警 事故进行中 9 小时没人管

不是 Spring4Shell 太狠,是网关设计给了它 9 小时的舞台。

# 1.3 七个"为什么":疑惑清单

复盘会上灵魂拷问:

  1. 没有网关的世界长啥样?为什么突然大家都要建网关?
  2. 市面上 Nginx、Kong、APISIX、Spring Cloud Gateway、Envoy 到底怎么选?
  3. 为什么"流量网关 + 业务网关"两层是主流架构?一层不行吗?
  4. 单机百万 QPS 的网关是怎么做到的?技术栈到底差在哪?
  5. 网关的鉴权、限流、协议转换、灰度路由,各自的坑在哪?
  6. 插件化听起来好,实际怎么设计才不会插件互相打架?
  7. 网关本身就是攻击面,怎么让它自身不成为整个系统的最短板?

这 7 问就是本文的地图。走完第 10 章会全部串起来。

# 二、架构决策三角:性能 × 可用 × 安全

网关是"所有请求的必经之路",天然处在三个方向的联合最优化上:

              性能(P99 <5ms、单核 5w QPS)
                     ▲
                    /│\
                   / │ \
                  /  │  \
                 /  网 │ 关\
                /   系  │ 统 \
               / 决 策  │ 三 角\
              ────────┼──────
        可用(多机房、健康检查、优雅启停)    安全(WAF、补丁、最小权限)
  • 功能极致丰富(100 个插件)→ QPS 掉一半。
  • 性能极致优化(只做转发)→ 治理能力被逼到别处。
  • 安全极致收紧(严格 WAF + 白名单)→ 用户误伤率飙升。

架构选择的本质,是选择"要接受哪个代价"。金融业务接受"多花 3ms 换安全",社交业务接受"少一层过滤换性能",SaaS 平台接受"两层网关换灵活"。

# 三、网关的"存在本质":把横切关注点从业务里抽出来

问:"我 10 个服务,每个自己起个 HTTP 端口,客户端直连不就行了吗?"这是新人的经典疑问,答案要拆三层。

# 3.1 没有网关时的"N × M 复杂度地狱"

假设有 3 个客户端(App、Web、桌面)× 10 个后端服务,每个都要做:鉴权、限流、监控、协议转换、CORS、SSL、日志——总共 7 × 3 × 10 = 210 处相同代码。

改一个鉴权规则?改 210 处,漏一处就是漏洞。加一个客户端类型?再写 70 处。这就是没有统一入口的代价。

# 3.2 网关的三大存在价值

价值一:解耦客户端与后端拓扑。

  • 无网关:客户端要知道 订单服务在 10.1.2.3、商品服务在 10.1.2.4,服务地址变了要发新版 App。
  • 有网关:客户端只知道 api.example.com,后端拓扑随便变,前端无感。

价值二:统一横切关注点。

  • 鉴权、限流、监控、日志、协议转换、灰度——一处实现,处处生效。
  • 业务服务代码里没有任何"鉴权 Filter""限流拦截器",纯业务。

价值三:安全边界。

  • 外网只能到达网关,内网服务对外零暴露。
  • 网关是唯一"上补丁的地方"——升 TLS、防 DDoS、加 WAF 都在一处做。

# 3.3 什么时候不该用网关

  • 单体应用:直接一层 Nginx 反代够了。
  • 服务 < 5 个 + 只有一个客户端:网关引入的复杂度大于收益。
  • 内部工具、纯 M2M 通信:直接服务发现 + 服务网格更合适。

结论:网关是"复杂度到达阈值后的必然选择"——微服务规模 > 10 或 客户端类型 > 2,就应该建。

# 四、五大网关横向对比:选型不能拍脑袋

# 4.1 5 类主流网关的"人设"

网关 语言/引擎 定位 一句话
Nginx / OpenResty C + Lua 最经典的反代 单机百万 QPS,Lua 加编程能力
Spring Cloud Gateway Java + Reactor Java 微服务网关 与 Spring 生态深度集成
Kong OpenResty + PostgreSQL/Cassandra 商业化插件市场 插件生态最丰富
APISIX OpenResty + etcd 国产开源新秀 性能与热更新极致
Envoy C++ + xDS Service Mesh 事实标准 云原生首选,Istio 底座

# 4.2 性能级横向对比

维度 Nginx SCG Kong APISIX Envoy
单核 QPS(纯转发) 60,000 8,000 25,000 35,000 40,000
P99 延迟(无插件) <1ms 3~5ms 2ms 1~2ms 1~2ms
热更新 reload(半有损) ✓ 秒级 ✓ 秒级(etcd) ✓ 毫秒级 ✓ xDS
可编程 Lua Java 代码 Lua 插件 Lua/Wasm 插件 Wasm/xDS
配置模型 文件 代码/YAML admin API admin API + etcd xDS
K8s 亲和度 Ingress-Nginx 主流 一般 Kong Ingress APISIX Ingress Istio 底层
学习曲线 平(reverse-proxy 部分) 平(Java 系) 中 中 陡(xDS 概念)
典型用户 几乎所有公司 国内 Java 微服务 国外大厂 国内互联网 字节 / Istio 生态

# 4.3 选型决策:先看语言栈,再看规模

是纯 Java 团队 + 小规模微服务? ── 是 ─→ Spring Cloud Gateway
    │                                     (代码即配置,Java 生态友好)
    否
    ↓
需要极致性能 + 云原生 + 服务网格? ── 是 ─→ Envoy + Istio
    │                                     (xDS 学习曲线,但云原生标配)
    否
    ↓
需要插件生态丰富 + 国际化支持? ── 是 ─→ Kong
    │                                    (商业化)
    否
    ↓
国内团队 + 需要高性能 + 热更新? ── 是 ─→ APISIX
    │
    否
    ↓
业务简单 + 追求稳定? ─→ Nginx / OpenResty(永远不会错的选择)

# 4.4 为什么本文事故用了 SCG 却出事

事故那家公司用 Spring Cloud Gateway——本身没错,但错在把网关当成 Spring MVC 应用来用,塞了大量业务代码进去。SCG 本应是"纯路由 + 少量 Filter",不应该有能被 Class Loader 打穿的接口暴露。

教训:网关的语言/框架本身不是问题,把它设计成什么样才是问题。

# 五、两层网关架构:为什么大厂都拆成"流量层 + 业务层"

单层网关的问题:变更风险太高——业务变个鉴权规则要发网关,网关一发全站抖动。业界主流答案是两层。

# 5.1 双层网关的分工

              ┌──────── 流量网关(全公司共用) ────────┐
              │  ・SSL 卸载(TLS 1.3)                │
              │  ・全局限流(防 DDoS、总带宽)         │
              │  ・IP 黑白名单                        │
              │  ・静态路由(按域名/前缀)             │
              │  ・LB / 健康检查                      │
外网 ────→   │  技术选型:Nginx / Envoy / F5          │
              │  变更频率:极低(配置改动小心翼翼)    │
              └────────────────────────────────────────┘
                              ↓
              ┌──────── 业务网关(各业务线独立部署) ────┐
              │  ・业务鉴权(JWT / OAuth 校验)         │
              │  ・业务限流(按 API/用户/租户)          │
              │  ・协议转换(HTTP↔gRPC / HTTP↔Dubbo)    │
              │  ・灰度路由(按 header/用户/百分比)     │
              │  ・业务级熔断降级                       │
              │  技术选型:SCG / APISIX / Kong          │
              │  变更频率:日常发布(业务需求)         │
              └──────────────────────────────────────────┘
                              ↓
                        后端微服务集群

# 5.2 拆两层的三个真正原因

原因一:变更频率天差地别。

  • 流量层:SSL 证书年度一换,路由规则季度一改——动一次要全公司评审。
  • 业务层:某个 API 加限流、某个业务线搞灰度——每天都在改。

放一起改一次全公司抖动,拆开各改各的互不影响。

原因二:技术栈可以不同。

  • 流量层追求性能 → Nginx / Envoy(C/C++)。
  • 业务层追求业务表达力 → APISIX / SCG(Lua/Java)。

原因三:安全域隔离。

  • 流量层在 DMZ,直接对外。
  • 业务层在内网,只接受流量层转发过来的流量。

如果本文事故网关是"业务网关"、前面还有一层"流量网关 + WAF" —— Spring4Shell 那种应用层攻击根本走不到 SCG,就在 WAF 层被 Class Loader 参数特征识别拒绝。

# 5.3 两层架构的部署形态

Internet
    ↓
CDN(静态资源、DDoS 第一层)
    ↓
四层 LB(LVS / F5,多机房入口)
    ↓
WAF(应用层防火墙,SQL 注入 / XSS / 已知漏洞规则)
    ↓
流量网关集群(Nginx / Envoy,多可用区)
    ↓
业务网关集群(按业务线水平拆分:电商/金融/开放平台)
    ↓
后端微服务

每一层都是"专一职能" —— CDN 干缓存和 DDoS 抗量,WAF 干应用层攻击识别,流量网关干 SSL 卸载和 LB,业务网关干业务治理。没有一个组件试图 "one-size-fits-all"。

# 六、单机百万 QPS 的秘密:网关的性能内核

网关性能好不好,直接决定全站 P99。5w QPS 是及格线,50w 是优秀线,100w+ 是顶级。

# 6.1 I/O 模型是命脉:epoll + 事件驱动

传统"一线程一连接"的 Servlet 模型(Tomcat)在网关场景死路一条——10 万连接 = 10 万线程 = 内存 + 上下文切换全废。

现代网关全都是Reactor 模型:

Main Reactor(Boss)
    ├── 只做 accept()
    └── 把新连接派发给 Sub Reactor

Sub Reactor 池(Worker,通常 = CPU 核数)
    ├── 每个 Sub Reactor 一个线程 + 一个 epoll fd
    ├── 管理 N 个连接的读/写就绪事件
    └── 事件到达 → 派发到业务处理线程池

业务处理线程池(可选,纯转发场景不需要)

关键收益:M:N 复用——4 核机器只有 4 个 Reactor 线程 + 少量业务线程,就能撑 100 万连接。这是 Netty、Nginx、Envoy 的共同原理。

# 6.2 内存管理:池化 + 零拷贝

池化对象:网关每秒创建/销毁上万个 HttpRequest 对象,全走 GC 就是死。Netty 的 ByteBuf 池、Envoy 的对象池——复用而非新建。

零拷贝:一个请求走完全链路可能经过 5 次内存拷贝(网卡→内核缓冲→用户空间→业务对象→……)。零拷贝技术(sendfile、mmap、splice)能把这减到 1 次。

mmap + sendfile 的收益:转发大文件时 CPU 使用率从 60% 降到 5%,因为数据不再进用户态。

# 6.3 连接复用:对后端建"连接池"

网关对下游后端的连接不能每次新建——否则 TCP 握手成本就把 QPS 拉下来。

Client ── (短连接 or 长连接) ── Gateway ── (长连接池) ── Backend
                                            │
                                            └── 每个后端节点维护 100~500 个持久连接
                                                复用发送多个请求(HTTP/1.1 keep-alive 或 HTTP/2 多路复用)

HTTP/2 的多路复用:一条 TCP 连接并发跑多个请求(stream),彻底消灭"队头阻塞"。gRPC 用的就是 HTTP/2 底层,所以 gRPC 网关性能极佳。

# 6.4 缓存所有能缓存的

  • 鉴权结果缓存:用 Token 换用户信息,缓存 15 分钟——QPS 提升 10 倍。
  • 路由结果缓存:请求 URL 匹配路由规则的结果缓存,避免每次跑正则。
  • DNS 缓存:后端服务 DNS 解析结果缓存,别每次都查 DNS。

警告:缓存意味着变更延迟——鉴权撤销、路由变更要考虑清楚缓存失效策略。

# 6.5 性能基线:什么样是"优秀"

指标 及格线 优秀 顶级
单核 QPS(纯转发) 5,000 30,000 60,000+
P99 延迟 <20ms <5ms <2ms
单机连接数 10 万 100 万 1000 万(C10M)
内存 QPS/GB 效率 1000 10,000 50,000
CPU 利用率(峰值) <70% <50% <30%

# 七、网关五大核心能力的落地细节

# 7.1 路由:匹配算法决定 QPS 上限

网关每个请求都要做路由匹配——从几百上千条路由规则里找到对应的后端。匹配算法直接决定性能。

常见匹配维度:Path(最主要)、Method、Header、Query、Cookie、SourceIP、Host。

匹配算法:

算法 复杂度 适用
顺序遍历(拿到请求逐条比对) O(N) 路由 < 100 条
前缀树 (Trie) O(URL长度) 大量前缀匹配
正则合并 DFA O(URL长度) 复杂正则场景
Radix Tree O(URL长度) Envoy / Kong 用这个

规则:路由数超过 1000 时必须用 Trie / Radix Tree,否则每请求 O(N) 匹配 CPU 会跑爆。

# 7.2 鉴权:不能同步阻塞

错误做法:每个请求同步调远程鉴权服务,鉴权服务一抖网关全挂。

正确做法(两级):

Level 1: JWT 本地校验(网关内做签名校验)
    ↓ 签名不通过 → 直接 401
    ↓ 签名通过 → 拿到 userId
    
Level 2: 权限缓存(Redis 或本地 Caffeine)
    ↓ 缓存命中 → 直接用
    ↓ 缓存未命中 → 异步调鉴权服务 + 结果缓存 15 分钟

鉴权服务挂了怎么办?降级策略:

  • 严格模式:全部拒绝(金融场景)。
  • 宽松模式:放行 + 记录日志(体验优先场景)。
  • 缓存模式:继续用缓存里的旧结果,直到缓存也过期(推荐)。

# 7.3 限流:多维度组合拳

单一限流维度必翻车:只限 API 级——单个 IP 刷你也管不了;只限 IP 级——公司 NAT 出口一堆用户被误伤。

推荐组合:

维度 阈值示例 阻止什么
全局 QPS 100 万 网关本身过载
API 级 QPS 每 API 1 万 单 API 抖动拖全局
用户级 QPS 每 userId 100/s 单用户脚本刷
IP 级 QPS 每 IP 500/s 单 IP 攻击
租户级 QPS 每 tenant 5000/s SaaS 多租户隔离

算法选择:令牌桶为主——既限速率又允许突发。禁用简单计数器(临界时刻双倍流量问题)。

分布式限流:单机限流不够时,用 Redis + Lua 脚本做全局令牌桶(详见第 16 篇限流熔断专题)。

# 7.4 协议转换:外部标准化、内部灵活

现代网关最有价值的能力之一。典型场景:

外部客户端 HTTP/JSON  ─── 网关协议转换 ───  内部服务多种协议
                                              ├── gRPC/Protobuf (性能敏感的服务)
                                              ├── Dubbo (Java 生态老系统)
                                              ├── Thrift (Facebook 系遗产)
                                              └── WebSocket (推送场景)

为什么值:

  • 客户端只需理解一种协议(HTTP/JSON),学习成本低。
  • 后端选择最合适的协议,不被前端绑架——gRPC 的性能 + Dubbo 的服务治理都能保住。

注意:协议转换是性能损耗最大的环节。Envoy 的 gRPC-Web 转换、SCG 的 Dubbo 适配都比纯转发慢 30~50%——只在必要时用。

# 7.5 灰度路由:从 1% 到 100% 的科学

灰度不是"发布个新版本试试",是有规则、有观察、有回滚的严格流程:

灰度维度(可组合):
    ・特定 userId 白名单(内部员工先用)
    ・特定 header(X-Gray:true)
    ・特定 IP 段(公司网 IP 段先灰)
    ・百分比(1% → 5% → 20% → 50% → 100%)
    ・特定地区(先深圳后全国)

每档观察:
    ・错误率
    ・P99 延迟
    ・业务转化率
    ・用户投诉

回滚:
    ・秒级回退(改路由权重为 0%)
    ・不需要重发布

关键点:灰度状态可视化 + 一键回滚——网关的灰度必须让运维在 5 秒内能把 100% 灰度回到 0%。

# 八、网关自身的高可用:单点即末日

网关既然是"必经之路",挂了 = 全站 500。网关的高可用是所有子系统里最严苛的。

# 8.1 多层冗余

DNS 多 A 记录(解析到多机房 VIP)
    ↓
VIP1(机房 A)              VIP2(机房 B)
    ↓                            ↓
LVS 集群(4 层 LB)        LVS 集群
    ↓                            ↓
网关集群 A(10 台)        网关集群 B(10 台)
    ↓                            ↓
后端服务

任何一层挂一半都不影响:DNS 剔除故障 VIP → LVS 剔除故障网关 → 网关跳过故障后端。

# 8.2 健康检查:不是"能 ping 就行"

四层健康检查(LVS 检 TCP 端口)只保证"进程活着"——但进程活着不代表服务健康(可能内存泄漏、DB 断了、GC 卡死)。

七层健康检查必须做:

LB 每 3 秒调网关 /health 接口
    ↓
/health 内部检查:
    ・网关自身状态
    ・关键下游服务连通性(Redis / 鉴权服务)
    ・内存/CPU 阈值
    ↓
任一异常 → 返回 5xx → LB 剔除

# 8.3 优雅启停:不能中断已有请求

错误:直接 kill -9——正在处理的请求全部 500。

正确:graceful shutdown:

1. 收到 SIGTERM 信号
2. 从注册中心/LB 注销自己(不接受新请求)
3. 等待 30~60 秒让在途请求处理完
4. 关闭 socket
5. 进程退出

Envoy 的 admin drain API、Nginx 的 nginx -s quit——都是这套逻辑。

# 8.4 配置回滚:秒级止损

网关配置改错是最常见的事故源——路由规则打错、限流阈值填反、鉴权规则漏一个 API。

必备能力:

  • 每次配置变更打版本号。
  • 一键回退到上一版本(<5 秒生效)。
  • 配置变更前自动 diff 通知。

APISIX + etcd 的架构天然支持——配置存 etcd,改一下变量秒级生效,回滚也是秒级。

# 九、反例合集:三个"典型翻车"

# 9.1 反例一:网关单点 + 假高可用

某公司一台 Nginx 做网关,前面放个 keepalived 备机。Nginx 进程死锁(端口还在但不响应)—— keepalived 只检 TCP 端口,认为主机还活着,不切换。全站半小时无响应。

教训:

  • 健康检查必须走应用层(HTTP 200,不只是 TCP)。
  • 至少 2 台真正的活跃节点(Active-Active),不是 Active-Standby。
  • 关键路径要定期演练**"随机杀主机"**。

# 9.2 反例二:业务下沉网关(本文事故的间接根因)

网关里塞了大量业务代码:优惠券计算、库存查询、用户画像判断……网关变成了"巨型业务应用":

  • 每次业务变更都要发网关。
  • 网关代码 30 万行,Spring MVC 各种接口暴露——攻击面等于业务应用。
  • 网关重启一次全站抖动 30 秒。

教训:

  • 网关只做通用能力:鉴权、限流、路由、监控、协议转换、灰度。
  • 业务逻辑永远在业务服务。
  • 网关代码要少而精——1 万行代码是合理规模,10 万行就是设计失控。

# 9.3 反例三:同步阻塞导致雪崩

某网关鉴权走 RPC 同步调用远程鉴权服务。鉴权服务某次发布慢启动,RT 从 5ms 涨到 500ms。网关工作线程池被阻塞,几秒内耗尽——请求排队 → 超时 → 客户端重试 → 更多排队 → 全站雪崩。

教训:

  • 网关里绝对不能有同步阻塞调用。
  • 所有远程调用必须异步 + 超时 + 熔断 + 降级。
  • 鉴权、限流决策要结果缓存,避免每请求打远程。

# 9.4 从事故版到最终版:网关演进

V0(事故版,本文案例):
  ✗ 单层 SCG,业务逻辑塞满
  ✗ 容器 root 运行,K8s 全权限
  ✗ 无 WAF 前置
  ✗ 补丁 SLA 14 天
  ✗ 4xx 异常无告警

V1(补救版):修主要漏洞
  ✓ 拆两层:Nginx 流量网关 + SCG 业务网关
  ✓ 容器 non-root + NetworkPolicy 白名单
  ✓ WAF 前置(ModSecurity / 云 WAF)
  ✓ 零日漏洞 SLA 24 小时

V2(工程化版):可运维、可观测
  ✓ APISIX 替代 SCG(性能 + 热更新)
  ✓ 双活多机房 + 优雅启停
  ✓ 全链路 traceId + Metrics
  ✓ 配置版本化 + 秒级回滚

V3(大规模版):Service Mesh 时代
  ✓ 边缘 Envoy + 内部 Istio
  ✓ mTLS 服务间加密
  ✓ 智能 DNS 就近接入
  ✓ 全局限流 + 分级熔断

# 十、综合案例串讲:把 7 问全部回扣

# 10.1 逐条回答一开始的 7 问

# 疑问 答案(章节)
① 没有网关的世界? §3:N×M 复杂度爆炸,横切关注点分散
② 五大网关怎么选? §4:语言栈 + 规模 + 云原生程度
③ 为啥要拆两层? §5:变更频率不同、技术栈不同、安全域不同
④ 百万 QPS 秘密? §6:Reactor + 池化 + 零拷贝 + HTTP/2
⑤ 五大能力细节? §7:路由 Trie、鉴权异步、限流多维、协议转换、灰度可回滚
⑥ 插件如何不打架? §7 + §8:链式设计 + 明确顺序 + 优雅启停
⑦ 网关自身怎么防? §8 + §9:多层冗余 + 应用层健康检查 + 反例三教训

# 10.2 事故重演:如果当时用了本文方案

回到开头 9 小时被拖 80 万条数据的事故:

若当时用了完整方案:

  1. 两层网关 → Spring4Shell 打到的是 SCG 业务网关,前面有 WAF 层,攻击特征在 WAF 就被识别(Class Loader 参数是 known IOC)。
  2. 容器 non-root + NetworkPolicy → 就算 RCE 成功,攻击者也没 root 权限,也访问不到内网数据库(只能访问白名单里的鉴权服务)。
  3. 补丁 SLA 24 小时 → 漏洞公开第二天就打上补丁,不给攻击者留窗口。
  4. 4xx 告警 → 探测阶段的大量 4xx 会立刻触发告警。

结果:事故止损从 9 小时缩到 5 分钟,数据泄露从 80 万条降到 0。

# 10.3 一次请求的"网关一生"完整时序

── T0: 用户点击 App 里的按钮 ─────────────────
Client 发出 POST /api/v1/order/create
  Header: Authorization: Bearer eyJ...
  Body: {"skuId":123,"qty":1}

── T0+2ms: 到达 CDN ──────────────────────
CDN 检测:非静态资源 → 直接透传
(若被 DDoS,CDN 层已限流)

── T0+8ms: 到达 WAF ──────────────────────
WAF 规则检查:
  ・SQL 注入模式?NO
  ・XSS 特征?NO
  ・已知 CVE payload?NO
  ・IP 黑名单?NO
→ 放行 + 打上 X-WAF-Score:0

── T0+10ms: 到达流量网关(Nginx)─────────────
・SSL 卸载(TLS 1.3 termination)
・全局限流(当前 QPS 60w,阈值 100w,放行)
・按域名路由到业务网关集群 A

── T0+12ms: 到达业务网关(APISIX)─────────────
Step 1: 路由匹配
  Radix Tree 匹配 /api/v1/order/*
  命中路由:order-service,upstream=lb://order-svc

Step 2: JWT 校验(本地)
  Header 解析 → 签名验证(用 KMS 拉取的公钥)→ 拿到 userId=U123

Step 3: 权限缓存查询
  Redis GET perm:U123:order:create
  命中 → 允许

Step 4: 限流
  ・API 级:/order/create 当前 QPS 8000 < 10000 ✓
  ・用户级:U123 当前 5/s < 100/s ✓
  ・IP 级:1.2.3.4 当前 50/s < 500/s ✓

Step 5: 灰度路由
  判断 X-Gray Header?否
  判断 U123 是否命中 5% 灰度? hash(U123) % 100 = 3 ✓
  路由到 v2 版本

Step 6: 协议转换(如果 upstream 是 gRPC)
  HTTP/JSON → gRPC 请求

Step 7: 转发
  从连接池取一条到 order-svc-v2 的持久连接
  发送 + 注入 traceId + X-User-Id

── T0+50ms: 后端处理完毕 ─────────────────
order-svc-v2 返回 200 + {"orderId":"..."}

── T0+52ms: 业务网关处理响应 ─────────────
・响应插件:日志记录、Metrics 打点
・协议反转换(gRPC → HTTP/JSON)
・限流计数扣减
・响应回写

── T0+55ms: 流量网关处理响应 ─────────────
・SSL 加密回程
・访问日志

── T0+65ms: 到达客户端 ─────────────────
用户看到"下单成功"

全程 55ms,其中:
  ・CDN → WAF → 流量层:10ms
  ・业务网关:40ms(含鉴权+路由+限流+转发)
  ・后端处理:50ms
  ・回程:15ms

# 10.4 四条网关设计哲学

哲学一:网关只做"横切",业务归业务。 每次想把业务逻辑塞进网关时,就想想 30 万行代码的 SCG 是怎么被打穿的。网关代码 1 万行是艺术,10 万行是灾难。

哲学二:单点即末日,冗余是本能。 网关是"必经之路",任何单点都会被墨菲定律照顾——多机房、多集群、多维度健康检查、优雅启停、秒级回滚,一样都不能少。

哲学三:性能是安全的前提。 一个 P99 15 秒的网关,攻击者根本不需要 RCE 就能把你打挂——直接压测就行。5ms P99 是及格线,不到这个数其他都不用谈。

哲学四:网关自身也是攻击面,最小化才安全。 容器 non-root、NetworkPolicy 白名单、补丁 SLA 24 小时、WAF 前置——"网关不该被打穿"这句话本身是错的,正确的是"网关被打穿了也没事"。

# 10.5 网关设计速查表

决策点 首选 备选 禁忌
架构层次 两层:流量 + 业务 单层(小规模) 单层 + 业务塞满
流量层 Nginx / Envoy HAProxy Java 系流量层
业务层 APISIX / SCG Kong 自研网关(除非有能力)
I/O 模型 Reactor + epoll / 一线程一连接
鉴权 JWT 本地 + 缓存 mTLS 每请求同步 RPC
限流 多维度组合 + 令牌桶 单维度 简单计数器
灰度 多维组合 + 一键回滚 百分比 硬发布无灰度
部署 多机房双活 + non-root 单机房主备 单点 + root
前置 CDN + WAF + LB WAF 网关直接对外网

# 10.6 上线检查清单(22 项)

架构

  • [ ] 拆分流量层与业务层
  • [ ] WAF 前置
  • [ ] 多机房多可用区
  • [ ] DNS 多 A 记录 + 智能解析

性能

  • [ ] 单核 QPS 压测过 3w
  • [ ] P99 延迟 < 10ms(无业务逻辑)
  • [ ] 后端连接池
  • [ ] HTTP/2 或 keep-alive

安全

  • [ ] 容器 non-root
  • [ ] K8s NetworkPolicy 白名单
  • [ ] TLS 1.3 + HSTS
  • [ ] 补丁 SLA ≤ 24 小时
  • [ ] 密钥走 KMS
  • [ ] 4xx / 5xx / 慢请求告警

可用

  • [ ] 应用层健康检查
  • [ ] 优雅启停(drain)
  • [ ] 配置版本化 + 秒级回滚
  • [ ] 演练"随机 kill"

观测

  • [ ] 访问日志(全字段)
  • [ ] Metrics(QPS/RT/错误率,按 API 粒度)
  • [ ] 分布式 tracing 注入
  • [ ] 网关自身健康仪表盘

治理

  • [ ] 鉴权结果缓存
  • [ ] 多维限流配置

# 十一、写在最后

API 网关是微服务的"咽喉要道"——门开得好,业务畅行;门设计不当,全站陪葬。本文案例中那家 SaaS 公司只是因为没有 WAF 前置 + 单层网关 + 补丁慢 14 天这三件事碰在一起,就交了 80 万条客户数据的学费。

网关设计的三条底线:

  • 两层比一层强,一层比没有强。 变更频率不同的能力,必须放在不同的层里。
  • 横切归网关,业务归业务。 想清楚"这段代码为什么在网关里"——如果答不上来,就该搬走。
  • 网关自身要能被打穿。 假设它一定会被攻破,多层防御让"被打穿"不等于"数据丢失"。

下次面对"我们要建一个网关"的需求时,希望你脑子里冒出的不是"用哪个开源",而是——"要拆几层、补丁怎么打、限流怎么配、灰度怎么回滚、被打穿了怎么办"。这,就是 API 网关设计的真正功力。

上次更新: 2026/07/02, 15:18:57
认证授权方案设计
路由库设计思想

← 认证授权方案设计 路由库设计思想→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式