本文是
WebRTC 拥塞控制
第 3 篇
导读
网络带宽使用状态
自适应阈值的设计
为什么要动态自适应?
阈值自适应算法
源码分析
Detect 函数
UpdateThreshold 函数
测试用例
导读
上一篇介绍了 Trendline 滤波器计算延迟梯度趋势的过程,求得了最终值 trendline_slope
。接下来,就要用这个斜率值与阈值进行比较,从而检测网络带宽的使用状态,比如是否过载。实际的带宽检测过程涉及到:调整延迟梯度斜率值、过载触发条件、阈值自适应,本篇将介绍这个过程。
网络带宽使用状态
WebRTC 定义了三种网络带宽的使用状态:Normal、Underuse、Overuse,即正常、低载、过载。
enum class BandwidthUsage {
kBwNormal = 0,
kBwUnderusing = 1,
kBwOverusing = 2,
};
下图展示了过载检测中三种信号的产生机制,其中,上下两条红色曲线表示动态阈值 ,蓝色曲线表示调整后的延迟梯度斜率值 。
可知,网络带宽使用状态的判定方法为:
- > ,overuse
- < -,underuse
- -,normal
自适应阈值的设计
为什么要动态自适应?
- 使用太大的阈值 ,那么当检测到过载信号时队列延迟可能已经很大了,或者根本检测不到网络拥塞,算法不够灵敏。
- 使用太小的阈值 ,对于很小的延迟梯度趋势 的增长,可能会被判定为网络带宽过载,使得发送码率降低,算法过于敏感。
- 固定的阈值会导致与 TCP(采用基于丢包的拥塞控制)的竞争中被饿死,要保证内部协议公平性(Intra-protocol fairness)。
比如,在 GCC 流与 TCP 流共存的场景下,延迟梯度 ,如果 ,那么 GCC 流会受到 TCP 流的过高的延迟梯度的影响,连续触发过载信号,降低自己的发送码率,导致 GCC 流的接收者被 "饿死"。Gcc Analysis[1] 解释了这个问题:
adaptive threshold is able to solve the starvation issue and allows the GCC flow to share the bottleneck fairly with the concurrent TCP flow,γ(t) follows m(t) with a smaller time constant which avoids the generation of a large number of consecutive overuse signals and prevents the starvation of the GCC flow.
理想的网络环境中,延迟梯度为 0,而在实际的网络环境中,延迟梯度则是不断变化的,让阈值跟随延迟梯度的变化而进行动态调整,可以降低 GCC 算法对延迟梯度变化的敏感度。
阈值自适应算法
GCC 提出的阈值自适应算法公式如下:
其中,,代表包组到达时间差,即距上一次阈值更新的时间差。 代表阈值的增长率(有可能负增长),增长的基准值是当前调整后的延迟梯度趋势值与当前阈值的差 - 。
系数按如下公式取值:
和 决定了阈值的减少和增加的速度。当延迟梯度斜率值 在阈值范围 内时,要降低阈值,否则,要升高阈值。总之,阈值动态调整的原则是: 随 而动。
GCC 草案[2] 建议 和 的取值分别是 0.00018 和 0.01,即阈值的增长速率要大于阈值的降低速率。上文已经解释了这样做的目的:降低过载检测算法的敏感度,保证在和 TCP 的竞争中的公平性。
源码分析
基于 WebRTC 71 版本。
网络带宽的过载检测与动态阈值的更新也是在 TrendlineEstimator
类中实现的,函数声明如下:
class TrendlineEstimator {
private:
void Detect(double trend,double ts_delta,int64_t now_ms);
void UpdateThreshold(double modified_trend,int64_t now_ms);
};
每个数据包组(除了首个包组)的到来都会触发过载检测和阈值的动态更新。
Detect 函数
该函数输入延迟梯度趋势的值、包组的发送时间差、包组的到达时间,从而评估网络带宽的使用状态,比如是否过载?
- 对延迟梯度趋势斜率值 进行调整。
constexpr int kMinNumDeltas = 60;
const double modified_trend =
std::min(num_of_deltas_, kMinNumDeltas) *
trend * threshold_gain_;
num_of_deltas_
表示包组间延迟梯度计算的次数,取值范围是 [2, 60],threshold_gain_
是 Trendline 滤波器的增益参数,其默认值为 4,这两个变量都会对延迟梯度趋势值进行放大。
这里的关键就在于为何要放大 ?
是一个斜率值,取值范围 (-1, 1),这个值很小,而阈值 要跟随 的变化而变化,这可能导致 很容易的超过 从而触发连续的过载信号。放大 可以使得算法不会因为 的波动而过于敏感。
- 过载信号触发条件
- 延迟梯度斜率值 > 当前阈值
- 过载总时长 > kOverUsingTimeThreshold(10ms)
- 过载次数 1
- 当前延迟梯度斜率值 > 上一次的延迟梯度斜率值,即延迟在不断恶化。
if (modified_trend > threshold_) {
if (time_over_using_ == -1) {
time_over_using_ = ts_delta / 2;
} else {
time_over_using_ += ts_delta;
}
overuse_counter_++;
if (time_over_using_ >
overusing_time_threshold_ &&
overuse_counter_ > 1) {
if (trend >= prev_trend_) {
time_over_using_ = 0;
overuse_counter_ = 0;
hypothesis_ =
BandwidthUsage::kBwOverusing;
}
}
}
值得一提的是,源码关于过载时间的计算是:过载时长等于包组发送时间差值send_delta_ms
的累加。但是在首次检测到过载时,过载时长会初始化为包组发送时间差的一半,我把这个做法理解为一种类似于 TCP 的慢启动策略。注意,在满足所有条件触发过载信号后,过载时长与过载次数这两个变量要重置为 0。
UpdateThreshold 函数
该函数动态更新延迟梯度趋势的阈值。
当延迟梯度斜率和阈值的差值大于 kMaxAdaptOffsetMs(15) 时,不更新阈值。
if (fabs(modified_trend) > threshold_ +
kMaxAdaptOffsetMs) {
last_update_ms_ = now_ms;
return;
}
这种情况可能发生在这样的场景:因为某些原因,网络链路的容量突然降低,导致延迟梯度瞬间急剧增长。
接下来就是新阈值的计算过程了,完全参考公式 (1)。注意,WebRTC 对 和 的取值分别是 0.0087 和 0.039,并非 GCC 草案的建议值。
int64_t time_delta_ms = std::min(now_ms - last_update_ms_, kMaxTimeDeltaMs);
threshold_ += k * (fabs(modified_trend) - threshold_) * time_delta_ms;
threshold_ = rtc::SafeClamp(threshold_, 6.f, 600.f);
还有两点值得一提:
- 阈值计算公式中的
time_delta_ms
指的是包组的到达时间差arrival_delta_ms
,而上文中过载时长则是根据包组的发送时间差send_delta_ms
来计算。 - 初始阈值设置为 12.5,计算后的阈值需要控制到 [6, 600] 这个区间内。GCC 草案解释了这么做的原因:
since a too small del_var_th(i) can cause the detector to become overly sensitive.
测试用例
继续使用了上篇的测试用例,一共构造了 41 个包组,来模拟过载检测的过程,输出如下:
观察日志输出,有几个比较关键的点:
- 初始的延迟梯度阈值设置为 12.5,随后开始动态自适应,一直调整到了 GCC 草案建议的阈值范围的最小值 6,在包组 21 到来并开始计算延迟梯度斜率之前,保持不变。
- 包组 21 到来之前,样本点数量未达到窗口大小,虽然不会进行延迟梯度斜率的计算,但是会执行过载检测和动态阈值更新,由于斜率初始化为 0,小于阈值 6,故认为网络状态正常。
- 包组 21 到来之后,样本点达到窗口大小 20,开始计算延迟梯度斜率,可以看出,阈值跟随斜率而变动,但由于斜率一直大于阈值,故网络一直处于过载状态。
至此,网络带宽过载检测的内容介绍完毕。下一篇将介绍三种网络带宽使用状态的信号(normal、overuse、underuse)如何驱动 AIMD 码率控制器工作,从而有效的进行拥塞控制,感谢阅读。
参考资料
[1]Gcc Analysis: https://c3lab.poliba.it/images/6/65/Gcc-analysis.pdf
[2]GCC 草案: https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02#section-5.4
码神说
一个有故事的程序员
坚持有诚意的技术原创