常见的五种服务限流算法及其实现

  常见的五种限流算法可简单概括为“两窗两漏一令牌”,下面将进行详细介绍:

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. 漏桶算法

介绍

将每个请求视为水滴加入漏桶进行存储

漏桶以固定速率匀速出水(处理请求)

若桶满则抛弃请求

优点

限流的绝对平均化。

缺点  不适合突发请求场景、请求延迟高:当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。
图例
示例  
实现  
using MutexLockUnique = std::unique_lock<std::mutex>;

class LeakyBucket
{
public: 
    explicit LeakyBucket(uint64_t iMaxSize, uint64_t iTakeMs) : m_lMaxSize(iMaxSize), m_lTakeMs(iTakeMs), m_lLastMs(-1) { }
    ~LeakyBucket() { }

    bool put(const uint64_t& x)
    {
        MutexLockUnique lock(m_mutex);
        if (m_queue.size() >= m_lMaxSize) { return false; }

        m_queue.push_back(x);
        m_notEmpty.notify_one();
        return true;
    }

    uint64_t take()
    {
        MutexLockUnique lock(m_mutex);
        m_notEmpty.wait(lock, [this] {  return !(this->m_queue.empty()); });
        assert(!m_queue.empty());

        int64_t lWaitMs = m_lTakeMs - (AbstractRateLimiter::getCurrentMilSec() - m_lLastMs);
        if (lWaitMs > 0)
        {
            AbstractRateLimiter::myMSleep(lWaitMs);
        }
        m_lLastMs = AbstractRateLimiter::getCurrentMilSec();

        uint64_t front(std::move(m_queue.front()));
        m_queue.pop_front();
        return front;
    }

private:
    int64_t                    m_lMaxSize;
    std::condition_variable m_notEmpty;

    mutable std::mutex        m_mutex;
    std::deque<uint64_t>    m_queue;

    int64_t                    m_lLastMs;    // 上一次take ms
    int64_t                    m_lTakeMs;    // take间隔 ms
};
typedef std::map<std::string, LeakyBucket*> LeakyBucketMap;

class LeakyBucketManager
{
private:
    LeakyBucketManager() {}
    virtual ~LeakyBucketManager() {}

    LeakyBucketManager(const LeakyBucketManager&) = delete;
    LeakyBucketManager& operator = (const LeakyBucketManager&) = delete;

public:
    static LeakyBucketManager* GetInstance()
    {
        static LeakyBucketManager manager;
        return &manager;
    }

    bool tryAcquire(const std::string& strKey, int32_t& lMilSec,
        const uint64_t lTokenNum, const uint64_t lCapacityNum, const double dRate)
    {
        MutexLockUnique lock(m_mutexLeakyBucket);
        lMilSec = -1;
        uint64_t lCurrentMilSec = AbstractRateLimiter::getCurrentMilSec();

        LeakyBucketMap::iterator it = m_mapLeakyBucket.find(strKey);
        if (it == m_mapLeakyBucket.end())
        {
            LeakyBucket* pLeakyBucket = new LeakyBucket(lCapacityNum, (1000 / dRate));
            m_mapLeakyBucket[strKey] = pLeakyBucket;

            it = m_mapLeakyBucket.find(strKey);
            if (it == m_mapLeakyBucket.end())
            {
                return false;
            }
        }
        
        if (!it->second->put(1)) { return false; }
        it->second->take();

        return true;
    }

private:
    LeakyBucketMap m_mapLeakyBucket;
    mutable std::mutex    m_mutexLeakyBucket;
};

#define AflGetLeakyBucketManager LeakyBucketManager::GetInstance
View Code

4. 漏斗算法

介绍

漏斗算法是《Redis深度历险》中提到的一种限流方案。漏斗有一定的容量,并且以一定速率漏水,漏斗的剩余空间即允许请求的空间。

漏斗算法的模型和漏桶算法在模型上是一致的,容器叫法不同,一个叫漏斗,一个叫漏桶,剩余空间直接决定了请求是否可以通过,只不过在漏斗算法中,一旦通过,请求便可以立即访问;

