深入浅出WebRTC—DelayBasedBwe

news2024/11/13 12:57:35

WebRTC 中的带宽估计是其拥塞控制机制的核心组成部分,基于延迟的带宽估计是其中的一种策略,它主要基于延迟变化推断出可用的网络带宽。

1. 总体架构

1.1. 静态结构

1)DelayBasedBwe 受 GoogCcNetworkController 控制,接收其输入并返回带宽估计值。

2)DelayBasedBwe 内部使用 InterArrivalDelta、TrendlineEstimator 和 AimdRateControl 来完成带宽估计。

3)InterArrivalDelta 用来计算到达时间延迟。

4)TrendlineEstimator 根据到达时间延迟判断带宽使用是否过载。

5)AimdRateControl 内部运行一个状态机,根据状态机不断调整带宽估计值。

1.2. 调用流程

1)GoogCcNetworkController 收到 TransportFeedback,调用 DelayBasedBwe::IncomingPacketFeedbackVector 来计算基于延迟的带宽估计值。

2)DelaybasedBwe 调用 InterArralDelta::ComputeDeltas 计算延迟差值。

3)DelaybasedBwe 调用 TrendlineEstimator::Update 传入延迟差值计算得到带宽过载情况。

4)DelaybasedBwe 调用 AimdRateControl::Udpate 传入带宽过载情况计算得到延迟带宽估计值。

5)GoogCcNetworkController 调用 SendSideBandwidthEstimation::UpdateDelayBasedEstimate 传入延迟带宽估计值,融合基于丢包的带宽估计值,获得最终的带宽估计值。

1.3. 逻辑架构

1)DelayBaseBWE 接收一坨输入,经过计算输出DelayBasedBwe::Result(见 DelayBasedBwe 数据结构)。

2)InterArrivalDlata 基于 TransportFeedback 计算延迟差。

3)TrendLineEstimator 基于延迟差计算 BandwidthUsage。

4)LinkCapacityEstimator 基于过载状态下的 ACK 码率估算链路容量并计算 3sigma 上下界。当AIMD 升码率时,基于链路容量估计值来决定是采用加性增还是乘性增。当 AIMD 降码率时,下降之后的码率不能高于链路容量估计值。3sigma 上下界用来判断当前链路容量估计值的有效性,如果新的 ACK 码率超出 3sigma 范围,则链路容量估计值要重置并重新进行估算。

5)DelayBaseBWE 设置 ALR 状态到 AimdRateControl,当根据带宽使用情况决策应该要升码率时,如果此时进入了 ALR 状态,不允许升码率,和之前码率保持一致。

6)网络非过载状态下,DelayBaseBWE 设置 Probe bitrate 到 AimdRateControl,用探测带宽更新估计值。此时探测带宽很可能是链路的真实带宽,这样更新是合理的。

7)AimdRateControl 使用 TrendLineEstimator 输出的 BandwidthUsage 更来状态机状态,然后根据新的控制状态来更新带宽估计值,如果是 kRcHold 则保持估计值不变,如果是 kRcIncrease 则采用加性增或乘性增来增加带宽估计值,如果是 kRcDecrease 则降低估计值。

DelayBaseBWE 在调用 AimdRateControl 之前,会根据 BandwidthUsage 进行前处理:

1)如果链路处于 overusing 状态,且没有可用的 ACK 码率,则估计值砍半,这个频率(间隔)由 RTT 决定。

2)如果链路处于normal/underusing 状态,且有可用的探测带宽,则使用探测带宽更新估计值。

其他情况,才会更新 AimdRateControl 状态机,由 AimdRateControl 给出带宽估计值。

2. DelayBasedBwe

2.1. 重要属性

1)video_inter_arrival_

用来计算报文到达延迟差。

2)video_delay_detector_

TrendLineEstimator,用来检测链路过载状态。

3)rate_control_

根据链路过载状态及其变化来对带宽进行估计。

2.2. 重要方法

1)IncomingPacketFeedbackVector

传入 TransportFeedback 以计算报文到达延迟差,同时还会传入 ACK 码率、探测码率和 ALR 状态。

2)OnRttUpdate

更新链路 RTT,AimdRateControl 使用。

3)LatestEstimate

从 AimdRateControl 获取最近延迟带宽估计值。

4)SetStartBitrate

设置 AimdRateControl 的起始码率。

5)SetMinBitrate

设置 AimdRateControl 的最小码率。

2.3. 数据结构

带宽使用有正常、欠载和过载三种状态。

enum class BandwidthUsage {
  kBwNormal = 0,
  kBwUnderusing = 1,
  kBwOverusing = 2,
  kLast
};

延迟带宽估计器返回值。

struct DelayBasedBwe::Result {
  // 带宽估计值是否有更新
  bool updated = false;
  // 是否是使用探测值进行的更新
  bool probe = false;
  // 更新后的目标码率估计值
  DataRate target_bitrate = DataRate::Zero();
  // 是否是从过载恢复
  bool recovered_from_overuse = false;
  // 根据算法计算得到的带宽使用情况
  BandwidthUsage delay_detector_state = BandwidthUsage::kBwNormal;
};

2.4. 源码分析

2.4.1. IncomingPacketFeedbackVector

收到 TransportFeedback 后,使用延迟差值更新趋势线估计器,更新 ALR 状态,然后调用 MaybeUpdateEstimate 获取估计结果。

DelayBasedBwe::Result DelayBasedBwe::IncomingPacketFeedbackVector(
    const TransportPacketsFeedback& msg,
    absl::optional<DataRate> acked_bitrate,
    absl::optional<DataRate> probe_bitrate,
    absl::optional<NetworkStateEstimate> network_estimate,
    bool in_alr) {
  // 按时间进行排序
  auto packet_feedback_vector = msg.SortedByReceiveTime();
  ...
  // 逐个处理feedback
  for (const auto& packet_feedback : packet_feedback_vector) {
    delayed_feedback = false;
    // 计算时延delta,并更新detector状态
    IncomingPacketFeedback(packet_feedback, msg.feedback_time);
    // 网络状态从欠载状态到正常状态,说明已经恢复
    if (prev_detector_state == BandwidthUsage::kBwUnderusing &&
        active_delay_detector_->State() == BandwidthUsage::kBwNormal) {
      recovered_from_overuse = true;
    }
    prev_detector_state = active_delay_detector_->State();
  }

  // 更新ALR状态
  rate_control_.SetInApplicationLimitedRegion(in_alr);
	
  // 从AIMD获取目标码率
  return MaybeUpdateEstimate(acked_bitrate, probe_bitrate,
                             std::move(network_estimate),
                             recovered_from_overuse, in_alr, msg.feedback_time);
}

