——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost
目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。
欢迎大家使用
——
文章目录
背景
事实上,带宽估计的算法有很多,有三种比较经典方案:
GCC 算法[ https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc-02] 是出自 Google 的一种延时估计和丢包相结合的拥塞控制算法,在 WebRTC 中被默认使用。
NADA 算法[https://datatracker.ietf.org/doc/html/rfc8698] 是思科公司提出的一种基于延时估计的方法,这种算法带宽利用率高,跟踪带宽变化方面表现很优秀。
SCReAM 算法[https://www.rfc-editor.org/rfc/rfc8298.html] 是爱立信公司提出的一种基于延时估计的算法,在 OpenWebRTC 中被采用。
这篇论文[Congestion Control for RTP Media: a Comparison on Simulated Environment] 比较了以上三种算法的效果。
我们后续可能会对其他算法的原理进行
一、 趋势线模块
1.1 模块简介
本模块主要针对趋势线滤波模块进行理论分析与解释,并对比旧的卡尔曼滤波器内容进行分析,来得到调整参数的参考。
1.2 到达时间模型
在https://blog.jianchihu.net/webrtc-research-interarrival.html说到了到达时间模型,主要包含几个包组时间差计算的概念:
● 到达时间差:𝑡𝑖−𝑡𝑖−1
● 发送时间差:𝑇𝑖−𝑇𝑖−1
● 时延变化:𝑑𝑖=𝑡𝑖−𝑡𝑖−1−(𝑇𝑖−𝑇𝑖−1)
这个时延变化用于评估时延增长趋势,判断网络拥塞状况。在[1]中,这个时延变化叫做单向时延梯度(one way delay gradient)。那这个时延梯度为什么可以判断网络拥塞情况呢?
1.3 时延梯度计算
首先我们通过一张图看下时延梯度的计算:
对于两个包组:𝑖以及𝑖−1,它们的时延梯度:
𝑑𝑖=𝑡𝑖−𝑡𝑖−1−(𝑇𝑖−𝑇𝑖−1) (1)
1.4 判断依据
网络无拥塞的正常情况下:
网络拥塞情况下:
第一张图是没有任何拥塞下的网络传输,时延梯度𝑑1:𝑡1−𝑡0−(𝑇1−𝑇0)=0。第二张图是存在拥塞时的网络传输,包在𝑡1时刻到达,因为网络发生拥塞,导致到达时间比原本要晚,原本应在虚线箭头处时刻到达,时延梯度𝑑1:𝑡1−𝑡0−(𝑇1−𝑇0)>0。
由上可知,正常无拥塞情况下,包组间时延梯度等于0,拥塞时大于0,我们可以通过数学方法估计这个时延梯度的变化情况评估当前网络的拥塞情况,这个就是WebRTC基于时延的带宽估计的理论基础。
1.4 线性回归
WebRTC用到了线性回归这个数学方法进行时延梯度趋势预测。通过最小二乘法求得拟合直线斜率,根据斜率判断增长趋势。
对于一堆样本点(𝑥,𝑦),拟合直线方程𝑦=𝑏𝑥+𝑎的斜率𝑏按如下公式计:
1.5 网络状态
在WebRTC中,定义了三种网络状态:normal,overuse,underuse,用于表示当前带宽的使用情况,具体含义跟单词本身含义一样。
例如如果是overuse状态,表示带宽使用过载了,从而判断网络发生拥塞,如果是underuse状态,表示当前带宽未充分利用。后面会介绍如何根据时延梯度增长趋势得到当前的网络状态。
1.6 代码导读
由TrendlineEstimator类实现。主要接口就一个:UpdateTrendline。传入包组时间差,时间,包大小等参数,判断当前网络状态。
void UpdateTrendline(double recv_delta_ms,
double send_delta_ms,
int64_t send_time_ms,
int64_t arrival_time_ms,
size_t packet_size);
说下输入的各个参数的含义:
● recv_delta_ms:包组接收时间差
● send_delta_ms:包组发送时间差
● send_time_ms:当前处理的RTP的包发送时间
● arrival_time_ms:当前处理的RTP的包到达时间
● packet_size:当前处理的RTP包的大小
内部相关函数调用如下:
TrendlineEstimator::UpdateTrendline
↓
LinearFitSlope
↓
TrendlineEstimator::Detect
↓
TrendlineEstimator::UpdateThreshold
下面结合代码说下UpdateTrendline函数内部计算过程。
1) 计算时延变化累积值:
// 时延变化:接收时间差 - 发送时间差
const double delta_ms = recv_delta_ms - send_delta_ms;
++num_of_deltas_;
num_of_deltas_ = std::min(num_of_deltas_, kDeltaCounterMax);
if (first_arrival_time_ms_ == -1)
first_arrival_time_ms_ = arrival_time_ms;
// 累积时延变化
accumulated_delay_ += delta_ms;
2)根据1)中的时延累积值计算得到平滑后的时延:
smoothed_delay_ = smoothing_coef_ * smoothed_delay_ +
(1 - smoothing_coef_) * accumulated_delay_;
3)将从第一个包到达至当前RTP包到达的经历时间,平滑时延值存到双端队列delay_hist_中:
delay_hist_.push_back(std::make_pair(
static_cast<double>(arrival_time_ms - first_arrival_time_ms_),
smoothed_delay_));
4)当队列delay_hist_大小等于设定的窗口大小时,开始进行时延变化趋势计算,得到直线斜率,直线横坐标为经历时间,纵坐标为平滑时延值:
if (delay_hist_.size() == window_size_) {
// 0 < trend < 1 -> 时延梯度增加
// trend == 0 -> 时延梯度不变
// trend < 0 -> 时延梯度减小
trend = LinearFitSlope(delay_hist_).value_or(trend);
}
5)通过计算得到的时延变化趋势拟合直线斜率,发送时间差,到达时间判断网络状态:
Detect(trend, send_delta_ms, arrival_time_ms);
1.7 LinearFitSlope函数
使用最小二乘法求解线性回归,得到时延变化增长趋势的拟合直线斜率。
// 在该函数调用前有这么一段注释,意思为:trend 可以看做(发送码率 - 网络容量)/ 网络容量
// Update trend_ if it is possible to fit a line to the data. The delay
// trend can be seen as an estimate of (send_rate - capacity)/capacity.
// 0 < trend < 1 -> the delay increases, queues are filling up
// trend == 0 -> the delay does not change
// trend < 0 -> the delay decreases, queues are being emptied
看下内部代码实现:
absl::optional<double> LinearFitSlope(
const std::deque<std::pair<double, double>>& points) {
RTC_DCHECK(points.size() >= 2);
// 所有节点(x,y),x,y值分别求和
double sum_x = 0;
double sum_y = 0;
for (const auto& point : points) {
sum_x += point.first;
sum_y += point.second;
}
// 求平均值
double x_avg = sum_x / points.size();
double y_avg = sum_y / points.size();
// 根据公式:斜率 k = sum (x_i-x_avg)(y_i-y_avg) / sum (x_i-x_avg)^2
double numerator = 0;
double denominator = 0;
for (const auto& point : points) {
numerator += (point.first - x_avg) * (point.second - y_avg);
denominator += (point.first - x_avg) * (point.first - x_avg);
}
if (denominator == 0)
return absl::nullopt;
return numerator / denominator;
}
1.8 Detect函数
该函数主要根据时延变化增长趋势计算当前网络状态,在WebRTC旧版GCC算法,接收端基于时延的带宽预测代码中,这部分属于过载检测器的内容,跟在卡尔曼滤波后面,我们不做讨论,我们讨论的全部是最新代码,全部在发送端进行带宽预测。
在Detect函数内部,会根据前面计算得到的斜率得到一个调整后的斜率值:modified_trend:
// 乘以包组数量以及阈值增益,因为计算得到的trend常常是一个非常小的值
const double modified_trend =
std::min(num_of_deltas_, kMinNumDeltas) * trend * threshold_gain_;
示例:假设不同到达时间的delta值为3ms、6ms、4ms、7ms、5ms、9ms、14ms,转换成笛卡尔坐标系图为:
然后与一个动态阈值threshold_做对比,从而得到网络状态
● modified_trend > threshold_,且持续一段时间,同时这段时间内,modified_trend没有变小趋势,认为处于overuse状态
● modified_trend < -threshold_,认为处于underuse状态
● -threshold_ <= modified_trend <= threshold_,认为处于normal状态
如下图所示,上下两条红色曲线表示动态阈值,蓝色曲线表示调整后的斜率值,阈值随时间动态变化,调整后的斜率值也动态变化,这样网络状态也动态变化。
本图截取自文末参考[1],倒数第三个状态应为normal,不是overuse
相关实现代码如下:
if (modified_trend > threshold_) {
if (time_over_using_ == -1) {
time_over_using_ = ts_delta / 2;
} else {
time_over_using_ += ts_delta;
}
overuse_counter_++;
// 满足modified_trend > threshold_状态的持续时间必须大于overusing_time_threshold_
if (time_over_using_ > overusing_time_threshold_ && overuse_counter_ > 1) {
// 当前计算得到的斜率值trend不能小于之前得到的的斜率值
// 斜率值变小说明overuse程度减轻,网络情况变好,不认为处于kBwOverusing
if (trend >= prev_trend_) {
time_over_using_ = 0;
overuse_counter_ = 0;
hypothesis_ = BandwidthUsage::kBwOverusing;
}
}
// modified_trend < -threshold_,认为处于underuse状态
} else if (modified_trend < -threshold_) {
time_over_using_ = -1;
overuse_counter_ = 0;
hypothesis_ = BandwidthUsage::kBwUnderusing;
// threshold_ <= modified_trend <= threshold_,认为处于normal状态
} else {
time_over_using_ = -1;
overuse_counter_ = 0;
hypothesis_ = BandwidthUsage::kBwNormal;
}
这个阈值threshold_是动态调整的,代码实现在UpdateThreshold函数中。
1.9 UpdateThreshold函数
阈值threshold_动态调整为了改变算法对时延梯度的敏感度。根据[1]主要有以下两方面原因:
1)时延梯度是变化的,有时很大,有时很小,如果阈值是固定的,对于时延梯度来说可能太大或者太小,这样就会出现检测不够敏感,无法检测到网络拥塞,或者过于敏感,导致一直检测为网络拥塞;
2)固定的阈值会导致与TCP(采用基于丢包的拥塞控制)的竞争中被饿死。
这个阈值根据如下公式计算:
每处理一个新包组信息,就会更新一次阈值,其中Δ𝑇表示距离上次阈值更新经历的时间,𝑚(𝑡𝑖)是前面说到的调整后的斜率值modified_trend。𝑘𝛾(𝑡𝑖)按如下定义:
𝑘𝑑与𝑘𝑢分别决定阈值增加以及减小的速度。
具体实现代码如下:
void TrendlineEstimator::UpdateThreshold(double modified_trend,
int64_t now_ms) {
// kγ(ti)值:kd,ku
const double k = fabs(modified_trend) < threshold_ ? k_down_ : k_up_;
const int64_t kMaxTimeDeltaMs = 100;
// 距离上一次阈值更新经过的时间∆T
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);
last_update_ms_ = now_ms;
}
1.10 总结
本文主要介绍了如何根据时延梯度得到网络状态,判断网络拥塞状况,并结合WebRTC相关源码进行分析。当我们得到当前网 络拥塞状况后,就要对发送码率进行调节,以适应当前网络。后续文章我们将研究如何根据网络状态进行相应码率调整。
1.11 参考
[1] Analysis and Design of the Google Congestion Control for Web Real-time Communication (WebRTC).http://dl.acm.org/ft_gateway.cfm?id=2910605&ftid=1722453&dwn=1&CFID=649873557&CFTOKEN=47458294.
[2] A Google Congestion Control Algorithm for Real-Time Communication draft-ietf-rmcat-gcc-02.https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02.
二、 码率变化模块
2.1 模块简介
码率更新模块是判断码率变化的关键,该模块会动态地更新需要的码率大小。首先我们得明白,对应的码率变化模块也有它自己的状态机。
这个状态机模型分三个状态:kRcHold,kRcIncrease和kRcDecrease 。
● “kRcIncrease”是未检测到拥塞的状态;
● “kRcDecrease”是检测到拥塞的状态;
● “kRcHold”是等待累积队列耗尽后进入“增加”状态的状态。
状态迁移过程取决于上述趋势线模块输出的三种网络状态,对应:underuse、overuse、normal。
大致的状态机切换图如下:
上述的状态机可以简述为:
● 当Overuse发生时,无论什么状态都进入衰减。
● 当Underuse发生时,无论什么状态都进入保持状态。
● 在保持和增长阶段,Normal状态将保持继续增长。
● 在衰减阶段,Normal状态会将状态拉回保持状态。
2.2 代码简介
uint32_t AimdRateControl::ChangeBitrate(uint32_t current_bitrate_bps,
uint32_t incoming_bitrate_bps,
int64_t now_ms) {
// 在调用函数update更新对应的链路状态估计,累积码率,噪声值后
// 会将updated置位,如果没置位则不会去更新码率。
if (!updated_) {
return current_bitrate_bps_;
}
// An over-use should always trigger us to reduce the bitrate, even though
// we have not yet established our first estimate. By acting on the over-use,
// we will end up with a valid estimate.
// 初始化未完成,如果不是一开始就Overuse,直接返回初始的码率即可。
if (!bitrate_is_initialized_ && current_input_.bw_state != kBwOverusing)
return current_bitrate_bps_;
updated_ = false;
// 这里对状态进行转换,这个函数是状态机状态转换函数
// 1. Underuse总是进入Hold状态。
// 2. Overuse总是进入Dec状态。
// 3. Normal状态维持,除非当前在Hold状态,此时会进入Inc状态。
ChangeState(current_input_, now_ms);
// Calculated here because it's used in multiple places.
const float incoming_bitrate_kbps = incoming_bitrate_bps / 1000.0f;
// Calculate the max bit rate std dev given the normalized
// variance and the current incoming bit rate.
const float std_max_bit_rate = sqrt(var_max_bitrate_kbps_ *
avg_max_bitrate_kbps_);
switch (rate_control_state_) {
// 保持状态不更新码率
case kRcHold:
break;
case kRcIncrease:
// 三个状态,在最大值附近,超过最大值,比最大值高到不知道哪里去
// 最大均值已初始化,且当前码率高于最大值加上三倍方差,此时进入
// 比最大值高到不知道哪里去的状态,同时认为这个均值并不是很好使,复位。
// Above声明了,但是没有找到相应调用点。
if (avg_max_bitrate_kbps_ >= 0 &&
incoming_bitrate_kbps >
avg_max_bitrate_kbps_ + 3 * std_max_bit_rate) {
ChangeRegion(kRcMaxUnknown);
avg_max_bitrate_kbps_ = -1.0;
}
//
if (rate_control_region_ == kRcNearMax) {
// Approximate the over-use estimator delay to 100 ms.
// 已经接近最大值了,此时增长需谨慎,加性增加。
const int64_t response_time = rtt_ + 100;
uint32_t additive_increase_bps = AdditiveRateIncrease(
now_ms, time_last_bitrate_change_, response_time);
current_bitrate_bps += additive_increase_bps;
} else {
// 由于没有Above状态的使用,因此认为比最大值高到不知道哪里去的状态属于
// 上界未定,放开手倍增码率。
uint32_t multiplicative_increase_bps = MultiplicativeRateIncrease(
now_ms, time_last_bitrate_change_, current_bitrate_bps);
current_bitrate_bps += multiplicative_increase_bps;
}
time_last_bitrate_change_ = now_ms;
break;
case kRcDecrease:
bitrate_is_initialized_ = true;
if (incoming_bitrate_bps < min_configured_bitrate_bps_) {
// 真的不能再低了....
current_bitrate_bps = min_configured_bitrate_bps_;
} else {
// Set bit rate to something slightly lower than max
// to get rid of any self-induced delay.
current_bitrate_bps = static_cast<uint32_t>(beta_ *
incoming_bitrate_bps + 0.5);
if (current_bitrate_bps > current_bitrate_bps_) {
// 本次速率仍然在增长
// Avoid increasing the rate when over-using.
if (rate_control_region_ != kRcMaxUnknown) {
// 如果上界可靠,则将码率设置在最大均值的beta_倍处,
// 默认的beta_为0.85,同paper。
current_bitrate_bps = static_cast<uint32_t>(
beta_ * avg_max_bitrate_kbps_ * 1000 + 0.5f);
}
// 进行修正,和上一轮迭代的码率取小,如果上界不定
// 则取上一次迭代的码率值。
current_bitrate_bps = std::min(current_bitrate_bps,
current_bitrate_bps_);
}
// 更新过新的码率值后,认为现在已经在最大均值附近。
// 注意,每次认为上界无效时,总会把最大均值复位
// 这里设置完对应状态后,即使上界无效,下面总会更新一个最大均值。
ChangeRegion(kRcNearMax);
if (incoming_bitrate_kbps < avg_max_bitrate_kbps_ -
3 * std_max_bit_rate) {
// 当前速率小于均值较多,认为均值不可靠,复位
avg_max_bitrate_kbps_ = -1.0f;
}
// 衰减状态下需要更新最大均值
UpdateMaxBitRateEstimate(incoming_bitrate_kbps);
}
// Stay on hold until the pipes are cleared.
// 降低码率后回到HOLD状态,如果网络状态仍然不好,在Overuse仍然会进入Dec状态。
// 如果恢复,则不会是Overuse,会保持或增长。
ChangeState(kRcHold);
time_last_bitrate_change_ = now_ms;
break;
default:
assert(false);
}
if ((incoming_bitrate_bps > 100000 || current_bitrate_bps > 150000) &&
current_bitrate_bps > 1.5 * incoming_bitrate_bps) {
// Allow changing the bit rate if we are operating at very low rates
// Don't change the bit rate if the send side is too far off
current_bitrate_bps = current_bitrate_bps_;
time_last_bitrate_change_ = now_ms;
}
return current_bitrate_bps;
}
加性码率增长代码如下:
uint32_t AimdRateControl::AdditiveRateIncrease(
int64_t now_ms, int64_t last_ms, int64_t response_time_ms) const {
assert(response_time_ms > 0);
double beta = 0.0;
if (last_ms > 0) {
// 时间间隔和RTT之比作为系数。
// 疑问,这里的时间点是经过采样的,可能会大于rtt?
beta = std::min((now_ms - last_ms) / static_cast<double>(response_time_ms),
1.0);
if (in_experiment_)
beta /= 2.0;
}
// 默认30fps,由于每个包不超过mtu,一般也就1100+,用这两个值估计每帧码率和每帧包数。
// 并计算平均每个包的大小,最终增加的比特数不超过1000。
double bits_per_frame = static_cast<double>(current_bitrate_bps_) / 30.0;
double packets_per_frame = std::ceil(bits_per_frame / (8.0 * 1200.0));
double avg_packet_size_bits = bits_per_frame / packets_per_frame;
uint32_t additive_increase_bps = std::max(
1000.0, beta * avg_packet_size_bits);
return additive_increase_bps;
}
乘性部分比较简单,也是根据时间差来调整系数。
uint32_t AimdRateControl::MultiplicativeRateIncrease(
int64_t now_ms, int64_t last_ms, uint32_t current_bitrate_bps) const {
double alpha = 1.08;
if (last_ms > -1) {
// 系数计算与文档中的1.05略有不同,使用时间差作为系数,1.08作为底数。
int time_since_last_update_ms = std::min(static_cast<int>(now_ms - last_ms),
1000);
alpha = pow(alpha, time_since_last_update_ms / 1000.0);
}
uint32_t multiplicative_increase_bps = std::max(
current_bitrate_bps * (alpha - 1.0), 1000.0);
return multiplicative_increase_bps;
}
最后一个是最大均值和方差的更新,主要在衰减状态时候进行估计。
oid AimdRateControl::UpdateMaxBitRateEstimate(float incoming_bitrate_kbps) {
const float alpha = 0.05f;
// 当前没有初始值,先设为当前码率,如果有的话,就用当前的值和均值做平滑。
if (avg_max_bitrate_kbps_ == -1.0f) {
avg_max_bitrate_kbps_ = incoming_bitrate_kbps;
} else {
avg_max_bitrate_kbps_ = (1 - alpha) * avg_max_bitrate_kbps_ +
alpha * incoming_bitrate_kbps;
}
// Estimate the max bit rate variance and normalize the variance
// with the average max bit rate.
const float norm = std::max(avg_max_bitrate_kbps_, 1.0f);
// 方差的平滑
var_max_bitrate_kbps_ = (1 - alpha) * var_max_bitrate_kbps_ +
alpha * (avg_max_bitrate_kbps_ - incoming_bitrate_kbps) *
(avg_max_bitrate_kbps_ - incoming_bitrate_kbps) / norm;
// 0.4 ~= 14 kbit/s at 500 kbit/s
if (var_max_bitrate_kbps_ < 0.4f) {
var_max_bitrate_kbps_ = 0.4f;
}
// 2.5f ~= 35 kbit/s at 500 kbit/s
if (var_max_bitrate_kbps_ > 2.5f) {
var_max_bitrate_kbps_ = 2.5f;
}
}
三、总结
本文主要介绍了检测对上调下降码率的影响,具体下调到多少码率,上涨至多少事由后续的ack模块参与计算。