
开篇一次让我排查到半夜的限流事故先说个真实场景。去年我给一套部署在Kubernetes里的Kong网关集群加限流用的是官方文档里最常见的 rate-limiting 插件配置。配完后本地测试一切正常单节点压测、不同消费者维度限流、每分钟60次的阈值全部按预期触发429。结果一上生产问题立刻来了——两个Pod轮询负载明明同一分钟内总请求量只有80次前端却收到了大量429而部分时段甚至出现了限流失效冲到120多次才被拦下来。当时的第一反应是插件没生效检查了配置、路由绑定、消费者关联都没有问题。后来把Kong日志打开才看到每个节点都在各自计数压根没有共享状态。说白了就是我在多节点集群里用了默认的 local 策略而 rate-limiting 的 local 策略每个Worker进程各自维护一份计数器互不相通。这个问题表面上是配置失误背后其实是绝大多数人都会踩的坑local 和 redis 两种策略原理完全不同适用场景完全不同选错了在单机环境下根本看不出来一上集群就原形毕露。这篇东西我打算把 rate-limiting 插件这两种策略完完整整讲透包括计数原理、键结构、多节点行为差异、Redis故障时的表现以及我在生产环境里排查和修复这个问题的完整思路。适合正在用Kong、准备给API加限流或者已经在集群上遇到限流不准、时灵时不灵问题的读者。1. rate-limiting插件两种策略的本质差异计数器“住”在哪里先看清楚 rate-limiting 插件最核心的一个概念——限流必须有一个“计数器”。无论你用哪种策略插件做的工作本质上是同一件事判断当前时间窗口内某个维度IP、Consumer、Credential等的请求量是否已达阈值。但计数器存在哪里、如何读写、如何同步直接决定了两种策略的差别。1.1 local策略节点内存里的本地计数器local策略的计数器直接存在Kong节点自身的内存中更准确地说是存在每个 nginx worker 进程的共享内存段里。Kong基于OpenResty每个worker进程是独立的OS进程它们之间通过 lua_shared_dict 这个nginx共享内存区域来读写计数器默认的共享内存名叫 kong_rate_limiting_counters。这里有个很多教程不会提的细节即使是在同一个Kong节点上如果配置了多个worker进程local策略在多个worker之间也是靠共享内存协调的。也就是说单节点多worker的情况下计数是准确的。但一旦你有两个或以上的Kong节点不管是多Pod、多VM还是多容器问题就来了——每个节点都有自己的共享内存节点之间完全没有同步机制。所以local策略的准确定义应该是以单个Kong节点为边界的、本地的、进程内计数器。它是单机限流方案不是分布式限流方案。1.2 redis策略外部存储里的全局计数器redis策略把计数器存在独立的Redis实例中。每个Kong节点在处理请求时都会通过 Redis 的原子操作比如 INCRBY 配合过期时间或者Lua脚本实现的滑动窗口对同一个Redis key做读写。这样一来所有Kong节点共享同一个计数状态天然就是全局准确的。因为Redis操作是原子的所以并发请求也不会出现“同时读到同一个计数再加一”的竞态问题。这一点是Redis策略作为分布式限流方案的核心基础。1.3 两种策略的优缺点推导把计数器所在的位置想清楚两种策略的优缺点就不需要死记了可以推导出来维度localredis数据位置节点内存独立Redis实例多节点同步无共享同一份状态性能开销极低纯内存操作每次请求一次RTT有网络开销依赖外部组件无需强依赖Redis可用性故障影响只影响单个节点Redis故障可能导致限流失效或拒绝请求典型场景开发环境、单节点、允许粗略限流生产环境、多节点集群、精确限流简单总结local是“快但各说各话”redis是“慢一点但口径统一”。后面我会逐个说明它们在真实环境中到底会怎么表现。2. 多节点部署下local策略为什么必然失控一次完整的故障复现继续说我开头提到的那次事故。当时我们的环境是这样的2个Kong Pod通过Service负载均衡限流配置为 consumer 维度、每分钟60次。压测脚本模拟100个并发用户实际跑出来的结果很有代表性。2.1 故障现场限流阈值形同虚设现象有两种分别对应两种触发条件。第一种是“限流提前触发”假设请求先全部打到节点A把节点A的计数器打到了60然后流量切到节点BB的计数器此时是0所以B继续放行。你从客户端看到的实际结果是——总共放了100多次请求出去但客户端收到4xx的时机完全随机没有任何规律。第二种是“限流滞后触发”如果流量轮流打到A和B更常见的是总请求量已经远超阈值但429迟迟不出现因为每个节点的计数器都只有总流量的一半左右。这两种现象本质上都是同一个根因计数器不是全局共享的。Kong只检查本节点的计数器节点之间彼此不知道对方已经放行了多少流量。2.2 排查过程我是如何定位到local策略的当时排查的顺序大致是这样先看Kong日志确认有没有报错——没有所有日志都正常。检查路由和插件绑定关系——绑定正确插件确实生效。单节点压测复现——单节点下限流完全准确60次的阈值稳稳触发。多节点同时压测——复现了生产上的乱象。打开每个节点的 /metrics 或者用Admin API查插件状态确认两个节点各自计数——计数互不相等证实没有同步。第5步是决定性的一步。当你在管理API里看到同一个consumer的计数器在两个节点上各自独立增长时答案就已经写在脸上了策略选错了。当时我用的配置是这样# 这是错误示范多节点集群用了local plugins: - name: rate-limiting route: route-id config: minute: 60 policy: local limit_by: consumer把 policy 改成 redis 之后就恢复正常了。整个故障的定位思路本身不算复杂但如果你没有意识到“计数器存在哪”这件事碰到多节点限流不准第一反应往往是怀疑Kong本身有bug那就会绕很大一圈。2.3 local策略还算可用的场景坦白说local策略并不是一无是处它在特定条件下确实能用而且性能极佳。具体来说下面这些场景我认为local策略绰绰有余单节点部署或者说你整个网关就一个实例没有水平扩展。多节点但完全不介意各节点各限各的比如内部服务、监控类系统总量不敏感。开发、联调、测试环境只需要验证限流逻辑本身。节点数量少且阈值很大、业务流量远低于阈值限流只是兜底措施。但在上面这些场景之外只要你的Kong是真正意义上的多节点生产集群我的建议是直接不要考虑local把redis策略作为默认选择。local策略省下的那一点点性能开销在限流失控带来的业务损失面前完全不值一提。3. redis策略的配置精解从单实例到哨兵再到集群如果说local策略的配置只有 policy: local 一句话那redis策略的配置就复杂多了而且很容易配置错。这里我不光讲字段还把每个字段背后影响什么也一并讲清楚。3.1 最基础的redis策略配置先给一个最常见的单机Redis配置plugins: - name: rate-limiting route: route-id config: minute: 60 policy: redis limit_by: consumer redis_host: 10.0.0.12 redis_port: 6379 redis_username: kong redis_password: password redis_database: 0这份配置把Kong的限流计数器存到 10.0.0.12:6379 这个Redis实例里。所有挂在同一个路由上的Kong节点都会读写同一份计数器限流就全局准确了。这里有个很关键的细节redis_username 和 redis_password 只有在Redis启用ACL时才需要同时配置。如果Redis没有启用ACL只需要redis_password甚至一个都不配默认无密码。我看到很多人在本地Redis没有密码的情况下还填了密码导致报错 NOAUTH Authentication required。3.2 连接池与超时参数别忽略这些“小参数”redis策略的每个请求都会访问Redis所以连接管理和超时控制直接影响网关性能。下面这几个参数最好在配置时就一并考虑redis_timeout单次Redis操作的超时时间默认2000ms。如果Redis响应慢这个值决定Kong会等待多久。建议不要超过默认值否则一个慢Redis会把整个网关拖垮。redis_keepalive_poolKong官方文档里有一个 keepalive 的池大小配置控制连接复用的最大空闲连接数。生产环境建议设置成和worker数差不多。redis_ssl是否启用TLS连接Redis如果Redis开启了TLS必须设为true并且需要准备好证书相关配置。其中 redis_timeout 是最容易被忽视的。如果你不设一旦Redis出问题比如网络抖动、主从切换每个请求都要等2秒才返回网关的P99延迟会崩掉。3.3 哨兵模式的配置方式如果Redis是高可用架构用的是哨兵Sentinel模式配置跟单实例很不一样。看这个例子plugins: - name: rate-limiting config: minute: 60 policy: redis limit_by: consumer redis_host: my-redis-sentinel redis_port: 26379 redis_username: kong redis_password: password redis_ssl: false redis_sentinel_master_name: mymaster redis_sentinel_role: master redis_sentinel_addresses: - 10.0.0.11:26379 - 10.0.0.12:26379 - 10.0.0.13:26379哨兵模式的核心区别是Kong先通过哨兵节点查询当前master是谁然后再去连那个master。所以你会发现哨兵模式下有 redis_sentinel_master_name、redis_sentinel_addresses 这些字段。这里踩过一个坑redis_sentinel_role 如果配成 slaveKong会优先从从节点读取计数器但在某些版本的Kong下主从切换后表现不稳定强烈建议固定用 master。3.4 集群模式的配置方式Redis Cluster模式的配置在Kong里反而比哨兵模式简单一些。不需要设置哨兵相关的字段只要设置一个初始节点列表即可plugins: - name: rate-limiting config: minute: 60 policy: redis limit_by: consumer redis_host: 10.0.0.10 redis_port: 6379 redis_username: kong redis_password: password redis_cluster_nodes: - 10.0.0.10:6379 - 10.0.0.11:6379 - 10.0.0.12:6379Kong会通过 redis_cluster_nodes 里的节点自动发现整个集群的拓扑然后把key分散到不同的slot去读写。如果某个节点挂了集群会自动重定向Kong的处理逻辑本身是支持Cluster的。不过提醒一句rate-limiting插件在Redis Cluster模式下计数器的key分布是均匀的但如果你的限流阈值非常高、QPS非常大单个计数器的热度可能成为Redis节点瓶颈。这种情况我们后面再展开。3.5 连接决策为什么哨兵比Cluster更适合限流我的个人经验是如果只是给Kong限流用哨兵模式往往比Cluster模式更合适。原因很简单限流计数器本质上是大量的小key、高频读写不需要特别大的数据量集群模式的数据分片优势在这里并不明显。哨兵模式的结构简单、主从切换机制成熟运维成本低得多。而且rate-limiting插件对Redis的可用性非常敏感与其追求横向扩展能力不如追求故障切换的确定性和低延迟。当然如果你的运维体系里已经强制使用Cluster那也没问题但要注意给限流设置独立的Redis哪怕是单独database避免和其他业务的key混在一起互相影响。4. rate-limiting的窗口机制为什么你的限流总是“差一点”配置对了策略你会遇到下一个问题限流的边界不精确。比如设定每分钟60次实际放行可能是57次、63次甚至更快地出现429。这不是插件bug而是窗口算法本身的特性。4.1 固定窗口算法的计数逻辑rate-limiting插件在默认配置下用的是固定窗口算法。它的工作方式是Kong把时间按固定间隔切分成窗口比如每分钟一个窗口窗口起始点是整分钟的0秒每个窗口一个计数器。请求进来时看当前时钟落在哪个窗口然后给这个窗口的计数器加1计数器值达到阈值就拒绝。这种算法实现简单、性能高代价是边界修正不精确。假设阈值是每分钟60次原本窗口内已经放行了59次突然在窗口最后1秒来了10个请求——按照算法这10个请求里只有1个能通过其余9个会直接429。然后到下一个窗口开始计数器归零又放60个。结果就是短时间内请求量波动剧烈看起来“时灵时不灵”。4.2 滑动窗口算法是怎么改进的为了避免固定窗口的边界毛刺rate-limiting插件支持把策略切换为 sliding window 模式4.x版本后通过 config.window_type 配置plugins: - name: rate-limiting config: minute: 60 policy: redis limit_by: consumer window_type: sliding滑动窗口算法不再以固定时间的整数颗粒为边界而是以“当前时间往前推一个周期”作为窗口范围。Redis策略下Kong使用Lua脚本在Redis中计算当前滑动窗口内的总请求数返回给节点做判断然后执行一个原子操作把当前请求加入窗口。这在Redis端是原子执行的不会因为多节点并发出现计数错误。滑动窗口的代价是Redis操作更复杂每次请求都需要在Redis里执行一段Lua脚本。不过在实际压测中只要Redis的延迟在1ms左右性能完全够用。如果你对限流的精确性有要求并且Redis资源充沛推荐直接用 sliding。4.3 时钟同步问题分布式限流里最隐蔽的坑说到窗口算法有一个东西必须单独提醒时钟。固定窗口算法依赖系统时钟来确定当前窗口的起止点。如果Kong节点和节点之间时钟不一致或者节点跟Redis所在机器的时钟偏差大会导致什么每个节点对“当前窗口”的判断不同步有的节点认为还在上一分钟有的已经进入下一分钟限流边界就会出现偏差。测试环境往往所有组件在一台机器上时钟完全一致问题不暴露。生产环境的虚机如果NTP没配好节点间时钟偏差可能在秒级甚至分钟级那限流精度就是灾难。所以无论你用local还是redis策略生产环境第一件事就是确认所有Kong节点、Redis所在主机都启用了NTP时钟同步。这是分布式限流最基础但最容易被忽略的前提条件。5. Redis故障时网关会怎么表现一次压测触发的Redis雪崩配置全对策略也对了然后你以为万事大吉了。直到某天Redis突然抖动了一下你发现网关全体请求异常你才开始怀疑人生。这个环节我用一次真实的Redis故障经历把rate-limiting插件在Redis不可用时的表现、排查链路和应对方案完整讲一遍。5.1 故障现象网关大面积502但Redis其实是通的那次压测我模拟了高并发场景QPS大概3000Redis和Kong在同一台物理机的两个容器里。压测跑到一半突然一堆接口开始报502。查看Kong的error.log大量日志是这种形式2025/01/15 14:32:11 [error] 123#0: *456789 [lua] rate-limiting.lua: ... failed to query redis: timeout但我ssh到Redis容器执行 ping返回PONGRedis明明活着。问题出在哪里高并发下每个请求都做一次Redis操作而且我的redis_timeout没设置默认是2000ms。3000QPS的请求量Redis的CPU被打高部分命令响应超过2秒Kong这边每个超时的请求都拿不到计数结果插件直接放行还是拒绝呢5.2 根因定位Redis连接池耗尽与超时叠加当时的根因有两层第一层Kong与Redis之间的连接没有做足够的复用高并发下连接池被打满新请求需要建立新连接TCP握手本身就大量消耗时间进一步加剧Redis压力。第二层redis_timeout 是2000msRedis响应变慢后大量请求都在等待Redis响应。Kong的worker进程被这些等待占满其他请求排队最终表现为整个网关的502雪崩。更麻烦的是Kong的rate-limiting插件在Redis请求失败时默认行为是“fail open”——即求值失败则放行。这意味着Redis抖动不仅造成502还造成限流失效。这个双重打击才是Redis故障最危险的地方。5.3 应对方案故障时的行为配置与隔离针对这个问题我给生产环境定的方案是这样的显式设置 redis_timeout 为 500ms不要用默认的2000ms。宁可让限流快速失败也不能让Redis慢请求拖垮整个网关。给限流配置独立的Redis实例或独立database跟其他业务缓存彻底隔离避免业务写入影响限流。把Kong和Redis的部署尽量靠近降低网络延迟。如果Kong在Kubernetes里Redis最好也在同一集群内不要跨云访问。给Redis加监控告警重点关注 INFO commandstats 里的限流相关命令调用量和延迟。一旦延迟超过阈值提前介入。如果Redis故障是可接受的比如内部测试环境可以接受fail open但生产环境我建议宁可fail closed——限流不可用时拒绝请求也不要不限流裸奔。关于第5点Kong本身有一个 config.redis_timeout 相关的行为控制但“限流失败时放行还是拒绝”这个开关在Kong的不同版本里命名不一样。如果你用的是Kong Gateway 3.x请在插件配置里关注 redis_ssl 之外的错误处理字段确保生产环境行为符合预期。5.4 缓存层的合理使用local策略和redis策略结合的现实路径忍不住说一个优化思路。有的人看到这里可能会觉得既然redis策略这么依赖Redis可用性那能不能把热数据缓存在本地减少Redis访问可以但rate-limiting插件本身不支持这个逻辑。如果你真的想减少Redis压力更现实的方式是调整 limit_by 和阈值设计或者接受固定窗口算法的高效性而不是让每个请求都做复杂的滑动窗口脚本。我之前试过一种折中限流维度用ip阈值给得比较宽只在核心接口上启用这样Redis访问频率大幅下降对Redis可用性的依赖也成正比降低。限流不是越精确越好够用且稳定才是正道。6. 实践配置与调优把我的生产配置直接给你参考最后给出一份我目前在生产环境实际使用的配置以及配好后怎么验证、怎么观察效果。配置本身是基于Kong Gateway 3.x的 Declarative Config YAML格式如果你用的是Kong Ingress Controller配置概念完全一样。6.1 一份完整的redis策略生产配置_format_version: 3.0 services: - name: order-api url: http://order-service:8080 routes: - name: order-create paths: - /v1/orders strip_path: false plugins: - name: rate-limiting config: minute: 600 policy: redis limit_by: ip window_type: fixed redis_host: kong-redis-sentinel redis_port: 26379 redis_username: kong redis_password: strong-password redis_ssl: false redis_timeout: 500 redis_sentinel_master_name: kong-master redis_sentinel_addresses: - 10.0.0.11:26379 - 10.0.0.12:26379 - 10.0.0.13:26379 fault_tolerant: false几个关键选择说下理由limit_by 用 ip 而不是 consumer因为我们的业务还没有统一的consumer体系用ip最直接。window_type 用 fixed虽然滑动窗口更平滑但这个接口的流量模式比较均匀fixed足够且fixed的Redis开销更低。fault_tolerant: false这是我在3.x上配置的限流故障行为开关。设为false时Redis如果取不到计数结果请求会被拒绝保证不裸奔。注意这个字段在不同版本里语义可能不同一定要以你当前版本的官方文档为准。6.2 验证限流是否真的生效的三种方法配置完不是就完事了你得验证。我推荐三种方法第一种单机压测。用 wrk 或者 ab 直接打你这个API观察返回429的请求数量是否和阈值一致。单机压测通过后才能进入多节点验证。第二种多节点交替压测。如果你的Kong有多个节点用轮询的方式打不同节点确保所有节点共享的Redis计数器生效。如果429出现的时机稳定、总放行量等于阈值说明全局限流正确。第三种观察Redis计数器。在压测过程中直接去看Redis里限流相关的keyredis-cli -p 6380 --scan --pattern kong_rate_limiting*如果计数器在不同节点触发时都在同一个Redis key上增长说明策略生效。通过key的TTL也能确认窗口是否按预期滚动。6.3 什么时候该升级成 sliding 更细的维度配置不是一劳永逸的。当你的业务出现以下信号时建议升级方案客户端经常在窗口边界附近出现“一下子429一下子放行”的陡增陡降影响体验。某个IP、某个user的突发流量峰值特别明显。你发现固定窗口在高峰期放行的总量和预期有较大偏差业务方提出精确限流诉求。这时候再切到 sliding配合 limit_by: consumer 或更细的 credential 维度。升级前先确认Redis能扛住Lua脚本的执行开销建议压测验证后再切不要在高峰期直接改配置。最后的经验总结几个月前那场半夜排查的教训让我把 local 和 redis 策略的边界彻底刻在了脑子里。如今我再给任何Kong集群配限流默认流程就三句话先想清楚Kong是不是多节点多节点就老老实实用redis再检查Redis高可用架构哨兵还是Cluster按运维体系选最后明确Redis故障时你要fail open还是fail closed别让“限流不可用”变成“网关裸奔”。另外分享一个很多资料里没写的小技巧在Kong的日志里搜 rate-limiting 相关关键字加一个 warn 级别过滤日常就能看到计数器的评估日志。比如出现 “rate-limiting: current window count: 59, limit: 60, retry after” 这种日志时说明插件正常工作而且你可以通过这些日志估算当前窗口的剩余容量比临时去Redis里翻key直观得多。限流配置不再是一锤子买卖上线后持续观察调整才能配出真正稳定、不添乱的限流策略。