ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

限速器的窗口边界:固定窗口为什么会放行两倍

限速器的窗口边界:固定窗口为什么会放行两倍 版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一、一个看起来合规的限速器限速器rate limiter是挡在业务前面的一道闸在一个时间范围里只放行固定数量的请求超出的拒绝、排队或者降级。它不负责让系统变快只负责让系统在被灌爆之前先自己认输。上游返回 HTTP 429 时这个状态码的含义就是「请求过多」按 IETF 的 RFC 6585 定义用于表示用户在给定时间内发送了过多请求通常是对面那道闸在起作用。1.1 最省事的那种写法固定窗口fixed window指的是把时间切成等长的格子每格独立计数格子一换计数清零。它只需要一个整数不需要保存任何请求的时间戳是三种实现里最省内存、也最好解释的一种。下面这段摘自本次实跑脚本可以直接复跑classFixedWindow:按自然整除的窗口切分window_id floor(t / 60)def__init__(self):self.bucket{}defallow(self,t):widint(t//WINDOW)usedself.bucket.get(wid,0)ifusedQUOTA:self.bucket[wid]used1returnTruereturnFalse其中WINDOW 60.0、QUOTA 10。逻辑没有任何毛病t // 60把时间轴切成 [0,60)、[60,120) 这样的格子每格最多放 10 个。这段代码在任何一次评审里都能过。1.2 它看自己永远合规问题不在代码在它能证明什么。这个计数器能回答的是「第 0 格里我放了 10 个、第 1 格里我放了 10 个」它回答不了「任意连续 60 秒里我最多放了多少个」。而配额这句话真正的含义是后者。把最坏情况摆出来第 59 秒进来 10 个全部放行第 61 秒又来 10 个也全部放行。这 20 个请求只隔了 2 秒却一次规则都没违反——落在任何一格里的数量都没超。每一分钟都合规两秒之内却打了双倍。判据一限速器自己报的「本分钟已用 N 次」不能当作守约证据。它能证明计数没写错证明不了放行口径。要证明守约得换一个统计量。二、三种实现的判定差异2.1 一张对照表同样是「每分钟 10 次」三种常见实现的判定依据完全不同。实现判定依据需要记录什么冷启动初始状态最坏情况本文实测固定窗口当前格子的已用计数 配额一个整数按格子存格子一开就是满额跨边界时 2.00x滑动窗口最近 60 秒的放行条数 配额每次放行的时间戳队列长度不超过配额队列为空从 0 累积1.00x令牌桶桶里令牌 ≥ 1令牌数 上次补充时刻桶是满的容量 配额1.90x突发额度这张表里最该盯的是第三列。「需不需要记录时间戳」直接决定了这个限速器在分布式部署下能不能对齐只存一个整数的实现多副本之间各算各的存时间戳的实现至少还有可能把记录共享出去。2.2 滑动窗口把时间戳排成队滑动窗口sliding window log指的是记录每一次放行的时间判定时先剔除已经滑出窗口的记录再看「剩下的还有几条」。classSlidingLog:记录每次放行时间戳检查最近 60 秒内放行数def__init__(self):self.logdeque()defallow(self,t):whileself.logandself.log[0]t-WINDOW:self.log.popleft()iflen(self.log)QUOTA:self.log.append(t)returnTruereturnFalse关键在while self.log[0] t - WINDOW这一句判定之前先把「已经滑出 60 秒」的记录从队头弹掉于是队列长度天然等于「最近 60 秒内放行过几次」。它不需要「格子」这个概念所以也没有格子边界。代价是内存队列长度最长等于配额看起来不多但这笔账是跟着每个被限速的 key 走的。按账号限速、按 IP 限速、按接口限速叠加起来就是配额乘以 key 的数量。2.3 令牌桶把速率和容量分开令牌桶token bucket指的是桶里存令牌请求来了先取一个取不到就拒。桶按固定速度补充攒满就不再补。classTokenBucket:容量 QUOTA补充速率 QUOTA/WINDOWdef__init__(self):self.tokensfloat(QUOTA)self.rateQUOTA/WINDOW self.last0.0defallow(self,t):self.tokensmin(QUOTA,self.tokens(t-self.last)*self.rate)self.lasttifself.tokens1.0:self.tokens-1.0returnTruereturnFalse它把两件事拆开了容量决定「能攒多少、能突发多少」突发指极短时间内集中通过的一批请求补充速率决定「长期平均多快」。这是固定窗口和滑动窗口都没有的自由度——也是它能被写成「每分钟 10 次」却放出更多请求的原因。注意self.tokens float(QUOTA)桶一开始是满的。这不算偷懒桶从空开始会让冷启动阶段的请求全部被拒冷启动指服务刚起来、计数器和桶都处于初始状态的那一段。但满桶启动也意味着一件事——它开局就允许一次性通过 10 个请求。这一点在第 4 章会变成一个具体数字。三、实测对齐与偏移两种相位3.1 实验设置与统计口径先把话说清楚这是离散时间轴上的模拟不是真实网关压测。脚本把 120 秒切成 1200 个 0.1 秒的步长按恒定速率生成请求逐个喂给三个实现记录每次判定结果。实验参数配额 10 次/分钟恒定 5 rps 灌入共 600 个请求、120 秒时间精度 0.1 秒Python 3.13.14只用标准库。统计量只有一个任意 60 秒滑窗内的最大放行次数。做法是把每个放行时刻当作右端点往回数 60 秒看这段区间里有几次放行取所有位置里的最大值。defrun(policy,reqs):passed[tfortinreqsifpolicy.allow(t)]# 任意 60s 滑窗内的最大放行数mx0j0foriinrange(len(passed)):whilepassed[i]-passed[j]WINDOW:j1mxmax(mx,i-j1)first_secsum(1fortinpassedift1.0)return{总放行:len(passed),任意60s窗口最大放行:mx,超额倍数:round(mx/QUOTA,3),首秒放行:first_sec}注意是而不是恰好 60 秒之前的那次放行不算进当前窗口也就是左闭右开。这个细节会影响恰好卡在边界上的计数复算时不要改。3.2 对齐相位三种实现看不出差别请求从 t0 开始正好与窗口起点对齐。三种实现的「任意 60 秒最大放行」分别是 10、10、19——固定窗口和滑动窗口都等于配额1.00x令牌桶是 1.90x。120 秒总放行分别是 20、20、29。如果只做这一组实验很容易得出结论「固定窗口和滑动窗口都对令牌桶因为允许突发所以多一点」。这个结论有一半是错的固定窗口在这里看起来守约不是因为守约而是因为相位正好。这也正是大部分限速器上线前的验证方式——从服务启动开始压压一段时间看被拒的数量对不对。这种压法恰好避开了固定窗口唯一会出问题的地方。3.3 偏移 30 秒固定窗口翻倍把请求整体后移 30 秒让请求落在窗口中间、跨过边界。相位phase指的就是「请求到达的时间起点相对于窗口起点的偏移」。完整的两组数据放在同一张表里实现对齐·任意 60 秒最大放行对齐·超额倍数对齐·120 秒总放行偏移 30 秒·任意 60 秒最大放行偏移·超额倍数固定窗口101.00x20202.00x滑动窗口101.00x20101.00x令牌桶191.90x29191.90x固定窗口从 10 变成 20滑动窗口纹丝不动令牌桶也纹丝不动。同一批请求、同一个配额只挪了 30 秒。首秒放行三种实现都变成 0——请求从第 30 秒才开始本来就没有首秒。为什么恰好是 2 倍第 30~32 秒这段先放满 10 个第 60~62 秒又放满 10 个两次密集放行相隔约 30 秒都落得进同一个 60 秒窗口里。两格各自合规合起来就是 20 个。# 边界场景把请求相位平移半个窗口看固定窗口的最坏情况shifted[t30.0fortinreqs]forname,polin[(固定窗口,FixedWindow()),(滑动窗口,SlidingLog()),(令牌桶,TokenBucket())]:rrun(pol,shifted)print(f{name:8s}任意60s最大放行{r[任意60s窗口最大放行]:3d}超额{r[超额倍数]:.2f}x 首秒{r[首秒放行]})实测里最值得记的一句是固定窗口的超额只在请求跨窗口边界时出现。对齐时它完全合规1.00x偏移半个窗口就变成 2.00x。也就是说这个缺陷不会在「从零开始压测」里暴露只会在「真实流量的相位恰好」里暴露——而真实流量的相位不受你控制。四、令牌桶的冷启动账先看首秒。三种实现的首秒放行都是 5这一点第一眼很奇怪桶里攒着 10 个令牌为什么首秒也只放 5 个因为输入速率就是 5 rps一秒只来了 5 个请求桶里的额度根本没被吃满。这是输入侧的限制不是限速器的功劳。实现初始可用额度实测首秒放行这个数说明什么固定窗口10格子开满5被输入速率卡住额度没用完滑动窗口0队列为空5同上但它是真的在累积令牌桶10满桶5同上突发能力被低速率掩盖了要看见桶的突发能力灌入速率必须高过补充速率。用低速率灌入永远看不出这件事——这也是「我这边的限速器很稳」这类结论最常见的来源压测的输入太温柔。再看总量。初始 10 个令牌加上 120 秒按 10/60 每秒补充的量到最后一个请求第 119.8 秒为止累计约 29.97 个令牌只够放行 29 个整令牌。实测就是 29。另两种实现都只有 20。同样一句「每分钟 10 次」令牌桶在 120 秒里多放了 9 个请求。这 9 个不算 bug。令牌桶的定义就是「允许突发、守住平均」多放的正是它被设计出来的行为。问题是如果你在文档里承诺的是「每分钟不超过 10 次」这 9 个就是违约——不是实现错了是措辞和实现对不上。顺便把上限算清容量 10 加上一个 60 秒窗口能补的 10 个任意 60 秒的理论上限是 20 次。实测 19 次差的那一次是离散步长和到达时刻凑出来的量级不变这一句是推算不是实测。判据二先想清楚要的是「任意 60 秒不超过 N」还是「长期平均不超过 N/60 每秒」。前者只能用滑动窗口后者才轮得到令牌桶。五、为什么「每分钟计数」不可信5.1 这句话本身就有歧义你看到的说法至少还要问清什么不问清会怎样每分钟 10 次按自然分钟算还是任意 60 秒算边界处可能放行两倍每秒 5 次是硬上限还是允许突发的平均值平均守住瞬时翻倍允许突发 10 次突发额度多久回填一次回填太快等于没限按 key 限速key 是账号、IP 还是 API key换个维度就绕过去了RFC 6585 在定义 429 时明确写了一件事规范不规定服务器怎么识别用户也不规定怎么计数请求可以按单个资源、整个服务器或一组服务器统计。也就是说计数口径从标准层面就是留给实现方决定的。你看到的「每分钟 N 次」只是一句措辞不是一份可执行的契约。5.2 计数窗口和统计窗口不是同一个东西限速器内部的计数器统计的是「它自己的窗口」你能观测到的流量统计的是「你的观测窗口」。两者只有在相位对齐时才重合。固定窗口在偏移时露出 2.00x本质就是这两套窗口错位。所以「限速器日志里每分钟都是 10 次」这句话什么都证明不了——它只是把自己的窗口又念了一遍。判据三验收限速器要用与实现无关的窗口也就是从放行记录里自己算任意 60 秒的最大值。这个统计量三种实现都能算不依赖任何内部计数器。这份资料是什么一份按「先跑通再排查」顺序整理的大模型学习路线图里面有一节专讲服务侧稳定性与调用治理该怎么拆步骤。放在资料包里扫码即可获取六、一次压测怎么证伪三个动作每个对准一种失败模式。目标不是「证明它没问题」是让它有机会出问题。动作具体做法看哪个数出现什么说明没守约错相位把压测起点从整分挪到半分钟或分多轮在不同时刻启动任意 60 秒最大放行比对齐时明显变大本文 10 → 20看滑窗最大记录每个放行时刻自己算任意 60 秒区间最大值滑窗最大值不是每分钟计数超过配额本文 2.00x看冷启动首秒冷启动后立刻以高于「配额/60」的速率灌入前若干秒的放行量一次放行接近配额上限满桶6.1 三个动作各自对准什么错相位对准固定窗口的边界缺陷。本文里 10 → 20 这一步是这一个动作单独造成的。看滑窗最大对准「计数对了但口径错了」。它既不需要知道对面用了哪种实现也不需要对端配合。看冷启动首秒对准令牌桶的满桶突发。本文 5 rps 的输入太低测不出这一项——这也是第一组数据里令牌桶看起来「首秒很乖」的原因。6.2 结果怎么读错相位后最大放行翻倍 → 对面是固定窗口或者固定窗口的变体按自然秒切分的计数也算。怎么错相位都在配额内、但冷启动瞬间能过一大批 → 对面是令牌桶且容量接近配额。两种都守得住 → 才谈得上「任意 60 秒不超过 N」。还有个容易踩的细节观察窗要长于一个窗口。只压 30 秒任何实现都不会超额因为一个窗口都还没走完。6.3 离散与连续复算时先对齐时钟真实网关跑在连续时间上不是本文这种 0.1 秒的离散步长。结论的量级不变但具体次数会随步长和到达分布变化得按自己的时间精度复算一遍。计时要用单调时钟。按 Python 官方文档time.monotonic()返回一个「不能倒退、且不受系统时钟调整影响」的时钟用墙上时钟的话一次校时或手工改时间就可能让窗口错乱甚至把一整格记录作废。这是模拟和真实实现之间第一个要改的地方。七、落到 AI 应用双口径与窗口边界7.1 上游通常不止一个口径调用大模型 API 时常见的是双口径每分钟请求数RPM数请求条数和每分钟 token 数TPM数 token 总量token 指模型把文本切成的片段。两本账各记各的任何一本超了都会被拒。具体限额各家不同、也会调整本文不写数值——截至 2026-10-22 我没有逐家核验过写了就是猜。上游口径客户端固定窗口的失配点边界处会怎样每分钟请求数边界处两个窗口各放满一次两秒内打出接近 2 倍的请求量每分钟 token 数请求条数守住了长 prompt 的 token 没算请求数没超token 先超并发连接数限速器管速率不管同时在飞几个响应变慢时并发堆高速率没超也被拒固定窗口在这里的麻烦是叠加的。平时请求稀疏边界效应看不出来一旦并发上来、失败后又有重放重试策略本身不在本文讨论范围只说它对流量的影响一次失败会变成两次请求请求就会在某个时刻堆起来。如果客户端限速器是固定窗口这堆请求正好压在窗口边界上就会在极短时间内打出接近 2 倍的量。结果是你自己的账显示合规上游的账显示你超了。然后连坐——不只超出的那几个被拒后面一段时间的请求也可能一起被拒。这时候如果按「我这边明明没超」去排查方向从一开始就是错的。7.2 三条能落地的改法把判定换成滑窗口径。要么直接用滑动窗口要么用令牌桶但把容量压到你能接受的最大突发量——容量等于配额就等于把一次配额全押在突发上。token 口径单独记。按字符数或 tokenizer 估出来的量和上游实际计数会有偏差留余量比精确计算更实际。被拒之后看Retry-After。429 响应可以带这个头RFC 6585 里写的是可以不是必须带了就按它等没带也别用固定间隔硬顶。7.3 收尾把限速写成滑窗口径回到标题那个问题固定窗口为什么会放行两倍因为它守的是「每一格不超过 10 个」而你以为它守的是「任意 60 秒不超过 10 个」。这两句话在相位对齐时数值一样在相位偏移时差一倍。三种实现没有哪一个「错」它们只是三份不同的契约。判断一个限速器是否守约不看它自己每分钟的计数看任意滑窗内的最大放行数。本文的实测把这句话压成了两组数字10 / 10 / 19和 20 / 10 / 19。同一批请求只挪了 30 秒。写文档时也一样把「每分钟 10 次」换成「任意 60 秒内不超过 10 次放行冷启动不预置额度」这句承诺才可验收。这份资料是什么在线可查的大模型知识库里面有 Agent Skills 与工程化落地的专题目录适合对照本文这种「一句话承诺怎么变成可验收指标」的写法。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表事实出处本文位置三组放行数字、超额倍数、首秒放行10/20、10/20、19/29偏移后 20/2.00x、10/1.00x、19/1.90x本机实测work/2026-10-22_限速实测.py日志work/2026-10-22_限速实测.logPython 3.13.14仅标准库 json 与 collections第 3、4 章实验参数配额 10 次/分钟、恒定 5.0 rps、600 个请求、120 秒、时间粒度 0.1 秒同上脚本常量TICK0.1、DURATION120.0、ARRIVAL_RATE5.0、QUOTA10、WINDOW60.03.1相位偏移实验为「请求整体后移 30.0 秒」同上脚本shifted [t 30.0 for t in reqs]3.3三种实现的判定逻辑按格子整除计数 / 时间戳队列 / 令牌补充同上脚本 FixedWindow、SlidingLog、TokenBucket 三个类2.1、2.2、2.3令牌桶 120 秒累计约 29.97 个令牌、放行 29 个理论上限 20容量 10 一窗口补充 10为本文推算由脚本常量推算10 119.8 × 10/60推算过程写在 4 章第 4 章429 的名称与用途Too Many Requests表示用户在给定时间内发送了过多请求速率限制RFC 6585《Additional HTTP Status Codes》IETF2012 年 4 月https://www.rfc-editor.org/rfc/rfc6585第 1 章、5.1该规范不规定服务器如何识别用户、也不规定如何计数请求Retry-After为可选同上5.1、7.2time.monotonic()返回不能倒退、不受系统时钟调整影响的时钟Python 3 文档 time 模块Python Software Foundationhttps://docs.python.org/3/library/time.html6.3本文为离散时间轴模拟不是真实网关压测结论是口径差异的量级不是某家产品的实测结果作者说明对应脚本 3.1 节的步长设定3.1、6.3上游按「每分钟请求数 每分钟 token 数」双口径限速属通用做法本文不引用任何厂商的具体限额数值作者说明截至 2026-10-22 未逐家核验限额故只写机制不写数值第 7 章「每分钟 N 次」可对应多种实现措辞需补齐口径才能验收本文实测第 3、4 章结合 RFC 6585 未定义计数方式这一条第 5 章、7.3附表 B术语速查表术语一句话解释在本文哪里用到限速器在一个时间范围里只放行固定数量请求的组件第 1 章开头固定窗口把时间切成等长格子、每格独立计数的限速实现1.1滑动窗口记录每次放行时间戳、只数最近一段时间的限速实现2.2令牌桶按固定速度往桶里补令牌、请求取到令牌才放行的实现2.3相位请求到达的时间起点相对于窗口起点的偏移量3.3滑窗最大放行把每个放行时刻当右端点往回数一段窗口取其中的最大条数3.1超额倍数滑窗最大放行数除以配额1.00x 表示刚好守住3.2突发极短时间内的集中放行令牌桶用容量换来的能力2.3、第 4 章冷启动服务刚起来、计数器与桶都处于初始状态的那一段2.1、第 4 章429HTTP 状态码含义是请求过多通常由对端限速触发第 1 章RPM每分钟请求数按请求条数计的口径7.1TPM每分钟 token 数按 token 总量计的口径7.1单调时钟只往前走、不受系统校时影响的计时方式6.3写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表