深入浅出WebRTC—ALR

ALR(Application Limited Region)指的是网络传输过程中,由于应用层的限制(而非网络拥塞)导致带宽未被充分利用的情况。在这种情况下,应用层可能因为处理能力、手动配置或其他因素无法充分利用可用带宽,导致实际传输速率低于网络最大可能提供的速率。因此,在进行拥塞控制或带宽估算时,识别和处理 ALR 状态对于避免不必要的码率下调或误判网络状况至关重要。

1. 配置

ALR 逻辑比较简单,配置项就 3 个,主要用来协助定义进入和退出ALR状态的规则。

struct AlrDetectorConfig {
  // ALR使用的带宽是估计带宽乘以一个比例系数
  double bandwidth_usage_ratio = 0.65;
  // 带宽使用从高点下降,且剩余可用带宽占总容量的比例达到或超过此值时,视为开始进入ALR状态。
  double start_budget_level_ratio = 0.80;
  // 当带宽使用回升,且实际使用比例再次超过此值时,认为已从ALR状态中恢复出来。
  double stop_budget_level_ratio = 0.50;
  std::unique_ptr<StructParametersParser> Parser();
};

2. 静态结构

ALR 实现只有两个类,AlrDetector 提供接口,其内部使用 IntervalBudget 来更新ALR状态,对外接口只有三个:

1)OnBytesSent:每发送完一个报文需要调用此接口,此接口完成Budget水位的更新。

2)SetEstimatedBitrate:设置估计带宽,估计带宽会影响Budget水位调整的细节。

3)GetApplicationLimitedRegionStartTime:如果有值,表示进入 ALR 状态,否则表示退出 ALR 状态。这里为什么不用一个 bool 值来表示是否处于 ALR 状态,是因为某些逻辑需要知道是什么时候进入 ALR 状态和什么时候退出 ALR 状态的。

3. 调用流程

ALR 处理逻辑主要涉及两个调用链,一个是发送报文后,调用 AlrDetector::OnBytesSent 更新Budget 水位,通过 Budget 水位才能判断当前 ALR 状态;另一个是带宽评估变化调用AlrDetector::SetEstimatedBitrate 设置估计带宽,估计带宽会影响水位更新细节和ALR状态判断规则。

4. 实现

4.1. Bucket模型

为了便于理解 WebRTC 是如何判断 ALR 状态,引入一个 Bucket 模型。Bucket 中的水位表示当前 Budget,可以认为是账户余额,IncreateBudget 会向账户中存入资金,增加账户余额,从而提高Bucket 中的水位;UseBudget 会从账户中支取资金,减少账户余额,从而降低 Bucket 中的水位。

IntervalBudget 是 Bucket 模型的实现者,只是 Budge t从金钱换成了数据,每过一段时间 t 计算应该发送的数据:估计带宽 * t,这些数据会存入 Budget,每次发送完报文,需要消耗报文对应数据量的 Budget。

相关逻辑代码如下:

void AlrDetector::OnBytesSent(size_t bytes_sent, int64_t send_time_ms) {
  ...

  int64_t delta_time_ms = send_time_ms - *last_send_time_ms_;
  last_send_time_ms_ = send_time_ms;
  // 减少Budget
  alr_budget_.UseBudget(bytes_sent);
  // 增加Budget
  alr_budget_.IncreaseBudget(delta_time_ms);

  ...
}

通过定义桶的高度,并在桶上面画上 80% 和 50% 两个刻度线,bytes_remaining_ 为当前 Budget 水位,从而形成了一个完整的 Bucket 模型,如下图所示,图中变量与代码中变量一一对应。

其中桶高度 max_bytes_in_budget_ 定义如下:

void IntervalBudget::set_target_rate_kbps(int target_rate_kbps) {
  // 更新目标速率
  target_rate_kbps_ = target_rate_kbps;

  // 计算时间窗口内最多可以发送多少数据,kWindowMs = 500
  max_bytes_in_budget_ = (kWindowMs * target_rate_kbps_) / 8;

  ...
}

