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 七个"为什么":疑惑清单
复盘会上灵魂拷问:
- 没有网关的世界长啥样?为什么突然大家都要建网关?
- 市面上 Nginx、Kong、APISIX、Spring Cloud Gateway、Envoy 到底怎么选?
- 为什么"流量网关 + 业务网关"两层是主流架构?一层不行吗?
- 单机百万 QPS 的网关是怎么做到的?技术栈到底差在哪?
- 网关的鉴权、限流、协议转换、灰度路由,各自的坑在哪?
- 插件化听起来好,实际怎么设计才不会插件互相打架?
- 网关本身就是攻击面,怎么让它自身不成为整个系统的最短板?
这 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 万条数据的事故:
若当时用了完整方案:
- 两层网关 → Spring4Shell 打到的是 SCG 业务网关,前面有 WAF 层,攻击特征在 WAF 就被识别(Class Loader 参数是 known IOC)。
- 容器 non-root + NetworkPolicy → 就算 RCE 成功,攻击者也没 root 权限,也访问不到内网数据库(只能访问白名单里的鉴权服务)。
- 补丁 SLA 24 小时 → 漏洞公开第二天就打上补丁,不给攻击者留窗口。
- 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 网关设计的真正功力。