常見的五種服務限流演算法及其實現
阿新 • • 發佈:2022-05-16
常見的五種限流演算法可簡單概括為“兩窗兩漏一令牌”,下面將進行詳細介紹:
1. 固定視窗演算法
介紹 |
固定時間週期劃分時間為多個時間視窗,如:每10秒為一個時間視窗。 在每個時間視窗內,每有一個請求,計數器加一。 當計數器超過限制,丟棄本視窗之後的所有請求。 當下一時間視窗開始,重置計數器。 |
優點 |
原理簡單,固定視窗計數。 |
缺點 | 無法處理前後密集型請求,例如每秒限制100次,前最後一秒的10ms請求100次,後最後一秒的前10ms請求100次,相當於200次/0.02s。 |
圖例 | |
示例 | |
實現 |
struct FixedWindow { int64_t lLastNo;View Code |
2. 滑動視窗演算法
介紹 |
以當前時間為截止時間,往前取一定的時間作為時間視窗,比如:往前取 10s 的時間 當有新的請求進入時,刪除時間視窗之外的請求,對時間視窗之內的請求進行計數統計,若未超過限制,則進行放行操作;若超過限制,則拒絕本次服務。 |
優點 |
有效處理固定視窗的突發缺點。 |
缺點 | 當時間區越長、精度越高,佔用的空間資源就越大。 |
圖例 | |
示例 | |
實現 |
/* key: ms, value: usereq */ typedef std::map<int64_t, int64_t> SlideWindow; typedef std::map<std::string, SlideWindow> SlideWindowMap; class SlideWindowManager { private: SlideWindowManager() {} virtual ~SlideWindowManager() {} SlideWindowManager(const SlideWindowManager&) = delete; SlideWindowManager& operator = (const SlideWindowManager&) = delete; using MutexLockUnique = std::unique_lock<std::mutex>; public: static SlideWindowManager* GetInstance() { static SlideWindowManager manager; return &manager; } bool tryAcquire(const std::string& strKey, int32_t& lMilSec, const uint64_t lReqNum, const uint32_t lSlideMs, const uint64_t lMaxReq) { MutexLockUnique lock(m_mutexSlideWindow); lMilSec = -1; uint64_t lCurrentMilSec = AbstractRateLimiter::getCurrentMilSec(); SlideWindowMap::iterator it = m_mapSlideWindow.find(strKey); if (it == m_mapSlideWindow.end()) { SlideWindow& slide = m_mapSlideWindow[strKey]; slide[lCurrentMilSec] = lReqNum; return true; } SlideWindow& slide = it->second; uint64_t lUseReq = 0; for (SlideWindow::iterator itWin = slide.begin(); itWin != slide.end(); ) { if ((lCurrentMilSec - itWin->first) >= lSlideMs) { // 滑動視窗過期; itWin = slide.erase(itWin); } else { lUseReq += itWin->second; itWin++; } } if ((lMaxReq - lUseReq) >= lReqNum) { SlideWindow::iterator itTemp = slide.find(lCurrentMilSec); if (itTemp == slide.end()) { slide[lCurrentMilSec] = 0; } slide[lCurrentMilSec] += lReqNum; return true; } #if 0 printf("CurrentMs[%lld], UseReq[%lld] \n", lCurrentMilSec, lUseReq); #endif // 未來需要等待釋放的請求數; uint64_t lWaitReq = lReqNum - (lMaxReq - lUseReq); for (SlideWindow::iterator itWin = slide.begin(); itWin != slide.end(); ) { lWaitReq -= itWin->second; if (lWaitReq <= 0) { lMilSec = itWin->first + lSlideMs - lCurrentMilSec; break; } } return false; } private: SlideWindowMap m_mapSlideWindow; mutable std::mutex m_mutexSlideWindow; }; #define AflGetSlideWindowManager SlideWindowManager::GetInstanceView Code |
3. 漏桶演算法
介紹 |
將每個請求視為水滴加入漏桶進行儲存 漏桶以固定速率勻速出水(處理請求) 若桶滿則拋棄請求 |
優點 |
限流的絕對平均化。 |
缺點 | 不適合突發請求場景、請求延遲高:當短時間內有大量的突發請求時,即便此時伺服器沒有任何負載,每個請求也都得在佇列中等待一段時間才能被響應。 |
圖例 | |
示例 | |
實現 |
4. 漏斗演算法
介紹 |
漏斗演算法是《Redis深度歷險》中提到的一種限流方案。漏斗有一定的容量,並且以一定速率漏水,漏斗的剩餘空間即允許請求的空間。 漏斗演算法的模型和漏桶演算法在模型上是一致的,容器叫法不同,一個叫漏斗,一個叫漏桶,剩餘空間直接決定了請求是否可以通過,只不過在漏斗演算法中,一旦通過,請求便可以立即訪問; 而漏桶演算法中,請求通過後,會被暫存在容器中,等待被勻速處理,兩者的差別即在於此。 |
優點 |
預熱限流和平滑限流兼備。 |
缺點 | |
圖例 | |
示例 | |
實現 |
5. 令牌桶演算法
介紹 |
以恆定的速度往令牌桶中放入令牌 當有請求過來則從令牌桶中獲取令牌進行後續請求 當獲取令牌失敗後則進行友好處理。 |
優點 |
預熱限流和平滑限流兼備。 |
缺點 | |
圖例 | |
示例 | |
實現 |
struct TokenBucket { int64_t lLastMs; // 上一次ms; int64_t lRemainToken; // 剩餘token }; typedef std::map<std::string, TokenBucket> TokenBucketMap; class TokenBucketManager { private: TokenBucketManager() {} virtual ~TokenBucketManager() {} TokenBucketManager(const TokenBucketManager&) = delete; TokenBucketManager& operator = (const TokenBucketManager&) = delete; using MutexLockUnique = std::unique_lock<std::mutex>; public: static TokenBucketManager* GetInstance() { static TokenBucketManager manager; return &manager; } bool tryAcquire(const std::string& strKey, int32_t& lMilSec, const uint64_t lTokenNum, const uint64_t lMaxTokenNum, const double dSpeed) { MutexLockUnique lock(m_mutexTokenBucket); lMilSec = -1; uint64_t lCurrentMilSec = AbstractRateLimiter::getCurrentMilSec(); TokenBucketMap::iterator it = m_mapTokenBucket.find(strKey); if (it == m_mapTokenBucket.end()) { TokenBucket tokenBucket; tokenBucket.lLastMs = lCurrentMilSec; tokenBucket.lRemainToken = lMaxTokenNum - lTokenNum; m_mapTokenBucket[strKey] = tokenBucket; return true; } #if 0 printf("CurrentMs[%lld], LastMs[%lld], RemainToken[%lld] \n", lCurrentMilSec, it->second.lLastMs, it->second.lRemainToken); #endif uint64_t lDiffTime = lCurrentMilSec - it->second.lLastMs; double dOneTime = 1000.0 / dSpeed; // 一個令牌的填充時間 if (lDiffTime > dOneTime) { // 如果間隔大於了一個令牌的填充時間 則進行填充; uint64_t lNowRemain = it->second.lRemainToken + (lDiffTime / dOneTime); if (lNowRemain >= lMaxTokenNum) { it->second.lLastMs = lCurrentMilSec; it->second.lRemainToken = lMaxTokenNum - lTokenNum; return true; } else { it->second.lLastMs = it->second.lLastMs + (uint64_t(lDiffTime / dOneTime) * dOneTime); it->second.lRemainToken = lNowRemain; } } if (it->second.lRemainToken >= lTokenNum) { it->second.lRemainToken -= lTokenNum; return true; } // 計算下一次滿足令牌的時間 uint64_t lNextTime = it->second.lLastMs + (lTokenNum - it->second.lRemainToken) * dOneTime; lMilSec = lNextTime - lCurrentMilSec; return false; } private: TokenBucketMap m_mapTokenBucket; mutable std::mutex m_mutexTokenBucket; }; #define AflGetTokenBucketManager TokenBucketManager::GetInstanceView Code |
以上均為單程序的服務限流,主要目的是瞭解五種常見限流演算法及其優缺點,詳細參見gitee專案:https://gitee.com/ma_you_sun0821vip/RateLimiter。
後續會補充分散式的服務限流,敬請期待!