news 2026/8/12 16:57:43

冷启动第一秒放进来 200 个连接,数据库直接被打挂:RateLimiter 的 WarmUp 和漏桶的死队列

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
冷启动第一秒放进来 200 个连接,数据库直接被打挂:RateLimiter 的 WarmUp 和漏桶的死队列

title: "冷启动第一秒放进来 200 个连接,数据库直接被打挂:RateLimiter 的 WarmUp 和漏桶的死队列"
tags: [限流, 令牌桶, 漏桶, 算法, Java, 后端]
category: 后端


限流加了,数据库还是被打挂

我们有一个给 B 端客户用的批量导入接口,平时 QPS 不高,但每天早高峰会有一波集中调用。为了护住后面的数据库,我在接口前加了一道 GuavaRateLimiter,限到 50 QPS。代码很简单:

// 限到每秒 50 个许可 private final RateLimiter limiter = RateLimiter.create(50.0); public ImportResult importBatch(BatchReq req) { limiter.acquire(); // 拿不到许可就阻塞等待 return importService.doImport(req); }

上线后头几天风平浪静。直到某天早上应用因为 OOM 重启,重启完成后第一秒就被灌进来 200 多个积压请求,数据库连接池瞬间被打满,后续所有请求全部超时,数据库那台机器 CPU 直接 100%。

当时的困惑是:不是限了 50 QPS 吗,怎么还会被冲垮?后来读了一遍RateLimiter的源码才明白两件事:第一,RateLimiter.create(50)用的是SmoothBursty,它允许把过去没用掉的令牌攒起来,冷启动时桶里可能已经有几百个令牌,所以重启后第一秒能一口气放行一大批;第二,真正的「冷启动保护」要用SmoothWarmingUp,而我们对它的「预热」语义理解反了。另一个隐患在我们同时自研的「漏桶」实现里:一个无界队列把请求全接住,结果下游恢复不了时队列无限增长,反倒成了 OOM 的第二个来源。

令牌桶与漏桶的本质差异

先说清楚两个算法的模型,它们护系统的姿势完全不同:

  • 令牌桶:以固定速率往桶里丢令牌,请求来了先取令牌,取不到就等或拒。桶有容量上限,能容忍短时突发(攒下的令牌一次性放出去)。
  • 漏桶:请求像水一样进桶,桶底下以固定速率漏水(处理)。桶满就溢出(拒绝)。它强制平滑输出,不关心你前面来多猛,下游速率恒定,突发会被排队或丢弃。

一句话对比:令牌桶「允许你偶尔冲一下」,漏桶「无论如何都匀速」,两者一个在入口宽容、一个在出口整形。选错模型,限流反而帮倒忙——我们这次就是「想要护数据库(该用漏桶的匀速)却用了令牌桶的突发」。

RateLimiter 的两种实现:Bursty 与 WarmingUp

RateLimiter.create有两个工厂方法,内部对应两套子类:

// 令牌桶(SmoothBursty):允许攒令牌,支持突发 public static RateLimiter create(double permitsPerSecond) { return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer()); } // 预热型(SmoothWarmingUp):有预热期,冷启动阶段速率从低到高爬升 public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) { RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit); rateLimiter.setRate(permitsPerSecond); return rateLimiter; }

逐行拆解:

  • 第 2 行create(double)走的是SmoothBursty,它维护maxBurstSeconds(默认 1 秒),意味着桶里最多攒permitsPerSecond * 1个令牌。
  • 第 6 行create(double, warmupPeriod, unit)SmoothWarmingUp,多了一个预热时长参数,比如 10 秒。

SmoothBursty的突发能力来自它的reserve逻辑:只要距离上次取令牌的时间足够长,它就把这段时间累积的令牌都算给你。acquire()在冷启动瞬间可能一次性放行的数量约等于「空闲时长 × 速率」。我们的应用重启后空闲了几十秒,桶里攒了几百令牌,所以第一秒才会「放行 200+」。

SmoothWarmingUp的「预热」恰恰是为了反突发:它让限流器在刚启动时只放很低的速率,随预热期推进逐步爬到目标速率。我们当时误以为「WarmUp 是启动快、后面慢」,其实是反的——它是「启动慢、后面快」,专门用来给连接池、缓存等冷资源留出热身时间。把它用对,重启后第一秒就不会再被冲爆。

