47.2 API 网关

一句话

API 网关是「把 API 治理从业务里剥离出来」的那一层——认证、配额、路由、版本、协议转换、开发者门户;它与反向代理的分界线不是技术,而是是否承担一套与业务无关、但每个 API 都必须有的治理职责。

与反向代理的分界

能力 反向代理(47.1 反向代理) API 网关 Service Mesh(东西向)
TLS 终结 / 压缩 / 缓冲 ✅ 主场 ✅ 通常不终结(mTLS 为主)
按 host/path 转发、负载均衡 ✅ 主场 ✅ ✅
认证 / 鉴权 一般只到 mTLS / IP 白名单 ✅ 主场(API key、OAuth2、JWT、scope) ✅(服务身份)
配额与计费 ❌ ✅ 主场(按 key/租户/路由) ❌
API 版本与生命周期 ❌ ✅(v1/v2 共存、废弃公告) ❌
协议转换 有限(HTTP↔HTTP) ✅(REST↔gRPC、WebSocket、SOAP) 有限
开发者门户 / 文档 / 沙箱 ❌ ✅ ❌
东西向服务间通信 不适合 不适合 ✅ 主场

判断标准只有一条:这一层要做的治理,是不是"每个 API 都要、但都不该写在业务里"? 是 → 网关;只是"转发与 TLS" → 反向代理就够了;只是"服务之间互调" → Service Mesh。

入口的四层/七层选型见 8.9 四层与七层负载均衡;入口与内部流量的边界见 47.5 南北向与东西向流量。

五个核心能力

能力 解决什么 没有会怎样 关键设计
认证(你是谁) 统一校验 API key / OAuth2 / JWT / mTLS 每个服务各写一套,密钥散落各处 认证结果向下传递(header/上下文),别让每个服务再解一次 token
授权(你能做什么) scope/角色/租户隔离 认证了但越权,或每个服务各写权限 粗粒度在网关(scope、路由级),细粒度在服务(资源级)
配额与限流 按 key/租户/路由的用量控制 单个客户打爆全站;无法做商业套餐 计数键要带业务含义(见 47.4 鉴权与限流应放在哪一层)
路由与版本 v1/v2 共存、灰度、路径重写 版本切换要停服;客户端绑死旧地址 路由规则与灰度主场在 47.3 路由与灰度
可观测与门户 按 key 的调用量/错误率、文档、沙箱 客户问"为什么 429"答不上来 日志/指标必须带 consumer、route、upstream 三个维度

🔑 网关的能力清单很容易越加越长,唯一该问的是"这条规则改一次要动几个服务":能在一个地方改、且对所有 API 生效的,才配放在网关。

控制面与数据面

控制面(配置):路由、插件、消费者、证书   ──下发──▶  数据面(网关实例,多副本)
     ▲                                                      │
     └──────── 运营/CI 声明式提交(GitOps、CRD、Admin API) ◀─┘
关注点 做法 反例
配置变更 声明式 + 版本化 + 可回滚;先灰度到 1 个实例 手改线上配置,改错全量生效
数据面扩容 无状态,按 QPS 水平扩;配置走共享存储 网关把配置存在本地磁盘,扩容后配置不一致
证书轮换 集中在控制面,热更新 每台机器手工换证书,漏一台就 502
消费者数据 独立存储,别塞进路由配置 10 万 API key 塞进一份 YAML,改一次全量重载

插件顺序:错了不会报错,只会错

请求 → ①认证 → ②配额/限流 → ③路由 → ④协议转换/改写 → ⑤日志/指标 → 上游
                                    └─ 响应 ← ⑤′ 日志 ← ④′ 转换 ← ③′ 路由 ← ②′ 计数 ← ①′
顺序错误 后果
先路由后认证 未认证请求也能探测内部路由(路径枚举);日志里出现"未知消费者"的海量垃圾
先限流后认证 只能按 IP 限(NAT 后连坐),无法按 key/租户限;恶意请求也能消耗配额
认证后不传递身份 下游服务拿不到 consumer,只能再解一次 token 或信任一个可伪造的 header
日志在限流前 被拒绝的请求不打日志 → 客户投诉"为什么 429"时无据可查
转换在限流前 不同协议/版本共享配额时口径不一致

