news 2026/11/2 6:38:14

Java Agent 限流器实测:200 线程抢 100 QPS 下的 19 倍超发、10 秒颗粒无收与 890 的首秒突发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Agent 限流器实测:200 线程抢 100 QPS 下的 19 倍超发、10 秒颗粒无收与 890 的首秒突发

先说这篇解决什么问题

问题:Agent 一旦失控(死循环重试、规划跑偏),烧的是真金白银的 token,所以每个 Agent 后端都要装"刹车"——限流器。Java 里自己写一个不超过 30 行:令牌桶、滑动窗口日志、固定窗口三选一。这篇把三个 30 行实现放到 200 线程、10 秒、1.07~2.96 亿次抢额请求下对拍,结果:写错一个 volatile,配额 100/s 的限流器实际放行了 1898/s;而两个"写对"的限流器,也有线程在 10 秒里颗粒无收。

适合谁读:给 Agent 调 LLM/工具的链路加配额与熔断、用自研限流器(或想搞清 Guava RateLimiter 这类实现为什么长那样)的 Java 后端。

能收获什么:三种限流器在同一压力下的放行总量、逐秒放行形状、线程公平性、阻塞式排队延迟四组对比数据;一个"非 volatile 检查-后-行动"双向失真的完整案例(超发 19~21 倍与永久饿死在同一个实现里先后出现,两套 JDK 复现);以及 2 亿次/10 秒的无锁尝试下,全程加锁方案的吞吐参考值。

近期背景

CSDN 10-28 的《智能Token工厂》一文(厂商视角,PPIO 在 AICC 算力大会的提法,属第三方材料,其中"吞吐提升 3~5 倍"等数字本文不采信)把 Agent 时代的计量与限流列为基础设施四道工序之一:"Agent 容易失控——一个死循环可能瞬间烧掉大量 Token,平台需要有熔断和配额机制"。结合 10-24 前后清华/港中文 AKV 缓存工程把 Agent 成本管理推上论文日榜,"给 Agent 装刹车"已经从最佳实践变成默认动作。刹车本身写得对不对,就是这篇的主题。

一、实测环境与口径

项实测值
JDK(主)17.0.18(17.0.18+8-LTS-264)
JDK(对拍)21.0.10(21.0.10+8-LTS-217)
os.name / version / archWindows 11 / 10.0 / amd64,16 核
被测对象三种自研限流器,同配置:limit=100 permits/s,burst=100
压力200 线程无间隔抢额 10 秒,总尝试 1.07~2.96 亿次(实现而异)
场景非阻塞 tryAcquire / 阻塞 acquire(最长等 500ms) / 首秒突发(只跑 1 秒)

换环境会变:尝试总数(锁吞吐)、各线程分到的额度;不变:放行总量相对配额的倍数关系、超发/饿死这两个方向性失败(两套 JDK 已复现)。复现判定见第九节。

二、非阻塞抢额:固定窗口超发 19~21 倍,另两个精确踩线

200 线程、10 秒、无思考间隔地抢(每秒真实尝试 1000 万~3000 万次):

限流器放行总数(10s)折算速率JDK 21 复测
令牌桶(100/s,桶容 100)1097≈110/s1098(≈110/s)
滑动窗口日志(100/s)1028≈103/s1000(≈100/s)
固定窗口(100/s)18983≈1898/s21364(≈2136/s)

令牌桶与滑动日志在 2 亿次争抢下精确守住了配额(多出的 3%~10% 是首秒突发与窗口边界,见第三节)。固定窗口超发 19 倍(JDK 21 上 21 倍)——限流器不是"大概齐",写错可见性就是 20 倍的账单差异。它的病根在第六节拆。

三、突发形状:令牌桶的首秒 199 是特性,固定窗口的 890 是事故

只跑 1 秒、同样 200 线程(JDK 17):

限流器首秒放行说明
令牌桶(桶容 100)199预存 100 个令牌 + 1 秒内又补充约 99 个,突发容量是设计特性
滑动窗口日志100滚动窗口没有"预存"概念,天然零突发
固定窗口890配额 100,放行 890

选型时这是一笔必须摊开算的账:令牌桶允许"攒着一起花"(对 LLM API 的 RPM 限制通常正合适),滑动窗口适合"任一滚动秒都不许超",而固定窗口在没写对之前连基本配额都保不住。顺带一个反直觉细节:滑动日志在 JDK 21 复测的逐秒放行形状里出现了"某日历秒放行 200、相邻秒为 0"——这不是 bug,滚动窗口约束的是"任意滚动 1 秒",日历秒只是它的一个切片,切片可以横跨两个窗口的尾部和头部。看监控选对时间桶,否则会把正确的限流器误判成超发。

四、公平性:两个"写对"的限流器,都有线程 10 秒颗粒无收

10 秒争抢结束时,每个线程分到的放行次数:

限流器单线程最少单线程最多
令牌桶061
滑动窗口日志086
固定窗口6253