正确用法:给数据库类接口加预热

// 预热 10 秒,期间速率从低逐步升到 50 QPS private final RateLimiter limiter = RateLimiter.create(50.0, 10, TimeUnit.SECONDS); public ImportResult importBatch(BatchReq req) { if (!limiter.tryAcquire()) { // 拿不到立即拒,不阻塞积压 throw new TooManyRequestsException("导入过于频繁,请稍后重试"); } return importService.doImport(req); }

逐行拆解:

  • 第 3 行create(50.0, 10, TimeUnit.SECONDS)让限流器用SmoothWarmingUp,重启后的前 10 秒速率是逐步爬升的,第一秒可能只放 5~10 个,给数据库连接池留出建立连接、预热缓存的时间。
  • 第 6 行tryAcquire()改成「拿不到就拒」,而不是acquire()的「拿不到就死等」。这点很关键:如果请求在限流器外排队等待,表面 QPS 是被限住了,但线程却被占着,连接池和线程池照样会被压垮。
  • 第 8 行直接抛业务异常,把压力交给客户端重试,比让服务端线程干等长。

漏桶的死队列:另一种 OOM

我们另一条链路曾自研过漏桶,用一个阻塞队列当「桶」:

public class LeakyBucket { private final int capacity; private final BlockingQueue<Runnable> bucket; private final ScheduledExecutorService leak = Executors.newSingleThreadScheduledExecutor(); // 固定速率「漏水」 public LeakyBucket(int capacity, int leakRatePerSec) { this.capacity = capacity; this.bucket = new LinkedBlockingQueue<>(); // 无界队列! leak.scheduleAtFixedRate(this::leak, 0, 1000 / leakRatePerSec, TimeUnit.MILLISECONDS); } public boolean offer(Runnable task) { if (bucket.size() >= capacity) return false; // 满了才拒 return bucket.offer(task); // 没满就进队列等着 } }

逐行拆解:

  • 第 6 行new LinkedBlockingQueue<>()用了无界队列,这是埋雷:offer几乎永远不会因为「队列满」而拒绝,所有请求都被接住堆在内存里。
  • 第 13 行bucket.size() >= capacity的判满逻辑其实对无界队列形同虚设——只要offer不抛异常,请求就一直进。
  • 真正的问题在「漏水」速率:当下游(数据库)长时间不可用时,漏水线程处理不动,bucket里的任务无限堆积,最终把堆内存吃光,触发 OOM。我们那次漏桶实现的 OOM,根因就是这个无界队列,而不是限流本身。

修法是把队列改成有界,并且让offer在队满时真正拒绝,而不是默默接住:

this.bucket = new ArrayBlockingQueue<>(capacity); // 有界,满了 offer 返回 false // offer 时已满 -> 直接拒绝,而不是堆积 public boolean offer(Runnable task) { return bucket.offer(task); // 队满自动返回 false,调用方据此拒流 }

两种限流该怎么选

维度令牌桶(SmoothBursty)令牌桶(WarmingUp)漏桶
对突发流量的态度允许攒令牌,容忍突发预热期抑制突发强制匀速,突发排队/丢弃
冷启动保护无,反而易冲爆有,速率渐升有,恒定输出
队列风险无队列无队列无界队列会 OOM
适合场景秒杀入口、可突发的业务护 DB、护缓存、冷资源下游脆弱、必须匀速的链路

我的经验:护数据库、护第三方这种「慢热且脆弱」的依赖,优先用漏桶式的匀速整形,或者用带预热的令牌桶;纯入口限流、允许短时突发的,才用普通SmoothBursty。而无论哪种,「拿不到就拒」都该优于「拿不到就等」。

补充一个我们踩过的细节:tryAcquire()不传等待时间时和acquire()一样会阻塞,区别在于它返回布尔值让你自己决定拒流策略。如果下游恢复极慢,即便用了tryAcquire拒流,被拒的请求在客户端重试时仍会再次打进来,形成「拒绝—重试—再拒绝」的脉冲。所以限流之外最好再配一层熔断(例如 Sentinel 的慢调用比例规则),让下游真正不可用时整条链路快速失败,而不是在限流器边缘反复横跳,把压力转化成无意义的重试风暴。

复盘数字