2.4.2. MaybeUpdateEstimate

1)在没有获取到 ACK 码率的情况下,如果网络过载,算法简单粗暴,每 200ms 下降 50%。在未获得测量值的前提下,保守处理比较好。

2)在获得探测码率的情况下,如果网络不过载,则使用探测码率更新估计值。这是因为探测码率是通过主动探测获得的,它能更快、更真实地反映网络当前的实际承载能力。

DelayBasedBwe::Result DelayBasedBwe::MaybeUpdateEstimate(
	absl::optional<DataRate> acked_bitrate,
	absl::optional<DataRate> probe_bitrate,
	absl::optional<NetworkStateEstimate> state_estimate,
	bool recovered_from_overuse,
	bool in_alr,
	Timestamp at_time) {
	Result result;

	// 网络过载,需要降码率
	if (active_delay_detector_->State() == BandwidthUsage::kBwOverusing) {
		if (acked_bitrate && 
				rate_control_.TimeToReduceFurther(at_time, *acked_bitrate)) {
			// 已经获取ACK码率,且符合调整频率限制,基于 ACK 码率更新估计码率
			result.updated = UpdateEstimate(at_time, acked_bitrate, &result.target_bitrate);
		} else if (!acked_bitrate && 
				rate_control_.ValidEstimate() &&
				rate_control_.InitialTimeToReduceFurther(at_time)) {
			// 未获得 ACK 码率,码率下降 50%
			rate_control_.SetEstimate(rate_control_.LatestEstimate() / 2, at_time);
			result.updated = true;
			result.probe = false;
			result.target_bitrate = rate_control_.LatestEstimate();
		}
	// 网络欠载或正常
	} else {
		// 已经获得了探测码率,则使用探测码率更新AIMD的估计值
		if (probe_bitrate) {
			result.probe = true;
			result.updated = true;
			rate_control_.SetEstimate(*probe_bitrate, at_time);
			result.target_bitrate = rate_control_.LatestEstimate();
		} else {
			// 没有探测码率,则由AIMD基于ACK码率计算估计码率
			result.updated =
				UpdateEstimate(at_time, acked_bitrate, &result.target_bitrate);
			result.recovered_from_overuse = recovered_from_overuse;
		}
	}

	BandwidthUsage detector_state = active_delay_detector_->State();

	// 更新内部状态
	if ((result.updated && prev_bitrate_ != result.target_bitrate) ||
		detector_state != prev_state_) {
		DataRate bitrate = result.updated ? result.target_bitrate : prev_bitrate_;
		prev_bitrate_ = bitrate;
		prev_state_ = detector_state;
	}

	result.delay_detector_state = detector_state;
	return result;
}

3. InterArrivalDelta

3.1. 重要属性

1)send_time_group_length_

约束一个分组的长度不能 send_time_group_length_,用来协助划分时间戳分组。

2)current_timestamp_group_

当前时间戳分组。

3)prev_timestamp_group_

上一个时间戳分组。

4)num_consecutive_reordered_packets_

连续乱序报文数量,乱序定义为分组间的到达时间差为负数。

3.2. 重要方法

1)ComputeDeltas

返回 true 表示达到计算条件,返回发送时间差和接收时间差。返回false,继续更新当前分组数据。

3.3. 报文分组

计算相邻两个报文的到达时间差的噪声太大,计算两个报文分组的到达时间差会更可靠。这就涉及到如何对报文进行分组。

3.3.1. 数据结构

struct SendTimeGroup {
    // 第一个报文
    bool IsFirstPacket() const { return complete_time.IsInfinite(); }
    // 所有报文大小之和
    size_t size;
    // 第一个报文的发送时间
    Timestamp first_send_time;
    // 不断更新的发送时间,但须保持单调递增
    Timestamp send_time;
    // 第一个报文的接收时间
    Timestamp first_arrival;
    // 不断更新的接收时间
    Timestamp complete_time;
    // 每次更新时当前系统时间(发送端收到反馈时的本地系统时间)
    Timestamp last_system_time;
};

3.3.2. 分组规则

1)收到的报文是当前分组第一个报文,则表示分组开始。

2)收到的报文和当前分组属于同一个 burst,则报文归属于当前分组。

3)收到的报文的发送时间与当前分组中第一个报文的发送时间之差小于等于 5ms,则报文归属当前分组。

4)否则,报文属于新的分组。

bool InterArrivalDelta::NewTimestampGroup(Timestamp arrival_time, Timestamp send_time) {
	if (current_timestamp_group_.IsFirstPacket()) {
		// 当前Group还未收到报文
		return false;
	} else if (BelongsToBurst(arrival_time, send_time)) {
		// 与当前Group同属于一个burst
		return false;
	} else { // 根据发送时间跨度来划分分组
		// send_time_group_length_ = TimeDelta::Millis(5)
		return send_time - current_timestamp_group_.first_send_time >
			send_time_group_length_;
	}
}

3.3.3. Burst 规则

1)发送时间相同,肯定属于同一个 burst。

2)由于链路拥塞,导致不同发送时间的报文也可能形成一个 burst,具体规则见注释。

