1. 程式人生 > 其它 >常見的五種服務限流演算法及其實現

常見的五種服務限流演算法及其實現

  常見的五種限流演算法可簡單概括為“兩窗兩漏一令牌”,下面將進行詳細介紹:

1. 固定視窗演算法

介紹

固定時間週期劃分時間為多個時間視窗,如:每10秒為一個時間視窗。

在每個時間視窗內,每有一個請求,計數器加一。

當計數器超過限制,丟棄本視窗之後的所有請求。

當下一時間視窗開始,重置計數器。

優點

原理簡單,固定視窗計數。

缺點 無法處理前後密集型請求,例如每秒限制100次,前最後一秒的10ms請求100次,後最後一秒的前10ms請求100次,相當於200次/0.02s。
圖例
示例  
實現
struct FixedWindow
{
    int64_t lLastNo;    
// 上一次No(記錄不同固定視窗區間標識); int64_t lRemainReq;  // 剩餘req }; typedef std::map<std::string, FixedWindow> FixedWindowMap; class FixedWindowManager { private: FixedWindowManager() {} virtual ~FixedWindowManager() {} FixedWindowManager(const FixedWindowManager&) = delete; FixedWindowManager
& operator = (const FixedWindowManager&) = delete; using MutexLockUnique = std::unique_lock<std::mutex>; public: static FixedWindowManager* GetInstance() { static FixedWindowManager manager; return &manager; } bool tryAcquire(const std::string
& strKey, int32_t& lMilSec, const uint64_t lReqNum, const uint32_t lFixedMs, const uint64_t lMaxReq) { MutexLockUnique lock(m_mutexFixedWindow); lMilSec = -1; uint64_t lCurrentMilSec = AbstractRateLimiter::getCurrentMilSec(); uint64_t lFixedWindowsNo = lCurrentMilSec / lFixedMs; FixedWindowMap::iterator it = m_mapFixedWindow.find(strKey); if (it == m_mapFixedWindow.end()) { FixedWindow FixedWindow; FixedWindow.lLastNo = lFixedWindowsNo; FixedWindow.lRemainReq = lMaxReq - lReqNum; m_mapFixedWindow[strKey] = FixedWindow; return true; } #if 0 printf("CurrentMs[%lld], CurrentNo[%lld], LastNo[%lld], RemainReq[%lld] \n", lCurrentMilSec, lFixedWindowsNo, it->second.lLastNo, it->second.lRemainReq); #endif if (lFixedWindowsNo == it->second.lLastNo) { // 在一個固定視窗內 if (it->second.lRemainReq >= lReqNum) { it->second.lRemainReq -= lReqNum; return true; } else { // 計算下一個最近視窗的時間間隔; lMilSec = (lFixedWindowsNo + 1) * lFixedMs - lCurrentMilSec; return false; } } it->second.lLastNo = lFixedWindowsNo; it->second.lRemainReq = lMaxReq - lReqNum; return true; } private: FixedWindowMap m_mapFixedWindow; mutable std::mutex m_mutexFixedWindow; }; #define AflGetFixedWindowManager FixedWindowManager::GetInstance
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::GetInstance
View 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::GetInstance
View Code

以上均為單程序的服務限流,主要目的是瞭解五種常見限流演算法及其優缺點,詳細參見gitee專案:https://gitee.com/ma_you_sun0821vip/RateLimiter

後續會補充分散式的服務限流,敬請期待!