WebRTC 拥塞控制之 AIMD 码率控制

网络带宽的检测过程以及在该过程中产生三种信号:overuse、normal、underuse。本文将介绍这三种信号如何驱动码率控制器工作,这个过程涉及到:三种码率控制状态(increase、decrease、hold)的切换、增加码率的算法(加性增大与乘性增大)、减少码率的算法(指数回退,即乘性减小)、最大码率方差的计算。

指数移动均值、方差、标准差、归一化

在介绍 WebRTC 码率控制算法之前,先来了解几个与统计学相关的概念。

  • 方差(Variance)

统计中的方差是每个样本值与样本均值之差的平方值的平均数。

  • 标准差(Standard Deviation)

标准差是方差的算术平方根,反映一个数据集的离散程度。

  • 归一化(Normalize)

把数据经过处理后使之限定在一定的范围内。

  • 指数平滑移动均值 (Exponential Moving Average)

EMA 是以指数式递减加权的移动平均,它是一种趋向类指标,其原理是一阶滞后滤波法。EMA 可以降低周期的性干扰,在波动频率较高的场景中有很好的效果。

AIMD 码率控制概述

AIMD 是 TCP 拥塞控制中码率调节的概念。在 计算机网络[2] 一书中,对 AIMD 算法做了如下描述:

在拥塞避免阶段,拥塞窗口是按照线性规律增大的,这常称为加法增大 AI(Additive Increase)。而一旦出现超时或 3 个重复的确认,就要把门限值设置为当前拥塞窗口值的一半,并大大减小拥塞窗口的数值。这常称为乘法减小 MD(Multiplicative Decrease)。二者合在一起就是所谓的 AIMD 算法。

状态切换

GCC 草案[3]  定义了三种码率控制状态:

The rate control subsystem has 3 states: Increase, Decrease and Hold. “Increase” is the state when no congestion is detected; “Decrease” is the state where congestion is detected,  and “Hold” is a state that waits until built-up queues have drained before going to “increase” state.

三种状态在 WebRTC 中表示如下:

enum RateControlState {
kRcHold, kRcIncrease, kRcDecrease
};

码率控制状态之间的切换,如下图所示:

WebRTC 拥塞控制之 AIMD 码率控制
状态切换
  1. 收到 overuse 信号,码率控制器总是进入 decrease 状态。
  2. 收到 underuse 信号,码率控制器总是进入 hold 状态。
  3. 收到 normal 信号,码率控制器提升当前状态。
  • 当前在 hold 状态,提升到 increase 状态。
  • 当前在 decrease 状态,提升到 hold 状态。

关于码率控制状态切换,有两点个人理解:

  • 收到过载信号,将码率降低,这很好理解,但是收到低载信号,为何进入码率保持状态?

我的理解是,收到 underuse 信号,说明网络上传输的流量很少,网络链路容量完全可以承载当前发送码率,不需要做任何改变,保持该码率即可。

  • 收到正常信号,hold 状态提升到 increase 状态很好理解,但是 decrease 状态为何要提升到 hold 状态?

我的理解是,在网络正常的情况下,原来的码率处于 decrease 状态,说明码率已经降低到适应当前网络状况了,所以转向 hold 状态。

控制算法

WebRTC 借鉴 AIMD 这种码率调节思想,设计了一套自己的算法:

WebRTC 拥塞控制之 AIMD 码率控制

注意,上面的码率调节算法最初用于 Receive-side BWE,在 WebRTC 最新的 Send-side BWE 中复用了该算法。

结合三种码率状态之间的切换与调节算法,下面的图应该不难理解,具体的码率调节过程下文会详细介绍。

WebRTC 拥塞控制之 AIMD 码率控制
码率控制状态机

源码分析

基于 WebRTC M71 版本

码率控制在 AimdRateControl 类中实现,该类的重要的成员函数如下:

class AimdRateControl {
public:
uint32_t Update(
const RateControlInput* input,
int64_t now_ms);
int GetNearMaxIncreaseRateBps() const;
private:
void ChangeState(
const RateControlInput& input,
int64_t now_ms);
uint32_t ChangeBitrate(
uint32_t current_bitrate,
const RateControlInput& input,
int64_t now_ms);
uint32_t MultiplicativeRateIncrease(
int64_t now_ms,
int64_t last_ms,
uint32_t current_bitrate_bps) const;
uint32_t AdditiveRateIncrease(
int64_t now_ms,
int64_t last_ms) const;
void UpdateMaxThroughputEstimate(
float estimated_throughput_kbps);
};

