
版權(quán)與內(nèi)容來源聲明本文為原創(chuàng)整理。文中涉及官方文檔、開源倉庫、論文與公開報道的內(nèi)容均在附表 A 中標(biāo)注來源引用官方原文保持原樣不作改寫。文中命令、版本號與界面截圖以本文成文時的實測/核驗結(jié)果為準標(biāo)注「待驗證」的部分請以你本地環(huán)境實際輸出為判斷依據(jù)。本文不推薦任何不合規(guī)的軟件獲取方式也不對任何收益結(jié)果作承諾。轉(zhuǎn)載請注明出處。一、一個看起來合規(guī)的限速器限速器rate limiter是擋在業(yè)務(wù)前面的一道閘在一個時間范圍里只放行固定數(shù)量的請求超出的拒絕、排隊或者降級。它不負責(zé)讓系統(tǒng)變快只負責(zé)讓系統(tǒng)在被灌爆之前先自己認輸。上游返回 HTTP 429 時這個狀態(tài)碼的含義就是「請求過多」按 IETF 的 RFC 6585 定義用于表示用戶在給定時間內(nèi)發(fā)送了過多請求通常是對面那道閘在起作用。1.1 最省事的那種寫法固定窗口fixed window指的是把時間切成等長的格子每格獨立計數(shù)格子一換計數(shù)清零。它只需要一個整數(shù)不需要保存任何請求的時間戳是三種實現(xiàn)里最省內(nèi)存、也最好解釋的一種。下面這段摘自本次實跑腳本可以直接復(fù)跑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 它看自己永遠合規(guī)問題不在代碼在它能證明什么。這個計數(shù)器能回答的是「第 0 格里我放了 10 個、第 1 格里我放了 10 個」它回答不了「任意連續(xù) 60 秒里我最多放了多少個」。而配額這句話真正的含義是后者。把最壞情況擺出來第 59 秒進來 10 個全部放行第 61 秒又來 10 個也全部放行。這 20 個請求只隔了 2 秒?yún)s一次規(guī)則都沒違反——落在任何一格里的數(shù)量都沒超。每一分鐘都合規(guī)兩秒之內(nèi)卻打了雙倍。判據(jù)一限速器自己報的「本分鐘已用 N 次」不能當(dāng)作守約證據(jù)。它能證明計數(shù)沒寫錯證明不了放行口徑。要證明守約得換一個統(tǒng)計量。二、三種實現(xiàn)的判定差異2.1 一張對照表同樣是「每分鐘 10 次」三種常見實現(xiàn)的判定依據(jù)完全不同。實現(xiàn)判定依據(jù)需要記錄什么冷啟動初始狀態(tài)最壞情況本文實測固定窗口當(dāng)前格子的已用計數(shù) 配額一個整數(shù)按格子存格子一開就是滿額跨邊界時 2.00x滑動窗口最近 60 秒的放行條數(shù) 配額每次放行的時間戳隊列長度不超過配額隊列為空從 0 累積1.00x令牌桶桶里令牌 ≥ 1令牌數(shù) 上次補充時刻桶是滿的容量 配額1.90x突發(fā)額度這張表里最該盯的是第三列。「需不需要記錄時間戳」直接決定了這個限速器在分布式部署下能不能對齊只存一個整數(shù)的實現(xiàn)多副本之間各算各的存時間戳的實現(xiàn)至少還有可能把記錄共享出去。2.2 滑動窗口把時間戳排成隊滑動窗口sliding window log指的是記錄每一次放行的時間判定時先剔除已經(jīng)滑出窗口的記錄再看「剩下的還有幾條」。classSlidingLog:記錄每次放行時間戳檢查最近 60 秒內(nèi)放行數(shù)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關(guān)鍵在while self.log[0] t - WINDOW這一句判定之前先把「已經(jīng)滑出 60 秒」的記錄從隊頭彈掉于是隊列長度天然等于「最近 60 秒內(nèi)放行過幾次」。它不需要「格子」這個概念所以也沒有格子邊界。代價是內(nèi)存隊列長度最長等于配額看起來不多但這筆賬是跟著每個被限速的 key 走的。按賬號限速、按 IP 限速、按接口限速疊加起來就是配額乘以 key 的數(shù)量。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它把兩件事拆開了容量決定「能攢多少、能突發(fā)多少」突發(fā)指極短時間內(nèi)集中通過的一批請求補充速率決定「長期平均多快」。這是固定窗口和滑動窗口都沒有的自由度——也是它能被寫成「每分鐘 10 次」卻放出更多請求的原因。注意self.tokens float(QUOTA)桶一開始是滿的。這不算偷懶桶從空開始會讓冷啟動階段的請求全部被拒冷啟動指服務(wù)剛起來、計數(shù)器和桶都處于初始狀態(tài)的那一段。但滿桶啟動也意味著一件事——它開局就允許一次性通過 10 個請求。這一點在第 4 章會變成一個具體數(shù)字。三、實測對齊與偏移兩種相位3.1 實驗設(shè)置與統(tǒng)計口徑先把話說清楚這是離散時間軸上的模擬不是真實網(wǎng)關(guān)壓測。腳本把 120 秒切成 1200 個 0.1 秒的步長按恒定速率生成請求逐個喂給三個實現(xiàn)記錄每次判定結(jié)果。實驗參數(shù)配額 10 次/分鐘恒定 5 rps 灌入共 600 個請求、120 秒時間精度 0.1 秒Python 3.13.14只用標(biāo)準庫。統(tǒng)計量只有一個任意 60 秒滑窗內(nèi)的最大放行次數(shù)。做法是把每個放行時刻當(dāng)作右端點往回數(shù) 60 秒看這段區(qū)間里有幾次放行取所有位置里的最大值。defrun(policy,reqs):passed[tfortinreqsifpolicy.allow(t)]# 任意 60s 滑窗內(nèi)的最大放行數(shù)mx0j0foriinrange(len(passed)):whilepassed[i]-passed[j]WINDOW:j1mxmax(mx,i-j1)first_secsum(1fortinpassedift1.0)return{總放行:len(passed),任意60s窗口最大放行:mx,超額倍數(shù):round(mx/QUOTA,3),首秒放行:first_sec}注意是而不是恰好 60 秒之前的那次放行不算進當(dāng)前窗口也就是左閉右開。這個細節(jié)會影響恰好卡在邊界上的計數(shù)復(fù)算時不要改。3.2 對齊相位三種實現(xiàn)看不出差別請求從 t0 開始正好與窗口起點對齊。三種實現(xiàn)的「任意 60 秒最大放行」分別是 10、10、19——固定窗口和滑動窗口都等于配額1.00x令牌桶是 1.90x。120 秒總放行分別是 20、20、29。如果只做這一組實驗很容易得出結(jié)論「固定窗口和滑動窗口都對令牌桶因為允許突發(fā)所以多一點」。這個結(jié)論有一半是錯的固定窗口在這里看起來守約不是因為守約而是因為相位正好。這也正是大部分限速器上線前的驗證方式——從服務(wù)啟動開始壓壓一段時間看被拒的數(shù)量對不對。這種壓法恰好避開了固定窗口唯一會出問題的地方。3.3 偏移 30 秒固定窗口翻倍把請求整體后移 30 秒讓請求落在窗口中間、跨過邊界。相位phase指的就是「請求到達的時間起點相對于窗口起點的偏移」。完整的兩組數(shù)據(jù)放在同一張表里實現(xiàn)對齊·任意 60 秒最大放行對齊·超額倍數(shù)對齊·120 秒總放行偏移 30 秒·任意 60 秒最大放行偏移·超額倍數(shù)固定窗口101.00x20202.00x滑動窗口101.00x20101.00x令牌桶191.90x29191.90x固定窗口從 10 變成 20滑動窗口紋絲不動令牌桶也紋絲不動。同一批請求、同一個配額只挪了 30 秒。首秒放行三種實現(xiàn)都變成 0——請求從第 30 秒才開始本來就沒有首秒。為什么恰好是 2 倍第 30~32 秒這段先放滿 10 個第 60~62 秒又放滿 10 個兩次密集放行相隔約 30 秒都落得進同一個 60 秒窗口里。兩格各自合規(guī)合起來就是 20 個。# 邊界場景把請求相位平移半個窗口看固定窗口的最壞情況shifted[t30.0fortinreqs]forname,polin[(固定窗口,FixedWindow()),(滑動窗口,SlidingLog()),(令牌桶,TokenBucket())]:rrun(pol,shifted)print(f{name:8s}任意60s最大放行{r[任意60s窗口最大放行]:3d}超額{r[超額倍數(shù)]:.2f}x 首秒{r[首秒放行]})實測里最值得記的一句是固定窗口的超額只在請求跨窗口邊界時出現(xiàn)。對齊時它完全合規(guī)1.00x偏移半個窗口就變成 2.00x。也就是說這個缺陷不會在「從零開始壓測」里暴露只會在「真實流量的相位恰好」里暴露——而真實流量的相位不受你控制。四、令牌桶的冷啟動賬先看首秒。三種實現(xiàn)的首秒放行都是 5這一點第一眼很奇怪桶里攢著 10 個令牌為什么首秒也只放 5 個因為輸入速率就是 5 rps一秒只來了 5 個請求桶里的額度根本沒被吃滿。這是輸入側(cè)的限制不是限速器的功勞。實現(xiàn)初始可用額度實測首秒放行這個數(shù)說明什么固定窗口10格子開滿5被輸入速率卡住額度沒用完滑動窗口0隊列為空5同上但它是真的在累積令牌桶10滿桶5同上突發(fā)能力被低速率掩蓋了要看見桶的突發(fā)能力灌入速率必須高過補充速率。用低速率灌入永遠看不出這件事——這也是「我這邊的限速器很穩(wěn)」這類結(jié)論最常見的來源壓測的輸入太溫柔。再看總量。初始 10 個令牌加上 120 秒按 10/60 每秒補充的量到最后一個請求第 119.8 秒為止累計約 29.97 個令牌只夠放行 29 個整令牌。實測就是 29。另兩種實現(xiàn)都只有 20。同樣一句「每分鐘 10 次」令牌桶在 120 秒里多放了 9 個請求。這 9 個不算 bug。令牌桶的定義就是「允許突發(fā)、守住平均」多放的正是它被設(shè)計出來的行為。問題是如果你在文檔里承諾的是「每分鐘不超過 10 次」這 9 個就是違約——不是實現(xiàn)錯了是措辭和實現(xiàn)對不上。順便把上限算清容量 10 加上一個 60 秒窗口能補的 10 個任意 60 秒的理論上限是 20 次。實測 19 次差的那一次是離散步長和到達時刻湊出來的量級不變這一句是推算不是實測。判據(jù)二先想清楚要的是「任意 60 秒不超過 N」還是「長期平均不超過 N/60 每秒」。前者只能用滑動窗口后者才輪得到令牌桶。五、為什么「每分鐘計數(shù)」不可信5.1 這句話本身就有歧義你看到的說法至少還要問清什么不問清會怎樣每分鐘 10 次按自然分鐘算還是任意 60 秒算邊界處可能放行兩倍每秒 5 次是硬上限還是允許突發(fā)的平均值平均守住瞬時翻倍允許突發(fā) 10 次突發(fā)額度多久回填一次回填太快等于沒限按 key 限速key 是賬號、IP 還是 API key換個維度就繞過去了RFC 6585 在定義 429 時明確寫了一件事規(guī)范不規(guī)定服務(wù)器怎么識別用戶也不規(guī)定怎么計數(shù)請求可以按單個資源、整個服務(wù)器或一組服務(wù)器統(tǒng)計。也就是說計數(shù)口徑從標(biāo)準層面就是留給實現(xiàn)方?jīng)Q定的。你看到的「每分鐘 N 次」只是一句措辭不是一份可執(zhí)行的契約。5.2 計數(shù)窗口和統(tǒng)計窗口不是同一個東西限速器內(nèi)部的計數(shù)器統(tǒng)計的是「它自己的窗口」你能觀測到的流量統(tǒng)計的是「你的觀測窗口」。兩者只有在相位對齊時才重合。固定窗口在偏移時露出 2.00x本質(zhì)就是這兩套窗口錯位。所以「限速器日志里每分鐘都是 10 次」這句話什么都證明不了——它只是把自己的窗口又念了一遍。判據(jù)三驗收限速器要用與實現(xiàn)無關(guān)的窗口也就是從放行記錄里自己算任意 60 秒的最大值。這個統(tǒng)計量三種實現(xiàn)都能算不依賴任何內(nèi)部計數(shù)器。這份資料是什么一份按「先跑通再排查」順序整理的大模型學(xué)習(xí)路線圖里面有一節(jié)專講服務(wù)側(cè)穩(wěn)定性與調(diào)用治理該怎么拆步驟。放在資料包里掃碼即可獲取六、一次壓測怎么證偽三個動作每個對準一種失敗模式。目標(biāo)不是「證明它沒問題」是讓它有機會出問題。動作具體做法看哪個數(shù)出現(xiàn)什么說明沒守約錯相位把壓測起點從整分挪到半分鐘或分多輪在不同時刻啟動任意 60 秒最大放行比對齊時明顯變大本文 10 → 20看滑窗最大記錄每個放行時刻自己算任意 60 秒?yún)^(qū)間最大值滑窗最大值不是每分鐘計數(shù)超過配額本文 2.00x看冷啟動首秒冷啟動后立刻以高于「配額/60」的速率灌入前若干秒的放行量一次放行接近配額上限滿桶6.1 三個動作各自對準什么錯相位對準固定窗口的邊界缺陷。本文里 10 → 20 這一步是這一個動作單獨造成的??椿白畲髮省赣嫈?shù)對了但口徑錯了」。它既不需要知道對面用了哪種實現(xiàn)也不需要對端配合??蠢鋯邮酌雽柿钆仆暗臐M桶突發(fā)。本文 5 rps 的輸入太低測不出這一項——這也是第一組數(shù)據(jù)里令牌桶看起來「首秒很乖」的原因。6.2 結(jié)果怎么讀錯相位后最大放行翻倍 → 對面是固定窗口或者固定窗口的變體按自然秒切分的計數(shù)也算。怎么錯相位都在配額內(nèi)、但冷啟動瞬間能過一大批 → 對面是令牌桶且容量接近配額。兩種都守得住 → 才談得上「任意 60 秒不超過 N」。還有個容易踩的細節(jié)觀察窗要長于一個窗口。只壓 30 秒任何實現(xiàn)都不會超額因為一個窗口都還沒走完。6.3 離散與連續(xù)復(fù)算時先對齊時鐘真實網(wǎng)關(guān)跑在連續(xù)時間上不是本文這種 0.1 秒的離散步長。結(jié)論的量級不變但具體次數(shù)會隨步長和到達分布變化得按自己的時間精度復(fù)算一遍。計時要用單調(diào)時鐘。按 Python 官方文檔time.monotonic()返回一個「不能倒退、且不受系統(tǒng)時鐘調(diào)整影響」的時鐘用墻上時鐘的話一次校時或手工改時間就可能讓窗口錯亂甚至把一整格記錄作廢。這是模擬和真實實現(xiàn)之間第一個要改的地方。七、落到 AI 應(yīng)用雙口徑與窗口邊界7.1 上游通常不止一個口徑調(diào)用大模型 API 時常見的是雙口徑每分鐘請求數(shù)RPM數(shù)請求條數(shù)和每分鐘 token 數(shù)TPM數(shù) token 總量token 指模型把文本切成的片段。兩本賬各記各的任何一本超了都會被拒。具體限額各家不同、也會調(diào)整本文不寫數(shù)值——截至 2026-10-22 我沒有逐家核驗過寫了就是猜。上游口徑客戶端固定窗口的失配點邊界處會怎樣每分鐘請求數(shù)邊界處兩個窗口各放滿一次兩秒內(nèi)打出接近 2 倍的請求量每分鐘 token 數(shù)請求條數(shù)守住了長 prompt 的 token 沒算請求數(shù)沒超token 先超并發(fā)連接數(shù)限速器管速率不管同時在飛幾個響應(yīng)變慢時并發(fā)堆高速率沒超也被拒固定窗口在這里的麻煩是疊加的。平時請求稀疏邊界效應(yīng)看不出來一旦并發(fā)上來、失敗后又有重放重試策略本身不在本文討論范圍只說它對流量的影響一次失敗會變成兩次請求請求就會在某個時刻堆起來。如果客戶端限速器是固定窗口這堆請求正好壓在窗口邊界上就會在極短時間內(nèi)打出接近 2 倍的量。結(jié)果是你自己的賬顯示合規(guī)上游的賬顯示你超了。然后連坐——不只超出的那幾個被拒后面一段時間的請求也可能一起被拒。這時候如果按「我這邊明明沒超」去排查方向從一開始就是錯的。7.2 三條能落地的改法把判定換成滑窗口徑。要么直接用滑動窗口要么用令牌桶但把容量壓到你能接受的最大突發(fā)量——容量等于配額就等于把一次配額全押在突發(fā)上。token 口徑單獨記。按字符數(shù)或 tokenizer 估出來的量和上游實際計數(shù)會有偏差留余量比精確計算更實際。被拒之后看Retry-After。429 響應(yīng)可以帶這個頭RFC 6585 里寫的是可以不是必須帶了就按它等沒帶也別用固定間隔硬頂。7.3 收尾把限速寫成滑窗口徑回到標(biāo)題那個問題固定窗口為什么會放行兩倍因為它守的是「每一格不超過 10 個」而你以為它守的是「任意 60 秒不超過 10 個」。這兩句話在相位對齊時數(shù)值一樣在相位偏移時差一倍。三種實現(xiàn)沒有哪一個「錯」它們只是三份不同的契約。判斷一個限速器是否守約不看它自己每分鐘的計數(shù)看任意滑窗內(nèi)的最大放行數(shù)。本文的實測把這句話壓成了兩組數(shù)字10 / 10 / 19和 20 / 10 / 19。同一批請求只挪了 30 秒。寫文檔時也一樣把「每分鐘 10 次」換成「任意 60 秒內(nèi)不超過 10 次放行冷啟動不預(yù)置額度」這句承諾才可驗收。這份資料是什么在線可查的大模型知識庫里面有 Agent Skills 與工程化落地的專題目錄適合對照本文這種「一句話承諾怎么變成可驗收指標(biāo)」的寫法。放在資料包里掃碼即可獲取附表 A本文引用事實與出處對照表事實出處本文位置三組放行數(shù)字、超額倍數(shù)、首秒放行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僅標(biāo)準庫 json 與 collections第 3、4 章實驗參數(shù)配額 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三種實現(xiàn)的判定邏輯按格子整除計數(shù) / 時間戳隊列 / 令牌補充同上腳本 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表示用戶在給定時間內(nèi)發(fā)送了過多請求速率限制RFC 6585《Additional HTTP Status Codes》IETF2012 年 4 月https://www.rfc-editor.org/rfc/rfc6585第 1 章、5.1該規(guī)范不規(guī)定服務(wù)器如何識別用戶、也不規(guī)定如何計數(shù)請求Retry-After為可選同上5.1、7.2time.monotonic()返回不能倒退、不受系統(tǒng)時鐘調(diào)整影響的時鐘Python 3 文檔 time 模塊Python Software Foundationhttps://docs.python.org/3/library/time.html6.3本文為離散時間軸模擬不是真實網(wǎng)關(guān)壓測結(jié)論是口徑差異的量級不是某家產(chǎn)品的實測結(jié)果作者說明對應(yīng)腳本 3.1 節(jié)的步長設(shè)定3.1、6.3上游按「每分鐘請求數(shù) 每分鐘 token 數(shù)」雙口徑限速屬通用做法本文不引用任何廠商的具體限額數(shù)值作者說明截至 2026-10-22 未逐家核驗限額故只寫機制不寫數(shù)值第 7 章「每分鐘 N 次」可對應(yīng)多種實現(xiàn)措辭需補齊口徑才能驗收本文實測第 3、4 章結(jié)合 RFC 6585 未定義計數(shù)方式這一條第 5 章、7.3附表 B術(shù)語速查表術(shù)語一句話解釋在本文哪里用到限速器在一個時間范圍里只放行固定數(shù)量請求的組件第 1 章開頭固定窗口把時間切成等長格子、每格獨立計數(shù)的限速實現(xiàn)1.1滑動窗口記錄每次放行時間戳、只數(shù)最近一段時間的限速實現(xiàn)2.2令牌桶按固定速度往桶里補令牌、請求取到令牌才放行的實現(xiàn)2.3相位請求到達的時間起點相對于窗口起點的偏移量3.3滑窗最大放行把每個放行時刻當(dāng)右端點往回數(shù)一段窗口取其中的最大條數(shù)3.1超額倍數(shù)滑窗最大放行數(shù)除以配額1.00x 表示剛好守住3.2突發(fā)極短時間內(nèi)的集中放行令牌桶用容量換來的能力2.3、第 4 章冷啟動服務(wù)剛起來、計數(shù)器與桶都處于初始狀態(tài)的那一段2.1、第 4 章429HTTP 狀態(tài)碼含義是請求過多通常由對端限速觸發(fā)第 1 章RPM每分鐘請求數(shù)按請求條數(shù)計的口徑7.1TPM每分鐘 token 數(shù)按 token 總量計的口徑7.1單調(diào)時鐘只往前走、不受系統(tǒng)校時影響的計時方式6.3寫在最后這篇用到的資料寫這篇文章時把相關(guān)的官方文檔和源碼又翻了一遍順手也整理了幾份配套的東西大模型學(xué)習(xí)路線圖從零基礎(chǔ)到能自己動手做 Agent按階段說明每一步該學(xué)什么、哪些可以先跳過《LangChain LangGraph MCP 智能體開發(fā)實戰(zhàn)》視頻課7 個模塊從私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知識庫在線可查Agent Skills 從入門到落地、Claude Skills 完全指南等專題按目錄瀏覽即可640 套 AI 大模型行業(yè)報告 經(jīng)典 PDF 書籍看行業(yè)落地案例和別人怎么做的時候用得上大模型零基礎(chǔ)到精通教學(xué)視頻跟著敲一遍比只讀文檔快得多資料是我自己整理的放在下面這個碼上掃碼即可獲取添加時備注「AI」優(yōu)先通過。資料按「先路線、再動手、最后查漏」的順序整理好了建議先看學(xué)習(xí)路線那一份照著它挑一條適合自己當(dāng)前基礎(chǔ)的路徑再往下看。