4.2. 估计带宽

从上面的 Bucket 模型可知,估计带宽会影响到桶的高度和 Budget 流入的速度。外部会向AlrDectector 实时更新估计带宽,但 AlrDetector 不会全部使用,而是乘以一个系数 0.65(这看起来像是一个经验值)再设置到 IntervalBudget。

void AlrDetector::SetEstimatedBitrate(int bitrate_bps) {
  RTC_DCHECK(bitrate_bps);
  int target_rate_kbps =
      static_cast<double>(bitrate_bps) * conf_.bandwidth_usage_ratio / 1000;
  alr_budget_.set_target_rate_kbps(target_rate_kbps);
}

4.3. 水位变化

Bucket 中的水位用 bytes_remaining_ 表示,80% 和 50% 两条水位线将桶的水位位置划分为三个区:A、B、C,则水位的变化可以穷举为:A -> B、A -> C、B -> A、B -> C、C -> A、C -> B六种情况。

WebRTC 实现定义如果水位处于 A 区,则一定是“进入ALR”状态,因为实际发送数据远少于应该发送数据;如果水位处于 C 区,则一定是“退出ALR”状态,因为实际发送数据已经大于应该发送数据。B 区是一个过渡区,它的 ALR 状态和上一个水位相关,下面我们看下水位在 A、B 和 C 三个区中动态变化中,ALR 状态的变化。

4.3.1. A -> B

刚开始处于“进入ALR”状态,bytes_remaining_ 比例从高于 80%,下降到低于 80% 但高于 50%,ALR 状态保持不变。

4.3.2. A -> C

刚开始处于“进入ALR”状态,bytes_remaining_ 比例从高于 80%,下降到低于 50%,变为“退出ALR”状态。

4.3.3. B -> A

刚开始可能处于“进入ALR”状态也可能处于“退出ALR”状态,bytes_remaining_ 从低于 80% 但高于50% 变为高于 80%。

1)如果刚开始处于“进入ALR”状态(从 A 区进入 B 区),则状态保持不变,仍为“进入ALR”状态;

2)如果刚开始处于“退出ALR”状态(从 C 区进入 B 区),则变为“进入ALR”状态。

总之,不管之前是什么状态,进入 A 区后肯定是“进入ALR”状态。

4.3.4. B -> C

刚开始可能处于“进入ALR”状态也可能处于“退出ALR”状态,bytes_remaining_ 从低于 80% 但高于50% 变为低于 50%。

1)如果刚开始处于“进入ALR”状态(从 A 区进入 B 区),则状态变为“退出ALR”状态;

2)如果刚开始处于“退出ALR”状态(从 C 区进入 B 区),则状态保持不变,仍为“退出ALR”状态。

总之,不管之前是什么状态,进入 C 区后肯定是“退出ALR”状态。

4.3.5. C -> A

刚开始处于“退出ALR”状态,bytes_remaining_ 从低于 50% 变为高于 80%,变为“进入ALR”状态。

4.3.6. C -> B

刚开始处于“退出ALR”状态,bytes_remaining_ 从低于 50% 变为高于 50% 但低于 80%,状态保持不变。

4.4. 状态机

以上 ALR 状态跟随水位变化可以用状态机表示如下:

对应源码为:

void AlrDetector::OnBytesSent(size_t bytes_sent, int64_t send_time_ms) {
  ...

  if (alr_budget_.budget_ratio() > conf_.start_budget_level_ratio && !alr_started_time_ms_) {
    // 进入ALR
    alr_started_time_ms_.emplace(rtc::TimeMillis());
    state_changed = true;
  } else if (alr_budget_.budget_ratio() < conf_.stop_budget_level_ratio &&
    alr_started_time_ms_) {
    // 退出ALR
    state_changed = true;
    alr_started_time_ms_.reset();
  }

  ...
}