  • 这次事故持续约 18 分钟,期间导入接口成功率跌到 0,连带把同库的订单查询也拖慢(连接池被占满)。
  • 加预热 + 改tryAcquire后,同样早高峰 200+ 积压请求,第一秒实际放行从 200+ 降到 12 个,数据库连接池使用率峰值从 100% 降到 63%。
  • 那条自研漏桶的无界队列,在改造前高峰期曾把单实例堆内存从 1.2 GB 推到 3.1 GB,是有名的「慢泄漏」,这次一并改成有界。

我的取舍

我不建议无脑用RateLimiter.create(permits)然后acquire()了事——那是给「能突发、能等待」的场景准备的,拿来护数据库恰恰是反面。我的判断是:凡是身后站着连接池、缓存、第三方慢依赖的接口,一律用WarmingUp+tryAcquire组合,宁可让客户端看到几个「稍后重试」,也别让服务端线程在限流器外排长队。至于漏桶,自研时一定要把「桶」做成有界,无界队列不是漏桶,是一个定时炸弹。

思考题

RateLimiter.create(50)RateLimiter.create(50, 10, SECONDS)分别放在一个「先 sleep 30 秒再瞬间打 300 个请求」的用例里,打印前 10 次acquire()各自的等待时间,你会清楚看到 Bursty 把积压令牌一次性放出、WarmingUp 把速率压在前几秒的差别。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/12 16:57:01

网页Java不是玄学,是跟电脑说人话!你还在门外干瞪眼?

Java是一种计算机语言, 其用途是开发软件, 这就好比汉语是在中国用于交流的语言, 西班牙语是在西班牙用于交流的语言, 编程要适合与计算机沟通就得使用计算机能认识的语言, 那么Java正是其中之一, 接下来会按顺序进行介绍&#xff1a;软件开发软硬件介绍软件开发软件开发, 是这…

作者头像 李华
网站建设 2026/8/12 16:55:31

Vue.js网页终端组件vue-web-terminal:从仿真到实战的完整指南

1. 项目概述&#xff1a;为什么我们需要一个网页终端&#xff1f; 在开发一个现代化的Web应用&#xff0c;尤其是面向开发者的工具平台、在线IDE、运维监控后台或者云服务控制台时&#xff0c;我们常常会遇到一个需求&#xff1a;如何在浏览器里直接运行命令行&#xff1f;想象…

作者头像 李华
网站建设 2026/8/12 16:54:41

Linux基础:进程概念

文章目录&#x1f680; 吐血整理&#xff01;小白也能秒懂的 Linux 进程概念大揭秘&#xff08;硬核详细版&#xff09;1. 祖师爷的宝训&#xff1a;冯诺依曼体系结构1.1 硬件的“权力游戏”2. 计算机里的大管家&#xff1a;操作系统 (OS)2.1 操作系统是个啥&#xff1f;2.2 管…

作者头像 李华
网站建设 2026/8/12 16:52:53

基于VSCode与Zephyr RTOS的STM32F103C8T6开发环境搭建与实战

在嵌入式开发领域&#xff0c;Zephyr RTOS 以其模块化、高度可配置和跨平台特性&#xff0c;正成为越来越多开发者的选择。然而&#xff0c;对于习惯了传统 IDE&#xff08;如 Keil、IAR&#xff09;的 STM32 开发者&#xff0c;尤其是使用 STM32F103C8T6 这类经典“蓝桥杯”最…

作者头像 李华
网站建设 2026/8/12 16:50:52

二元混合气焊接成本高?节气装置一套搞定

一、前言&#xff1a;制造业二元混合气焊接的成本痛点在自动化焊接批量生产的制造行业里&#xff0c;二元混合气凭借焊缝成型好、飞溅少、抗氧化能力强的工艺优势&#xff0c;成为汽车零部件、工程机械、钢结构等高精度焊接场景的标配保护气体。但长期深耕车间成本管控的工艺工…

作者头像 李华