最近我在生产环境里做了一次网关治理问题很典型外部合作伙伴通过 API 访问订单和库存数据某天有个大客户搞了个定时任务每分钟打进来上万请求直接把后端服务 CPU 打满其他客户全部跟着遭殃。第一反应是上限流但如果只是按 IP 粗暴限制又很可能把正常客户一起误伤甚至把 VIP 客户的重要交易给切断。最终落地的一套方案是把 Kong 网关上的 OAuth2 认证和 Rate Limiting 插件组合起来先用 OAuth 识别“谁在调用”再基于这个身份做分级限流同时设计了一套自定义的分层降级策略保证系统过载时不是一刀切返回 429而是按客户重要程度、业务类型逐步降级。这套方案上线后效果比较理想今天把从选型到配置、再到踩坑的完整过程写出来给同样在做 API 网关保护的朋友一个参考。1. 这个案例解决的痛点1.1 传统 IP 限流为什么不够用很多刚接触网关限流的人第一个想到的就是按 IP 限流。IP 限流确实简单Kong 里创建一条路由挂上 rate-limiting 插件limit_byip填一个每分钟次数基本就能工作。但放到真实业务里IP 限流有非常明显的短板NAT 出口导致误伤。很多企业内部出口是同一个公网 IP几十号人共享一个地址限 100 次/分钟一个人刷几遍就触发全公司跟着被限制。恶意用户换 IP 成本极低。爬虫或者攻击者通过代理池轮换 IP按 IP 限流基本等于没限。分不清客户优先级。IP 限流眼里所有请求都一样VIP 客户和免费客户共用一套阈值后端一旦紧张VIP 也会被限这是业务上很难接受的。无法做配额管理。很多对外开放平台需要给每个合作方分配独立的调用配额比如 A 公司 500 次/分钟B 公司 100 次/分钟IP 维度完全做不到。只要 API 面向多租户、多合作伙伴限流维度就一定要从“网络层”上升到“身份层”。也就是说限流的 key 应该是调用者身份。1.2 OAuth 在限流链路中的角色Kong 网关本身提供了多种认证插件比如 key-auth、basic-auth、jwt、oauth2。这个案例里我选择 OAuth2并不是因为它配置起来最简单而是因为对外部合作伙伴开放 API 时OAuth2 有不可替代的优势凭证可以独立管理、吊销。每个合作伙伴分配独立的 client_id 和 client_secret某个客户异常时不用改代码直接吊销凭证。支持 scope 权限范围。可以给不同的客户端分配不同的 scope比如只读订单、可写订单、只读库存。鉴权范围天然可以映射到限流级别。令牌有生命周期。access token 会过期必须定期刷新这天然形成了一个验证客户端的周期。标准协议合作方接入成本低。对方只要按照标准 OAuth2 客户端模式开发就能快速对接。在限流这个场景里OAuth 最大的贡献是提供了一个稳定的调用者身份标识。Kong 会把消费者信息注入到请求头中比如X-Consumer-ID、X-Consumer-Username、X-Consumer-Custom-IDRate Limiting 插件则可以直接基于这个消费者来做计数。提示身份标识一定要在认证成功之后获取。如果请求连 OAuth 都过不了根本不会进入限流环节所以限流插件一般挂在认证插件之后执行这条链路是 Kong 约定好的。1.3 为什么还要引入分层降级策略限流只是保护手段的第一层。限流触发之后怎么处理才是体验好坏的关键。传统做法是直接返回 429 Too Many Requests客户端收到后要么死等要么重试重试又加剧了拥堵。我这里的“分层降级”核心思路是当系统过载时不是把请求直接掐死而是按照不同维度、不同优先级执行阶梯式的保护策略。具体到这次项目我把降级划分成了四个层级层级触发条件处理方式L0网关整体负载过高或后端响应超时率超标全局限流收紧普通客户端降级到只读缓存L1单个消费者超过软限制返回 429同时在响应头里附带 Retry-After降低客户端重试频率L2单个消费者超过硬限制请求直接拒绝并返回结构化错误码提示客户端需要升级配额或稍后重试L3Redis 或认证服务不可用自动切换到本地限流策略降级为按 IP 的粗粒度保护这样处理后系统压力大时普通用户先被限流VIP 用户仍能保持较高吞吐后端非常紧张时所有用户都被限制但响应仍然是友好的、可理解的客户端可以根据错误码做合理的退避而不是盲目重试。2. OAuth 认证与 Rate Limiting 的协作设计2.1 整体链路是怎么走的整个请求链路是这样的客户端先向 Kong 的/oauth2/token端点发起请求携带 client_id 和 client_secret换取 access token。客户端拿着 access token 调用业务 API比如GET /v1/orders。Kong 先执行 oauth2 插件校验 token 是否有效、scope 是否匹配。认证通过后rate-limiting 插件开始执行基于消费者维度检查调用次数。未超限则转发到上游服务超限则按照降级策略返回 429 或者执行其他兜底逻辑。OAuth 是防线Rate Limiting 是堤坝降级策略是泄洪道。三者缺一不可。2.2 消费者模型怎么设计这是整个方案最关键的建模环节。Kong 里有几个概念Consumer消费者、OAuth2 Credential客户端凭证、Service服务、Route路由。标准做法是每个接入的合作伙伴对应一个 ConsumerConsumer 上可以打 tag比如tiervipapporder-platform。每个 Consumer 下挂一个或多个 OAuth2 Credential对应合作伙伴的实际应用。限流插件绑在 Consumer 上或 Consumer 对应的凭证维度上实现区分客户端粒度的配额。我这次给不同合作伙伴分了三个档位黄金合作伙伴500 次/分钟面向核心交易场景允许突发的峰值 800 次/分钟。白银合作伙伴200 次/分钟面向日常查询场景。测试客户20 次/分钟只用于联调环境。这些档位不是写死在代码里的而是通过 Kong 的 Admin API 动态调整。客户签了新的合同、临时需要扩容直接调接口改一下消费者插件配置即可。2.3 Rate Limiting 的策略和维度选择Kong 的 rate-limiting 插件支持多种策略维度维度适用场景说明consumer按消费者账号限流同一消费者的多个凭证共享一个额度credential按客户端凭证限流每个 OAuth client 单独计数互不影响ip按来源 IP 限流只能做兜底不能作为主策略header按自定义请求头限流网关下发固定值比如后端服务名真正的生产环境一般需要组合使用。我在这套方案里的推荐做法是主策略按消费者限流这样合作伙伴的所有应用共享总配额符合商业合同约定。如果某些客户内部有多个系统需要分别分配额度那就再叠加一层按凭证的限流但要注意避免同一维度同时多次配置否则计数会互相干扰。2.4 存储策略怎么选Rate Limiting 插件有几种存储策略local存储在单个 Kong 节点内存中性能高但不共享。多节点部署时每个节点各计各的限额会变成节点数倍。cluster旧版本中通过数据库共享计数器性能较差仅限于单数据库场景。redis所有 Kong 节点共享同一个 Redis 计数性能好、一致性高是目前生产环境的标准选择。我强烈建议使用 Redis 策略。Kong 的限流计数是滑动窗口Redis 支持 Lua 脚本原子执行窗口计数在并发环境下不会出现超卖问题。实际压测中单 Redis 节点支撑几万 QPS 的计数查询是完全没问题的。3. Kong 网关上的完整配置过程3.1 前置环境准备先说明一下我这边的基础环境Kong Gateway 3.4 版本部署了三个节点做负载均衡前面挂了 NLB。Redis 6.2单独的两个节点做哨兵模式专门留给限流计数使用。所有配置通过 Kong Admin API 进行操作生产环境建议用声明式配置文件同步方便版本管理。安装步骤不是重点官方文档很详细我略过。需要注意的是 Redis 数据库不要和业务缓存混用我单独分配了数据库 2 给限流插件这样清理和观测都方便。3.2 创建服务和路由先创建一个订单查询服务上游指向后端服务。# 创建 service curl -X POST http://127.0.0.1:8001/services \ -H Content-Type: application/json \ -d { name: order-api, url: http://order-service.internal:8080 } # 创建 route对外暴露 /v1/orders 路径 curl -X POST http://127.0.0.1:8001/services/order-api/routes \ -H Content-Type: application/json \ -d { name: orders-route, paths: [/v1/orders], strip_path: false, protocols: [https] }这里的strip_path要按你后端接口的实际路径来定。我的后端接口本身就包含/v1/orders所以不剥路径。3.3 配置 OAuth2 插件给这个服务挂上 oauth2 插件。我这里选用的是client_credentials模式也就是客户端凭证模式适合服务器到服务器之间的调用。curl -X POST http://127.0.0.1:8001/services/order-api/plugins \ -H Content-Type: application/json \ -d { name: oauth2, config: { scopes: [read_orders, write_orders], mandatory_scope: true, enable_client_credentials: true, enable_authorization_code: false, enable_password_grant: false, enable_implicit_grant: false, provision_key: a86f62f8-3a2c-4a32-9f10-4d1f7d15b9a3, token_expiration: 7200, refresh_token_ttl: 86400, global_credentials: true } }几个参数说明一下mandatory_scopetrue客户端请求 token 时必须携带 scope避免拿到一个万能 token。global_credentialstrueOAuth2 凭证全局可见不限制只能访问某个 service 或 route。token_expiration7200access token 有效期 2 小时。provision_keyKong 内置的一个随机密钥用于某些流程下直接签发 token要保管好。3.4 创建消费者和 OAuth 客户端创建黄金合作伙伴消费者curl -X POST http://127.0.0.1:8001/consumers \ -H Content-Type: application/json \ -d { username: partner-gold, custom_id: P-10001, tags: [tier:gold, partner:acme] }给这个消费者创建 OAuth 客户端凭证curl -X POST http://127.0.0.1:8001/consumers/partner-gold/oauth2 \ -H Content-Type: application/json \ -d { name: acme-order-system, client_id: acme-order-client, client_secret: acme-order-secret-2024, redirect_uris: [], scopes: [read_orders, write_orders] }这里需要特别注意OAuth 客户端创建后Kong 会自动生成一个client_id。如果你也像我一样手动指定就一定要保证全局唯一否则后面换取 token 时会出现 401。3.5 给消费者配置独立的 Rate Limiting 插件关键步骤来了。为了让不同合作伙伴拥有不同配额Rate Limiting 插件必须挂在消费者维度而不是全局服务维度。给黄金合作伙伴配置 500 次/分钟curl -X POST http://127.0.0.1:8001/consumers/partner-gold/plugins \ -H Content-Type: application/json \ -d { name: rate-limiting, config: { minute: 500, policy: redis, limit_by: consumer, redis_host: 10.0.0.10, redis_port: 6379, redis_ssl: true, redis_database: 2, fault_tolerant: true, hide_client_headers: false } }给普通测试客户配置 20 次/分钟操作完全一样只需要替换消费者标识和minute参数。这种按消费者绑定插件的做法调整配额时不需要改路由直接改对应消费者的插件配置即可。实际操作里我可以直接通过 Kong Manager 面板操作也可以写一个脚本批量处理。3.6 配置全局兜底限流除了按消费者限流我还加了一道全局的硬性防线在每个入口 Service 上挂一个按 IP 维度的限流插件限额可以放宽一些比如 2000 次/分钟。这样即使某个消费者被恶意刷量、绕过了身份维度IP 维度的兜底仍然能挡住流量洪峰。curl -X POST http://127.0.0.1:8001/services/order-api/plugins \ -H Content-Type: application/json \ -d { name: rate-limiting, config: { minute: 2000, policy: redis, limit_by: ip, redis_host: 10.0.0.10, redis_port: 6379, redis_ssl: true, redis_database: 2 } }这里要特别提醒实际使用中不要给同一个 Service 上既挂按 consumer 限流的插件又挂多个其他维度的 rate-limiting 插件除非你非常清楚它们的执行顺序和计数叠加效果。Kong 允许一个请求经过多个 rate limiting 实例但多个限流器同时生效会各自计数复杂度过高时很难定位问题。我们的做法是消费者维度放在消费者上IP 兜底维度放在 Service 上两层之间不互相冲突。3.7 验证整体链路是否打通配置完成后先用 curl 验证一遍# 1. 获取 access token curl -X POST https://kong.example.com/oauth2/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeclient_credentialsclient_idacme-order-clientclient_secretacme-order-secret-2024scoperead_orders # 2. 携带 token 访问业务 API curl -X GET https://kong.example.com/v1/orders/10001 \ -H Authorization: Bearer access_token第一次请求能正常返回说明认证链路通了。然后连续请求超过 500 次第 501 次就会收到 429。Kong 默认的 429 响应体是一个 JSONKong JSON{ message: API rate limit exceeded }同时在响应头里能看到限流信息X-RateLimit-Limit-Minute: 500 X-RateLimit-Remaining-Minute: 0这一步说明限流插件已经生效而且计数维度确实是当前消费者。4. 自定义分层降级策略的落地4.1 降级策略的总体原则限流插件本身只负责“阻挡”它不关心被挡下来的业务是否重要、客户端能否优雅处理。所以我在网关前面又做了一层自定义的降级控制核心原则是核心链路优先保障。比如订单创建、支付回调这些操作VIP 消费者永远有最高的配额。非核心链路先降级。查询、报表、导出这些可以进行本地缓存或者允许延迟的操作配额更低。降级不是把用户踢出去而是让用户在受限情况下依然能完成一部分操作只是数据可能是旧的或者是异步的。这层策略放在哪里实现我实践下来最稳妥的方式是网关负责识别和转发降级决策由网关里面的一层“策略服务”来下发。策略服务维护了一张动态规则表Kong 通过自定义插件或直连 Redis 查询这张表决定当前这个消费者是走正常链路、降级链路还是直接拒绝。4.2 分层配额在网关层面的映射我最终把配额分为了四个维度维度配置位置配额粒度说明全局维度Service 插件每 IP 每分钟 2000防单点洪峰消费者维度Consumer 插件黄金 500/分、白银 200/分主配额接口维度Route 插件查询接口 5000/分交易接口 300/分保护关键接口异常维度策略服务动态调整后端故障时自动调低接口维度的限流是额外加的。同样一个消费者调用查询接口和交易接口重要性完全不同。我使用路径前缀进行区分为/v1/orders/query和/v1/orders/create分别创建 Route再针对交易接口单独配置更严格的限流。4.3 响应头里的降级信号限流不能只给一个 429 就完了客户端需要知道“我什么时候可以重试”网关端要通过响应头把这个信息明确传出去。Kong 的 rate-limiting 插件默认会在响应头里携带X-RateLimit-Limit-MinuteX-RateLimit-Remaining-Minute但对于降级策略来说我还希望额外输出两个自定义信号X-Downgrade: quota_exceeded或者system_overloaded让客户端明白是自身超限还是网关整体过载。X-Downgrade-Level: L1/L2/L3告诉客户端当前被降级到了哪一层客户端可以根据这个层级的语义做出不同反应。这些自定义头我通过 Kong 的response-transformer插件配合前置策略服务来实现。当策略服务判断某个消费者的配额触发时会在 Redis 中写入一个标记请求进网关读到标记后response-transformer 就把对应的响应头加上。4.4 后端故障时的主动降级除了请求量超限后端服务不可用也是常见的故障场景。Kong 本身有健康检查和负载均衡但健康检查的粒度最多到上游节点做不到“某个客户的交易请求优先进入健康节点普通查询请求直接返回旧数据”这种级别。我的做法是在策略服务里维护每个后端接口的“健康分”健康分由请求成功率、平均响应时间、错误率实时计算。当订单服务的错误率超过 5% 时健康分下降策略服务自动把白银和测试客户的配额下调一半同时把这些客户的部分查询请求转发到一个只读缓存节点而黄金客户的交易请求不受影响。Kong 侧通过自定义 Nginx 指令和 upstream 的备用节点实现了这个分流。当然这部分属于较高级的玩法如果你用的是 Kong Gateway Enterprise有更成熟的主动健康检查和流量控制能力开源版则可以借助外部策略服务配合request-transformer对请求头打标然后在 Kong 的不同 upstream 之间做转发。4.5 自定义 429 响应的业务包装默认的 429 返回体信息太少客户端不知道是自己超限了还是系统过载了也不知道该不该做重试。我通过自定义错误处理把 429 的返回体改成结构化格式{ code: 1001, message: rate_limit_exceeded, detail: 当前账号每分钟调用限额 200 次已用完, retry_after_seconds: 38, request_id: 7f2a9c3e-5a2b-4f6d-8e1c-0a8f7c2a91e3 }需要说明的是Kong OSS 默认的 rate-limiting 返回体很难直接改结构除非你修改 Kong 的模板。我这边是在网关前面再挂了一层封装逻辑或者通过自定义插件处理 429。如果你的团队有能力维护一个小型自定义插件这其实是最灵活的路径。自定义插件可以在超额时直接控制返回体、添加响应头、记录审计日志。4.6 怎么保证降级策略本身的高可用降级策略服务看起来虽然不复杂但它本身不能成为新的单点。如果策略服务挂了网关必须有兜底动作。我的经验是给 Kong 配置了一个本地降级规则当 Kong 从 Redis 读取策略服务下发的配额表失败时自动回退到默认配置所有消费者 50 次/分钟。这个 50 次/分钟的额度足以保证核心客户能继续使用又不会把后端压垮。L3 降级一定不要依赖那些已经不可用的组件否则就是连环雪崩。要保证降级规则本身不受故障组件影响所以默认值要写在 Kong 的本地配置文件里而不是存到 Redis。5. 高并发踩坑与排查实录5.1 多节点限流计数不一致这套系统部署了三台 Kong 节点上线第一天就发现一个问题单台节点压测时限流是准的但三台节点轮流承受请求时客户端实际的可用次数明显超过了配置值。排查后发现原因很直接。一开始有个服务用policylocal配置了限流本地策略的计数器存在各个节点的内存里三个节点各有各的计数500 次/分钟的限额实际变成了 1500 次。后来把所有生产环境的限流插件统一改成policyredis后现象消失。注意local策略只适合单节点部署或者临时调试。只要你的 Kong 是多节点就一定要用 Redis不要有侥幸心理。5.2 限流 Redis 的时钟问题限流计数放在 Redis 后有个比较隐蔽的问题是客户端与 Redis 之间的时间偏差。Kong 限流窗口是按秒计算的滑动窗口如果某个 Kong 节点的系统时钟和 Redis 服务器差了几秒会导致窗口边界不一致表现是偶尔提前触发限流、偶尔延迟触发。处理方式是部署 NTP 时间同步服务并且把 Kong 和 Redis 都指向同一个 NTP 源。这个问题在节点不多时不容易发现一旦扩容到多个可用区时间漂移就会明显提前做好时间同步很重要。5.3 OAuth token 过期引发误限流有个客户反馈明明配额没有用完但请求偶尔会返回 429。最初以为是限流规则出错后来看审计日志发现这个 429 不是 rate-limiting 插件返回的而是 oauth2 插件返回的 token 过期响应状态码也是 429。这里引出一个容易混淆的点Kong 的 oauth2 插件在 token 无效时会返回 401但某些配置下返回的是 403 或 429具体取决于你的版本和配置。生产环境测试时一定要把认证失败和限流触发的状态码区分开不要一看到 429 就怀疑限流配置。5.4 客户端盲目重试加剧限流限流触发后客户端如果没有正确处理Retry-After头会立即重试而重试请求同样会被限流形成一个死循环最后客户以为接口挂了。客户端必须实现对 429 的合理安排。我一般建议客户端采用指数退避第一次重试延迟 1 秒第二次 2 秒第三次 4 秒上限 30 秒。网关端Retry-After头就是给客户端用的别忽略它。5.5 配额调整没有立即生效用 Admin API 调整消费者配额后我发现部分节点还在使用旧配额。原因是 rate-limiting 插件配置被 Kong 节点缓存了调整后需要一点时间才能在所有节点间同步。正常情况下几秒内会生效但如果多个节点同时更新配置可能出现不一致。最稳妥的办法是调整配额后通过 Admin API 检查所有节点的插件配置是否一致或者直接重启加载最新配置。6. 复盘这套方案的适用边界和后续扩展这套“OAuth Rate Limiting 分层降级”的组合在对外提供 API、多合作伙伴共用网关的场景下效果非常明显。它能精准识别每个消费者的身份、配额、优先级也能在系统过载时有序地决定“先保护谁、先牺牲谁”。不过也要说清楚边界。如果只是内部系统调用或者接入方很少上 OAuth 的成本可能高于收益这时候用 key-auth 加简单的按消费者限流就够了。如果业务量级非常大比如每秒数万请求网关层的 Lua 限流可能会成为瓶颈这时候应该把限流下沉到 Redis 的令牌桶或者后端服务自身。我个人在实际运维中的体会是限流配置不是一次性的事而是要持续调整。上线后我会持续观察每个消费者的实际调用曲线发现某家客户长期用不满配额可以适当地调低一点把资源留给真正有需求的客户发现某条接口频繁触发限流就要分析是配额不够还是业务异常。Kong Admin API 可以很方便地查询每个消费者的插件配置和使用情况结合监控面板能做到比较精细的容量管理。这次项目里还有一个我最后才补上的细节限流日志的主动采集。Kong 默认不会记录每次限流触发的详细日志所以我在插件里加了一层日志输出把请求路径、消费者 ID、触发维度、剩余配额全部记录下来推送到日志平台。这样每次限流发生后都能回溯是哪个客户、哪个接口、被哪一层策略拦下来的。没有这块数据降级策略后期的优化基本靠猜。最后再分享一个小技巧如果你刚开始做类似改造不要一上来就把降级策略写得特别复杂。先把 OAuth 认证和基本的消费者限流配好让 429 能正确返回再逐步加入软硬配额、动态降级、自定义响应。每增加一个环节就压测一次不然多个问题混在一起定位会非常痛苦。分层保护的思路是好的但落地时一定要一层一层来。