而漏桶算法中,请求通过后,会被暂存在容器中,等待被匀速处理,两者的差别即在于此。

优点

预热限流和平滑限流兼备。

缺点  
图例
示例  
实现  
using MutexLockUnique = std::unique_lock<std::mutex>;

class LeakyPipe
{
public:
    explicit LeakyPipe(uint64_t lCapacity, uint64_t iTakeMs) : m_lCapacity(lCapacity), m_lReamin(lCapacity), m_lTakeMs(iTakeMs), m_lLastMs(-1) { }
    ~LeakyPipe() { }

    bool put(const uint64_t& x)
    {
        MutexLockUnique lock(m_mutex);
        if (m_queue.size() >= m_lCapacity) { return false; }

        m_queue.push_back(x);
        m_notEmpty.notify_one();
        return true;
    }

    uint64_t take()
    {
        MutexLockUnique lock(m_mutex);
        m_notEmpty.wait(lock, [this] {  return !(this->m_queue.empty()); });
        assert(!m_queue.empty());

        // 计算剩余空间;
        m_lReamin += (AbstractRateLimiter::getCurrentMilSec() - m_lLastMs) / m_lTakeMs;
        if (m_lReamin > m_lCapacity) { m_lReamin = m_lCapacity; }

        if (m_lReamin <= 0)
        {
            int64_t lWaitMs = m_lTakeMs - (AbstractRateLimiter::getCurrentMilSec() - m_lLastMs);
            if (lWaitMs > 0)
            {
                AbstractRateLimiter::myMSleep(lWaitMs);
            }
            ++m_lReamin;
        }
        --m_lReamin;
        m_lLastMs = AbstractRateLimiter::getCurrentMilSec();

        uint64_t front(std::move(m_queue.front()));
        m_queue.pop_front();
        return front;
    }

private:
    std::condition_variable m_notEmpty;
    mutable std::mutex        m_mutex;
    std::deque<uint64_t>    m_queue;

    int64_t                    m_lLastMs;        // 上一次take ms
    int64_t                    m_lTakeMs;        // take间隔 ms
    int64_t                    m_lCapacity;    // 容量;
    int64_t                    m_lReamin;        // 剩余容量;
};
typedef std::map<std::string, LeakyPipe*> LeakyPipeMap;

class LeakyPipeManager
{
private:
    LeakyPipeManager() {}
    virtual ~LeakyPipeManager() {}

    LeakyPipeManager(const LeakyPipeManager&) = delete;
    LeakyPipeManager& operator = (const LeakyPipeManager&) = delete;

public:
    static LeakyPipeManager* GetInstance()
    {
        static LeakyPipeManager manager;
        return &manager;
    }

    bool tryAcquire(const std::string& strKey, int32_t& lMilSec,
        const uint64_t lTokenNum, const uint64_t lCapacityNum, const double dRate)
    {
        MutexLockUnique lock(m_mutexLeakyPipe);
        lMilSec = -1;
        uint64_t lCurrentMilSec = AbstractRateLimiter::getCurrentMilSec();

        LeakyPipeMap::iterator it = m_mapLeakyPipe.find(strKey);
        if (it == m_mapLeakyPipe.end())
        {
            LeakyPipe* pLeakyPipe = new LeakyPipe(lCapacityNum, (1000 / dRate));
            m_mapLeakyPipe[strKey] = pLeakyPipe;

            it = m_mapLeakyPipe.find(strKey);
            if (it == m_mapLeakyPipe.end())
            {
                return false;
            }
        }

        if (!it->second->put(1)) { return false; }
        it->second->take();

        return true;
    }

private:
    LeakyPipeMap m_mapLeakyPipe;
    mutable std::mutex    m_mutexLeakyPipe;
};

#define AflGetLeakyPipeManager LeakyPipeManager::GetInstance
View Code

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

后续会补充分布式的服务限流,敬请期待!

posted on 2022-05-16 19:15  仲达超  阅读(519)  评论(0编辑  收藏  举报

导航