bool InterArrivalDelta::BelongsToBurst(Timestamp arrival_time,Timestamp send_time) {

	// 计算数据包到达时间和当前时间组完成时间(最近报文的 arrival_time)之间的时间差
	TimeDelta arrival_time_delta =
	arrival_time - current_timestamp_group_.complete_time;

	// 计算数据包发送时间和当前时间组发送时间(分组当前最大的 send_time)之间的时间差
	TimeDelta send_time_delta = send_time - current_timestamp_group_.send_time;

	// send_time一样,属于同一个burst发出来
	if (send_time_delta.IsZero())
		return true;

	// 计算传播时间差,即到达时间差减去发送时间差,表示在网络中的传播时间
	TimeDelta propagation_delta = arrival_time_delta - send_time_delta;

	// 即使send_time不一样,如果同时满足以下条件,也认为属于同一个burst:
	// 1)传播时间差为负,说明与当前分组最后一个报文是积压在链路然后一起发送过来
	// 2)到达时间差小于等于5ms
	// 3)报文到达时间与当前分组第一个包的到达时间之差小于100ms
	if (propagation_delta < TimeDelta::Zero() &&
		arrival_time_delta <= kBurstDeltaThreshold &&
		arrival_time - current_timestamp_group_.first_arrival < kMaxBurstDuration)
		return true;

	return false;
}

3.4. Delta 计算

Delta 的计算方式比较简单,两组报文的 send_time 相减和 complete_time 相减分别得到 send_time_delta 和 arrival_time_delta。但还需要处理乱序、系统时间漂移等异常情况,以防异常数据污染,使得计算结果变得不可靠。

1)系统时间漂移

system_time 是收到反馈的本地系统时间,通过比较前后两次系统时间的差值,如果超出正常范围,则认为系统时间可以被人为修改,数据需要重置,重新进行统计。

2)乱序

连续出现 3 次 arrival_time_delta 为负,说明网络乱序情况非常严重,基于当前数据计算结果不可靠,需要进行重置。

bool InterArrivalDelta::ComputeDeltas(Timestamp send_time, Timestamp arrival_time,
  Timestamp system_time, size_t packet_size, TimeDelta* send_time_delta,
  TimeDelta* arrival_time_delta, int* packet_size_delta) {
  bool calculated_deltas = false;
  
  if (current_timestamp_group_.IsFirstPacket()) {
    // 如果是第一个数据包,则仅存储发送和到达时间,因为需要至少两帧数据来计算差异。
    current_timestamp_group_.send_time = send_time;
    current_timestamp_group_.first_send_time = send_time;
    current_timestamp_group_.first_arrival = arrival_time;
  } else if (current_timestamp_group_.first_send_time > send_time) {
    // 如果当前数据包的发送时间早于第一个发送时间,表明是乱序包,直接返回
    return false;
  } else if (NewTimestampGroup(arrival_time, send_time)) {
    // First packet of a later send burst, the previous packets sample is ready.
    if (prev_timestamp_group_.complete_time.IsFinite()) {
      // 两组报文的发送时间差
      *send_time_delta =
        current_timestamp_group_.send_time - prev_timestamp_group_.send_time;
      // 两组报文的接收时间差
      *arrival_time_delta = current_timestamp_group_.complete_time -
        prev_timestamp_group_.complete_time;

      TimeDelta system_time_delta = current_timestamp_group_.last_system_time -
      prev_timestamp_group_.last_system_time;

      // 发现系统时间偏移,重置
      if (*arrival_time_delta - system_time_delta >= kArrivalTimeOffsetThreshold) {
        Reset();
        return false;
      }

      // 两组报文乱序到达,超过3个乱序报文,重置
      if (*arrival_time_delta < TimeDelta::Zero()) {
        ++num_consecutive_reordered_packets_;
        if (num_consecutive_reordered_packets_ >= kReorderedResetThreshold) {
          Reset();
        }
        return false;
      } else {
        num_consecutive_reordered_packets_ = 0;
      }

      // 两组报文大小差
      *packet_size_delta = static_cast<int>(current_timestamp_group_.size) -
        static_cast<int>(prev_timestamp_group_.size);
      // 完成差异计算
      calculated_deltas = true;
    }
    // current -> prev
    prev_timestamp_group_ = current_timestamp_group_;
    // The new timestamp is now the current frame.
    current_timestamp_group_.first_send_time = send_time;
    current_timestamp_group_.send_time = send_time;
    current_timestamp_group_.first_arrival = arrival_time;
    current_timestamp_group_.size = 0;
  } else {
    // 属于当前 group 的正常报文,更新 send_time
    current_timestamp_group_.send_time =
      std::max(current_timestamp_group_.send_time, send_time);
  }
  // Accumulate the frame size.
  current_timestamp_group_.size += packet_size;
  current_timestamp_group_.complete_time = arrival_time;
  current_timestamp_group_.last_system_time = system_time;

  return calculated_deltas;
}

4. TrendLineEstimator

4.1. 重要属性

1)delay_hist_

保存用来计算延迟斜率的元素,元素定义如下:

struct PacketTiming {
  // 相对队列第一组报文到达的相对时间,即 X 坐标
  double arrival_time_ms;
  // 平滑后的累积延迟差,即 Y 坐标
  double smoothed_delay_ms;
  // 平滑前的累积延迟差
  double raw_delay_ms;
};

2)threshold_

计算的延迟斜率通过与 threshold_ 比较来判断网络使用状态。

4.2. 重要方法

1)Update

内部调用 UpdateTrendLine。

2)UdpateTrendline

传入新的时间差值,更新网络使用状态。

3)State

获取当前网络使用状态。

4.3. 源码分析

4.3.1. UpdateTrendline

这是 TrendLineEstimator 的主函数,延迟差值累积到一定数量后,计算一个拟合后的斜率,然后基于斜率判断带宽使用情况,逻辑比较清晰,处理流程如下图所示:

void TrendlineEstimator::UpdateTrendline(double recv_delta_ms,
                                         double send_delta_ms,
                                         int64_t send_time_ms,
                                         int64_t arrival_time_ms,
                                         size_t packet_size) {
  // 计算延迟差
  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;

  // 平滑后延迟累积,使用指数加权平均算法:smoothing_coef_ = 0.9
  smoothed_delay_ = smoothing_coef_ * smoothed_delay_ +
                    (1 - smoothing_coef_) * accumulated_delay_;

  // 插入新元素
  delay_hist_.emplace_back(
      static_cast<double>(arrival_time_ms - first_arrival_time_ms_),
      smoothed_delay_, accumulated_delay_);

  // 确保按照 arrival_time_ms 升序排列
  if (settings_.enable_sort) {
    for (size_t i = delay_hist_.size() - 1;
         i > 0 &&
         delay_hist_[i].arrival_time_ms < delay_hist_[i - 1].arrival_time_ms;
         --i) {
      std::swap(delay_hist_[i], delay_hist_[i - 1]);
    }
  }

  // 维持窗口大小
  if (delay_hist_.size() > settings_.window_size)
    delay_hist_.pop_front();

  // Simple linear regression.
  double trend = prev_trend_;

  // 当累积的数据量达到这个大小时,触发趋势线的更新
  if (delay_hist_.size() == settings_.window_size) {
    trend = LinearFitSlope(delay_hist_).value_or(trend);
    if (settings_.enable_cap) {
      absl::optional<double> cap = ComputeSlopeCap(delay_hist_, settings_);
      if (trend >= 0 && cap.has_value() && trend > cap.value()) {
        trend = cap.value();
      }
    }
  }

  // 基于趋势线计算,检测网络使用状态
  Detect(trend, send_delta_ms, arrival_time_ms);
}

4.3.2. ComputeSlopeCap

考虑到最小二乘法对异常值比较敏感,需要对通过最小二乘法计算的斜率进行约束,约束值的计算方法是:取历史数据中前七个数据中最小原始延迟数据,取历史数据中后七个数据中最小延迟数据,一前一后两个数据计算一个斜率,用这个斜率来约束通过最小二乘法计算的斜率。

absl::optional<double> ComputeSlopeCap(
    const std::deque<TrendlineEstimator::PacketTiming>& packets,
    const TrendlineEstimatorSettings& settings) {
  // 从前数7个数据,寻找最小原始延迟数据点
  TrendlineEstimator::PacketTiming early = packets[0];
  for (size_t i = 1; i < settings.beginning_packets; ++i) {
    if (packets[i].raw_delay_ms < early.raw_delay_ms)
      early = packets[i];
  }

  // 从后数7个数据,寻找最小原始延迟数据点
  size_t late_start = packets.size() - settings.end_packets;
  TrendlineEstimator::PacketTiming late = packets[late_start];
  for (size_t i = late_start + 1; i < packets.size(); ++i) {
    if (packets[i].raw_delay_ms < late.raw_delay_ms)
      late = packets[i];
  }

  // 检查late和early两个数据点之间的时间跨度,如果小于1ms,
  // 则认为时间跨度太小,不足以进行有效计算
  if (late.arrival_time_ms - early.arrival_time_ms < 1) {
    return absl::nullopt;
  }

  // 使用找到的两个数据点计算斜率
  return (late.raw_delay_ms - early.raw_delay_ms) /
           (late.arrival_time_ms - early.arrival_time_ms) +
           settings.cap_uncertainty;
}

4.3.3. Detect

使用调整后斜率与 threshold_ 进行比较,判断网络使用情况,租后还需要更新阈值,以适应网络变化。

1)斜率小于 -threshold_,标识网络为 kBwUnderusing;

2)斜率处于 [-threshold, threshold] 范围内,标识网络为 kBwNormal;

3)斜率大于 threshold_ 且满足以下三个条件则标识网络为 kBwOverusing:

a)连续处于 overuse 状态超过 10ms

b)连续处于 overuse 状态两次及以上

c)最后一次计算的斜率比之前的斜率更大。

void TrendlineEstimator::Detect(double trend, double ts_delta, int64_t now_ms) {
  // 初始阶段,样本数不足,假设网络正常
  if (num_of_deltas_ < 2) {
    hypothesis_ = BandwidthUsage::kBwNormal;
    return;
  }

  // 调整原始trend,具体原理未知,kMinNumDeltas=60,threshold_gain_=4.0
  const double modified_trend =
    std::min(num_of_deltas_, kMinNumDeltas) * trend * threshold_gain_;

  // 更新上次调整后trend
  prev_modified_trend_ = modified_trend;

  if (modified_trend > threshold_) {
    if (time_over_using_ == -1) {
      // 初始化计时器。假设自上次采样以来,有一半时间处于过度使用网络的状态。
      time_over_using_ = ts_delta / 2;
    } else {
      // 累加 overusing 时长
      time_over_using_ += ts_delta;
    }

    overuse_counter_++; // 累加 overuse 次数

    // 处于 overuse 状态很长时间,且检测到 overuse 大于一次
    if (time_over_using_ > overusing_time_threshold_ && overuse_counter_ > 1) {
      if (trend >= prev_trend_) { // trend 还在加强
        time_over_using_ = 0;
        overuse_counter_ = 0;
        hypothesis_ = BandwidthUsage::kBwOverusing;
      }
    }
  } else if (modified_trend < -threshold_) {
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwUnderusing;
  } else { // [-threshold, threshold]
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwNormal;
  }

  prev_trend_ = trend; // 更新trend

  // 动态更新阈值
  UpdateThreshold(modified_trend, now_ms);
}

4.3.4. UpdateThreshold

threshold_ 初始值为 12.5,在计算过程中会进行动态更新,以适应网络条件变化。

void TrendlineEstimator::UpdateThreshold(double modified_trend, int64_t now_ms) {
  if (last_update_ms_ == -1)
    last_update_ms_ = now_ms;

  // kMaxAdaptOffsetMs = 15.0
  // 调整后趋势与当前阈值的差值超过了kMaxAdaptOffsetMs,则不进行阈值调整。
  // 这可以防止因网络突发的大延迟(如路由变化或瞬时拥塞)导致阈值被错误地大幅提高或降低。
  if (fabs(modified_trend) > threshold_ + kMaxAdaptOffsetMs) {
    last_update_ms_ = now_ms;
    return;
  }

  // k_up_(0.0087), k_down_(0.039), threshold_(12.5)
  // 选择阈值调整系数
  const double k = fabs(modified_trend) < threshold_ ? k_down_ : k_up_;

  // 约束time_delta_ms最大值为100ms
  const int64_t kMaxTimeDeltaMs = 100;
  int64_t time_delta_ms = std::min(now_ms - last_update_ms_, kMaxTimeDeltaMs);

  // trend如果在normal区间内,则减小threshold_
  // trend如果在normal区间外,则增大threshold_
  threshold_ += k * (fabs(modified_trend) - threshold_) * time_delta_ms;

  // 约束threshold_在[6, 600]范围
  threshold_ = rtc::SafeClamp(threshold_, 6.f, 600.f);

  last_update_ms_ = now_ms;
}