5. ALR应用

5.1. ProbeController

进入 ALR 状态后,真实发送的码率可能会远低于链路真实容量,如果长时间处于ALR状态而不进行带宽探测,持续的 ACK 反馈码率会影响最终估计码率,从而导致无法估计带宽失真。因此,专门设置了一个 ALR 带宽探测机制,进入 ALR 状态后,ProbeController 会立即启动一个 ALR 带宽探测。

1)GoogCcNetworkController 在 OnProcessInterval 中更新 ALR 开始时间

NetworkControlUpdate GoogCcNetworkController::OnProcessInterval(ProcessInterval msg) {
  ...

  // 获取ALR状态
  absl::optional<int64_t> start_time_ms =
      alr_detector_->GetApplicationLimitedRegionStartTime();

  // 设置ALR状态
  probe_controller_->SetAlrStartTimeMs(start_time_ms);

  ...
}

2)在 OnTransportPacketsFeedback 中更新 ALR 结束时间

NetworkControlUpdate GoogCcNetworkController::OnTransportPacketsFeedback(
    TransportPacketsFeedback report) {
  ...

  // 获取ALR状态
  absl::optional<int64_t> alr_start_time =
      alr_detector_->GetApplicationLimitedRegionStartTime();

  // 退出ALR状态
  if (previously_in_alr_ && !alr_start_time.has_value()) {
    int64_t now_ms = report.feedback_time.ms();
    acknowledged_bitrate_estimator_->SetAlrEndedTime(report.feedback_time);
    probe_controller_->SetAlrEndedTimeMs(now_ms);
  }
  
  ...
}

3)ProbeController 会定时检测 ALR 状态,适时启动 ALR 带宽探测,探测码率是当前评估码率的 2 倍,带宽探测结果在带宽探测机制中获得。

std::vector<ProbeClusterConfig> ProbeController::Process(Timestamp at_time) {
  ...

  // 以两倍估算带宽进行探测:alr_probe_scale("alr_scale", 2)
  if (TimeForAlrProbe(at_time) || TimeForNetworkStateProbe(at_time)) {
    return InitiateProbing(
        at_time, {estimated_bitrate_ * config_.alr_probe_scale}, true);
  }

  ...
}

5.2. AcknowledgedBitrateEstimator

ACK 码率估计器使用贝叶斯估计算法,其中很重要的一个参数就是数据样本的不确定性,应用如果进入 ALR 状态,则说明此时真实发送的码率低于链路容量,当前 ACK 样本不能真实反映链路带宽,则应该适当增加当前数据样本的不确定性,使得带宽评估值更加真实可靠。

1)GoogCcNetworkController 在 OnSentPacket 中设置 ALR 状态

NetworkControlUpdate GoogCcNetworkController::OnSentPacket(SentPacket sent_packet) {
  alr_detector_->OnBytesSent(sent_packet.size.bytes(), sent_packet.send_time.ms());
  acknowledged_bitrate_estimator_->SetAlr(
      alr_detector_->GetApplicationLimitedRegionStartTime().has_value());
  ...
}

2)在 OnTransportPacketsFeedback 中更新 ALR 结束时间

NetworkControlUpdate GoogCcNetworkController::OnTransportPacketsFeedback(
    TransportPacketsFeedback report) {
  ...

  // 获取ALR状态
  absl::optional<int64_t> alr_start_time =
      alr_detector_->GetApplicationLimitedRegionStartTime();

  // 退出ALR状态
  if (previously_in_alr_ && !alr_start_time.has_value()) {
    int64_t now_ms = report.feedback_time.ms();
    acknowledged_bitrate_estimator_->SetAlrEndedTime(report.feedback_time);
    probe_controller_->SetAlrEndedTimeMs(now_ms);
  }

  ...
}

