算法:限流之漏桶算法实现

简介

本章介绍漏桶Leaky Bucket算法在流量限速场景的原理,以及C++实现和相关测试验证。

常见的限流算法有计数限流,固定窗口限流,滑动窗口限流,漏桶算发限流,令牌桶算法限流。漏桶算法是限流算法的一种,其原理是将待处理数据统一放在一个桶中,然后根据匀速从桶中拿出数据处理。

漏桶算法可应用与多种场景,本章讲诉网络流程控制限制场景的使用,对外发的网络数据进行控制,限制外发的数据的最高流量。

原理

在这里插入图片描述

示例:

在上图中,我们假设网络为一台主机提供了3Mbps的带宽,主机以12Mbps的速率在2s内发送一个数据突发,总共发送24Mbits的数据。主机静默5秒,然后以2Mbps的速率发送数据3秒,总共发送6Mbits的数据。主机在10秒内总共发送了30Mbits的数据。漏桶在相同的10秒内以3Mbps的速率发送数据,使流量平滑。

原理:

  • 设置一个固定容量的FIFO队列,上次应用将发送数据添加到FIFO队列。因桶的大小固定,超出容量的数据将丢弃或者等待桶可接收新的数据。

  • 在一个线程以固定速率从队列中拿取数据包真实发送到网络。固定速率发送时,计算发送一包数据时间,如果未到达该时间,线程睡眠等待。根据限制的速度计算,比如限速50MB/s,发送一包数据为5M,那到下一次包发送需要间隔100毫米,真是发送一包数据可能只能只用了30毫米,那就需要睡眠70毫米的时间。

需求:

项目的需求是文件下载的服务器端限流,下载请求过来后(请求其他模块有限制,这里忽略这个问题),在独立线程读取文件并发送到该请求的socket上,要求发送到网络的总数据要限流,但是数据不能丢弃,可以延迟等待发送。根据项目需求设计流程如下:

设计:

根据项目需求,将漏桶算法改造适应项目需求:

  • 投递业务数据的线程是多线程,保障线程安全。
  • 真实发送数据的线程还是使用投递业务数据的线程。
  • 队列使用锁来保障排队等待发送,在未获取到锁时排队(未保证其先进先出)。