5. LinkCapacityEstimator

5.1. 重要属性

1)estimate_kbps_

估计的链路容量。

2)deviation_kbps_

链路容量估计值的方差,用来量化估计值的不确定性或波动性,初始值为 0.4。

5.2. 重要方法

1)UpperBound

链路容量估计的上界,等于链路容量估计值加上 3 倍标准差。

2)LowerBound

链路容量估计的下界,等于链路容量估计值减去 3 倍标准差。

3)OnOveruseDetected

当 AIMD 检测到 overuse 时,调用此接口,传入 ACK 码率,LinkCapacityEstimator 对 ACK 码率使用 0.05 的权重来更新估计值。网络处于 overuse 状态时,ACK 码率能够真实反映链路容量。

void LinkCapacityEstimator::OnOveruseDetected(DataRate acknowledged_rate) {
  Update(acknowledged_rate, 0.05);
}

4)OnProbeRate

探测码率具有更好的实时性,LinkCapacityEstimator 对探测码率赋予更高的权重(0.5)来更新估计值。

void LinkCapacityEstimator::OnOveruseDetected(DataRate acknowledged_rate) {
  Update(acknowledged_rate, 0.05);
}

5)estimate

获取链路容量估计值。

5.3. 源码分析

5.3.1. UpperBound/LowerBound

可以认为链路容量估计值符合正态分布,正态分布中大约 99.7% 的数据位于均值的三倍标准差之内,可以提供一个相对宽松但合理的上限和下限估计。

UpperBound 取链路容量估计值加上 3 倍标准差,LowerBound 取链路容量估计值减去 3 倍标准差。

DataRate LinkCapacityEstimator::UpperBound() const {
  if (estimate_kbps_.has_value())
    return DataRate::KilobitsPerSec(estimate_kbps_.value() +
                                    3 * deviation_estimate_kbps());
  return DataRate::Infinity();
}

DataRate LinkCapacityEstimator::LowerBound() const {
  if (estimate_kbps_.has_value())
    return DataRate::KilobitsPerSec(
        std::max(0.0, estimate_kbps_.value() - 3 * deviation_estimate_kbps()));
  return DataRate::Zero();
}

double LinkCapacityEstimator::deviation_estimate_kbps() const {
  // 计算标准差之前先恢复归一化
  return sqrt(deviation_kbps_ * estimate_kbps_.value());
}

5.3.2. Update

AimdRateControl 在网络处于过载状态时,会调用此接口传入 ACK 码率,来更新链路容量估计值。只有处于网络过载状态的 ACK 码率才能真实反映当前链路容量。LinkCapacityEstimator 使用指数移动平均算法对链路容量值进行平滑,并计算归一化方差。

void LinkCapacityEstimator::Update(DataRate capacity_sample, double alpha) {
  double sample_kbps = capacity_sample.kbps();
  if (!estimate_kbps_.has_value()) {
    estimate_kbps_ = sample_kbps;
  } else {
    // 使用指数移动平均算法来平滑估计值
    estimate_kbps_ = (1 - alpha) * estimate_kbps_.value() + alpha * sample_kbps;
  }

  // 方差归一化参数
  const double norm = std::max(estimate_kbps_.value(), 1.0);

  // 计算估计值与当前样本值之间的差,相当于样本值与平均值的差
  double error_kbps = estimate_kbps_.value() - sample_kbps;

  // 计算归一化方差并进行平滑,注意这里的归一化并没有完全归一,单位是 kbps。
  // 如果要做完全归一化,分母应该是 norm 的平方。
  deviation_kbps_ =
    (1 - alpha) * deviation_kbps_ + alpha * error_kbps * error_kbps / norm;

  // 对归一化方差进行约束(这是个经验值)
  deviation_kbps_ = rtc::SafeClamp(deviation_kbps_, 0.4f, 2.5f);
}

限制 deviation_kbps_ 在 [0.4, 2.5] 范围,这应该是一个经验值。相当于 500Kbps 的估计值,归一化方差范围对应的估计误差大约在 14kbps 到 35kbps 之间。

(500-x)^2 / 500 = 0.4
|500-x| = sqrt(500 * 0.4)
|500-x| ~= 14kbps

(500-x)^2 / 500 = 2.5
|500-x| = sqrt(500 * 2.5)
|500-x| ~= 35kbps

6. AimdRateControl

6.1. 重要属性

1)link_capacity_

链路容量估计器,用来辅助调整估计带宽,具体参考源码分析。

6.2. 重要方法

1)SetStartBitrate

设置起始估计值。

2)SetMinBitrate

设置估计值的下限。

3)LatestEstimate

获取最近的估计值。

4)SetRtt

更新 RTT,当进行加性提速时,需要使用 RTT 来计算增速。

5)Update

外部周期性调用,传入 ACK 码率,驱动 AimdRateControl 的状态机运转。

6)SetInApplicationLimitedRegion

设置 ALR 状态,ALR 状态不允许提速(如果有配置的话)。

6.3. 状态机

码率控制状态定义如下:

enum class RateControlState { 
  kRcHold, 
  kRcIncrease, 
  kRcDecrease
};

码率控制状态在网络过载信号的刺激下会作出相应的响应和变化,形成一个状态机,如下图所示。

状态机状态变化规则说明:

1)状态机一开始进入 Incr 状态。

【注】Incr 状态码率每次增长系数见源码分析

2)检测到 overuse 信号,不管当前处于什么状态,都转换到 Decr 状态。

【注】Decr 状态码率每次下降系数为

3)检测到 underuse 信号,不管当前处于什么状态,都转换到 Hold 状态。

【注】即使之前处于 Incr 状态,也必须转换到 Hold 状态。因为 underuse 信号,说明网络正处于排空期,在排空完成前不要提升速率,否则可能会增加延迟。

