共计 2071 个字符,预计需要花费 6 分钟才能阅读完成。
为什么需要网络速度控制?
在网络编程中,不加以控制的传输速度可能导致严重的后果。以 TCP 拥塞控制(Congestion Control)为例,当网络出现拥堵时,TCP 协议会通过慢启动(Slow Start)、拥塞避免(Congestion Avoidance)等机制动态调整发送速率。这种设计告诉我们:无节制的流量输出会引发丢包、延迟增加甚至整个网络瘫痪。

但在应用层,我们常常需要更主动、更精确的控制能力,比如:
- 视频直播时需要稳定在特定码率以避免观众缓冲
- 文件传输服务要限制单用户带宽保证公平性
- 爬虫程序必须控制请求频率防止被封禁
主流算法对比
1. 滑动窗口(Sliding Window)
- 原理:通过动态调整可发送数据量实现控制
- 优点:与 TCP 协议栈深度集成
- 缺点:难以精确控制时间维度上的速率
2. 漏桶算法(Leaky Bucket)
- 原理:以固定速率处理请求,超限则丢弃或排队
- 优点:绝对平滑的输出曲线
- 缺点:无法应对突发流量
3. 令牌桶算法(Token Bucket)
- 原理:系统按速率生成令牌,请求需获取令牌才能执行
- 优点:允许合理范围内的突发传输
- 适用场景:本文选择的实现方案
令牌桶的 C ++20 实现
#include <chrono>
#include <mutex>
#include <thread>
class TokenBucket {
using Clock = std::chrono::steady_clock;
std::mutex mutex_;
double rate_; // 令牌 / 微秒
double capacity_; // 桶容量
double tokens_; // 当前令牌数
Clock::time_point last_time_; // 最后更新时间
// 计算当前应有多少令牌
void update_tokens() {auto now = Clock::now();
// 转换为微秒防止整数溢出
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - last_time_);
double new_tokens = elapsed.count() * rate_;
tokens_ = std::min(tokens_ + new_tokens, capacity_);
last_time_ = now;
}
public:
TokenBucket(double rate, double capacity)
: rate_(rate / 1'000'000), // 转换为每微秒的速率
capacity_(capacity),
tokens_(capacity),
last_time_(Clock::now()) {}
// 动态调整速率
void set_rate(double new_rate) {std::lock_guard<std::mutex> lock(mutex_);
update_tokens();
rate_ = new_rate / 1'000'000;
}
// 尝试获取令牌
bool try_consume(double tokens = 1.0) {std::lock_guard<std::mutex> lock(mutex_);
update_tokens();
if (tokens_ >= tokens) {
tokens_ -= tokens;
return true;
}
return false;
}
};
关键设计点说明
- 时间计算 :使用
steady_clock避免系统时间调整的影响 - 线程安全:所有状态变更通过 mutex 保护
- 动态调整 :
set_rate()可实时修改速率 - 精度处理:以微秒为单位计算避免浮点误差
性能测试数据
测试环境:i7-11800H @ 2.3GHz, Ubuntu 22.04
| 目标速率 | CPU 占用率 | 实际误差 |
|---|---|---|
| 1 Mbps | <0.1% | ±0.3% |
| 100 Mbps | 0.8% | ±1.2% |
| 1 Gbps | 3.5% | ±2.7% |
多线程测试(8 线程竞争):
– 速率稳定性保持在±5% 以内
– 无死锁或数据竞争
实际应用中的避坑指南
1. 时钟选择
- 必须使用
std::chrono::steady_clock system_clock会受 NTP 同步影响导致速率异常
2. 整数溢出防护
// 错误的实现 - 可能溢出
auto elapsed = now - last_time_;
double ms = elapsed.count(); // 可能超过整数范围
// 正确的实现
auto elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
3. 异常安全
- 在长时间阻塞操作前应先检查可用令牌
- 考虑添加 try_consume_timeout 接口
扩展思考:分布式限流
当前方案仅限于单机限流,要扩展到分布式系统需要考虑:
- 一致性存储:如何跨节点同步令牌状态?
- Redis 原子操作
-
分布式锁方案
-
时钟同步:不同机器间的时钟偏差如何处理?
- 采用中央授时服务
-
放宽精度要求
-
配额分配:
- 静态划分(每个节点固定配额)
- 动态申请(类似 TCP 拥塞窗口调整)
这些挑战提醒我们:分布式系统没有银弹,必须根据具体业务场景权衡一致性、可用性和性能。
正文完
