限流算法實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"限流簡介"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"什麼是限流"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在不同場景下限流的定義也各不相同,可以是每秒請求數、每秒事務處理數、網絡流量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我們所說的限流指的是限制到達系統併發請求數,使得系統能夠正常的處理部分用戶的請求,來保證系統的穩定性。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"爲什麼限流"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接口無法控制調用方的行爲。熱點業務突發請求、惡意請求攻擊等會帶來瞬時的請求量激增,導致服務佔用大量的 CPU、內存等資源,使得其他正常的請求變慢或超時,甚至引起服務器宕機。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照請求次數進行收費的接口需要根據客戶支付的金額來限制客戶可用的次數。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"限流的行爲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"限流的行爲指的就是在接口的請求數達到限流的條件時要觸發的操作,一般可進行以下行爲。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拒絕服務:把多出來的請求拒絕掉"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務降級:關閉或是把後端服務做降級處理。這樣可以讓服務有足夠的資源來處理更多的請求"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"特權請求:資源不夠了,我只能把有限的資源分給重要的用戶"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"延時處理:一般會有一個隊列來緩衝大量的請求,這個隊列如果滿了,那麼就只能拒絕用戶了,如果這個隊列中的任務超時了,也要返回系統繁忙的錯誤了"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"彈性伸縮:用自動化運維的方式對相應的服務做自動化的伸縮"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"限流架構"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"單點限流"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/04\/048a60ba7145d0526efcafe24f6bfced.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們的系統應用只部署在一個節點上來提供服務時,就可以採用單點限流的架構來對應用的接口進行限流,只要單點應用進行了限流,那麼他所依賴的各種服務也得到了保護。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"分佈式限流"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了提供高性能的服務,往往我們的應用都是以集羣結構部署在多個節點上的。這時候單點限流只能限制傳入單個節點的請求,保護自身節點,無法保護應用依賴的各種服務資源。那麼如果在集羣中的每個節點上都進行單點限流是否可行呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c2\/c2102260f1c8d48eedbc6f5ca378db3d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們的應用集羣中有三個節點,爲了保護應用依賴的資源我們限制資源每秒最大請求數爲300個,如果超過這個限制那麼資源將因過載導致不再可用。這樣分配到集羣中的每個應用節點的每秒最大請求數爲100個纔可以滿足保護資源的要求,超過100則拒絕服務提示業務繁忙。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如某一秒內有300個請求打到應用集羣,應用集羣再去請求所依賴的資源服務,是滿足資源服務每秒300個最大請求數的限制的,所以這些請求都能夠得到處理並且正常返回。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,如果因爲種種原因負載均衡調度器把這 300 個請求中的 50 個分配給了節點1,50 個分配給了節點2,剩餘 200 個分配給了節點3。因爲我們之前限制每個節點的每秒最大請求數爲 100,所以就會出現節點1的50個請求全部正常返回、節點2上的 50 個請求全部正常返回,而節點三上的200個請求只有100個正常返回,另外100個被拒絕服務。這種集羣中每個節點都進行單點限流的方式顯然不能滿足我們的業務需要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以使用基於各種中間件的分佈式限流來解決集羣結構下應用限流不準確的問題。將限流的配置以及通過整個集羣的請求數都保存在中間件中,然後通過計算來判斷是否達到限流行爲的觸發條件。分佈式限流可以統一地限制整個集羣的流量,整個集羣的請求數得到了限制,那麼集羣所依賴的資源服務也就得到了保障。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3a\/3a3216134e0b5b5e01a1ef995006aa3f.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"限流算法"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"固定窗口計數器"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/98\/983208010921718643307d0a00c98ed5.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將時間按照設定的週期劃分爲多個窗口"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在當前時間窗口內每來一次請求就將計數器加一"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果計數器超過了限制數量,則拒絕服務"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當時間到達下一個窗口時,計數器的值重置"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種算法很好實現,但是會出現限流不準確的問題,例如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/53\/53b6b380f936724351aa69bab345b3d7.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設限制每秒通過5個請求,時間窗口的大小爲1秒,當前時間窗口週期內的後半秒正常通過了5個請求,下一個時間窗口週期內的前半秒正常通過了5個請求,在這兩個窗口內都沒有超過限制。但是在這兩個窗口的中間那一秒實際上通過了 10 個請求,顯然不滿足每秒5個請求的限制。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"滑動窗口計數器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/4b\/4b1fdfdcbc10d92505b56f3c78b876e3.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將設定的時間週期設爲滑動窗口的大小,記錄每次請求的時刻"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當有新的請求到來時將窗口滑到該請求來臨的時刻"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"判斷窗口內的請求數是否超過了限制,超過限制則拒絕服務,否則請求通過"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"丟棄滑動窗口以外的請求"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種算法解決了固定窗口計數器出現的通過請求數是限制數兩倍的缺陷,但是實現起來較爲複雜,並且需要記錄窗口週期內的請求,如果限流閾值設置過大,窗口週期內記錄的請求就會很多,就會比較佔用內存"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"漏桶算法"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ad\/ada50f7641c3179e481de16a56a4497e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將進來的請求流量視爲水滴先放入桶內"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"水從桶的底部以固定的速率勻速流出,相當於在勻速處理請求"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當漏桶內的水滿時(超過了限流閾值)則拒絕服務"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個算法可以比較平滑均勻的限制請求,Nginx 中的 limit_req 模塊的底層實現就是用的這種算法,具體可參考【NGINX和NGINX Plus的速率限制】(https:\/\/www.nginx.com\/blog\/rate-limiting-nginx)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是漏桶算法也有一定的缺陷,因爲水從桶的底部以固定的速率勻速流出,當有在服務器可承受範圍內的瞬時突發請求進來,這些請求會被先放入桶內,然後再勻速的進行處理,這樣就會造成部分請求的延遲。所以他無法應對在限流閾值範圍內的突發請求。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"令牌桶算法"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bc\/bc5ab2d15002268f09faf860d5ba2a9f.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照一定的速率生產令牌並放入令牌桶中"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果桶中令牌已滿,則丟棄令牌"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"請求過來時先到桶中拿令牌,拿到令牌則放行通過,否則拒絕請求"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種算法能夠把請求均勻的分配在時間區間內,又能接受服務可承受範圍內的突發請求。所以令牌桶算法在業內使用也非常廣泛。接下來會詳細介紹該算法的實現。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"令牌桶算法實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們採用 Redis + Lua 腳本的方式來實現令牌桶算法,在 Redis 中使用 Lua 腳本有諸多好處,例如:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"減少網絡開銷:本來多次網絡請求的操作,可以用一個請求完成,原先多次請求的邏輯放在 Redis 服務器上完成。使用腳本,減少了網絡往返時延。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原子操作:Redis會將整個腳本作爲一個整體執行,中間不會被其他進程或者進程的命令插入。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複用:客戶端發送的腳本會永久存儲在Redis中,意味着其他客戶端可以複用這一腳本而不需要使用代碼完成同樣的邏輯。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複用:客戶端發送的腳本會永久存儲在Redis中,意味着其他客戶端可以複用這一腳本而不需要使用代碼完成同樣的邏輯。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這其中最重要的方法就是原子操作。將 Redis 的多條命令寫成一個 Lua 腳本,然後調用腳本執行操作,相當於只有一條執行腳本的命令,所以整個 Lua 腳本中的操作都是原子性的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Redis 中使用 Lua 腳本主要涉及 "},{"type":"codeinline","content":[{"type":"text","text":"Script Load"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"Eval"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"Evalsha"}]},{"type":"text","text":" 三個命令:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Eval ${lua_script}"}]},{"type":"text","text":" 可以直接執行 Lua 腳本。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Script Load ${lua_script}"}]},{"type":"text","text":" 命令是將腳本載入 Redis,載入成功後會返回一個腳本的sha1值,一旦載入則永久存儲在 Redis 中,後續可以通過 "},{"type":"codeinline","content":[{"type":"text","text":"Evalsha ${sha1}"}]},{"type":"text","text":" 來直接調用此腳本。我們採用先 Load 腳本得到 Sha1 值,再調用這個 sha1 值來執行腳本的方式可以減少像"},{"type":"codeinline","content":[{"type":"text","text":"eval ${lua_script}"}]},{"type":"text","text":" 命令這樣每次都向 Redis 中發送一長串 Lua 腳本帶來的網絡開銷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Redis 中的 Hash 數據結構來存儲限流配置,每個 Hash 表的 Key 爲限流的粒度,可以是接口Uri、客戶端 IP、應用uuid或者他們的組合形式。每個 Hash 表爲一個令牌桶,Hash 表中包含如下字段:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"last_time"}]},{"type":"text","text":" 最近一次請求的時間戳,毫秒級別。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"curr_permits"}]},{"type":"text","text":" 當前桶內剩餘令牌數量,單位爲:個。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"bucket_cap"}]},{"type":"text","text":" 桶的容量,即桶內可容納最大令牌數量,代表限流時間週期內允許通過的最大請求數。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"period"}]},{"type":"text","text":" 限流的時間週期,單位爲:秒。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"rate"}]},{"type":"text","text":" 令牌產生的速率,單位:個\/秒,"},{"type":"codeinline","content":[{"type":"text","text":"rate = bucket_cap \/ period"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的令牌桶算法描述中生產令牌的方式是按照一定的速率生產令牌並放入令牌桶中,這種方式需要一個線程不停地按照一定的速率生產令牌並更新相應的桶,如果被限流的接口(每個桶)令牌生產的速率都不一樣,那麼就需要開多個線程,很浪費資源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了提高系統的性能,減少限流層的資源消耗,我們將令牌的生產方式改爲:"},{"type":"text","marks":[{"type":"strong"}],"text":"每次請求進來時一次性生產上一次請求到本次請求這一段時間內的令牌"},{"type":"text","text":"。隨意每次請求生成的令牌數就是 "},{"type":"codeinline","content":[{"type":"text","text":"(curr_time -last_time) \/ 1000 * rate"}]},{"type":"text","text":",注意:這裏兩次時間戳的差值單位是毫秒,而令牌產生速率的單位是 個\/秒,所以要除以 1000,把時間戳的差值的單位也換算成秒。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"令牌桶算法的實現邏輯爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/dd\/ddf70b559052761bb0454c161c745d7c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如我們的限流策略是一分鐘內最多能通過600個請求,那麼相應的令牌產生速率爲 600 \/ 60 = 10 (個\/秒) 。那麼當限流策略剛剛配置好這一時刻就有突發的10個請求進來,此時令牌桶內還沒來的及生產令牌,所以請求拿不到令牌就會被拒絕,這顯然不符合我們要求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了解決這一問題,我們在限流策略剛剛配置好後的第一個請求來臨時將當前可用令牌的值設置爲桶的最大容量 600,將最近一次請求時間設置爲本次請求來臨時一分鐘後的時間戳,減去出本次請求需要的令牌後更新桶。這樣,在這一分鐘以內,有下一次請求進來時,從 Hash 表內取出配置計算當前時間就會小於最近一次請求的時間,隨後計算生成的令牌就會是一個小於0的負數。所以在更新桶這一步,要根據生成的令牌是否爲負數來決定是否更新最後一次請求時間的值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用 Lua 腳本實現上述邏輯:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nlocal key = KEYS[1] -- 要進行限流的Key,可以是 uri\nlocal consume_permits = tonumber(ARGV[1]) -- 請求消耗的令牌數,每個請求消耗一個\nlocal curr_time = tonumber(ARGV[2]) -- 當前時間\n\nlocal limiter_info = redis.pcall(\"HMGET\", key, \"last_time\", \"curr_permits\", \"bucket_cap\", \"rate\", \"period\")\nif not limiter_info[3] then\n return -1\nend\nlocal last_time = tonumber(limiter_info[1]) or 0\nlocal curr_permits = tonumber(limiter_info[2]) or 0\nlocal bucket_cap = tonumber(limiter_info[3]) or 0\nlocal rate = tonumber(limiter_info[4]) or 0\nlocal period = tonumber(limiter_info[5]) or 0\n\nlocal total_permits = bucket_cap\nlocal is_update_time = true\nif last_time > 0 then\n local new_permits = math.floor((curr_time-last_time)\/1000 * rate)\n if new_permits <= 0 then\n new_permits = 0\n is_update_time = false\n end\n\n total_permits = new_permits + curr_permits\n if total_permits > bucket_cap then\n total_permits = bucket_cap\n end\nelse\n last_time = curr_time + period * 1000\nend\n\nlocal res = 1\nif total_permits >= consume_permits then\n total_permits = total_permits - consume_permits\nelse\n res = 0\nend\n\nif is_update_time then\n redis.pcall(\"HMSET\", key, \"curr_permits\", total_permits, \"last_time\", curr_time)\nelse\n redis.pcall(\"HSET\", key, \"curr_permits\", total_permits)\nend\nreturn res"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述腳本在調用時接收三個參數,分別爲:限流的key、請求消耗的令牌數、 當前時間戳(毫秒級別)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的業務代碼中,先調用 Redis 的 "},{"type":"codeinline","content":[{"type":"text","text":"SCRIPT LOAD"}]},{"type":"text","text":" 命令將上述腳本 Load 到 Redis 中並將該命令返回的腳本 sha1 值保存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在後續的請求進來時,調用 Redis 的"},{"type":"codeinline","content":[{"type":"text","text":" EVALSHA"}]},{"type":"text","text":" 命令執行限流邏輯,根據返回值判斷是否對本次請求觸發限流行爲。假如限流的 key 爲每次請求的 uri,每次請求消耗 1 個令牌,那麼執行 Evalsha 命令進行限流判斷的具體操作爲:"},{"type":"codeinline","content":[{"type":"text","text":"EVALSHA ${sha1} 1 ${uri} 1 ${當前時間戳} "}]},{"type":"text","text":"(第一個數字 1 代表腳本可接收的參數中有 1 個Key,第二個數字 1 代表本次請求消耗一個令牌);執行完這條命令後如果返回值是 1 代表桶中令牌夠用,請求通過;如果返回值爲 0 代表桶中令牌不夠,觸發限流;如果返回值爲 -1 代表本次請求的 uri 未配置限流策略,可根據自己的實際業務場景判斷是通過還是拒絕。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文主要介紹了四種限流的算法,分別爲:固定窗口計數器算法、滑動窗口計數算法、漏桶算法、令牌桶算法。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"固定窗口計數算法簡單易實現,其缺陷是可能在中間的某一秒內通過的請求數是限流閾值的兩倍,該算法僅適用於對限流準確度要求不高的應用場景。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"滑動窗口計數算法解決了固定窗口計數算法的缺陷,但是該算法較難實現,因爲要記錄每次請求所以可能出現比較佔用內存比較多的情況。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"漏桶算法可以做到均勻平滑的限制請求,Ngixn 熱 limit_req 模塊也是採用此種算法。因爲勻速處理請求的緣故所以該算法應對限流閾值內的突發請求無法及時處理。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"令牌桶算法解決了以上三個算法的所有缺陷,是一種相對比較完美的限流算法,也是限流場景中應用最爲廣泛的算法。使用 Redis + Lua腳本的方式可以簡單的實現。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"參考鏈接"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.nginx.com\/blog\/rate-limiting-nginx\/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.infoq.cn\/article\/qg2tx8fyw5vt-f3hh673"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/segmentfault.com\/a\/1190000019676878"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/en.wikipedia.org\/wiki\/Token_bucket"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:360技術(ID:qihoo_tech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/HCo2ILhq2-Psc5nwcfvD5g","title":"xxx","type":null},"content":[{"type":"text","text":"限流算法實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章