共计 2330 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要控制网络速度?
在 TCP 协议中,拥塞控制(Congestion Control)机制已经帮我们做了基础的流量调节。比如当网络出现丢包时,TCP 会通过 ” 慢启动 ” 和 ” 拥塞避免 ” 算法自动降低发送速率。但在实际开发中,我们经常需要更主动地控制流量,比如:

- 防止某个服务占用过多带宽影响其他业务
- 保证 API 调用不超过第三方服务的速率限制
- 在 P2P 应用中平衡上传 / 下载带宽
这时候就需要我们在应用层实现网络速度控制(Rate Limiting)。
常见算法对比
1. 滑动窗口(Sliding Window)
- 原理:统计固定时间窗口内的请求数
- 优点:实现简单,内存占用小
- 缺点:窗口边界处可能产生两倍速率的突发流量
2. 漏桶算法(Leaky Bucket)
- 原理:以恒定速率处理请求,超出的请求排队或丢弃
- 优点:输出流量绝对平滑
- 缺点:突发流量时延迟较高
3. 令牌桶算法(Token Bucket)
- 原理:系统以固定速率生成令牌,请求需要获取令牌才能执行
- 优点:允许一定程度的突发流量(取决于桶大小)
- 缺点:实现相对复杂
对于大多数网络限流场景,令牌桶在平滑性和突发容忍度之间取得了较好的平衡,下面我们重点实现它。
令牌桶的 C ++20 实现
线程安全类设计
#include <chrono>
#include <mutex>
#include <thread>
class TokenBucket {
public:
// 构造函数:指定令牌生成速率 (个 / 秒) 和桶容量
TokenBucket(uint32_t rate, uint32_t capacity)
: rate_(rate), capacity_(capacity), tokens_(capacity) {last_time_ = std::chrono::steady_clock::now();
}
// 尝试获取 n 个令牌,返回实际获取的数量
uint32_t tryAcquire(uint32_t n) {std::lock_guard<std::mutex> lock(mutex_);
refill();
uint32_t available = std::min(n, tokens_);
tokens_ -= available;
return available;
}
// 动态调整速率
void setRate(uint32_t new_rate) {std::lock_guard<std::mutex> lock(mutex_);
refill(); // 先补充令牌
rate_ = new_rate;
capacity_ = std::max(capacity_, new_rate); // 确保容量≥速率
tokens_ = std::min(tokens_, capacity_); // 修正当前令牌数
}
private:
void refill() {auto now = std::chrono::steady_clock::now();
// 计算距离上次补充经过了多少秒(使用 duration_cast 确保时间单位)auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(now - last_time_).count() / 1e9;
// 计算应补充的令牌数(注意防溢出)uint32_t new_tokens = static_cast<uint32_t>(elapsed * rate_);
if (new_tokens > 0) {tokens_ = std::min(tokens_ + new_tokens, capacity_);
last_time_ = now;
}
}
std::mutex mutex_;
uint32_t rate_; // 令牌 / 秒
uint32_t capacity_; // 桶容量
uint32_t tokens_; // 当前令牌数
std::chrono::steady_clock::time_point last_time_; // 上次补充时间
};
关键代码解释
- 时间计算:
- 使用
steady_clock而不是system_clock,因为后者可能受系统时间调整影响 -
将纳秒转换为秒时使用
duration_cast确保精度 -
令牌补充:
- 仅在获取令牌时惰性计算(避免独立计时线程)
-
new_tokens > 0检查确保长时间未操作时不会一次补充过多令牌 -
线程安全:
- 所有状态变更通过
std::lock_guard保护 - 动态调整速率时先补充令牌保持状态一致
性能测试
测试环境
- CPU: Intel i7-11800H
- OS: Linux 5.15
单线程不同速率下的 CPU 占用
| 目标速率 | 实际速率 | CPU 占用 |
|---|---|---|
| 1,000/s | 998/s | <1% |
| 100,000/s | 99,850/s | 3.2% |
| 1,000,000/s | 998,200/s | 28.7% |
多线程准确性测试(目标速率 100,000/s)
| 线程数 | 总速率 | 误差率 |
|---|---|---|
| 1 | 99,850 | -0.15% |
| 4 | 99,720 | -0.28% |
| 8 | 99,640 | -0.36% |
避坑指南
- 时钟选择:
- 必须用
steady_clock,其单调性保证速率计算正确 -
实测
system_clock在 NTP 调整时会导致速率异常 -
整数溢出防护:
elapsed * rate_可能溢出,实际代码中应使用 64 位整数计算-
令牌补充使用
std::min防止超过容量 -
异常安全:
- 在获取令牌期间抛出异常会导致锁未释放
- 解决方案:使用 RAII 风格的
lock_guard
扩展思考:分布式限流
要实现跨多台机器的全局速率限制,可以考虑:
- Redis 方案:
- 使用
INCR和EXPIRE命令实现简单计数器 - 优点:实现简单
-
缺点:网络延迟影响精度
-
分布式令牌桶:
- 通过共识算法同步各节点状态
-
参考 Google 的分布式计数器实现
-
中间件方案:
- 使用 Nginx 或 Envoy 的限流模块
- 优点:对业务代码无侵入
你会选择哪种方案?欢迎在评论区分享你的见解。
正文完