Update 函数

该函数为驱动 AimdRateControl 工作的外部接口函数,其内部调用私有成员函数 ChangeBitrate

ChangeBitrate  函数

该函数为整个码率控制算法的核心实现函数,主要流程包括:

  1. 根据输入的带宽检测信号,更新码率控制状态,在 ChangeState 函数中实现。
  2. 根据新的码率控制状态和最大码率的标准差,进行相应的码率控制,计算新的码率。
  • 如果码率控制状态为 hold,码率保持不变。
  • 如果码率控制状态为 increase,根据当前码率是否接近网络链路容量对其进行 加性增大 或者 乘性增大
  • 如果码率控制状态为 decrease,对码率进行 乘性减小,并更新当前网络链路容量估计值的方差,即最大码率的方差。
  1. 控制新的码率值在一定范围内。

当码率控制算法计算出的新的码率值过大,超过发送端的发送能力,则需要将新的码率值限制到一定区间内,这也避免了发送端码率增长过快。

关于 2 中详细的码率控制过程,在源码分析的最后会继续介绍。

ChangeState 函数

该函数输入带宽检测信号,从而对码率控制状态进行切换。

switch (input.bw_state) {
case BandwidthUsage::kBwNormal:
if (rate_control_state_ == kRcHold) {
rate_control_state_ = kRcIncrease;
}
break;
case BandwidthUsage::kBwOverusing:
if (rate_control_state_ != kRcDecrease) {
rate_control_state_ = kRcDecrease;
}
break;
case BandwidthUsage::kBwUnderusing:
rate_control_state_ = kRcHold;
break;
}

关于状态切换,有一点 WebRTC 源码并未体现:当收到 normal 信号时,如果当前状态是 decrease,应该提升到 hold 状态。

除此之外,码率控制状态的切换过程与上文描述的一致。

AdditiveRateIncrease 函数

该函数执行加性增大算法,输入当前时间和上次码率被更新的时间,返回增加的码率大小。

returnstatic_cast<uint32_t>(
(now_ms - last_ms) *
GetNearMaxIncreaseRateBps() / 1000);

根据源码可知:加性增大的码率大小 = 距上次码率更新所经历的时间(秒) * 每秒应该增加的码率大小。所以,真正的加性增大算法在 GetNearMaxIncreaseRateBps 函数中实现。

注意,GetNearMaxIncreaseRateBps 函数名告诉我们,在码率接近网络链路容量时,进行加性增大。

GetNearMaxIncreaseRateBps 函数

该函数会计算在当前码率下执行加性增大算法后增加的码率值。

计算当前码率下每帧的大小,假设帧率为 30fps。

double bits_per_frame =
static_cast<double>(current_bitrate_bps_) / 30.0;

计算每帧的包数,假设每包大小为 1200B。

double packets_per_frame =
std::ceil(bits_per_frame / (8.0 * 1200.0));

注意,每帧的包数向上取整,至少为 1,也就是说,如果每帧大小小于单个包大小 1200 bytes,也会认为每帧含有一个包。

计算当前码率下每个包实际的大小。

double avg_packet_size_bits =
bits_per_frame / packets_per_frame;

计算 response_time,其大小为 rtt 加上 100 ms 的发送端 BWE 延迟,rtt 默认值为 200ms。

constint64_t response_time = in_experiment_ ?
(rtt_ + 100) * 2 : rtt_ + 100;

加性增大的原则应该是:每一个 response_time 增加一个包的大小

那么,每秒包含 (1000 / response_time) 个 response_time interval,则每秒应该增加 (1000 / response_time)个包的大小,于是,加性增大的码率值就计算出来了。

constexprdouble kMinIncreaseRateBps = 4000;
returnstatic_cast<int>(std::max(
kMinIncreaseRateBps,
(avg_packet_size_bits * 1000) / response_time));

有两点需要注意:

  1. 加性增大的码率值至少为 4Kbps,比如在低码率场景下,加性增大的码率值可能小于 4Kbps。
  2. GCC 草案规定每一个 response_time interval,码率增加至多半个包的大小,而非 WebRTC 源码中一个包的大小。

