设计一个 API 网关系统

本文系统设计一个高可用 API 网关系统:需求澄清与量级估算、网关分层架构(控制面/数据面)、动态路由与负载均衡、鉴权认证(JWT/签名/多租户)、限流熔断与降级、灰度发布与版本管理、性能与高可用保障,并给出架构图、路由表设计与伪代码。

微服务架构里,几十上百个服务各有各的地址、协议与鉴权方式,客户端直接连会让「服务发现、安全、限流、灰度」散落到每个服务里。API 网关把所有外部流量收敛到一个统一入口,承担路由转发、鉴权、限流、熔断、灰度与可观测等横切职责,让后端服务专注于业务本身。本文按照系统设计面试的标准答题结构,设计一个生产级的 API 网关系统。

一句话:API 网关是「所有流量的前门」——把「每个服务都要做的事」集中到一处做一遍,用配置而非改代码驱动路由与策略,同时保证自身不成为新的瓶颈与单点。

一、需求澄清与量级估算

1.1 需求澄清

面试官给出题目「设计一个 API 网关系统」后,先通过提问明确边界:

  • 流量来源:移动 App、H5、开放平台第三方、内部服务间,覆盖哪些?
  • 协议:HTTP/HTTPS、WebSocket、gRPC,是否需要协议转换?
  • 路由能力:按路径/域名/Host 路由?是否需要动态上线新服务?
  • 安全职责:鉴权(JWT/OAuth)、签名校验、IP 白名单、防刷,哪些必需?
  • 治理能力:限流、熔断、降级、灰度发布、请求日志,是否需要?
  • 性能要求:网关增加的额外时延上限?峰值 QPS?
  • 高可用:网关自身如何避免单点?

明确假设(面向面试的合理假设):

需求项假设
流量来源App + H5 + 第三方开放平台
协议HTTP/HTTPS 为主,少量 WebSocket
路由动态路由,服务上线自动生效
安全JWT 鉴权 + 签名校验 + IP 黑白名单
治理限流 + 熔断 + 灰度 + 全量日志
性能网关额外时延 < 1ms,峰值 100 万 QPS
高可用多机房多副本,故障自动摘除

1.2 量级估算

指标估算值推导
峰值 QPS100 万全部外部流量收敛入口
网关节点500 台无状态水平扩展
后端服务200 个微服务规模
路由条目1 万条服务 × 接口 × 版本
额外时延P99 < 1ms网关只做转发与轻策略
日志量日千亿条全量访问日志

一句话:100 万 QPS、每个请求都要过一遍「路由 + 鉴权 + 限流」——网关必须是「无状态、内存路由、旁路策略」的组合,任何一步去查数据库都会把延迟打穿。

二、高层架构设计

   ┌──────────────────────────┐    ┌──────────────────────────┐
   │ 客户端 (App/H5/第三方)    │    │ 运维/开发 (配置路由/策略)   │
   └────────────┬─────────────┘    └────────────┬─────────────┘
                │ API 请求(HTTPS)               │ 配置/发布
                ▼                               ▼
   ┌─────────────────────────────────────────────────────────┐
   │                网关数据面 (Gateway Data Plane)            │
   │   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐    │
   │   │ 协议解析  │ │ 鉴权认证  │ │ 路由转发  │ │ 限流熔断  │    │
   │   │ 解/加密   │ │ JWT/签名 │ │ 负载均衡  │ │ 降级重试  │    │
   │   └──────────┘ └──────────┘ └──────────┘ └──────────┘    │
   │   无状态集群,水平扩展,全内存处理热路径                  │
   └──────┬──────────────────────────────────────────────────┘
          │ 转发到后端
          ▼
   ┌─────────────────────────────────────────────────────────┐
   │                后端服务集群 (200 个服务)                  │
   │  服务A (v1/v2)   服务B   服务C   ...   服务N              │
   └─────────────────────────────────────────────────────────┘

   ┌─────────────────────────────────────────────────────────┐
   │                网关控制面 (Gateway Control Plane)         │
   │  路由/策略配置中心 → 推送到数据面各节点                    │
   │  服务注册发现(Nacos/Consul) → 动态路由表                   │
   │  监控指标(时延/错误率/QPS) 告警 日志                      │
   └─────────────────────────────────────────────────────────┘

整体拆为两部分:

  1. 数据面:无状态转发集群,执行路由、鉴权、限流、熔断,全内存热路径。
  2. 控制面:配置中心 + 服务注册发现 + 监控,把路由与策略下发到数据面。

2.1 控制面与数据面分离

网关的高可用建立在「数据面无状态、控制面可降级」上:

