限流与滥用:四个配额与一次被刷的导出

一个导出按钮在一小时里被调了 340 次,CPU 打满 47 分钟。他给注册、登录、导出与 AI 四类入口装上配额,每条规则四十分钟,一次性投入 2.67 小时,把最坏情况写成一个可以提前算清的数字,也让每一次误伤都有章可循。

限流不是为了挡住坏人,而是为了让一个坏请求只坏一次。

一、一小时三百四十次导出

2026 年 9 月的一个周一上午,他在客户现场开会,手机弹出数据库 CPU 告警。回到电脑前,监控显示 CPU 从九点十二分起贴着 100%,持续 47 分钟。慢查询列表里反复出现同一条语句:把整个工作区的任务、评论与附件元数据打包成 CSV。

查访问日志,一个付费账号在一小时里调了 340 次导出接口,平均每 10.6 秒一次。对方没有恶意——他在用脚本把历史数据同步到自己的表格,每改一次筛选条件就重跑一遍,脚本里没有 sleep,也不读响应码。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

导出是同步执行的,每次要把几十万行拼成字符串再压缩。340 次串行请求占满连接池,其他用户的看板刷新开始超时。47 分钟里没有数据丢失,但有三封工单问「网站是不是挂了」,还有一位客户在当天下午续费时犹豫了一下。

事后他做的第一件事不是加机器,而是问自己:这个接口本来允许一个人一天调多少次?答案在套餐页上写着「每天 3 次导出」,但代码里没有任何一行检查它。文案与实现之间,缺的就是一个计数器。

那天下午他做了一次复盘,把 340 次请求的间隔画成一条线:多数间隔在 10 到 12 秒,中间夹着几次超过 30 秒的停顿,说明脚本在慢响应后重试。日志里没有任何一条记录告诉他「这个账号今天已经导出过 3 次」,因为根本没有这个字段。

限流常被理解成防攻击,日常更常见的却是「自己人的脚本」:同步、备份、批量导入、模型重跑。这些请求带着合法令牌,行为却和压测一样。配额的作用是给这类请求一个明确上限,让它在越界时收到可读的拒绝,而不是把整台机器拖慢。

二、四类配额:把闸门装在人最多的四个门口

他没有给 14 个入口全装配额,只挑四类:注册、登录、导出、AI 调用。判据是两条——能被脚本重复调用,且单次成本明显高于一次普通页面请求。其余十个入口靠认证、权限与输入校验守住,不需要再叠一层计数。

入口账号级配额IP 级配额理由
注册无(按 IP 计)10 次/小时,全站 200 次/天344 个账号的站,一天 200 次注册已远超正常增长
登录5 次/15 分钟20 次/小时5 次覆盖手滑,20 次挡住撞库;与锁定、防枚举共用计数
导出3 次/天,并发 1无单次导出吃 CPU,并发 1 防止自己把自己拖垮
AI 调用免费 20 次/天,付费 500 次/天无成本随调用线性增长,配额必须与套餐对齐

注册为什么按 IP 而不按账号:注册时还没有账号可计。10 次/小时对家庭宽带足够,对共享办公网的误伤靠全站 200 次/天兜底;做活动时临时调高全站上限,活动结束调回,并在变更记录里写一行。

登录为什么用两把尺:账号级 5 次/15 分钟防定向撞库,IP 级 20 次/小时防同一来源扫多个账号。两个计数器独立,任一触发即拒绝;文案统一为「尝试次数过多,请 15 分钟后再试」,不区分账号是否存在,与认证篇要求的 <50 ms 响应时间差一致。

导出为什么只按账号:导出必须登录,IP 在这里没有额外信息;并发 1 比次数更重要,因为两次导出同时跑会把内存翻倍。第二次请求直接返回 429,不排队——排队只是把 CPU 峰值往后挪,并没有消除它。

AI 为什么按套餐:免费 20 次够体验,付费 500 次对应重度用户;超限后不自动降级到免费额度,而是拒绝并提示升级,避免账单和体验同时失控。配额写在套餐页,也写在接口文档的错误码表里。

计数器放哪:单实例时进程内存就够;一旦多实例或重启,内存计数归零,等于每次发版送用户一轮新额度。他用 Postgres 一张四列的小表(计数键、窗口起点、计数、更新时间)加一条 upsert,每天写入几千次,压力可以忽略。选它不是因为它优雅,而是因为它已经在备份里。

