限流不是为了挡住坏人,而是为了让一个坏请求只坏一次。
一、一小时三百四十次导出
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 分钟来自一个人的记录与估算;换一个产品、换一个入口,数字会完全不同,配额应以压测与成本核算为准。
关键参考与核验范围
- Cloudflare WAF 速率限制规则 :用于核对外部边缘层限流的规则模型与计数维度。
- OWASP ASVS :用于定位反自动化与资源限制在应用安全验证标准中的归属。
- OWASP Cheat Sheet Series :用于核对认证、会话与输入处理相关条目的通用做法。
- CWE Top 25 :用于核对资源耗尽类弱点的编号与分类位置。
以上链接于 2026-10-26 实测可公开访问;未核验各标准的版本号与条款编号。核验日期:2026-10-26。
边界句。 本文只讨论单人 SaaS 在应用层为四类入口设置配额与拒绝语义,不讨论边缘清洗、WAF 规则调优与基于身份的访问控制。
系列导航
- 上一篇:依赖与供应链:四十一个直接依赖、一百八十六条告警
- 下一篇:日志与审计:记九类、脱敏五列、留一百八十天
- 回到目录:一个人的安全基线:十四个入口、十七个密钥与一次 72 小时响应
- 相关阅读:API 配额与限流:免费额度、突发与一次误伤
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。