4)在 Decr 状态,检测到 normal 信号,进入 Hold 状态,降速可以结束了。

5)在 Hold 状态,检测到 normal 信号,进入 Incr 状态,需要开始提速了。

【注】Hold 是一个暂态,会不停往上疯狂试探网络容量的上界。因此,Hold 的意思,更像是等待网络排空的Hold,而不是码率保持的 Hold。

6)在 Incr 状态,检测到 normal 信号,继续保持 Incr 状态,提速还不能停。

【注】说明这是一个相对激进的带宽评估状态机,不会停在刚刚好,而是一定会走到 underuse。

状态机运行相关代码如下:

void AimdRateControl::ChangeState(const RateControlInput& input, Timestamp at_time) {
  switch (input.bw_state) {
    case BandwidthUsage::kBwNormal:
      // 带宽使用状态正常
      // 如果之前是保持,现在可以提升估计值了
      // 如果之前是提升,现在继续提升
      // 如果之前是降低,现在继续降低
      if (rate_control_state_ == RateControlState::kRcHold) {
        time_last_bitrate_change_ = at_time;
        rate_control_state_ = RateControlState::kRcIncrease;
      }
      break;
    case BandwidthUsage::kBwOverusing:
      // 网络过载,不管之前是什么控制状态,现在都要降低估计值
      if (rate_control_state_ != RateControlState::kRcDecrease) {
        rate_control_state_ = RateControlState::kRcDecrease;
      }
      break;
    case BandwidthUsage::kBwUnderusing:
      // 网络欠载,不管之前是什么控制状态,现在都要保持估计值
      rate_control_state_ = RateControlState::kRcHold;
      break;
    default:
      RTC_DCHECK_NOTREACHED();
  }
}

6.4. 源码分析

6.4.1. ChangeBitrate

使用输入参数中的网络使用信号更新状态机,然后根据新的状态指示作出相应处理。有几个处理细节需要注意:

1)ACK 码率是提速和降速的基础,这是因为 ACK 码率是一个链路容量的一个观测值,也是当前链路容量的可靠估计值。

2)LinkCapacityEstimator 是基于 ACK 码率的估计,当收到新的 ACK 码率超出估计值的 3 倍标准差,有理由认为当前的估计值与真实值有较大偏差,需要重置。

void AimdRateControl::ChangeBitrate(const RateControlInput& input,
                                    Timestamp at_time) {
  absl::optional<DataRate> new_bitrate;

  // ACK 码率
  DataRate estimated_throughput =
      input.estimated_throughput.value_or(latest_estimated_throughput_);

  // 更新
  if (input.estimated_throughput)
    latest_estimated_throughput_ = *input.estimated_throughput;

  // 未设置或者未评估,
  if (!bitrate_is_initialized_ && input.bw_state != BandwidthUsage::kBwOverusing)
    return;

  // 状态机更新
  ChangeState(input, at_time);

  switch (rate_control_state_) {
    // 保持
    case RateControlState::kRcHold:
      break; // 码率不变

    // 提速
    case RateControlState::kRcIncrease: {
      // ACK 码率超过估计值的 3*sigma 范围,需要重置 LinkCapacityEstimator
      if (estimated_throughput > link_capacity_.UpperBound())
        link_capacity_.Reset();

      // 以 ACK 码率的 1.5 倍作为提速上限
      DataRate increase_limit =
          1.5 * estimated_throughput + DataRate::KilobitsPerSec(10);

      // 发送端,设置了 ALR 状态不允许升速
      if (send_side_ && in_alr_ && no_bitrate_increase_in_alr_) {
        increase_limit = current_bitrate_;
      }

      if (current_bitrate_ < increase_limit) {
        DataRate increased_bitrate = DataRate::MinusInfinity();
        if (link_capacity_.has_estimate()) {
          // 链路容量估计值有效,认为目标码率与链路容量较为接近,采用加性增进行调整。
          DataRate additive_increase =
              AdditiveRateIncrease(at_time, time_last_bitrate_change_);
          increased_bitrate = current_bitrate_ + additive_increase;
        } else {
          // 链路容量估计值无效,则使用乘性增方式来探索新的链路容量。
          DataRate multiplicative_increase = MultiplicativeRateIncrease(
              at_time, time_last_bitrate_change_, current_bitrate_);
          increased_bitrate = current_bitrate_ + multiplicative_increase;
        }
        new_bitrate = std::min(increased_bitrate, increase_limit);
      }

      time_last_bitrate_change_ = at_time;
      break;
    }

    // 降速
    case RateControlState::kRcDecrease: {
      DataRate decreased_bitrate = DataRate::PlusInfinity();

      // 降速目标设置为 ACK 码率的 85%
      decreased_bitrate = estimated_throughput * beta_;
      if (decreased_bitrate > DataRate::KilobitsPerSec(5) &&
          subtract_additional_backoff_term_) {
        decreased_bitrate -= DataRate::KilobitsPerSec(5);
      }

      // 如果降速目标还是比当前码率高,则尝试使用链路容量估计值的 85%
      if (decreased_bitrate > current_bitrate_) {
        if (link_capacity_.has_estimate()) {
          decreased_bitrate = beta_ * link_capacity_.estimate();
        }
      }

      // 有可能降速目标还是比当前码率高
      if (decreased_bitrate < current_bitrate_) {
        new_bitrate = decreased_bitrate;
      }

      // 更新 last_decrease_
      if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {
        if (!new_bitrate.has_value()) {
          last_decrease_ = DataRate::Zero();
        } else {
          last_decrease_ = current_bitrate_ - *new_bitrate;
        }
      }

      // ACK 码率超过估计值的 3*sigma 范围,需要重置 LinkCapacityEstimator
      if (estimated_throughput < link_capacity_.LowerBound()) {
        link_capacity_.Reset();
      }

      bitrate_is_initialized_ = true;

      // 使用 overuse 信号下的 ACK 码率更新链路容量估计
      link_capacity_.OnOveruseDetected(estimated_throughput);

      // Stay on hold until the pipes are cleared.
      rate_control_state_ = RateControlState::kRcHold;

      time_last_bitrate_change_ = at_time;
      time_last_bitrate_decrease_ = at_time;
      break;
    }
    default:
      RTC_DCHECK_NOTREACHED();
  }

  // 更新当前估计值
  current_bitrate_ = ClampBitrate(new_bitrate.value_or(current_bitrate_));
}