窗口怎么选:固定窗口最简单,代价是边界处可能双倍突发——14:59 打满,15:00 再来一轮。登录与注册用固定窗口可以接受;导出按天计,边界问题不明显。要更平滑就上滑动窗口或令牌桶,但那是第二版的事,第一版先让上限存在。

还有一个容易忽略的维度:计数键要用内部 ID,不要用邮箱或 IP 原文。邮箱会变,也会被大小写与别名绕过;IP 会 NAT。内部账号 ID 稳定,IP 只在注册与登录的匿名阶段使用,并且先做一次规范化。

四类规则都只做拒绝,不做排队。队列看起来更友好,实际上会把压力藏起来:请求在队列里等着,客户端超时后重试,队列更长。一个人的系统没有容量规划团队,明快的 429 比温柔的排队更容易排查。

三、超限之后:429、Retry-After 与三档处置

拒绝的方式比拒绝本身更重要。返回 500 会让客户端重试,返回 403 会让用户以为账号被封,只有 429 明确说了「现在不行,等一会再来」,并且能带一个等待秒数。

档位触发条件响应是否告警
放行未达配额正常处理,响应头带剩余次数否
限流达到配额429 + Retry-After + 可解析的 JSON单账号连续 3 次才记
短封超限后仍高频重试429 + Retry-After 300 秒,按 IP 维度是,进每日巡检

Retry-After 要给秒数,不能写「稍后重试」。按天计的导出返回距次日零点的秒数,登录锁定返回 900;客户端拿到数字才能退避,拿不到就只能盲猜,而盲猜的默认行为通常是立刻重试。

响应体要能被脚本读懂,例如 {"error":"rate_limited","limit":3,"window":"day","retry_after":43200}。他见过把限流做成 HTML 错误页的接口,脚本解析失败后当成成功继续重试,等于没有限。

429 不能泄露账号是否存在。登录限流按账号计数,但对不存在的账号要返回同样的文案与量级相近的时间,否则限流本身就成了枚举工具——这一点与注册、登录、重置三处响应时间差 <50 ms 是同一条纪律。

三档里只有「短封」会告警。单账号偶尔超限是正常用户,连续重试才像脚本;告警进每日巡检邮件,不打电话、不半夜叫醒,一个人的团队经不起狼来了,叫醒三次里两次是误报,第三次就没人看了。

配额要写进文档和套餐页。用户在触发前应该知道上限,触发后应该知道何时恢复;藏在代码里的配额,第一次遇到时一定会变成工单,而工单的成本远高于把那行数字写进文档。

监控三个数:429 比例、按入口分的 top 10 计数键、被短封的 IP 数。429 比例突然升高可能是攻击,也可能是自己发版把阈值写错了;以一个人的运维经验,后者的概率更高,所以先看发布时间再谈攻击。

错误文案要给出恢复时间而不是原因细节。「导出次数已用完,明天 00:00 恢复」比「超出配额」有用;后者会让用户立刻再试一次,前者会让他去安排明天的工作。

他把 429 的返回也记进审计,但只记计数键的哈希与入口,不记完整键。这样既能统计哪些入口最容易被刷,又不会让日志本身变成一份可被滥用的账号清单。

四、二点六七小时的实现成本

四类规则,每类 40 分钟:写计数键与窗口逻辑 15 分钟、接到路由上 10 分钟、写三条测试(未超限、刚好超限、超限后恢复)10 分钟、补文档与错误文案 5 分钟。合计 160 分钟,2.67 小时,一次性投入。

复算:
四类规则 = 注册 + 登录 + 导出 + AI = 4 类
实现成本 = 4 × 40 分钟 = 160 分钟 = 2.67 h(一次性)
被刷事件 = 1 小时 340 次,平均每 10.6 秒一次(3,600 ÷ 340)
CPU 打满 = 47 分钟
按导出配额 3 次/天,340 次相当于 113.3 天的额度(340 ÷ 3)

2.67 小时对比 47 分钟的全站变慢,账面上并不夸张;真正的差别是确定性。没有配额时,任何一次脚本失控都能把 CPU 打满,而你不知道下一次是 47 分钟还是 4 小时;有了配额,最坏情况被写成一个数字。

