新闻详情

新闻详情

首页 / 资讯中心 / 详情

固定窗口限流在整点放进了 3 倍流量:换成令牌桶后,我把削峰这件事想明白了

发布时间:2026/10/1 15:33:18来源:尧图网络
固定窗口限流在整点放进了 3 倍流量:换成令牌桶后,我把削峰这件事想明白了
title: 固定窗口限流在整点放进了 3 倍流量换成令牌桶后我把削峰这件事想明白了date: 2026-10-01tags: [限流, 令牌桶, 漏桶, Guava RateLimiter, Redis Lua, 高并发, Java]2025 年 6 月我们优惠券秒杀上线第一次全链路压测网关用的固定窗口限流每分钟最多 6000 次。压测数据出来我盯着流量曲线皱了眉——第 59 秒放进 6000 个请求第 60 秒开始的下一个窗口又立刻放进 6000 个两秒内实际涌入 12000 个请求下游库存服务的线程池直接打满超时雪崩。限流器的每分钟 6000在数学上没错在保护下游这件事上完全失效。这次事故之后我们把核心接口统一换成了令牌桶这篇文章把固定窗口的缺陷、令牌桶和漏桶的取舍、以及生产落地的两套实现讲透。一、固定窗口为什么必然出现两倍突刺固定窗口的逻辑很简单按时间切片计数片内计数器清零。它的突刺来自窗口边界窗口1: |----59s 零星请求----| 第60s 蜂拥 6000 个 | 窗口2: 第61s 蜂拥 6000 个 |----之后慢慢来----|窗口交界的前后各 1 秒内系统实际承受了两个窗口配额的叠加。极端情况下瞬时 QPS 是标称值的两倍以上。滑动窗口能缓解这个问题但滑动窗口的滑只是把突刺摊薄并没有为突发流量提供缓冲语义——这正是令牌桶存在的意义。二、令牌桶桶和令牌各自管一件事令牌桶的设计分两个角色令牌生成器以速率 r比如每秒 100 个匀速往桶里放令牌桶容量 b比如 200封顶桶满了新令牌被丢弃。请求方每个请求进来先尝试取一个令牌取到就放行取不到就拒绝或排队。突发流量的答案就在桶容量 b 上平时流量低令牌持续积累到桶满流量突增的瞬间桶里积攒的 200 个存量令牌允许系统一口气放行 200 个请求之后回落到速率 r 的节奏。匀速补充 存量透支这就是削峰的全部秘密。Guava 的RateLimiter是最广为人知的 Java 实现我们看它的核心方法public double acquire() { return acquire(1); } public double acquire(int permits) { long microsToWait reserve(permits); // 睡到下一个令牌可用的时刻 stopwatch.sleepMicrosUninterruptibly(microsToWait); return 1.0 * microsToWait / TimeUnit.SECONDS.toMicros(1L); } final long reserve(int permits) { checkPermits(permits 0); synchronized (mutex()) { return reserveAndGetWaitLength(permits, stopwatch.readMicros()); } }逐行解释-reserve在锁内计算当前时刻到第 N 个令牌可用时刻的等待时长并把这笔账记在nextFreeTicketMicros上。- 关键设计是预支消费当前请求可以立刻拿到令牌透支但下一个请求要为这笔透支买单等待。- 所以acquire返回的是等待时长——如果你监控到这个返回值从 0 涨到几百毫秒说明下游已经过载这是比拒绝计数更早的过载信号。Guava 里还有个容易被忽视的参数RateLimiter.create(100, 1, TimeUnit.SECONDS)的第二个参数是 warmup 期冷启动阶段发放速率低于设定值给连接池、JIT 预热留时间。我们网关流量从 0 拉满的场景里这个参数帮我们避免了冷启动打挂连接池。三、漏桶和令牌桶一字之差语义完全不同漏桶的模型是请求先进桶排队桶以恒定速率 r 流出处理桶满则拒绝新请求。它和令牌桶的核心区别维度令牌桶漏桶突发流量允许桶内存量令牌可透支不允许恒速流出输出曲线有尖峰均值受控绝对平滑适合场景保护自己兼顾突发吞吐保护下游流量整形典型实现Guava RateLimiterNginx limit_req、MQ 削峰我的取舍是API 网关入口用令牌桶内部服务之间的重 IO 调用用漏桶。理由是入口需要吃住突发流量保证体验内部下游数据库、第三方接口的吞吐天花板是刚性的平滑输出比突发友好更重要。我们给第三方支付回调接口配的就是漏桶恒速消费回调消息攒多的排队等绝不给支付渠道制造流量毛刺。四、手写一个漏桶40 行看清恒速语义漏桶在生产里常被消息队列替代但理解它的恒速实现对做流量整形很有价值。一个基于阻塞队列的最小漏桶public class LeakyBucketLimiter { private final BlockingQueueRunnable bucket; private final ScheduledExecutorService drainer; public LeakyBucketLimiter(int capacity, long leakIntervalMs) { this.bucket new ArrayBlockingQueue(capacity); this.drainer Executors.newSingleThreadScheduledExecutor(); // 定时器就是桶底的洞每 leakIntervalMs 毫秒放行一个请求 drainer.scheduleAtFixedRate(() - { Runnable task bucket.poll(); if (task ! null) { task.run(); } }, 0, leakIntervalMs, TimeUnit.MILLISECONDS); } public boolean submit(Runnable task) { // 桶满直接拒绝不排队不阻塞把压力挡在外面 return bucket.offer(task); } }逐行解释- 桶容量 capacity 决定最大排队量offer 失败即满溢调用方应立刻降级而不是傻等。-scheduleAtFixedRate是恒速出口的来源不管桶里积了多少出口速率永远是每 tick 一个。- 单线程 drainer 同时保证了任务的串行执行——这正是保护刚性下游想要的语义。这个实现的真实可用版本还要加两点任务执行超时中断防止一个慢任务拖住整个出口以及优雅关闭时排空队列。生产上如果下游是 HTTP 接口我会直接用信号量Semaphore 固定速率获取的方式改写省去自建线程的运维负担。五、滑动窗口介于两者之间的第三选择如果既嫌固定窗口突刺、又不想引入令牌桶的状态维护滑动窗口是折中。用 Redis ZSet 的实现思路public boolean allowSlidingWindow(String key, int limit, long windowMs) { long now System.currentTimeMillis(); String zsetKey rl: key; // 用 pipeline 合并三次往返清理、计数、记录 try (Pipeline p jedis.pipelined()) { // 移除窗口之外的旧记录 p.zremrangeByScore(zsetKey, 0, now - windowMs); ResponseLong count p.zcard(zsetKey); // 用时间戳作为 member 和 score天然去重 p.zadd(zsetKey, now, String.valueOf(now) : ThreadLocalRandom.current().nextInt()); p.expire(zsetKey, windowMs / 1000 1); p.sync(); return count.get() limit; } }逐行解释- ZSet 的 member 存请求唯一标识score 存时间戳窗口滑动就变成一次按 score 的范围删除。- 先删旧再计数再写入三步用 pipeline 压成一次网络往返QPS 上万时这个优化决定 Redis 的负载。- 缺点也直白每个请求都要写 ZSet高峰期内存和 CPU 开销远高于令牌桶的 O(1) 状态。我们的网关在 2 万 QPS 时 ZSet 方案的 Redis CPU 占用是令牌桶 Lua 的 6 倍这也是我们最终选令牌桶的直接依据。三种算法的适用边界用一句话概括固定窗口图省事滑动窗口图折中令牌桶图弹性漏桶图平滑。选型时先问自己一个问题——下游是弹性资源还是刚性资源弹性资源用令牌桶给突发留口子刚性资源用漏桶保住底线。六、warmup 参数冷启动的保护伞回到 Guavacreate的 warmup 变体值得单独说// 冷启动 30 秒内发放速率从很低线性爬升到 100 QPS RateLimiter limiter RateLimiter.create( 100.0, // 稳态速率 30, TimeUnit.SECONDS); // 预热期 // 冷启动阶段 acquire 的等待会明显偏长这是特性不是 bug double waitSeconds limiter.acquire();逐行解释- warmup 期内的冷却令牌生产速率低于稳态系统刚拉起时只放行少量流量。- 随着令牌持续被消耗爬坡机制让发放速率逐渐逼近设定值。- 我们数据库连接池初始化 15 秒、JIT 预热 10 秒的场景下30 秒 warmup 刚好把冷启动的连接池建满过程挡在限流器后面避免了发布后第一波请求打挂连接池的老问题。七、生产落地单机够用集群要 LuaGuava RateLimiter 只能管住单机。集群部署时每台限 100 QPS、10 台机器实际放行 1000 QPS配额没有全局语义。集群限流的落地我们用的是 Redis Lua-- key: 限流标识, ARGV: 1速率r, 2桶容量b, 3当前时间微秒, 4请求令牌数 local rate tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local now tonumber(ARGV[3]) local requested tonumber(ARGV[4]) -- 桶里现有令牌 上次剩余 这段时间新生成的封顶容量 local bucket redis.call(hmget, KEYS[1], tokens, timestamp) local tokens math.min(capacity, tonumber(bucket[1]) or capacity) local last tonumber(bucket[2]) or now tokens math.min(capacity, tokens (now - last) / 1000000 * rate) local allowed 0 if tokens requested then tokens tokens - requested allowed 1 end -- 写回剩余令牌和时间戳 redis.call(hmset, KEYS[1], tokens, tokens, timestamp, now) redis.call(expire, KEYS[1], math.ceil(capacity / rate) * 2) return allowed逐行解释- 令牌数按上次时间戳到现在的间隔线性补充和 Guava 的惰性生成是同一个思路只是搬进了 Redis。- 整个计算在 Lua 脚本里原子完成避免多个实例并发读改写导致的超卖。- expire 按装满一桶的时间 × 2设置空闲 key 自动清理不占内存。上线后压测数据10 台集群配置全局 5000 QPS 令牌桶实测整点边界瞬时放行峰值从 12000 降到 5400桶内 400 存量透支下游线程池水位从 100% 回到 63%超时归零。一个必须提的坑Lua 里的时间戳必须由客户端传入不要在脚本里调redis.call(TIME)——后者虽然更准但 TIME 命令会让脚本无法在主从复制时确定性重放Redis 会拒绝执行。我们第一版就栽在这里脚本直接报错Write commands not allowed after non deterministic commands。八、复盘真实数字事故场景秒杀压测固定窗口 6000/分钟突刺峰值窗口边界 2 秒内 12000 请求下游线程池 100%批量超时修复集群令牌桶 5000 QPS 桶容量 400修复后瞬时峰值 5400下游水位 63%超时 0改造耗时Lua 脚本 1 天 网关接入 2 天 压测验证 1 天九、思考题打开你项目的网关限流配置确认三件事算法是固定窗口还是令牌桶如果是集群限流配额是全局的还是单机乘以实例数桶容量设了多少、够不够覆盖一次合理的突发欢迎在评论区贴出你的答案。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Ubuntu 22.04 安装 Miniconda、PyTorch 与 YOLOv8 并完成 CPU 推理 2026/10/1 16:12:42

Ubuntu 22.04 安装 Miniconda、PyTorch 与 YOLOv8 并完成 CPU 推理

Ubuntu 22.04 安装 Miniconda、PyTorch 与 YOLOv8 并完成 CPU 推理本文记录在 VMware Ubuntu 22.04 虚拟机中安装 Miniconda、PyTorch CPU 版和 Ultralytics YOLOv8,并使用 YOLOv8n 对示例图片进行目标检测的完整过程。一、实验环境项目配置操作系统Ubuntu 22.04.5 …

阅读更多 →
9.30blog 2026/10/1 16:12:42

9.30blog

2026年9月24日 14:09 1.printf 占位符的作用是用“”后面的数据或词句替换语句中的占位符(指定格式) 例如:printf(“There are %d apples”,2); 生成There are 2 apples 例2printf(“%s will come tonight\n”,“张三”); 例3printf(“%s say…

阅读更多 →
带父母孩子去阳澄湖吃蟹,湖景包厢到底适不适合一家人坐进去 2026/10/1 16:12:42

带父母孩子去阳澄湖吃蟹,湖景包厢到底适不适合一家人坐进去

先说结论:适合,但前提是你对“湖景包厢”的期待不是一块招牌,而是老人孩子坐下来之后真实的体验。带家人出门吃饭,最容易出问题的往往不是菜好不好吃,而是环境名不副实、桌子挤、上菜慢,老人孩子都别扭。我…

阅读更多 →
(146页PPT)某大型企业基于战略的全面绩效管理体系设计方案(附下载方式) 2026/10/1 16:12:42

(146页PPT)某大型企业基于战略的全面绩效管理体系设计方案(附下载方式)

篇幅所限,本文只提供部分资料内容,完整资料请看下面链接 (146页PPT)某大型企业基于战略的全面绩效管理体系设计方案.pptx_基于物联网的消防监控方案资源-CSDN下载 资料解读:《(146页PPT)某大型…

阅读更多 →
Designer Skills五大集合全景图:33个插件覆盖研究到交付,找到最适合你的安装路径 2026/10/1 16:12:42

Designer Skills五大集合全景图:33个插件覆盖研究到交付,找到最适合你的安装路径

Designer Skills五大集合全景图:33个插件覆盖研究到交付,找到最适合你的安装路径 【免费下载链接】designer-skills Designer Skills Collection: agentic skills, commands, and plugins for design — from research to systems, UI, interaction, and…

阅读更多 →
Unity热更新安全加固:从AssetBundle清单签名到本地缓存防篡改 2026/10/1 16:12:35

Unity热更新安全加固:从AssetBundle清单签名到本地缓存防篡改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