6.4.2. AdditiveRateIncrease

加性提速根据 rtt_ 和 current_bitrate_ 来计算增速,相当于每个 rtt 多发送一个 packet,这样的增速看上去是比较温和的。

DataRate AimdRateControl::AdditiveRateIncrease(Timestamp at_time,
                                               Timestamp last_time) const {
  double time_period_seconds = (at_time - last_time).seconds<double>();
  double data_rate_increase_bps =
      GetNearMaxIncreaseRateBpsPerSecond() * time_period_seconds;
  return DataRate::BitsPerSec(data_rate_increase_bps);
}
double AimdRateControl::GetNearMaxIncreaseRateBpsPerSecond() const {
  RTC_DCHECK(!current_bitrate_.IsZero());
  // 假定视频帧率为30FPS,计算每帧的时间间隔 kFrameInterval 为大约33.3毫秒
  const TimeDelta kFrameInterval = TimeDelta::Seconds(1) / 30;

  // 根据当前比特率和帧间隔,计算单帧数据的大小,这代表了每帧数据传输所需的总比特数
  DataSize frame_size = current_bitrate_ * kFrameInterval;

  // 设定每数据包的大小为1200字节
  const DataSize kPacketSize = DataSize::Bytes(1200);

  //  计算每帧数据需要多少个这样的数据包,取上界值(因为即使最后一包不满,也按一整个包计算)
  double packets_per_frame = std::ceil(frame_size / kPacketSize);

  // 基于总帧大小和包的数量,计算平均每个包的大小
  DataSize avg_packet_size = frame_size / packets_per_frame;

  // 注:纯粹数学推导,avg_packet_size == kPacketSize

  // 在真实RTT的基础上加上100ms再乘以2,使得加性增速更加保守?
  TimeDelta response_time = rtt_ + TimeDelta::Millis(100);
  response_time = response_time * 2;

  // 按照ping-pong模式(收到ack后再发送下一个报文)一秒钟可以多发送多少数据。
  double increase_rate_bps_per_second =
      (avg_packet_size / response_time).bps<double>();

  double kMinIncreaseRateBpsPerSecond = 4000;
  return std::max(kMinIncreaseRateBpsPerSecond, increase_rate_bps_per_second);
}

6.4.3. MultiplicativeRateIncrease

乘性提速会计算一个增益值,初始化增益为 8%,会根据时间频率来调整增益系数。

DataRate AimdRateControl::MultiplicativeRateIncrease(
    Timestamp at_time,
    Timestamp last_time,
    DataRate current_bitrate) const {
  // 初始化增益系数
  double alpha = 1.08;

  // 如果之前调整过码率,距离上次调整码率时间过去越久,增益系数越大
  if (last_time.IsFinite()) {
    auto time_since_last_update = at_time - last_time;
    alpha = pow(alpha, std::min(time_since_last_update.seconds<double>(), 1.0));
  }

  // 根据增益系数计算增速,限制最低码率增速为 1Kbps
  DataRate multiplicative_increase =
      std::max(current_bitrate * (alpha - 1.0), DataRate::BitsPerSec(1000));

  return multiplicative_increase;
}

7. 总结

基于延迟的带宽估计,以处于非 ALR 状态的 ACK 码率为基础,基于延迟梯度估计链路真实带宽,实现对链路带宽的动态跟踪,算法的核心逻辑可以表述为:

1)网络过载,说明网络产生拥塞,当前发送码率过高,需要降低发送码率。

2)网络欠载,说明网络正在排空,不要急着提升发送码率,先保持当前发送码率一段时间。

3)网络正常,可以试着增加发送码率,探测下带宽的上限。

同时,在网络非过载状态下,使用探测带宽对估计值进行校正,使估计更加精准。算法在计算延迟梯度、判断网络使用状况都做了精心的设计,会尽量排除随机噪声的干扰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1936304.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++STL初阶(7):list的运用与初步了解

在了解了vector之后&#xff0c;我们只需要简单学习List与vector不一样的接口即可 1.list的基本接口 1.1 iterator list中&#xff0c;与vector最大的区别就是迭代器由随机迭代器变成双向迭代器 string和vector中的迭代器都是随机迭代器&#xff0c;支持-等&#xff0c;而LIS…

MOGONET:患者分类与biomarker识别

为了充分利用组学技术的进步并更全面地了解人类疾病&#xff0c;需要新的计算方法来综合分析多种类型的组学数据。多组学图卷积网络 (MOGONET&#xff0c;Multi-Omics Graph cOnvolutional NETworks)是一种用于生物医学分类的新型多组学整合方法。MOGONET 包含特定组学的学习和…

Keil开发IDE

Keil开发IDE 简述Keil C51Keil ARMMDK DFP安装 简述 Keil公司是一家业界领先的微控制器&#xff08;MCU&#xff09;软件开发工具的独立供应商。Keil公司由两家私人公司联合运营&#xff0c;分别是德国慕尼黑的Keil Elektronik GmbH和美国德克萨斯的Keil Software Inc。Keil公…

三、初识C语言(3)

1.操作符 &#xff08;1&#xff09;算术操作符 - * / % 商 余&#xff08;取模&#xff09; 小算法&#xff1a; 若a<b&#xff0c;则a%b a 若a%b c&#xff0c;则0 < c < b-1 若两个int 类型数相除&#xff0c;结果有小数会被舍弃。 保留小数…

苹果电脑pdf合并软件 苹果电脑合并pdf 苹果电脑pdf怎么合并

在数字化办公日益普及的今天&#xff0c;pdf文件因其跨平台兼容性强、格式稳定等特点&#xff0c;已经成为工作、学习和生活中不可或缺的文件格式。然而&#xff0c;我们常常面临一个问题&#xff1a;如何将多个pdf文件合并为一个&#xff1f;这不仅有助于文件的整理和管理&…

