dubbo-go 中的 TPS Limit 設計與實現
前言
Apache Dubbo 是由阿里開源的一個RPC框架,除了基本的 RPC 功能以外,還提供了一整套的服務治理相關功能。目前它已經是 Apache 基金會下的頂級專案。
而 dubbo-go 則是 Dubbo 的 Go 語言實現。
最近在 dubbo-go 的 todo list 上發現,它還沒有實現 TPS Limit 的模組,於是就抽空實現了這個部分。
TPS limit 實際上就是限流,比如說限制一分鐘內某個介面只能訪問 200 次,超過這個次數,則會被拒絕服務。在 Dubbo 的 Java 版本上,只有一個實現,就是 DefaultTPSLimiter 。
DefaultTPSLimiter 是在服務級別上進行限流。雖然 Dubbo 的官方文件裡面聲稱可以在 method 級別上進行限流,但是我看了一下它的原始碼,實際上這個是做不到的。當然,如果自己透過實現 Filter 介面來實現 method 級別的限流,那麼自然是可以的——這樣暴露了 Dubbo Java 版本實現的另外一個問題,就是 Dubbo 的 TpsLimitFilter 實現,是不允許接入自己 TpsLimiter 的實現的。這從它的原始碼也可以看出來:
它直接寫死了 TpsLimiter 的實現。
這個實現的目前只是合併到了 develop 上,等下次釋出正式版本的時候才會釋出出來。
設計思路
於是我大概參考了一下 Dubbo 已有的實現,做了一點改進。
Dubbo 裡面的核心抽象是 TpsLimiter 介面。 TpsLimitFilter 只是簡單呼叫了一下這個介面的方法而已:
這個抽象是很棒的。但是還欠缺了一些抽象。
實際上,一個 TPS Limit 就要解決三個問題:
- 對什麼東西進行 limit 。比如說,對服務進行限流,或者對某個方法進行限流,或者對IP進行限流,或者對使用者進行限流;
- 如何判斷已經 over limitation 。這是從演算法層面上考慮,即用什麼演算法來判斷某個呼叫進來的時候,已經超過配置的上限了;
- 被拒絕之後該如何處理。如果一個請求被斷定為已經 over limititation 了,那麼該怎麼處理;
所以在 TpsLimiter 介面的基礎上,我再加了兩個抽象:
TpsLimiter
TpsLimitStrategy
RejectedExecutionHandler
TpsLimiter 對應到 Java 的 TpsLimiter ,兩者是差不多。在我的設想裡面,它既是頂級入口,還需要承擔解決第一個問題的職責。
而 TpsLimitStrategy 則是第二個問題的抽象的介面定義。它代表的是純粹的演算法。該介面完全沒有引數,實際上,所有的實現需要維護自身的狀態——對於大部分實現而言,它大概只需要獲取一下系統時間戳,所以不需要引數。
最後一個介面 RejectedExecutionHandler 代表的是拒絕策略。在 TpsLimitFilter 裡面,如果它呼叫 TpsLimiter 的實現,發現該請求被拒絕,那麼就會使用該介面的實現來獲取一個返回值,返回給客戶端。
實現
其實實現沒太多好談的。不過有一些微妙的地方,我雖然在程式碼裡面註釋了,但是我覺得在這裡再多說一點也是可以的。
首先提及的就是拒絕策略 RejectedExecutionHandler ,我就是提供了一種實現,就是隨便 log 了一下,什麼都沒做。因為這個東西是強業務相關的,我也不能提供更加多的通用的實現。
方法與服務雙重支援的 TpsLimiter
TpsLimiter 我只有一個實現,那就是 MethodServiceTpsLimiterImpl 。它就是根據配置,如果方法級別配置了引數,那麼會在方法級別上進行限流。否則,如果在服務級別( ServiceKey )上有配置,那麼會在服務級別進行限流。
舉個最複雜的例子:服務 A 限制 100 ,有四個方法,方法 M1 配置限制 40 ,方法 M2 和方法 M3 無配置,方法M4配置限制 -1 :那麼方法 M1 會單獨限流 40 ; M2 和 M3 合併統計,被限制在 100 ;方法 M4 則會被忽略。
使用者可以配置具體的演算法。比如說使用我接下來說的,我已經實現的三種實現。
FixedWindow 和 ThreadSafeFixedWindow
FixedWindow 直接對應到 Java 的 DefaultTpsLimiter 。它採用的是 fixed-window 演算法:比如說配置了一分鐘內只能呼叫 100 次。假如從 00:00 開始計時,那麼 00:00-01:00 內,只能呼叫 100 次。只有到達 01:00 ,才會開啟新的視窗 01:00-02:00 。如圖:
Fixed-Window圖示
Fixed-Window實現
這裡有一個很有意思的地方。就是這個實現,是一個幾乎執行緒安全但是其實並不是執行緒安全的實現。
在所有的實現裡面,它是最為簡單,而且效能最高的。我在衡量了一番之後,還是沒把它做成執行緒安全的。事實上, Java 版本的也不是執行緒安全的。
它只會在多個執行緒透過第 67 行的檢測之後,才會出現併發問題,這個時候就不是執行緒安全了。但是在最後的 return 語句中,那一整個是執行緒安全的。它因為不斷計數往上加,所以多個執行緒同時跑到這裡,其實不會有什麼問題。
現在我要揭露一個最為奇詭的特性了:併發越高,那麼這個 race condition 就越嚴重,也就是說越不安全。
但是從實際使用角度而言,有極端 TPS 的還是比較少的。對於那些 TPS 只有幾百每秒的,是沒什麼問題的。
為了保持和 Dubbo 一致的特性,我把它作為預設的實現。
此外,我還為它搞了一個執行緒安全版本,也就是
ThreadSafeFixedWindowTpsLimitStrategyImpl ,只是簡單的用 sync 封裝了一下,可以看做是一個 Decorator 模式的應用。
如果強求執行緒安全,可以考慮使用這個。
SlidingWindow
這是我比較喜歡的實現。它跟網路協議裡面的滑動視窗演算法在理念上是比較接近的。
具體來說,假如我設定的同樣是一分鐘 1000 次,它統計的永遠是從當前時間點往前回溯一分鐘內,已經被呼叫了多少次。如果這一分鐘內,呼叫次數沒超過 1000 ,請求會被處理,如果已經超過,那麼就會拒絕。
我再來描述一下, SldingWindow 和 FixedWindow 兩種演算法的區別。這兩者很多人會搞混。假如當前的時間戳是 00:00 ,兩個演算法同時收到了第一個請求,開啟第一個時間視窗。
那麼 FixedWindow 就是 00:00-01:00 是第一個視窗,接下來依次是 01:00-02:00 , 02:00-03:00 , ...。當然假如說 01:00 之後的三十秒內都沒有請求,在 01:31 又來了一個請求,那麼時間視窗就是 01:31-02:31 。
而 SildingWindow 則沒有這種概念。假如在 01:30 收到一個請求,那麼 SlidingWindow 統計的則是 00:30-01:30 內有沒有達到 1000 次。它永遠計算的都是接收到請求的那一刻往前回溯一分鐘的請求數量。
如果還是覺得有困難,那麼簡單來說就是 FixedWindow 往後看一分鐘, SlidingWindow 回溯一分鐘。
這個說法並不嚴謹,只是為了方便理解。
在真正寫這個實現的時候,我稍微改了一點點:
我用了一個佇列來儲存每次訪問的時間戳。一般的寫法,都是請求進來,先把已經不在視窗時間內的時間戳刪掉,然後統計剩下的數量,也就是後面的 slow path 的那一堆邏輯。
但是我改了的一點是,我進來直接統計佇列裡面的數量——也就是請求數量,如果都小於上限,那麼我可以直接返回 true ,即 quick path 。
這種改進的核心就是:我只有在檢測到當前佇列裡面有超過上限數量的請求數量時候,才會嘗試刪除已經不在視窗內的時間戳。
這其實就是,是每個請求過來,我都清理一下佇列呢?還是隻有佇列元素超出數量了,我才清理呢?我選擇的是後者。
我認為這是一種改進……當然從本質上來說,整體開銷是沒有減少的——因為 golang 語言裡面 List 的實現,一次多刪除幾個,和每次刪除一個,多刪幾次,並沒有多大的區別。
演算法總結
無論是 FixedWindow 演算法還是 SlidingWindow 演算法都有一個固有的缺陷,就是這個時間視窗難控制。
我們設想一下,假如說我們把時間視窗設定為一分鐘,允許 1000 次呼叫。然而,在前十秒的時候就呼叫了 1000 次。在後面的五十秒,伺服器雖然將所有的請求都處理完了,然是因為視窗還沒到新視窗,所以這個時間段過來的請求,全部會被拒絕。
解決的方案就是調小時間視窗,比如調整到一秒。但是時間視窗的縮小,會導致 FixedWindow 演算法的 race condition 情況加劇。
那些沒有實現的
基於特定業務物件的限流
舉例來說,某些特殊業務用的針對使用者 ID 進行限流和針對 IP 進行限流,我就沒有在 dubbo-go 裡面實現。有需要的可以透過實現 TpsLimiter 介面來完成。
全域性 TPS limit
這篇文章之前討論的都是單機限流。如果全侷限流,比如說針對某個客戶,它購買的服務是每分鐘呼叫 100 次,那麼就需要全侷限流——雖然這種 case 都不會用 Filter 方案,而是另外做一個 API 接入控制。
比如說,很常用的使用 Redis 進行限流的。針對某個客戶,一分鐘只能訪問 100 次,那我就用客戶 ID 做 key , value 設定成 List ,每次呼叫過來,隨便塞一個值進去,設定過期時間一分鐘。那麼每次統計只需要統計當前 key 的存活的值的數量就可以了。
這種我也沒實現,因為好像沒什麼需求。國內討論 TPS limit 都是討論單機 TPS limit 比較多。
這個同樣可以透過實現 TpsLimiter 介面來實現。
Leaky Bucket 演算法
這個本來可以是 TpsLimitStrategy 的一種實現的。後來我覺得,它其實並沒有特別大的優勢——雖然號稱可以做到均勻,但是其實並做不到真正的均勻。透過調整 SlidingWindow 的視窗大小,是可以接近它宣稱的均勻消費的效果的。比如說調整到一秒,那其實就已經很均勻了。而這並不會帶來多少額外的開銷。
本文為雲棲社群原創內容,未經允許不得轉載。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69949601/viewspace-2665882/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 如何實現百萬TPS?詳解JMQ4的儲存設計MQ
- Titan 的設計與實現
- LFU 的設計與實現
- Java中的多級快取設計與實現Java快取
- dubbo-go 中如何實現遠端配置管理Go
- Python實現火柴人的設計與實現Python
- 如何在SQL Server中實現 Limit m,n 的功能SQLServerMIT
- 限流 SDK 的設計與實現
- Picker元件的設計與實現元件
- Steps 元件的設計與實現元件
- Redis設計與實現Redis
- 《redis設計與實現》Redis
- Cobar SQL審計的設計與實現SQL
- PouchContainer CRI的設計與實現方法AI
- Spring IOC容器的設計與實現Spring
- RocketMQ Compaction Topic的設計與實現MQ
- RedisSyncer同步引擎的設計與實現Redis
- linux核心設計與實現Linux
- Python實現微博輿情分析的設計與實現Python
- SAP Cloud for Customer Extensibility的設計與實現Cloud
- Context真正的實現與Context設計模式Context設計模式
- 淺析pplx庫的設計與實現。
- 旅遊網站的設計與實現網站
- 事件匯流排的設計與實現事件
- 認證授權的設計與實現
- Reactive Spring實戰 -- 理解Reactor的設計與實現ReactSpring
- Redis 設計與實現 (九)--LuaRedis
- OpenMP 原子指令設計與實現
- 淺談VueUse設計與實現Vue
- Redis 設計與實現 4:字典Redis
- [Hook] 跨程式 Binder設計與實現 - 設計篇Hook
- Redis 設計與實現 (五)--多機資料庫的實現Redis資料庫
- 設計模式在Python中的完美實現設計模式Python
- Web 魔方模擬器的設計與實現Web
- 《Go元件設計與實現》-netpoll的總結Go元件
- 聊聊「訂單」業務的設計與實現
- 摺疊皮膚元件的設計與實現元件
- 聊聊支付流程的設計與實現邏輯