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,每次变更走评审,避免某天有人手滑调错限流值而无人知晓。下一节解决最后一个问题:跨服务的请求,到底慢在哪一环。
