webrtc的pacing分析

If you can’t explain it simply, you don’t understand it well enough.-Albert Einstein

 根据流量对于时延的敏感性,可以把数据流分为弹性流(elastic traffic)和非弹性流(inelastic traffic)。所谓的时延是否敏感,衡量的标准就是数据流是否能够在时间轴上拉伸。RTP的实时视频流属于非弹性流,而文件传输则是弹性流。
 不管什么流,数据流的发送速率应该同网络的分配给相应数据流可用带宽(available bandwidth)相匹配。关于pacing在网络中的作用,参考[1].关于数据流的pacing,有一些相关研究,一个反对[2],一个赞成[3].基于在youtube上的实验结果[5],表明pacing降低丢包率,降低rtt。在BBR里面,就有一个pacing速率sk_pacing_rate。在我看来,网卡向外发送几个数据包,几乎是不占什么时间的,但是pacing的存在,需要将数据包的发送时间"拉伸"。比如在BBR中,瞬时发送一个数据包(1500Byte)后,需要等待一段时间 Δ t = 1500 B y t e p a c i n g _ r a t e \Delta t=\frac{1500Byte}{pacing\_rate} Δt=pacing_rate1500Byte之后,再发送下一个数据包,。这样就可以保证,最后数据的发送速率为pacing_rate。
 接下来说下webrtc中的pacing的大致原理paced_sender.cc。可能有不准的地方。
 类 IntervalBudget中主要两个变量,发送速率(target_rate_kbps_),允许向外发送的字节(bytes_remaining_)。线程池每5ms(kMinPacketLimitMs)进入PacedSender::Process处理一次。假设发送的速率设定为200KB/s,假设一个数据包长度为1500Byte。那么每隔5ms可以向外发送1000byte的数据,但是实际上pacing模块按照数据包(1500Byte)为单位向网络中发送数据包。只要bytes_remaining_>0。上个时间间隔若"多发"了字节,需要在本次时间间隔内"少发”(bytes_remaining_ = bytes_remaining_ + bytes)。

  void IncreaseBudget(int64_t delta_time_ms) {
    int64_t bytes = target_rate_kbps_ * delta_time_ms / 8;
    if (bytes_remaining_ < 0) {
      // We overused last interval, compensate this interval.
      bytes_remaining_ = bytes_remaining_ + bytes;
    } else {
      // If we underused last interval we can't use it this interval.
      bytes_remaining_ = bytes;
    }
  }
  void UseBudget(size_t bytes) {
    bytes_remaining_ = std::max(bytes_remaining_ - static_cast(bytes),
                                -kWindowMs * target_rate_kbps_ / 8);
  }

 那么发包序号如下,前面为时刻,括号内为发送出去的包序号,0ms(bytes_remaining=1000),5ms(包1, bytes_remaining_=-500),10ms(包2,bytes_remaining_=-1000),15ms(不发,bytes_remaining_=0),20ms(包4,bytes_remaining_=-500)……就是每隔15ms向外发送两个数据包。
 但是视频数据的产生是按照视频帧率产生的,而通过paceing模块处理,数据包的发送速率是按照target_rate_kbps_发送的。这样数据包就要在发送缓冲区中排队。排队造成了时延。数据时延超过了一定的阈值,下面的代码,就要采取冒险策略,增加发送速率。

void PacedSender::Process() {
  if (!paused_ && elapsed_time_ms > 0) {
    size_t queue_size_bytes = packets_->SizeInBytes();
    if (queue_size_bytes > 0) {
      // Assuming equal size packets and input/output rate, the average packet
      // has avg_time_left_ms left to get queue_size_bytes out of the queue, if
      // time constraint shall be met. Determine bitrate needed for that.
      packets_->UpdateQueueTime(clock_->TimeInMilliseconds());
      int64_t avg_time_left_ms = std::max(
          1, kMaxQueueLengthMs - packets_->AverageQueueTimeMs());//队列中的最大
          //延时kMaxQueueLengthMs为2000ms,队列时延较大,avg_time_left_ms较小
      int min_bitrate_needed_kbps =
          static_cast(queue_size_bytes * 8 / avg_time_left_ms);
      if (min_bitrate_needed_kbps > target_bitrate_kbps)//超出了之前的速率,
      //就要增加速率
        target_bitrate_kbps = min_bitrate_needed_kbps;
    }
    media_budget_->set_target_rate_kbps(target_bitrate_kbps);
    elapsed_time_ms = std::min(kMaxIntervalTimeMs, elapsed_time_ms);
    UpdateBudgetWithElapsedTime(elapsed_time_ms);
  }
}

 其计算数据包在PacketQueue中的排队时延的方法很有意思。