容量与延迟:多一跳的真实成本

指标 量级(同机房、成熟实现) 说明
单实例吞吐 1 万–5 万 QPS 取决于插件数与 payload;带 JWT 验签会明显下降
额外延迟 +0.5–3 ms P99 每多一个远程插件(如远程鉴权)再加 1 RTT
连接数 网关 → 上游要连接池复用 不复用则每个请求一次 TCP/TLS 握手

算一笔账:入口 10 万 QPS,单实例 2 万 QPS → 至少 5 个实例;按 N+1 冗余配 6 个。若每次请求都做远程鉴权,鉴权服务要扛 10 万 QPS(成本见 47.4 鉴权与限流应放在哪一层);改成 JWT 本地验签后,鉴权服务的压力降到接近 0,只保留密钥轮换。

⚠️ 网关的容量不能只看 QPS:长连接(WebSocket/SSE)会把"并发连接数"变成第一约束。10 万条长连接在 6 个网关上,单实例 1.7 万连接,内存与 fd 才是瓶颈。

失效模式

失效 触发 现象 防线
网关变单点 副本数不足 / 同一可用区 网关挂 = 全站 502 多副本 + 多 AZ + 健康检查摘除
配置错误全量生效 一次提交影响所有路由 全站 404/401 配置灰度(先 1 实例)+ 自动回滚 + 语法校验
插件 bug 新版本插件、正则写错 全站 5xx 或 CPU 打满 插件版本与网关版本解耦;危险插件先灰度
超时级联 网关超时 > 上游超时 上游还在处理,网关已断开并重试 → 放大 网关超时 > 上游超时,重试要有预算(见 35.5 超时、重试与退避)
连接池耗尽 上游变慢,连接被占满 网关对新请求直接 503 池上限 + 快速失败 + 熔断(见 35.6 熔断与降级)
配额存储故障 限流用 Redis,Redis 挂了 全局限流失效(或被误判为全拒) 降级策略要显式:宁可放行(保可用)还是全拒(保安全)

什么时候不该上网关

你的情况 选择
只有 1–3 个服务、调用方是自己人 反向代理 + 服务内鉴权(47.1 反向代理)
纯内部服务互调,没有外部 API Service Mesh(mTLS + 流量治理)
需要"给不同客户看不同 API 组合" BFF(按端聚合)而不是网关
只是要统一加 header / 改路径 反向代理或 sidecar,不值得多一跳
网关里开始写业务逻辑(聚合、计算) 停手:这是 ESB 复活,复杂度会指数上升

APISIX 对照:一条路由的治理链

