作者:泰一
来源:码神说公众号
- 导读
- 网络带宽使用状态
- 自适应阈值的设计
- 为什么要动态自适应?
- 阈值自适应算法
- 源码分析
- Detect 函数
- UpdateThreshold 函数
- 测试用例
导读
上一篇介绍了 Trendline 滤波器计算延迟梯度趋势的过程,求得了最终值 trendline_slope
。接下来,就要用这个斜率值与阈值进行比较,从而检测网络带宽的使用状态,比如是否过载。实际的带宽检测过程涉及到:调整延迟梯度斜率值、过载触发条件、阈值自适应,本篇将介绍这个过程。
网络带宽使用状态
WebRTC 定义了三种网络带宽的使用状态:Normal、Underuse、Overuse,即正常、低载、过载。
enumclass BandwidthUsage {
kBwNormal = 0,
kBwUnderusing = 1,
kBwOverusing = 2,
};
下图展示了过载检测中三种信号的产生机制,其中,上下两条红色曲线表示动态阈值:
蓝色曲线表示调整后的延迟梯度斜率值:
过载检测原理
可知,网络带宽使用状态的判定方法为:
自适应阈值的设计
为什么要动态自适应?
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 提出的阈值自适应算法公式如下:
源码分析
基于 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 函数
该函数输入延迟梯度趋势的值、包组的发送时间差、包组的到达时间,从而评估网络带宽的使用状态,比如是否过载?
constexprint kMinNumDeltas = 60;
constdouble modified_trend =
std::min(num_of_deltas_, kMinNumDeltas) *
trend * threshold_gain_;
num_of_deltas_
表示包组间延迟梯度计算的次数,取值范围是 [2, 60],threshold_gain_
是 Trendline 滤波器的增益参数,其默认值为 4,这两个变量都会对延迟梯度趋势值进行放大。
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;
}
这种情况可能发生在这样的场景:因为某些原因,网络链路的容量突然降低,导致延迟梯度瞬间急剧增长。
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
「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。