实现

  • LeakySpeedLimiter

    包含漏桶的限制速度。提供等待投递的操作。

    等待投递操作:

    • 根据上一包数据的大小,计算这一包数据发送应该需要的时间,从而得到当前包发送的时间点。

    • 如果当前时间大于当前包的发送时间点则直接发送;如果当前时间小于等于当前包的发送时间点,则睡眠等待到发送时间点才继续运行,并发送当前包的数据

    代码实现如下:

    • 头文件

      #ifndef LEAKYSPEEDLIMITER_H
      #define LEAKYSPEEDLIMITER_H
      
      #include <climits>
      #include <chrono>
      #include <mutex>
      #include <thread>
      #include <queue>
      
      class LeakySpeedLimiter
      {
      public:
          LeakySpeedLimiter(unsigned long long limitSpeed, unsigned long long capacity = ULLONG_MAX);
      
          bool grantAccess(unsigned long long dataSize);
      
      private:
          // 限速速度(字节/s)
          unsigned long long m_limitSpeed;
          // 最后一次发送的时间
          std::chrono::steady_clock::time_point m_lastTime;
          // 最后一次发送的数据包大小
          unsigned long long m_lastDataSize = 0;
          // mutex
          std::mutex m_mtx;
      };
      
      #endif // LEAKYSPEEDLIMITER_H
      
      
    • 实现文件

      #include "leakyspeedlimiter.h"
      
      LeakySpeedLimiter::LeakySpeedLimiter(unsigned long long limitSpeed, unsigned long long capacity)
          : m_limitSpeed(limitSpeed > 0 ? limitSpeed : ULLONG_MAX)
          , m_lastTime(std::chrono::steady_clock::now())
      {
      }
      
      bool LeakySpeedLimiter::grantAccess(unsigned long long dataSize)
      {
          std::unique_lock<std::mutex> lck(m_mtx);
      
          // 为保持速率恒定,发送的数据包需要更加数据大小计算固定时间间距,需要经历该时间才能发送下一包
          // 计算上一次的数据包发送要经历的时间
          auto needTime = m_lastDataSize * 1000000 / m_limitSpeed;
          // 计算本次数据包发送的时间点
          auto nextTime = m_lastTime + std::chrono::microseconds(needTime);
      
          auto curTime = std::chrono::steady_clock::now();
          // 当前时间在发送时间点前,需要等到到该时间才能继续发送数据
          if (curTime < nextTime)
          {
              std::this_thread::sleep_until(nextTime);
          }
      
          m_lastDataSize = dataSize;
      
          m_lastTime = nextTime;
      
          return true;
      }
      
      
  • main

    包含漏桶限速对象的调用及测试结果打印。

    • sendDatatoNet线程模拟多线程发送数据。
    • statisticNetwork统计流量结果和每个线程发送数据的百分比。
    #include "leakyspeedlimiter.h"
    
    #include <iostream>
    #include <map>
    #include <sstream>
    #include <iomanip>
    
    // 网络发送字节数,用于统计
    unsigned long long sendCount = 0;
    std::mutex mutexCount;
    std::map<unsigned int, unsigned long long> mapTheadIdCount;
    
    // 网络数据发送测试线程函数
    void sendDatatoNet(LeakySpeedLimiter* speedLimiter)
    {
        // 每次发送的数据包大小
        const int sizeOnePacket = 2 * 1024;
    
        while(true)
        {
            // 等待获取发送权限
            speedLimiter->grantAccess(sizeOnePacket);
    
            // 统计总的发送包数量
            std::unique_lock<std::mutex> lck(mutexCount);
            sendCount += sizeOnePacket;
    
            // do 真实的发送数据操作
    
            // 统计每个线程发送的数包
            auto threadId = std::this_thread::get_id();
            auto theId = *(unsigned int *)&threadId;
    
            auto it = mapTheadIdCount.find(theId);
            if (it != mapTheadIdCount.end())
            {
                it->second += sizeOnePacket;
            }
            else
            {
                mapTheadIdCount.insert(std::make_pair(theId, sizeOnePacket));
            }
        }
    }
    
    void statisticNetwork()
    {
        auto lastTime = std::chrono::steady_clock::now();
        while(true)
        {
            // 1秒统计一次
            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    
            // 计算投递时间差
            auto curTime = std::chrono::steady_clock::now();
            auto elapsedMs = std::chrono::duration<double, std::milli>(curTime - lastTime).count();
    
            lastTime = curTime;
    
            // 打印总速率
            std::unique_lock<std::mutex> lck(mutexCount);
            if (elapsedMs > 0)
            {
                // * 1000 / elapsedMs为毫秒转换为秒
                auto curSpeed = (double)sendCount * 1000 /  1024 / 1024 / elapsedMs;
    
                std::cout << "speed: " << curSpeed << " MB/s" << std::endl;
            }
    
            // 打印每个线程发送的百分比
            std::cout << "thread send count: ";
    
            for (auto it: mapTheadIdCount)
            {
                std::cout << it.first << "(" << std::setfill(' ') << std::setw(2) << 100 * it.second / sendCount  << "%),";
            }
    
            std::cout << std::endl;
    
            mapTheadIdCount.clear();
            sendCount = 0;
        }
    }
    
    int main(int argc, char *argv[])
    {
        // 构造限速器:限速50MB/s
        LeakySpeedLimiter speedLimiter(50 * 1024 * 1024);
    
        // 启动网络发送线程
        for (int i = 0; i < 10; ++i)
        {
            new std::thread(sendDatatoNet, &speedLimiter);
        }
    
        // 启动统计
        statisticNetwork();
    
        return 0;
    }
    
    

    测试输出如下以及分析如下:

    • 发送速度保持在50MB/s左右,和设置的限速速度保持一致。
    • 从输出来看,数据发送的线程获取令牌长期来看是公平的。
    speed: 49.5628 MB/s
    thread send count: 2340(10%),2980( 9%),5084( 9%),12536(10%),19472( 9%),21576( 9%),21632(10%),21816( 9%),24740(10%),24816( 9%),
    speed: 49.9564 MB/s
    thread send count: 2340(10%),2980(10%),5084( 9%),12536( 9%),19472(10%),21576(10%),21632(10%),21816(10%),24740(10%),24816( 9%),
    speed: 50.1313 MB/s
    thread send count: 2340(10%),2980(10%),5084( 8%),12536( 9%),19472(10%),21576( 9%),21632( 9%),21816(10%),24740(10%),24816(10%),
    speed: 49.9819 MB/s
    thread send count: 2340( 9%),2980( 9%),5084(10%),12536(10%),19472( 9%),21576(10%),21632(10%),21816(10%),24740(10%),24816( 9%),
    speed: 50.1356 MB/s
    thread send count: 2340( 9%),2980( 9%),5084( 9%),12536( 9%),19472(10%),21576(10%),21632(10%),21816(10%),24740( 9%),24816(10%),
    speed: 50.0046 MB/s
    thread send count: 2340(10%),2980(10%),5084(10%),12536(10%),19472( 9%),21576( 9%),21632(10%),21816( 9%),24740( 9%),24816( 9%),
    
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaobaiPlayGame

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值