MultiplicativeRateIncrease 函数

该函数执行乘性增大算法,输入当前时间和上次码率被更新的时间,返回增加的码率大小。

double alpha = 1.08;
if (last_ms > -1) {
auto time_since_last_update_ms =
rtc::SafeMin<int64_t>(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);

将距上次码率更新所经过的时间作为指数,计算增长系数 alpha。alpha 初值取 1.08,最大值也为 1.08,也就是说,乘性增大的码率值最大不会超过当前码率的 8%。

UpdateMaxThroughputEstimate 函数

该函数输入网络带宽过载状态下的码率估计值 estimated_throughput_kbps,计算网络链路容量的方差。

注意,该函数只有在收到过载信号降低码率时才会被调用,这是因为:一旦检测到过载并准备降低码率时,说明当前网络链路已经达到了最大吞吐量(带宽),此时的码率估计值才能代表网络链路容量 ,从而作为样本并计算其方差。

可以认为,网络链路容量 link capacity 代表了网络吞吐量 throughput,也即带宽 bandwidth

也可以认为,过载状态下的码率估计值 estimated_throughput_kbps 即为最大码率,计算网络链路容量的方差就是计算最大码率的方差。

  • 一次指数平滑法计算最大码率的指数移动均值

estimated_throughput_kbps 为接近 link capacity 的最大码率估计值,即样本值。

avg_max_bitrate_kbps_ 为最大码率的估计值 estimated_throughput_kbps 的均值,即样本均值。

constfloat alpha = 0.05f;
avg_max_bitrate_kbps_ =
(1 - alpha) * avg_max_bitrate_kbps_ +
alpha *estimated_throughput_kbps;
  • 一次指数平滑法计算最大码率的方差

这里求最大码率的方差并不是进行简单平均,而是也采用了指数移动平均求取方差,并使用最大码率均值对方差进行归一化。

constfloat norm =
std::max(avg_max_bitrate_kbps_, 1.0f);
var_max_bitrate_kbps_ =
(1 - alpha) * var_max_bitrate_kbps_ +
alpha *
(avg_max_bitrate_kbps_ -
estimated_throughput_kbps) *
(avg_max_bitrate_kbps_ -
estimated_throughput_kbps) / norm;

对于求取最大码率均值与方差时的指数平滑系数,GCC 草案的建议值为 0.95。

  • 将归一化后的方差控制到 [0.4, 2.5] 区间范围之内。
// 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;
}

再谈 ChangeBitrate 函数

在了解了码率控制状态如何切换、加性增大与乘性增大算法以及如何计算最大码率方差之后,我们再来看一下码率增加或者减少的详细过程。

  • 计算最大码率标准差

UpdateMaxThroughputEstimate 函数计算出了最大码率的方差,只需要对其开根号,就可以得到标准差。

constfloat std_max_bit_rate =
sqrt(var_max_bitrate_kbps_ *
avg_max_bitrate_kbps_);

注意,因为这个方差使用了最大码率均值进行归一化,所以开根号之前要乘以最大码率均值,还原真正的方差值。

最大码率标准差表征了链路容量 link capacity 的估计值相对于均值的波动程度。

  • 增加码率

关于码率增加是选择加性增大还是乘性增大,GCC 草案规定如下:

The system does a multiplicative increase if the current bandwidth estimate appears to be far from convergence, while it does an additive increase if it appears to be closer to convergence. “Close” is defined as three standard deviations around this average.

如果 rate_control_region_ == kRcNearMax,即当前码率接近 link capacity,此时增长需放慢,选择加性增大。

如果 rate_control_region_ == kRcMaxUnknown,即当前码率远未达到 link capacity,此时可放手增大,选择乘性增大。

rate_control_region_ 初始化为 kRcMaxUnknown,也就是说,WebRTC 码率控制模块以增加码率(乘性增大)的状态启动。

case kRcIncrease:
if (avg_max_bitrate_kbps_ >= 0 &&
estimated_throughput_kbps >
avg_max_bitrate_kbps_ +
3 * std_max_bit_rate)
{
rate_control_region_ = kRcMaxUnknown;
avg_max_bitrate_kbps_ = -1.0;
}
if (rate_control_region_ == kRcNearMax)
{
uint32_t additive_increase_bps =
AdditiveRateIncrease(now_ms,
time_last_bitrate_change_);

new_bitrate_bps +=
additive_increase_bps;
} else
{
uint32_t multiplicative_increase_bps =
MultiplicativeRateIncrease(now_ms,
time_last_bitrate_change_,
new_bitrate_bps);

new_bitrate_bps +=
multiplicative_increase_bps;
}
time_last_bitrate_change_ = now_ms;
break;

