API 网关是每张架构图里都有、却很少有人解释的那个方框。它位于系统边缘,随着时间推移不断揽下职责:路由、认证、限流、请求改写,有时甚至还有它本不该承担的业务规则。如果图上只画了一支穿过网关图标的箭头,就没人知道它到底承担了其中哪些职责,这些问题会在事故中冒出来,而不是在评审时。
本文为一种常见架构绘制 API 网关架构图:一个 Web 应用、若干移动应用和合作伙伴都位于同一个网关之后,并且每个前端都有自己的后端。文章说明网关该做什么、不该做什么,令牌校验和限流如何工作,以及应该标注什么,才能让这张图回答真实的问题。
哪些职责属于网关
| 职责 | 放在网关? | 原因 |
|---|---|---|
| TLS 终止 | 是 | 证书集中在一个地方管理 |
| 按路径或主机路由 | 是 | 客户端只看到一个入口 |
| 令牌校验 | 是 | 无效请求在到达服务之前就被拒绝 |
| 粗粒度授权,例如 scope | 通常是 | 没有 orders:read 的合作伙伴令牌永远到不了 Orders |
| 细粒度授权 | 否 | 只有服务知道这个用户能否取消这笔订单 |
| 限流与配额 | 是 | 保护服务,并执行合作伙伴的套餐限制 |
| 请求 ID、访问日志、链路追踪 | 是 | 对边缘有统一一致的视图 |
| 为某个页面聚合调用 | 放在 BFF | 把界面专属的逻辑挡在共享网关之外 |
| 业务规则 | 否 | 它们属于服务,在那里才能被测试 |
负载均衡器把请求分发到各个实例上,但它不知道调用方是谁。服务网格处理系统内部服务之间的流量。网关处理从外部进入的流量,并且了解客户端:它们的令牌、路由和配额。很多系统三者都在用。
在边缘校验令牌
在示例中,客户端通过身份提供商登录,并在每个请求中携带一个 JSON Web Token(RFC 7519)。网关以 JWKS 文档的形式获取并缓存提供商的公钥,用它校验签名,然后检查各项声明:签发者(iss)、受众(aud)和过期时间(exp)。
这里值得遵循 RFC 8725 中的 JWT 当前最佳实践。函数库必须允许你指定允许的算法,并且不得使用其他算法,所以要配置允许列表,而不是信任令牌头部。当同一个签发者为多个应用签发令牌时,令牌必须携带受众,而且每个应用都必须检查受众是否是自己。
有效的令牌说明调用方是谁,但不说明对方能做什么。网关可以拒绝缺少 scope 的请求,但这个用户能否修改这笔订单,仍然由服务来决定。
限流
按客户端计数,而不是按 IP 地址。 在 NAT 之后,许多用户可能共用一个地址,而一个爬虫也可能使用许多地址。用 API 密钥、OAuth 客户端或用户作为限流的键。
共享计数器。 如果有多个网关实例且各自计数,每分钟 100 次的限制就会变成每个实例 100 次。Redis 这样的共享存储能在所有实例之间为每个客户端维护同一个计数。
选定算法并写下来。 固定窗口每分钟重置一次,会在窗口边界处允许突发流量。滑动窗口可以把它平滑掉。令牌桶在保持平均速率的同时,允许在设定大小以内的短时突发。
用 429 响应。 RFC 6585 定义了 429 Too Many Requests。响应应当说明原因,并且可以带上 Retry-After,RFC 9110 将它定义为一个 HTTP 日期或需要等待的秒数。RFC 6585 并没有规定服务器如何识别客户端、如何计数请求:这部分是你自己的设计,所以要画在图上。IETF 有一份草案定义了标准的 RateLimit 和 RateLimit-Policy 头部字段,但截至撰写本文时(2026 年 10 月),它仍是草案。
决定计数存储宕机时怎么办。 失败时放行(fail open)能让 API 在没有限流的情况下继续运行;失败时拒绝(fail closed)则保护服务、拒绝流量。两者都可能是对的,只要这是一个有意的决定,而不是意外。
为前端服务的后端(BFF)
Sam Newman 的 Backends For Frontends 模式,用每种用户体验各自对应的服务端组件,取代一个通用的 API 后端。每个 BFF 只专注于一个界面,通常由构建该界面的团队维护,这样团队就能随着页面的变化调整 API。共享的通用后端往往会成为瓶颈,文章还指出,做聚合的边缘服务容易积累臃肿的代码。
需要几个 BFF?Newman 引用了 Stewart Gleadow 的一条简单规则:一种体验,一个 BFF。如果两个应用提供相同的体验、并由同一个团队负责,它们可以共用一个;例如 SoundCloud 的 iOS 和 Android 听众应用就共用一个 BFF。
合作伙伴的情况不同。他们需要稳定、带版本号的 API,而不是围绕某个页面设计的 API,所以他们的路由不经过 BFF,直接到达服务的公开 API。
示例:一家网店
这家网店有一个 React Web 应用、iOS 和 Android 应用,以及通过 API 创建订单的合作伙伴。WAF 在网关之前过滤流量。网关校验令牌,在 Redis 中按客户端计数请求,并按路径前缀路由。Web 团队和移动团队各自负责一个 BFF,两个 BFF 都会调用私有网络内的 Orders、Catalog 和 Accounts 服务。合作伙伴的调用直接到达 Orders 服务带版本号的 API。
图表内容
- Web app
- Mobile app
- Partner system
- WAF
- API gateway
- Identity provider
- Rate limit counters
- Private network
- Web BFF
- Mobile BFF
- Orders service
- Catalog service
- Accounts service
- HTTPS
- filtered
- sign in (OIDC)
- JWKS, cached
- count per client
- /web/*
- /mobile/*
- /partner/v1/*
这张图的读法如下:
- 从左到右,一个请求。 客户端经过 WAF 调用网关,网关再把请求路由到某个 BFF 或合作伙伴 API。
- 虚线连线不在请求路径上。 Web 应用通过身份提供商登录,网关获取并缓存提供商的签名密钥。
- 网关下方, Redis 保存所有网关实例共享的限流计数器。
- 私有网络内, 每个 BFF 只调用其页面需要的服务。
- Markdown 块是网关策略:它检查什么、限额是多少,以及哪些事留给服务。
需要标注什么
- 路由: 每个后端的路径前缀或主机。
- 每个入口的认证: 令牌类型、签发者,以及网关检查什么。
- 限额: 计数键、次数和窗口,以及客户端会收到什么。
- 网关上的超时与重试。 边缘的重试会成倍放大本已吃力的服务的负载。
- 故障行为: 身份提供商、计数存储或某个后端不可用时会发生什么。
- 归属。 网关配置通常由平台团队负责;BFF 由各个界面团队负责。
常见错误
- 在网关里写业务逻辑。 改写和条件判断会逐渐长成没人能测试的规则。保持网关轻薄。
- 把网关当作唯一的安全检查。 网关后面的服务不应该仅仅因为请求到达了就信任它。零信任架构图展示了如何在每一跳上画出检查。
- 只按 IP 限流, 这会惩罚共用地址的用户,却放过不断更换地址的客户端。
- 每个实例使用本地计数器, 这会让限额乘以实例数量。
- 所有东西共用一个 BFF, 那又变回了一个通用后端。
- 没有细节的 429。 不知道该等多久的客户端只能猜,而且通常猜得太短。
关于网关背后的部分,请参阅微服务架构图。在 Kubernetes 上,Kubernetes Ingress 图介绍了经常位于网关之前、或替代网关部分职责的那一层。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加 C4 模型节点包中的 容器,Web 应用和移动应用各一个,再添加一个 外部系统 表示合作伙伴。
- 添加安全节点包中的 WAF 和 身份提供商。
- 添加云节点包中的 API 网关,以及数据节点包中的一个 Redis 节点,用于限流计数器。
- 添加基础节点包中的一个 框架,命名为 Private network,并在其中放入 C4 模型节点包中的 容器 节点,表示两个 BFF 和三个服务。
- 从左到右连接它们,为每条路由标注路径,并把登录和获取密钥画成虚线。再添加一个基础节点包中的 Markdown 块,写明网关策略。
这个示例共用 14 个图表节点,在 Free 套餐的限制之内。
常见问题
API 网关和负载均衡器是一回事吗? 不是。负载均衡器把请求分发到各个实例。网关理解 API 请求:路由、令牌和配额。很多网关都运行在负载均衡器之后。
有了 API 网关,还需要服务网格吗? 它们解决的是不同的问题。网关处理来自外部的流量;网格处理服务之间的调用,例如双向 TLS 和重试。如果两者都在用,就分开画。
网关已经校验过令牌,服务还需要再校验吗? 需要,针对服务所依赖的那些声明。网关会尽早拒绝无效令牌,而每个服务仍要自己对操作进行授权。
应该有几个 BFF? 每种用户体验一个。提供相同体验、由同一团队负责的两个应用可以共用一个。
429 响应应该包含什么?
一段简短说明,以及一个写明需要等待秒数的 Retry-After 头部,这样客户端会先退避,而不是立即重试。