苏州金龙海格汽车入选2024中国汽车行业可持续发展实践案例

2024年7月11日-13日&#xff0c;由中国汽车工业协会主办的第14届中国汽车论坛在上海嘉定举办。本届论坛隆重发布了“2024中国汽车行业可持续发展实践案例”&#xff0c;苏州金龙因在坚持绿色可持续发展方面做出的努力和贡献获评2024中国汽车行业可持续发展实践案例“绿色发展”…

Ideal窗口中左右侧栏消失了

不知道大家在工作过程中有没有遇到过此类问题&#xff0c;不论是Maven项目还是Gradle项目&#xff0c;突然发现Ideal窗口右侧图标丢失了&#xff0c;同事今天突然说大象图标不见了&#xff0c;不知道怎样刷新gradle。 不要慌张&#xff0c;下面提供一些解决思路&#xff1a; 1…

HarmonyOS ArkUi 唤起系统APP:指定设置界面、浏览器、相机、拨号界面、选择通讯录联系人

效果&#xff1a; 完整工具类&#xff1a; import { common, Want } from kit.AbilityKit; import { BusinessError } from kit.BasicServicesKit; import { call } from kit.TelephonyKit; import { promptAction } from kit.ArkUI; import { contact } from kit.Contacts…

PHP宠物店萌宠小程序系统源码

&#x1f43e;萌宠生活新方式&#x1f43e; &#x1f3e1;【一键直达萌宠世界】 你是否也梦想着拥有一家随时能“云撸猫”、“云吸狗”的神奇小店&#xff1f;现在&#xff0c;“宠物店萌宠小程序”就是你的秘密花园&#xff01;&#x1f31f;只需轻轻一点&#xff0c;就能瞬…

使用Velero备份与恢复K8s集群及应用

作者&#xff1a;红米 环境 3台虚拟机组成一主两从的测试集群&#xff0c;使用NFS作为动态存储。 主机IP系统k8s-master192.168.1.10centos7.9k8s-node1192.168.1.11centos7.9k8s-node2192.168.1.12centos7.9 1、介绍 1.1 简介 备份容灾 一键恢复 集群迁移 支持备份pv&…

CH04_依赖项属性

第4章&#xff1a;依赖项属性 本章目标 理解依赖项属性理解属性验证 依赖项属性 ​ 属性与事件是.NET抽象模型的核心部分。WPF使用了更高级的依赖项属性&#xff08;Dependency Property&#xff09;功能来替换原来.NET的属性&#xff0c;实现了更高效率的保存机制&#xf…

卷积神经网络【CNN】--池化层的原理详细解读

池化层&#xff08;Pooling Layer&#xff09;是卷积神经网络&#xff08;CNN&#xff09;中的一个关键组件&#xff0c;主要用于减少特征图&#xff08;feature maps&#xff09;的维度&#xff0c;同时保留重要的特征信息。 一、池化层的含义 池化层在卷积神经网络中扮演着降…

python调用chrome浏览器自动化如何选择元素

功能描述&#xff1a;在对话框输入文字&#xff0c;并发送。 注意&#xff1a; # 定位到多行文本输入框并输入内容。在selenium 4版本中&#xff0c;元素定位需要填写父元素和子元素名。 textarea driver.find_element(By.CSS_SELECTOR,textarea.el-textarea__inner) from …

ACM中国图灵大会专题 | 图灵奖得主Manuel Blum教授与仓颉团队交流 | 华为论坛:面向全场景应用编程语言精彩回顾

ACM 中国图灵大会&#xff08;ACM Turing Award Celebration Conference TURC 2024&#xff09;于2024年7月5日至7日在长沙举行。本届大会由ACM主办&#xff0c;in cooperation with CCF&#xff0c;互联网之父Vinton Cerf、中国计算机学会前理事长梅宏院士和廖湘科院士担任学术…

linux进程——状态——linux与一般操作系统的状态

前言&#xff1a;博主在之前的文章已经讲解了PCB里面的pid——主要讲解了父子进程PID&#xff0c; 以及fork的相关内容。 本节进入PCB的下一个成员——状态&#xff0c; 状态是用来表示一个进程在内存中的状态的&#xff0c; 进程在内存中肯能处于各种状态&#xff0c; 比如运行…

十七、【机器学习】【非监督学习】- K-均值 (K-Means)

系列文章目录 第一章 【机器学习】初识机器学习 第二章 【机器学习】【监督学习】- 逻辑回归算法 (Logistic Regression) 第三章 【机器学习】【监督学习】- 支持向量机 (SVM) 第四章【机器学习】【监督学习】- K-近邻算法 (K-NN) 第五章【机器学习】【监督学习】- 决策树…

前端Vue组件化实践:自定义发送短信验证码弹框组件

在前端开发中&#xff0c;随着业务逻辑的日益复杂和交互需求的不断增长&#xff0c;传统的整体式开发方式逐渐暴露出效率低下、维护困难等问题。为了解决这些问题&#xff0c;组件化开发成为了一种流行的解决方案。通过组件化&#xff0c;我们可以将复杂的系统拆分成多个独立的…

使用 OpenCV 和 YOLO 模型进行实时目标检测并在视频流中显示检测结果

文章目录 Github官网简介视频帧推理视频设备ID安装依赖 检测示例分类示例姿势估计 Github https://github.com/ultralytics/ultralytics 官网 https://docs.ultralytics.com/zhhttps://docs.ultralytics.com/zh/quickstart/ 简介 Ultralytics 是一个软件公司&#xff0c;专…

深入浅出WebRTC—NACK

WebRTC 中的 NACK&#xff08;Negative Acknowledgment&#xff09;机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象&#xff0c;尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响&#xff0c;从而保持通信的连续性和质量…

【VUE】v-if和v-for的优先级

v-if和v-for v-if 用来显示和隐藏元素 flag为true时&#xff0c;dom元素会被删除达到隐藏效果 <div class"boxIf" v-if"flag"></div>v-for用来进行遍历&#xff0c;可以遍历数字对象数组&#xff0c;会将整个元素遍历指定次数 <!-- 遍…