数据面:只管转发与执行策略,不持有可写状态
  - 路由表、限流配额等配置从控制面拉取,本地缓存一份
  - 控制面故障 → 数据面用本地缓存继续工作(策略保守化)

控制面:管配置下发与服务发现
  - 配置变更 → 秒级推送到全部数据面节点
  - 路由表快照版本化,节点原子切换

好处:
  - 数据面任意扩缩容,加机器即加吞吐
  - 控制面短暂不可用,线上流量不受影响

一句话:把「决策配置」与「执行流量」拆成控制面和数据面,数据面无状态可水平扩、控制面挂了也能靠本地缓存撑着——网关自己才不会成为单点。

三、核心组件设计

3.1 动态路由与负载均衡

路由是网关的第一职责:请求 → 哪个服务 → 哪个实例:

路由匹配(从高优先级到低优先级):
  ① 域名/Host → 服务组
  ② 路径前缀 → 具体服务:/api/v1/user/** → user-service
  ③ 请求头/参数 → 灰度分组(canary 标签)
  ④ 版本:/api/v2/order → order-service v2

服务发现:
  服务实例注册到 Nacos/Consul(含健康状态)
  网关订阅服务列表 → 构建本地路由表(服务 → 健康实例列表)
  实例下线/不健康 → 路由表剔除,秒级生效

负载均衡策略:
  加权轮询(按实例容量权重)
  最少连接 / 一致性哈希(按用户 id 哈希固定路由,利于会话保持)
;; 伪代码:路由表构建与匹配
(defn build-route-table [services]
  ;; 服务注册信息 → {prefix → [{service, version, nodes[]}]}
  (group-by-path services))

(defn route-request [req route-table]
  (let [match (longest-prefix-match (:path req) route-table)
        node  (pick-node match)                 ; 加权轮询/哈希
        node]                                    ; 返回目标实例))

;; 实例健康感知
(defn mark-unhealthy [node]
  (remove-node! route-table node))              ; 剔除故障实例

要点:路由是「配置驱动」而非「改代码驱动」——新服务上线注册即路由生效,旧服务下线即摘除;路由匹配用最长前缀 + 优先级链,让特殊规则(灰度)优先于通用规则。

3.2 鉴权与认证

网关是鉴权的天然位置——一次鉴权,所有后端服务免鉴权:

鉴权模型:
  JWT(无状态):
    客户端登录后拿 token(含 uid/角色/过期时间,签名保证不可篡改)
    网关验签 + 验过期 + 验角色 → 通过则透传用户身份头
    无状态:网关无需查库,验签即可
    缺点:无法立即撤销 → 用黑名单兜底

  签名校验(开放平台):
    第三方请求带 appKey + 时间戳 + 签名(MD5/HMAC)
    网关用 appKey 对应的密钥重算签名比对
    防重放:时间戳窗口(5 分钟)+ nonce 缓存

  多租户隔离:
    按 appKey/租户 id 隔离路由与配额
    租户级黑名单、租户级限流
;; 伪代码:JWT 校验
(defn verify-jwt [token]
  (when-let [claims (jwt/verify token secret)]    ; 验签名
    (when (> (:exp claims) (now))
      (when (not (blacklisted? (:jti claims)))   ; 撤销检查
        claims))))

;; 通过后把身份注入转发请求头
(if-let [claims (verify-jwt token)]
  (forward! (assoc req :header-user claims))
  (resp 401 "unauthorized"))

一句话:网关把鉴权收敛成「一次验签、一份信任」——后端服务默认信任网关注入的用户身份,各自不用再写鉴权代码;JWT 保无状态,签名校验保开放平台安全,租户隔离保多租户公平。

3.3 限流与熔断

网关承载全部流量,保护后端是它的核心价值:

限流(防打爆):
  全局维度:网关总 QPS 上限
  服务维度:每个后端服务的调用配额
  用户/租户维度:单用户单接口频率
  实现:令牌桶/滑动窗口,配额存 Redis 或本地分片计数

熔断(防雪崩):
  监控后端错误率/时延:连续失败超过阈值 → 打开熔断器
  熔断期间直接返回降级响应(不把流量打到已故障的服务)
  半开探测:过一段冷却时间放少量流量试探,恢复则关闭熔断

降级:
  超时快速失败(默认 3s)→ 返回缓存数据或友好提示
  重试:只对幂等请求重试一次,避免重试风暴
;; 伪代码:服务级熔断
(defn should-trip? [service]
  (let [stats (circuit-stats service)]
    (or (> (:error-rate stats) 0.5)              ; 错误率 > 50%
        (> (:p99-latency stats) 3000))))         ; P99 > 3s

(defn call-backend [req service]
  (cond
    (circuit-open? service)  (degraded-resp service)  ; 熔断降级
    (exceed-quota? service)  (resp 429 "too many")
    :else (proxy-forward req service)))

要点:限流是「进门前限量」,熔断是「出门遇险关门」——限流挡住超量请求保护所有服务,熔断在单个服务故障时快速止损并降级,二者配合才避免「一个服务慢拖垮全站」的雪崩。

3.4 灰度发布

新版本不能全量上线,网关是灰度执行的绝佳位置:

灰度方案:
  版本路由:/api/v2/** 只路由到新版本实例,v1 继续服务老流量
  流量灰度:按用户 id/地域/设备,把 x% 流量路由到新版本
  标签路由:请求带 canary 头 → 网关命中灰度分组
  逐步放量:5% → 20% → 50% → 100%,每步观察监控

回滚:
  监控异常 → 切回旧版本路由(配置秒级生效)
  灰度实例摘除,流量全回 v1
配置示例(灰度规则):
  route /api/order/**:
    version: v1
    weight: 80
    gray:
      version: v2
      weight: 20
      rule: uid_hash % 100 < 20   # 20% 用户走 v2

一句话:灰度发布把「上线」从「开关切换」变成「比例调节」——网关按权重和规则把流量分给新旧版本,监控无异常再逐步放量,出问题秒级回滚,发布风险从「事故」变成「可预期的小波动」。

3.5 性能与高可用

网关自己必须是「又快又稳」,否则全站受害:

性能设计:
  全异步 IO(Netty/异步 HTTP),避免线程池阻塞
  热路径只读内存路由表,不查库、不跨机 RPC
  连接池与 TLS 会话复用,减少握手开销
  响应压缩(gzip/br),减小带宽

高可用:
  多机房多副本部署,DNS/LB 就近接入
  数据面无状态:机器故障自动摘除,流量由健康节点承接
  配置中心多副本,路由表本地缓存兜底
  全链路压测:验证网关在峰值 + 故障叠加下的表现
手段解决的问题
全异步 IO高并发下的线程阻塞
内存路由 + 本地缓存控制面故障不中断转发
多机房多副本单机房故障不挂
连接池/TLS 复用降低时延与握手开销
全链路压测提前暴露容量与雪崩点

结论:网关的性能红线是「自身时延 < 1ms」——用异步 IO + 内存热路径做到;高可用红线是「自身不成为单点」——用无状态多副本 + 本地缓存兜底做到;两条红线都守住,网关才能放心地承担「所有流量的前门」。

四、深入权衡

4.1 网关聚合层数

方案优点缺点
单层网关简单,一个入口路由/策略耦合,扩展受限
双层(入口 + 业务网关)入口管安全,业务网关管路由多一跳延迟
服务网格(Sidecar)治理下沉到边车,应用无感复杂度高,运维重

结论:主流是「入口网关 + 服务网格/业务网关」两层——入口管全局安全与流量,业务网关管服务路由与治理;层数每加一层都加延迟,够用即可。

4.2 限流状态放本地还是集中

本地限流:每节点独立配额,零延迟,但总量不准(节点数 × 单节点)
集中限流:Redis 计数,总量精确,但每个请求多一次 RTT,且 Redis 是热点
折中:本地分片配额 + 周期性与集中计数校准(微误差可接受)

一句话:限流精度与延迟不可兼得——生产上用「本地配额为主、Redis 校准兜底」的混合方案,让限流既准又不成为性能瓶颈。

4.3 网关承载业务逻辑吗

网关容易「什么都往里塞」导致膨胀变慢。权衡原则:

适合放网关的:横切能力(鉴权/限流/熔断/灰度/日志/协议转换)
不适合放网关的:业务逻辑(算价/库存/推荐规则)

结论:网关只做「所有服务都要做的事」,业务逻辑留在各自服务——保持网关「薄而快」,是它长期不腐化的关键纪律。

五、总结

API 网关系统的骨架是「控制面与数据面分离」:数据面是无状态的转发集群,用内存路由表承载路由、鉴权、限流、熔断、灰度等横切职责,额外时延控制在毫秒内;控制面负责配置下发与服务发现,故障时可被数据面的本地缓存兜底。三个核心工程决策是:一是配置驱动,新服务上线注册即路由、灰度放量即改权重,运维不需要改代码;二是前移治理,鉴权限流熔断全部收敛到网关,后端服务只管业务;三是性能与高可用双红线,用异步 IO 与内存热路径保证快,用多副本与本地缓存保证稳。最终,网关以「一个入口、一套策略、处处生效」的姿态,成为微服务架构里安全、稳定、高效的流量总闸。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「design」更多文章

  1. 设计一个短视频系统
  2. 设计一个分布式缓存系统
  3. 设计一个日志检索系统