webrtc系列(二)——拥塞控制算法

1,729 阅读3分钟

背景

网络拥塞是基于IP协议的数据报交换网络中常见的一种网络传输问题,它对网络传输的质量有严重的影响, 网络拥塞是导致网络吞吐降低, 网络丢包等的主要原因之一, 这些问题使得上层应用无法有效的利用网络带宽获得高质量的网络传输效果。特别是在通信领域, 网络拥塞导致的丢包, 延迟, 抖动等问题, 严重的影响了通信质量, 如果不能很好的解决这些问题, 一个通信产品就无法在现实环境中正常使用。

丢包带宽估计

基于丢包的拥塞控制比较简单,其基本思想是根据丢包的多少来判断网络的拥塞程度. 丢包越多则认为网络越拥塞,那么我们就要降低发送速率来缓解网络拥塞; 如果没有丢包,这说明网络状况很好,这时候就可以提高发送码率,向上探测是否有更多的带宽可用. 实现该算法有两点:一是获得接收端的丢包率,一是确定降低码率和提升码率的阈值。

As_hat(i) =As_hat(i-1),2%<=丢包率<=10%,保持不变 As_hat(i) = As_hat(i-1)(1-0.5p),丢包率>10%,下降,p是丢包率 As_hat(i) = 1.05*As_hat(i-1),丢包率<2%,上升

webrtc中发送端收到RTCP RR报文并解析得到丢包率后,根据下图公式计算发送端码率: 当丢包率大于0.1时,说明网络发生拥塞,此时降低发送端码率; 当丢包率小于0.02时,说明网络状况良好,此时增大发送端码率; 其他情况下,发送端码率保持不变。

延迟带宽估计

Goog-REMB

基于接收端的延迟算法,利用延迟值,通过卡尔曼滤波器估计出下一时刻的网络带宽趋势,效果的准确性和公平性不如Transport-CC,目前已经被webrtc的新版本所淘汰

Transport-CC

基于发送端的延迟算法,也是利用区间延迟值,通过TrendLine滤波器(最小二乘法滤波器),通过斜率的增加或者减小来判断当前网络的拥塞状况

时延梯度计算

首先我们通过一张图看下时延梯度的计算:

1636882526357.png 对于两个包组:i以及i−1,它们的时延梯度:

1636882566700.png

在 WebRTC 中,延迟梯度不是一个个包来计算的,而是通过将包分组,然后计算这些包组之间的延迟,这样做可以减少计算次数,同时减少误差。 包组的划分原则为在一个burst_time间隔内的一系列的包构成一个组。建议burst_time为5ms.

TrendLine滤波器(最小二乘法滤波器)

使用到了线性回归的方法进行时延趋势预测,通过最小二乘法求得拟合的直线斜率

1636882812267.png

对于一堆样本点(x,y),拟合直线方程y=ax+b的斜率a按如下公式计算

1636882967496.png

具体方式就是对距离平方和的最小值,根据求导推出a的值,具体可以参考www.cnblogs.com/paiandlu/p/…

代码实现路径,goog_cc/trendline_estimator.cc,实现和上述推导公式一致

absl::optional<double> LinearFitSlope(
    const std::deque<TrendlineEstimator::PacketTiming>& packets) {
  RTC_DCHECK(packets.size() >= 2);
  // Compute the "center of mass".
  double sum_x = 0;
  double sum_y = 0;
  for (const auto& packet : packets) {
    //计算x,y的总值
    sum_x += packet.arrival_time_ms;
    sum_y += packet.smoothed_delay_ms;
  }
  //计算x,y的平均值
  double x_avg = sum_x / packets.size();
  double y_avg = sum_y / packets.size();
  // Compute the slope 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& packet : packets) {
    double x = packet.arrival_time_ms;
    double y = packet.smoothed_delay_ms;
    numerator += (x - x_avg) * (y - y_avg);
    denominator += (x - x_avg) * (x - x_avg);
  }
  if (denominator == 0)
    return absl::nullopt;
  return numerator / denominator;
}

整体调用流程为

LinearFitSlope  UpdateTrendline
                ↓
LinearFitSlope  LinearFitSlope
                ↓
TrendlineEstimator::Detect
                ↓
TrendlineEstimator::UpdateThreshold
UpdateTrendline