令牌桶和滑动日志在 200 线程的抢额里出现了"零配额线程":先到先得 + 单把锁,谁抢到归谁,慢线程在10 秒里一次都没成功过。固定窗口反而因为自身超发"雨露均沾"(6~253)。结论:限流器默认不做公平性。如果你的业务是"每个 Agent 任务都必须推进",要么在线程间引入轮转/队列,要么接受"有的任务就是抢不到"并在上层重试——这两条路的成本结构完全不同,得提前选。

五、阻塞式排队:令牌桶等待 p50 15ms,滑动日志 1ms,固定窗口 3778 次超时

同一批限流器换成阻塞式acquire(最多等 500ms)(200 线程、10 秒,JDK 17):

限流器成功获取(含等待>0)等待 p50 / p99 / max超时(>500ms)
令牌桶1295(≈130/s)15 / 26 / 34 ms0
滑动窗口日志1199(≈120/s)1 / 1 / 500 ms13
固定窗口15(≈1.5/s)1 / 498 / 500 ms3778

两个"写对"的限流器在阻塞模式下依然精确放行(120~130/s),排队延迟 p50 都在毫秒级——阻塞式比拒绝-重试对调用方友好得多,代价是 200 个调用线程都被挂住。固定窗口在阻塞模式下暴露出第二张面孔:10 秒只放行 15 次、3778 次等到超时——与非阻塞模式的 19 倍超发正好相反。

六、固定窗口的双向失真:同一个实现,先超发 19 倍,再饿死 10 秒

我的固定窗口实现是教科书式的"检查-后-行动":

// bucketStart 与 count 都是普通字段(非 volatile) long sec = System.nanoTime() / 1_000_000_000L; if (sec != bucketStart) { synchronized (this) { if (sec != bucketStart) { bucketStart = sec; count = new AtomicInteger(); } } } return count.incrementAndGet() <= limit ? 0 : -1;

复位逻辑在同步块里做了双重检查,看起来"该有的都有了",但bucketStart和count本身是普通字段:读到的可能是过期引用。实测它在两个方向同时失真——非阻塞模式超发 19~21 倍,阻塞模式把 200 个线程里的绝大多数饿到超时。超发方向的具体交错路径(哪些线程拿到了哪个旧计数器、复位为什么被跳过或重复生效)我没有完全定位出来,这一点如实列入第八节;但失真的类别(共享可变状态无可见性保障)与修复方向是确定的:要么全程持锁(本文的令牌桶/滑动日志就是这么写的,两套 JDK 都精确守住了配额),要么把(bucketStart, count)打包成单个 volatile 引用整体替换。限流器的正确性 budget 比它的性能 budget 贵得多——这两个方向在生产上的名字分别叫"超支烧钱"和"服务不可用"。

七、锁的代价:2 亿次尝试 / 10 秒,就是单锁的吞吐上限

非阻塞模式下每个线程不休息地抢,三个限流器 10 秒内各自承受了 1.07 亿 / 1.18 亿 / 2.03 亿次尝试(JDK 21 为 1.48 亿 / 1.57 亿 / 2.96 亿)。这就是单把 synchronized 锁在 16 核 200 线程下的实际吞吐量级:每秒 1000 万~3000 万次判定。对"每个 Agent 任务每秒最多几十次工具调用"的真实负载,这余量巨大;但如果你把限流器放进每请求路径的高 QPS 网关,这就是要换成 Striped/分段锁或无因果锁结构的信号。反过来,第五节阻塞模式下滑动日志 p50 只有 1ms——睡眠轮询的阻塞式抢额对锁的压力小两个数量级。

八、适用边界与风险提示

  1. 全部实验在本机 Windows 11、16 核、单 JVM 进程内,限流器是进程内实现;分布式限流(Redis+Lua 等)的失败模式不同,本文结论不直接迁移。
  2. 尝试总数、各线程分配额度受线程调度影响,逐次运行会波动;放行总量的倍数关系与超发/饿死两个方向在两套 JDK 上均复现。
  3. 固定窗口超发方向的精确交错路径未完全定位(已知类别是共享 (bucket,counter) 无可见性保障;饿死方向由"线程长期持有旧计数器引用"解释)。我把它当作"这一类写法不可用"的证据,而不是对该实现每一行汇编码级的证明。
  4. 阻塞式实验的测台有一处口径修正,见第九节判定标准前的说明:阻塞模式下"等待>0 的成功"最初被计成了拒绝,靠"timeouts=0 却有 1294 次拒绝"的自相矛盾抓出来——先怀疑测台这条纪律又一次生效。
  5. 未单独实验、只作方向提示:Guava RateLimiter 的突发平滑语义、令牌桶预存容量的取值对冷启动的影响、多 key 场景下锁分段与公平性的组合。

AI 辅助说明:Exp4 代码与全部数据均为本机 JDK 17.0.18 / 21.0.10 实跑输出,数字取自 stdout 留档;第六节引用的限流器代码就是实验中被测的那份实现,未做任何事后修改。

九、复现命令与判定标准

