3.3 API 网关:鉴权、限流与路由收敛

场景:每个服务都要自己鉴权,迟早要乱

拆完服务的第一个月,前端同事找我们开会:原来一个页面只调一个接口,现在要同时调订单、商品、用户三个域的接口,每个都要带 token,鉴权逻辑在三个仓库里各写了一遍。更麻烦的是,有三处校验逻辑出了三个 bug:商品服务的 token 校验忘了校过期时间,被安全扫描揪出来。

同时客户端直连内网 gRPC 也不现实,移动端网络环境复杂,我们需要一个统一入口处理鉴权、限流、协议转换和路由。这就是 API 网关的定位,它的引入背景在 服务拆分边界 里有更细的讨论。

网关要替业务服务挡多少事

把网关职责列清楚,再决定哪些放网关、哪些留在服务内:

能力 放网关 放业务服务 说明
token 校验 是 否 统一入口,逻辑只写一遍
细粒度权限 否 是 比如能否看他人订单,业务相关性太强
接口限流 是 兜底 网关按维度限流,服务内做自我保护
协议转换 是 否 客户端 HTTP 进,内部 gRPC 出
请求日志 是 补充 网关记访问日志,业务记详细日志

我们的选型结论是直接用腾讯云 API 网关,不自建 Kong:五人团队没有精力维护一套高可用网关集群,云厂商的网关自带多可用区容灾。内部服务间调用则仍然走 gRPC 直连加 Consul,两条路互不影响。云产品的基本情况见 腾讯云架构实践。

网关选型我们也认真比较过自建和云厂商的方案:

方案 运维成本 可控性 故障定位 结论
自建 Kong 或 APISIX 高,要维护集群与插件 极高 自己搞定 团队规模不支持
Nginx 加 lua 脚本 中 中 需自行打通 上手快但扩展难
腾讯云 API 网关 低,控制台配置 中等 有访问日志 最终选中

我们并不需要极致的写性能,更在意健康检查和 gRPC 生态的契合度。同样地,网关选型我们还看中一点:运维交接成本低。创业团队人员流动不可预见,云控制台配出来的路由,比一套散落在多台机器上的自定义 lua 脚本好交接太多。

落地:从配置到内网打通

第一步在控制台创建服务,后端类型选 VPC 内,指向后端服务的 gRPC 端口;第二步配置路由,把 /api/v1/products 映射到 product-svc,/api/v1/orders 映射到 order-svc,收敛之后客户端只需要知道一个网关地址。这里有个前置动作:网关要与后端服务的 VPC 网络打通,安全组规则按最小权限放行网关子网到服务端口的访问,具体操作见 安全组与 VPC 配置。路由配置示意如下:

gateway:
  base_url: https://api.example.com
  routes:
    - path: /api/v1/products
      service: product-svc
    - path: /api/v1/orders
      service: order-svc
      auth: required

第三步开启鉴权,网关侧校验 JWT 的签名和过期时间,校验通过后把用户 id 塞进 header 传给后端,后端只信任这个 header,不再重复解析 token:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        uid := r.Header.Get("X-User-Id")
        if uid == "" {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithUser(r.Context(), uid)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

这套方案的要点在于信任边界只画一条线:网关之内默认可信,header 由网关写入,内网服务不再各自解析 token。想冒充用户只能先攻破网关,攻击面大幅收窄。网关自身的 token 密钥要定期轮换,我们用的是半个月一次的滚动密钥,放在配置中心而不是代码仓库里。

第四步配置限流。按接口维度分桶:商品列表 2000 QPS,下单接口 500 QPS,超出的直接返回 429。限流不是一刀切,我们分了三个层级来配:

限流维度 示例 目的
全局限流 全网关 5000 QPS 保护后端整体不被打穿
路径限流 下单 500 QPS 热点接口单独保护
用户级限流 单用户 10 次每秒 防刷单

网关后端的 CVM 我们买了三台做轮询,实例不够时扩容也方便,新购参考 腾讯云 CVM 新用户特惠。

事情凑在一起还有第四个变化:小程序、H5、APP 三端接进来之后,鉴权方式开始分裂——APP 用 JWT,小程序用平台登录态。网关这里加一层插件式的鉴权策略,不同 path 绑定不同策略,业务服务还是只认 header 里的用户身份。要没有网关,这事儿得在每个服务里各写一遍三端的校验逻辑,画面太美不敢想。

踩坑记录

第一个坑是限流值拍脑袋。上线时把下单接口限在 200 QPS,大促当天真实流量翻了两百倍,正常用户被大面积 429。复盘后学了乖:限流值按压测结果的三倍设,压测流程在 单体拆分前的评估 里。另外监控上要单独加一条 429 告警,限流命中的速率突然飙升,往往意味着真实流量超过预期,而不是接口变慢了。

第二个坑是 header 大小写。网关传 X-User-Id,Go 的 Header.Get 内部做了大小写不敏感处理所以没事,但有一次 PHP 写的老服务也接进来,那边严格匹配小写,排查了一晚上。最后统一约定所有自定义 header 用小写加连字符。

第三个坑是网关成了单点。云网关本身是集群,但我们自己的 DNS 解析只配了一个入口域名,好在腾讯云那边有多线路解析。真正出问题的一次是后端服务全部健康检查失败,网关把请求全转发给了错误页面。教训是网关要配默认错误页和降级策略。

第四个坑是调试看不到原始请求。网关会把 response 缓存一部分,排查偶发 502 时抓不到现场。后来开启了网关访问日志投递到 CLS,和 统一日志平台 的做法一致,才算有了排查依据。

第五个坑是 CORS 配置漏网。新上线的小程序 H5 页面跨域调网关,控制台报跨域错误,排查发现网关只放开了主域名的跨域规则,子域名没配。这类配置项建议在上线清单里固定一项专人核对,避免每次都靠前端同学反馈才发现。

第六个坑是后端超时与网关超时不匹配。网关默认超时 60 秒,而后端有个对账接口偶尔要跑 70 秒,于是出现一批状态码 504 的失败单。排查花了大半天,因为后端日志里这些请求显示成功。统一原则:网关超时必须大于后端任何接口的最大处理时间,慢接口例外清单要在网关上单独配。

收益与小结

上线三个月,网关带来的几处收益是看得见的:客户端不再直连三个域名,只需要记住一个入口; token 校验逻辑从三个仓库收敛到一处;新增服务只要在网关配一条路由就能对外,发版节奏明显轻了。客户端结构也稳定多了,网关把 /api/v1/orders 的协议从 HTTP 转成 gRPC 时客户端完全无感知,服务端拆分与协议升级得以平滑进行,参考 单体拆分的演进路径。

网关的本质是把横切关注点从业务代码里剥离出来:鉴权只写一遍,限流有统一出口。代价是多一跳延迟与一个需要爱护的新组件,好处是服务内部保持干净。网关自身的配置也要纳入版本管理:我们导出控制台配置存进 Git,每次变更走评审,避免某天有人手滑调错限流值而无人知晓。下一节解决最后一个问题:跨服务的请求,到底慢在哪一环。

延伸阅读