成本里最容易被低估的是测试。「刚好第 3 次放行、第 4 次拒绝」这种边界,不写测试就一定会在某次重构后错位;他给每类规则留了三条用例,跑一遍 6 秒,比事后翻日志便宜得多。

配额也会误伤。共享办公网里十个人同时注册,10 次/小时的上限会在第 11 个人身上触发;他的处理是把注册的 IP 上限放在全站 200 次/天之内,并在错误文案里给出人工联系入口。误伤的代价是一封邮件,不误伤的代价可能是一整天的垃圾账号。

这套东西不需要 Redis,也不需要网关。一个人维护的系统里,每引入一个组件就多一个要备份、要升级、要监控的东西;Postgres 里一张小表足够撑住 344 个账号的计数,等真的撑不住再换,而不是提前为不存在的规模付费。

上线后的第一个月,他每周看一眼 429 的分布:导出触发 6 次、AI 触发 31 次、登录触发 22 次,注册一次没触发。前两个是配额在正常工作,登录的 22 次里有一半来自同一个人——那说明文案不够清楚,而不是阈值太紧。

上线顺序也有讲究:先上导出与 AI 两类,因为它们直接对应 CPU 与现金成本;再上登录,因为它会改变用户能感知的行为,需要提前在更新说明里写一句;注册最后上,配合一次小流量观察。

他没有做「按用户等级动态调整配额」这种设计。344 个账号、120 个付费用户,两个固定档位足够;动态配额需要一套规则引擎和一张解释表,维护成本会超过它省下的资源。

五、行动清单与复算验收

八条清单按「先定义、再实现、再验证」排:前两条决定上限,中间四条保证上限真的生效,最后两条让上限在运行时可被验证。

  • 列出可被脚本重复调用且单次成本高的入口,只给这些入口装配额
  • 为每个入口写下账号级与 IP 级两个数字,并说明理由
  • 注册同时设 IP 上限与全站日上限,活动期临时调整有记录
  • 登录按账号 5 次/15 分钟、IP 20 次/小时,文案不区分账号是否存在
  • 导出按账号 3 次/天且并发 1,第二次并发请求直接拒绝不排队
  • AI 调用按套餐设日配额,超限拒绝并提示升级,不静默降级
  • 超限统一返回 429,带 Retry-After 秒数与可解析的 JSON
  • 计数器落共享存储,多实例与重启后不清零

清单里的每一条都能在半小时内验证:把配额临时调成 1,手动触发两次,看第二次的响应码与 Retry-After;再重启一次实例,确认计数没有归零。验证完把配额改回去,变更记录里留一行。

验收标准:任一入口在达到配额后返回 429 与正确的 Retry-After,计数在实例重启后仍然有效,且 429 比例与短封数量能在每日巡检里看到。

使用边界与实操验收

边界一:限流不是 DDoS 防护。 它保护的是应用层资源与成本,挡不住流量型攻击;大流量清洗要在边缘层做,具体规则与计费以服务商文档为准,本篇只讨论应用内的配额。

边界二:固定窗口会双倍突发。 窗口边界前后各打满一次,实际速率可达配额的两倍;对登录这类需要精确的场景,应改用滑动窗口或令牌桶,代价是多一点存储与一次额外的读。

边界三:多实例必须共享计数。 进程内计数在滚动发布时会归零,等于每次发版都送用户一轮新额度;计数键要带窗口起点,过期靠定时清理,而不是靠重启。

边界四:配额是产品承诺的一部分。 套餐页写 3 次就必须是 3 次,改上限要同步改文案与账单说明;用户按文档写脚本、代码却按另一套数字拒绝,这属于缺陷而不是限流。

边界五:本文的次数、耗时与配额均为教学假设。 340 次、47 分钟、四类规则各 40 分钟来自一个人的记录与估算;换一个产品、换一个入口,数字会完全不同,配额应以压测与成本核算为准。

关键参考与核验范围

以上链接于 2026-10-26 实测可公开访问;未核验各标准的版本号与条款编号。核验日期:2026-10-26。

边界句。 本文只讨论单人 SaaS 在应用层为四类入口设置配额与拒绝语义,不讨论边缘清洗、WAF 规则调优与基于身份的访问控制。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元