# APISIX 对照配置(未在本机验证;字段以 APISIX 3.x 为准)
routes:
  - name: order-api
    uri: /api/order/*
    plugins:
      jwt-auth: {}            # ① 认证:校验签名与过期,注入 consumer
      limit-count:            # ② 配额:按消费者计,60 秒 1000 次
        count: 1000
        time_window: 60
        key_type: var
        key: consumer_name
        rejected_code: 429
      proxy-rewrite:          # ③ 路由/改写:去掉 /api 前缀
        regex_uri: ["^/api/(.*)", "/$1"]
    upstream:
      type: roundrobin
      nodes:
        "order:8080": 1

这份 APISIX 配置未在本机验证,只用于对照插件字段;本节可验证的落地是下面的 Envoy 配置。

三条能直接对照上文的细节:jwt-auth 在 limit-count 之前(先认人再计数);key: consumer_name 用的是认证注入的变量(不是 IP);rejected_code: 429 而不是 500(见 35.7 限流 (漏桶 / 令牌桶 / 滑动窗口 / 自适应) 里"拒绝也要有礼貌")。

具体用什么:五个中间件,一份 Envoy 配置

“API 网关”不是自己写一个:生产上就是下面几类中间件,差别在控制面形态、插件生态、部署成本。

中间件 形态 认证 / 配额插件 配置入口 适合
Apache APISIX etcd 或 standalone(apisix.yaml) jwt-auth、key-auth、limit-count、limit-req、traffic-split Admin API / 声明式文件 / Ingress 国内生态、插件多、需要动态路由
Kong DB-less(kong.yml)或 Postgres key-auth、jwt、rate-limiting、request-transformer、prometheus deck / Admin API / Kong Ingress 插件生态成熟、团队熟悉 Lua
Envoy Gateway K8s Gateway API + Envoy 数据面 SecurityPolicy、RateLimitPolicy、BackendTrafficPolicy CRD(GitOps) 已在 K8s、想要声明式与多租户
Higress Istio + Envoy Wasm 插件、认证/限流策略 Ingress / Gateway API / 控制台 阿里生态、需要 Wasm 扩展
云 API 网关 托管 内置 Key/配额/WAF/日志 控制台 / Terraform 不想自建控制面与高可用

Envoy 是这些网关共同的数据面。下面是它最小可用的一份“认证 + 单实例限流 + 路由”配置,envoy --mode validate 校验通过、实跑无 token 返回 401:

static_resources:
  listeners:
    - name: ingress
      address:
        socket_address: { address: 0.0.0.0, port_value: 10000 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                route_config:
                  name: api_routes
                  virtual_hosts:
                    - name: api
                      domains: ["api.example.com"]
                      routes:
                        - match: { prefix: "/api/order" }
                          route: { cluster: order_api, timeout: 2s }
                http_filters:
                  - name: envoy.filters.http.jwt_authn
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
                      providers:
                        idp:
                          issuer: "https://issuer.example.com"
                          audiences: ["order-api"]
                          remote_jwks:
                            http_uri:
                              uri: "https://issuer.example.com/.well-known/jwks.json"
                              cluster: jwks
                              timeout: 1s
                            cache_duration: { seconds: 300 }
                      rules:
                        - match: { prefix: "/api/order" }
                          requires: { provider_name: idp }
                  - name: envoy.filters.http.local_ratelimit
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
                      stat_prefix: http_local_rate_limiter
                      token_bucket: { max_tokens: 100, tokens_per_fill: 100, fill_interval: 1s }
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
    - name: order_api
      type: STRICT_DNS
      load_assignment:
        cluster_name: order_api
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address: { address: 127.0.0.1, port_value: 8080 }
    - name: jwks
      type: STRICT_DNS
      load_assignment:
        cluster_name: jwks
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address: { address: 127.0.0.1, port_value: 8081 }

三个映射关系:jwt_authn = 认证(验签、aud、过期),local_ratelimit = 单实例限流(跨实例的全局配额要接 RLS/Redis,见 35.7 限流),route = 路由与超时。

envoy --mode validate -c envoy.yaml
# configuration '/etc/envoy/envoy.yaml' OK(Envoy 1.40.1 实测)

curl -i -H 'Host: api.example.com' http://127.0.0.1:10000/api/order
# HTTP/1.1 401 Unauthorized
# www-authenticate: Bearer realm="http://api.example.com/api/order"
curl -s localhost:9901/stats | grep -E 'http.ingress_http.(downstream_rq_401|downstream_rq_429)'

一句话选型:要开箱的 API 治理(key/配额/门户)选 APISIX 或 Kong;已经在 K8s 且想 GitOps 选 Envoy Gateway;只想在 Envoy 上做认证与限流,就用上面这份配置,控制面自己管。

上线:配置怎么发、密钥怎么换、配额挂了怎么办

动作 做法 判据 / 坑
配置发布 声明式提交 → schema 校验 → 先上 1 个实例 → 观察 10 分钟 → 全量 401/404/429 比例不跳变;“全量生效”是网关事故第一名
配置回滚 保留上一版,回滚 = 提交上一版 + 同样灰度 5 分钟内恢复;前提是配置在 Git 里
密钥轮换 新 key 先发、旧 key 保留 ≥ 1 个 token TTL,双 key 重叠 轮换期 401 不上升;直接替换 = 存量客户端全部掉线
消费者吊销 删 consumer + 等缓存过期(或主动清缓存) 只删配置不清缓存,吊销延迟 = 缓存 TTL
配额存储故障 显式选 fail-open(保可用)或 fail-closed(保安全),按路由分级 不选,就是让实现替你选;不同插件行为并不一致
上游变更 走服务发现 / EDS,不手改 nodes 扩容不应该需要改网关配置
# 四种结果各打一次:401 / 403 / 200 / 429
curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/api/order/1
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $LOW_SCOPE" https://api.example.com/api/order/1
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN"     https://api.example.com/api/order/1
for i in $(seq 1 1001); do curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" https://api.example.com/api/order/1; done | sort | uniq -c

# 429 必须带 Retry-After,否则客户端立刻重试,限流反而成了加速器
curl -si -H "Authorization: Bearer $TOKEN" https://api.example.com/api/order/1 | grep -i '^retry-after'

# 故障演练:把配额用的 Redis 停掉,看是放行还是全拒——这个行为必须是你选过的

🔑 网关配置是生产资产,不是部署产物:没有灰度、没有回滚、没有故障演练的网关,就是一个能一次改挂全站的单点。

反模式

反模式 后果
把网关做成 ESB:编排、聚合、转换全塞进去 每次改动都要动网关,发布耦合所有团队
只在网关鉴权,内网信任一切 内网横向移动即可绕过;零信任下不可接受(见 38.8 零信任)
网关里存业务状态(购物车、会话) 网关变成有状态服务,扩容/故障切换全乱
用网关做服务发现(硬编码上游列表) 上游扩缩容要改网关配置
认证结果用可伪造的 header 传给下游 下游信任 header → 任意伪造身份
所有 API 共用一套超时/重试 快接口被慢接口拖死,重试放大
配额计数放在网关内存 多副本各自计数 → 实际配额 = 配置值 × 副本数
配置改动全量直接生效 一条错误路由 = 全站 401/404
密钥一刀切换掉 旧 key 立刻失效,存量客户端全量 401
配额存储故障没演练 Redis 挂时行为未知:要么全拒,要么配额失效

自检

Q1 反向代理和 API 网关的分界线在哪?举一个"该用反向代理、不该用网关"的例子。

▸ 答案:分界线是"是否承担每个 API 都需要、但不该写在业务里的治理"(认证、配额、版本、协议转换、开发者门户)。若只是 TLS 终结 + 按路径转发 + 负载均衡,反向代理足够:例如一个内部管理后台,3 个服务、调用方全是公司员工,加网关只会多一跳、多一套配置要维护,收益为零。

Q2 为什么"先路由后认证"是危险的?

▸ 答案:未认证的请求也能打到路由匹配逻辑,攻击者可以枚举路径、探测哪些内部路由存在(404/401/403 的差异就是信息泄漏);同时日志里会出现大量"未知消费者"的噪声,掩盖真实攻击。正确顺序是认证 → 配额 → 路由:不认人就不进入路由判断。

Q3 入口 10 万 QPS,网关单实例 2 万 QPS,为什么要配 6 个而不是 5 个?

▸ 答案:5 个是"刚好扛住"的数学下限,任何一个实例故障或滚动发布,剩余 4 个就要扛 10 万(每个 2.5 万),超过单实例容量 → 雪崩。N+1 冗余(6 个)保证单实例故障时仍有 5 个在工作。若做多 AZ,还要保证"挂一个 AZ 后剩余容量 ≥ 峰值"。

Q4 网关的配额计数放在实例内存里,会有什么问题?怎么改?

▸ 答案:每个副本各自计数,实际放行量 = 配置值 × 副本数(6 个副本、1000/分钟 → 实际 6000/分钟),配额形同虚设;扩容时配额还会突然变大。改法:集中计数(Redis/专用配额服务)+ 本地预扣(批量领令牌,减少远程调用),并接受"预扣未用完"的少量浪费。

Q5 网关超时应该比上游超时更长还是更短?为什么?

▸ 答案:更长。上游先超时并返回(或快速失败),网关才能拿到明确结果;如果网关先超时,它已经断开连接、可能触发重试,而上游还在继续处理——同一请求被处理两次,重试还会放大下游压力。经验规则:网关超时 = 上游超时 + 余量(含网络与排队),重试只在"连接未建立/明确可重试"时做。

Q6 密钥轮换为什么要“双 key 重叠”?重叠窗口至少要多久?

▸ 答案:客户端手里的旧 key 数量不可控,替换是渐进的:新旧 key 必须同时有效,直到旧 key 的存量客户端全部换完。窗口 ≥ max(token TTL, 客户端缓存/发布周期) + 观测时间;窗口内盯 401 比例,降到 0 再下线旧 key。只保留新 key,等于在轮换瞬间把所有没升级的客户端踢下线。

关联