3)ALR 刚结束,码率增速会比正常快,增加贝叶斯估计器历史数据的方差,也就是历史数据的贡献变小,能够更快速响应码率变化。

void AcknowledgedBitrateEstimator::IncomingPacketFeedbackVector(
    const std::vector<PacketResult>& packet_feedback_vector) {
  ...

  for (const auto& packet : packet_feedback_vector) {
    // ALR刚结束,设置码率估计器快速响应新的码率
    if (alr_ended_time_ && packet.sent_packet.send_time > *alr_ended_time_) {
      bitrate_estimator_->ExpectFastRateChange();
      alr_ended_time_.reset();
    }
    ...
  }
}

4)贝叶斯估计器在更新数据时,如果当前正处于 ALR 状态,会为数据样本赋予一个更大的不确定性,使得其在整体数据中的贡献占比降低。

void BitrateEstimator::Update(Timestamp at_time, DataSize amount, bool in_alr) {
  ...

  float scale = uncertainty_scale_;
  if (is_small_sample && bitrate_sample_kbps < bitrate_estimate_kbps_) {
    scale = small_sample_uncertainty_scale_;
  } else if (in_alr && bitrate_sample_kbps < bitrate_estimate_kbps_) {
    // Optionally use higher uncertainty for samples obtained during ALR.
    scale = uncertainty_scale_in_alr_;
  }

  ...
}

5.3. DelayBasedBWE

由于在 ALR 状态下获取的反馈不是链路满载下的反馈,基于这种反馈向上调整带宽估计值很可能是不准确的,因此,ALR 状态保持原来的估计值,是比较明智的。

void AimdRateControl::ChangeBitrate(const RateControlInput& input, Timestamp at_time) {
  absl::optional<DataRate> new_bitrate;
  ...
  switch (rate_control_state_) {
    case RateControlState::kRcHold:
      break;
    case RateControlState::kRcIncrease: {
      // ALR状态不允许升速
      if (send_side_ && in_alr_ && no_bitrate_increase_in_alr_) {
        increase_limit = current_bitrate_;
      }
    ...
  }
  ...
}

5.4. LossBasedBweV2

基于丢包的带宽估计器,在全局搜索最优带宽和固有丢包率组合时,需要先构造候选带宽。如果当前正处于 ALR 状态,ACK 码率不能反映网络真实带宽,不应该将 ACK 码率作为候选带宽(可配置)。

std::vector<LossBasedBweV2::ChannelParameters> LossBasedBweV2::GetCandidates(bool in_alr) const {
  ...
  // 添加一个基于 ACK 码率但进行了回退因子调整的候选带宽
  if (acknowledged_bitrate_.has_value() &&
      config_->append_acknowledged_rate_candidate) {
    if (!(config_->not_use_acked_rate_in_alr && in_alr) ||
        (config_->padding_duration > TimeDelta::Zero() &&
         last_padding_info_.padding_timestamp + config_->padding_duration >=
             last_send_time_most_recent_observation_)) {
      bandwidths.push_back(*acknowledged_bitrate_ *
                           config_->bandwidth_backoff_lower_bound_factor);
    }
  }
  ...
}

6. 总结

识别 ALR 状态对 WebRTC 的拥塞控制来说非常重要,很多人可能没有意识到这一点。为什么这么说,是因为,WebRTC 的拥塞控制算法本质上是一种“刀尖上跳舞”的算法,只有当你要求的最大带宽超过链路容量时,才需要做拥塞控制,此时 WebRTC 会在链路容量的上限疯狂试探。如果带宽随便你使用,怎么用都用不完,怎么用都不会造成拥塞,那也就没必要做拥塞控制了。

ALR 状态本质上是用来标识当前带宽是否够用,进入 ALR 状态和退出 ALR 状态,所需要的控制策略是不一样的,相关算法都需要做调整。ALR 状态就像一个全局开关,开和关直接控制着拥塞控制的行为。

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值