"D:/installSoftware/jdk-17.0.18/bin/javac" -encoding UTF-8 -d out17 src/Exp4.java "D:/installSoftware/jdk-17.0.18/bin/java" -XX:+PerfDisableSharedMem -cp out17 Exp4

判定标准(对不上先怀疑测台):① 令牌桶 10 秒放行必须落在 1050~1150 区间(100/s × 10s + 首秒突发约 100),固定窗口必须数倍于 1000;② 滑动日志的逐秒形状里允许出现"相邻日历秒 200/0",但任意滚动 1 秒内不得超过 100;③ 阻塞模式下令牌桶与滑动日志的 timeouts 必须接近 0,固定窗口必然出现大量超时;④ 若你的复测里固定窗口恰好守住了 100/s,先检查编译的是不是同一份源码(bucketStart/count是否被顺手改成了 volatile)——那等于换了被测对象。

十、带走三条判断

第一,限流器先审可见性,再谈性能:一个非 volatile 的检查-后-行动,能同时产出"超发 20 倍"和"全员饿死"两种事故,而修好它只要全程持锁——先正确,再优化。

第二,突发是设计参数不是 bug:令牌桶首秒 199 对 LLM API 是刚需,滑动日志的零突发是另一种刚需;监控时用滚动窗口口径看数据,别拿日历秒误判。

第三,默认不公平:先到先得的限流器必然产生零配额线程,"每个任务都必须推进"的业务要在上层补排队或轮转,这不是限流器的锅但会算在它头上。

一个具体自检动作:翻出你项目里的限流器实现,找(窗口起点, 计数器)这对字段——只要其中一个不是 volatile 或不在同一把锁内读写,就按第六节的双向失真对待,压测一轮再看数字。

两个我还没想透的问题:你们给 Agent 任务的限流是按"每任务"还是按"每用户"?前者会不会让大任务饿死小任务?另外阻塞式抢额你们设的最大等待是多少,超时后走降级还是直接失败?欢迎评论区聊聊。

如果你正要给 Agent 平台补配额与熔断,第二节那张三实现对比表和第六节那个双向失真的案例值得一起收藏——刹车装错方向的代价,比没有刹车更贵。

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

Audio学习7(PipeWire/PulseAudio)

1 概念 PipeWire 是一个由 Red Hat 主导、目前已成为 Linux 桌面音频/视频服务基座的现代多媒体框架&#xff0c;它在同一套体系内统一处理音频采集与播放、视频/摄像头流以及跨应用的实时信号路由&#xff0c;核心特点是以有向图&#xff08;graph&#xff09;方式组织节点和链…

作者头像 李华
网站建设 2026/11/2 6:36:11

自用-牛客网-华为机试编程模拟题4-python

单向链表import sysdef solve():data list(map(int, sys.stdin.buffer.read().split()))idx 0n data[idx]; idx 1h data[idx]; idx 1nxt {h: None}for _ in range(n - 1):a data[idx]; idx 1b data[idx]; idx 1nxt[a] nxt.get(b, None)nxt[b] ak data[idx]; idx …

作者头像 李华
网站建设 2026/11/2 6:34:47

微软砸钱做云,WPS靠本地化追:同样叫365,背后竟是这般算计?

说起办公软件&#xff0c;你可能会想到Office 365&#xff0c;毕竟它背靠国际大厂微软&#xff0c;功能多&#xff0c;用的人也多。而WPS 365名气就没有那么大&#xff0c;但用起来还是挺顺手的&#xff0c;毕竟更符合我们国人的操作习惯。这里普及一个小知识像这类办公软件&am…

作者头像 李华
网站建设 2026/11/2 6:34:02

华为OD机试真题 新系统 2026-10-28 PythonJS【AI推理任务并行额度】

目录 题目 思路 Code 题目 题目内容: 有 n 个 AI 推理任务按顺序排队,第 i 个任务需占用 vram[i] GB 显存;调度器可选定一个任务 i 为基准,向左右连续合并相邻任务(即连续子数组),合并条件是被合并任务的显存占用不低于 vram[i]。 已知任务的并行执行额度值 = vr…

作者头像 李华
网站建设 2026/11/2 6:34:00

CISP-PTE 爆破篇

本文为 CISP-PTE 备考学习笔记&#xff0c;所有操作仅在官方授权靶场、本地自建靶场环境完成。 文中 Payload 仅用于原理学习、靶场练习。严禁直接套用在任何未授权公网业务系统。未经授权对网站进行注入、扫描属于违法行为&#xff0c;请遵守《网络安全法》。 本文目的&#x…

作者头像 李华
网站建设 2026/11/2 6:32:35

前期论文阅读

1.GIT-BO: High-Dimensional Bayesian Optimization Using Tabular Foundation Models降维操作利用TabPFN对所有候选点进行计算目标函数的预测均值U和方差&#xff0c;利用TabPFN的预测均值构建预测均值梯度的二阶矩阵H()&#xff0c;算法选择 H 的前 r 个特征向量作为梯度引导…

作者头像 李华