简介
本章介绍漏桶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%),