如果当前网络吞吐量的评估值与最大码率均值的差大于最大码率标准差的 3 倍。认为最大码率均值不可靠,丢弃之前的码率评估数据,复位并重新计算。

同时,认为当前的 link capacity 是未知的,设置 rate_control_region_ 为 kRcMaxUnknown,乘性增大码率以探索 link capacity,即最大码率。

  • 降低码率

根据公式 1,码率的降低以接收端反馈后的码率估计值 estimated_throughput_bps 为基准,而非当前码率,回退系数为 0.85。

如果回退后的码率大于当前发送码率(当 estimated_throughput_bps 出现较大波动时可能会出现这种情况),则以最大码率均值 avg_max_bitrate_kbps_ 为基准进行回退。

总之,码率降低的原则是:降低后的新的码率必须保证小于等于当前的发送码率

码率降低后将 rate_control_region_ 设置为 kRcNearMax,说明此时码率已经接近 link capacity,之后如果收到 normal 信号需要增大码率,只能选择加性增大。

new_bitrate_bps =
static_cast<uint32_t>(beta_ *
estimated_throughput_bps + 0.5);

if (new_bitrate_bps > current_bitrate_bps_)
{
if (rate_control_region_ != kRcMaxUnknown)
{
new_bitrate_bps =
static_cast<uint32_t>(beta_ *
avg_max_bitrate_kbps_ * 1000 + 0.5f);
}
new_bitrate_bps = std::min(
new_bitrate_bps,
current_bitrate_bps_);
}
rate_control_region_ = kRcNearMax;

下面要判断网络是否发生退化:即降低后的码率值是否小于当前码率与回退系数以及网络退化系数的乘积。其中,回退系数依然为 0.85,网络退化系数为 0.9。

如果码率降低的过多,那么这次降低的值 last_decrease_ 将视为无效,不会用于 BWE 周期的计算。

constexprfloat kDegradationFactor = 0.9f;
if (smoothing_experiment_ &&
new_bitrate_bps <
kDegradationFactor * beta_ *
current_bitrate_bps_) {
last_decrease_ = absl::nullopt;
} else {
last_decrease_ =
current_bitrate_bps_ - new_bitrate_bps;
}

接下来,判断本次最大码率评估值(样本值)相对于最大码率均值(样本均值)的离散程度是否在合理范围内。

如果差值小于 3 个标准差,认为最大码率均值不可靠,复位。

if (estimated_throughput_kbps <
avg_max_bitrate_kbps_ -
3 * std_max_bit_rate) {
avg_max_bitrate_kbps_ = -1.0f;
}

接下来,更新最大码率均值与方差,将码率控制状态设置为 hold,直到网络链路缓冲区队列清空。

如果码率降低后,网络依然拥塞,那么会继续触发 overuse 信号降低码率。

UpdateMaxThroughputEstimate(
estimated_throughput_kbps);
// Stay on hold until the pipes are cleared.
rate_control_state_ = kRcHold;
break;

最后,将新的码率控制到区间 [min_configured_bitrate_bps_, 1.5f * estimated_throughput_bps + 10000] 之内。

最大限制码率是 estimated_throughput_bps 的线性函数,限制码率的原因上文已经做过解释,不再赘述。

参考资料

[1] 指数平滑法: https://mp.weixin.qq.com/s?__biz=MzU3MTUyNDUzMA==&mid=2247483869&idx=1&sn=3f2d15cafed26ce21bf440f3378749d1&chksm=fcdf9500cba81c1658a561dfc57bb9fb3f5e217cbcdebbc1300dc1b2d750849ef4a96bbf05fc&token=926771124&lang=zh_CN#rd

[2] AIMD 算法: 计算机网络第7版

[3] GCC 草案: https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02#section-5.5

作者:于吉太
来源:码神说
原文:https://mp.weixin.qq.com/s/WMf26i2OdNWEgvRPfDyVKQ

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论