class PacketQueue {
 public:
  explicit PacketQueue(const Clock* clock)
      : bytes_(0),
        clock_(clock),
        queue_time_sum_(0),
        time_last_updated_(clock_->TimeInMilliseconds()) {}
  virtual ~PacketQueue() {}

  void Push(const Packet& packet) {
    if (!AddToDupeSet(packet))
      return;

    UpdateQueueTime(packet.enqueue_time_ms);

    // Store packet in list, use pointers in priority queue for cheaper moves.
    // Packets have a handle to its own iterator in the list, for easy removal
    // when popping from queue.
    packet_list_.push_front(packet);
    std::list::iterator it = packet_list_.begin();
    it->this_it = it;          // Handle for direct removal from list.
    prio_queue_.push(&(*it));  // Pointer into list.
    bytes_ += packet.bytes;
  }  
  void FinalizePop(const Packet& packet) {
    RemoveFromDupeSet(packet);
    bytes_ -= packet.bytes;
    queue_time_sum_ -= (time_last_updated_ - packet.enqueue_time_ms);//(2)更新排
   // 队时延
    packet_list_.erase(packet.this_it);
    RTC_DCHECK_EQ(packet_list_.size(), prio_queue_.size());
    if (packet_list_.empty())
      RTC_DCHECK_EQ(0, queue_time_sum_);
  }
  void UpdateQueueTime(int64_t timestamp_ms) {
    RTC_DCHECK_GE(timestamp_ms, time_last_updated_);
    int64_t delta = timestamp_ms - time_last_updated_;
    // Use packet packet_list_.size() not prio_queue_.size() here, as there
    // might be an outstanding element popped from prio_queue_ currently in the
    // SendPacket() call, while packet_list_ will always be correct.
    queue_time_sum_ += delta * packet_list_.size();//(1)计算数据包的排队时延
    time_last_updated_ = timestamp_ms;
  }
}

 每次上层向PacketQueue中Push数据包,需要调用一次UpdateQueueTime来更新下时戳,并计算下排队时延(上面代码的标号(1))。线程池每次进入PacedSender::Process函数,也需要更新下排队的时延(packets_->UpdateQueueTime(clock_->TimeInMilliseconds()))。本质就是向PacketQueue注入时间的变化(delta)。
 以下图为例,横轴表示时间的流逝,上面的竖线,表示上层向队列中Push的数据包。其中t1,t2,t3表示PacedSender::Process函数的调动间隔,假设插入m个数据包,数据包成员packet.enqueue_time_ms记录进入队列的时间。假设数据包之间没有间隔(实际上有很小的间隔也不影响分析)。在t1时刻,线程进入PacedSender::Process函数,队列为空,但是需要更新time_last_updated_为t1。瞬时插入m个包后, queue_time_sum_为0;在t2时刻,进入PacedSender::Process函数,更新time_last_updated_为t2,queue_time_sum_=m*(t2-t1),发送出一个数据包后(最先进入队列的数据包),调用FinalizePop,更新排队时延(标号(2)),queue_time_sum_=m*(t2-t1)-t1。之后又插入n个数据包。在t3时刻,进入PacedSender::Process函数,更新time_last_updated_为t3,更新queue_time_sum_=(m-1)(t2-t1)+(m+n-1)(t3-t2)。代码中标号(1)计算的就是已有的数据包在之前时间间隔内的排队时间与新的时间间隔的排队时延。在Push函数中,是先更新队列时戳,再改变队列的长度(新入队的包还不需要排队时延)。实际上数据包的的入队可能在时间间隔内的任何时刻,但是不影响结果。
webrtc的pacing分析_第1张图片
[1]BBR与CoDel
[2]Aggarwal A, Savage S, Anderson T. Understanding the performance of TCP pacing[C]//INFOCOM 2000. Nineteenth Annual Joint Conference of the IEEE Computer and Communications Societies. Proceedings. IEEE. IEEE, 2000, 3: 1157-1165.
[3]Wei D, Cao P, Low S, et al. TCP pacing revisited[C]//Proceedings of IEEE INFOCOM. 2006.
[4]Carousel: Scalable Traffic Shaping at End Hosts(2017-sigcomm)
[5]Trickle: Rate Limiting YouTube Video Streaming(2012)
[6]A Model Predictive Control Approach to Flow Pacing for TCP(2017)

你可能感兴趣的:(webrtc)