这个方法主要是做一些数据的统计计算之后,调用LinearFitSlope计算斜率

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;

  // Exponential backoff filter.
  accumulated_delay_ += delta_ms;
  BWE_TEST_LOGGING_PLOT(1, "accumulated_delay_ms", arrival_time_ms,
                        accumulated_delay_);
  //根据1)中的时延累积值计算得到平滑后的时延:smoothing_coef_为0.9
  smoothed_delay_ = smoothing_coef_ * smoothed_delay_ +
                    (1 - smoothing_coef_) * accumulated_delay_;
  BWE_TEST_LOGGING_PLOT(1, "smoothed_delay_ms", arrival_time_ms,
                        smoothed_delay_);

  if (delay_hist_.size() > settings_.window_size)
    delay_hist_.pop_front();

  // Simple linear regression.
  double trend = prev_trend_;
  //当队列delay_hist_大小等于设定的窗口大小时,开始进行时延变化趋势计算,得到直线斜率
  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_);
      // We only use the cap to filter out overuse detections, not
      // to detect additional underuses.
      if (trend >= 0 && cap.has_value() && trend > cap.value()) {
        trend = cap.value();
      }
    }
  }
  BWE_TEST_LOGGING_PLOT(1, "trendline_slope", arrival_time_ms, trend);
  //传入trend来计算当前网络状态
  Detect(trend, send_delta_ms, arrival_time_ms);
}
Detect

该函数主要根据时延变化增长趋势计算当前网络状态。 在Detect函数内部,会根据前面计算得到的斜率得到一个调整后的斜率值:modified_trend

void TrendlineEstimator::Detect(double trend, double ts_delta, int64_t now_ms) {
  if (num_of_deltas_ < 2) {
    hypothesis_ = BandwidthUsage::kBwNormal;
    return;
  }
  const double modified_trend =
      std::min(num_of_deltas_, kMinNumDeltas) * trend * threshold_gain_;
  prev_modified_trend_ = modified_trend;
  BWE_TEST_LOGGING_PLOT(1, "T", now_ms, modified_trend);
  BWE_TEST_LOGGING_PLOT(1, "threshold", now_ms, threshold_);
  //1.
  if (modified_trend > threshold_) {
    if (time_over_using_ == -1) {
      // Initialize the timer. Assume that we've been
      // over-using half of the time since the previous
      // sample.
      time_over_using_ = ts_delta / 2;
    } else {
      // Increment timer
      time_over_using_ += ts_delta;
    }
    overuse_counter_++;
    // 2/3
    if (time_over_using_ > overusing_time_threshold_ && overuse_counter_ > 1) {
       //4
      if (trend >= prev_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 {
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwNormal;
  }
  prev_trend_ = trend;
  UpdateThreshold(modified_trend, now_ms);
}

const double modified_trend = std::min(num_of_deltas_, kMinNumDeltas) * trend * threshold_gain_;

num_of_deltas_ 表示包组间延迟梯度计算的次数,取值范围是 [2, 60],threshold_gain_ 是 Trendline 滤波器的增益参数,其默认值为 4,这两个变量都会对延迟梯度趋势值进行放大。 这里的关键就在于为何要放大 的意义trend在于是一个斜率值,取值范围 (-1, 1),这个值很小,而阈值 要跟随 的变化而变化,这可能导致 很容易的超过 从而触发连续的过载信号。放大 可以使得算法不会因为 的波动而过于敏感。

判断为过载的条件

  • 延迟梯度斜率值 > 当前阈值
  • 过载总时长 > kOverUsingTimeThreshold(10ms)
  • 过载次数 >1
  • 当前延迟梯度斜率值 > 上一次的延迟梯度斜率值,即延迟在不断恶化。
UpdateThreshold

阈值threshold_动态调整为了改变算法对时延梯度的敏感度。根据主要是,时延梯度是变化的,有时很大,有时很小,如果阈值是固定的,对于时延梯度来说可能太大或者太小,这样就会出现检测不够敏感,无法检测到网络拥塞,或者过于敏感,导致一直检测为网络拥塞;

void TrendlineEstimator::UpdateThreshold(double modified_trend,
                                         int64_t now_ms) {
   //根据斜率上升还是下降定义K系数 k_down_(0.039)  k_up_(0.0087),
  const double k = fabs(modified_trend) < threshold_ ? k_down_ : k_up_;
  const int64_t kMaxTimeDeltaMs = 100;
  //计算上一次更新time的时间,和100ms取最小值
  int64_t time_delta_ms = std::min(now_ms - last_update_ms_, kMaxTimeDeltaMs);

  //将k和time_delta_ms,进行计算,更新阈值
  threshold_ += k * (fabs(modified_trend) - threshold_) * time_delta_ms;
  threshold_ = rtc::SafeClamp(threshold_, 6.f, 600.f);
  last_update_ms_ = now_ms;
}