GoogCcNetworkController 是 GCC 的控制中心,它由 RtpTransportControllerSend 通过定时器和 TransportFeedback 来驱动。GoogCcNetworkController 不断更新内部各个组件的状态,并协调组件之间相互配合,向外输出目标码率等重要参数,实现拥塞控制功能。
1. 静态结构
GoogCcNetworkController 继承自 NetworkContollerInterface,其内部调动了一大堆类来实现拥塞控制,包括丢包估计器、延时估计器、带宽探测控制器以及一些码率相关的估计器。
1)RtpTransportControllerSend
RtpTransportControllerSend 是 GoogCcNetworkController 的驱动器,主要包括定时器和 TranportFeedback 两种驱动;同时,也是 GoogCcNetworkController 融入 WebRTC 整体架构的接口,GoogCcNetworkController 的重要输出包括目标码率、带宽探测、拥塞状态等,都是通过 RtpTransportControllerSend 带向其他模块。
2)ProbeBitrateEstimator
带宽探测由 GoogCcNetworkController 触发,但执行不归 GoogCcNetworkController 管,不过,带宽探测的结果最终要回到 GoogCcNetworkController 这里来,ProbeBitrateEstimator 就是负责带宽探测结果评估的,评估后的探测带宽直接交给 GoogCcNetworkController 。
3)AlrDetector
GoogCcNetworkController 带宽评估器大部分时候是在链路容量的极限边缘试探,这是它们的预设工况。但当发送码率明显小于估计带宽(分配带宽),这时候工况发生变化,WebRTC 需要能检测到这种变化,并及时通知相关模块进行调整,否则估计值可能会产生很大偏差,AlrDetector 就是干这事的。
4)CongestionWindowPushbackController
从名字可以看出来与拥塞状态相关。经典的拥塞控制算法一般都会用到拥塞控制窗口,比如 TCP 的拥塞控制。WebRTC 的拥塞控制虽然走的不是 TCP 那条路子,但还是尊重基于拥塞窗口判断拥塞状态的有效性,具体体现是,不管你前面耍了多少花活,捣鼓了各种估计器,最终的估计带宽还是得基于拥塞状态进行调整。CongestionWindowPushbackController 就是就是这个调整器。
5)SendBandwidthEstimation
SendBandwidthEstimation 内部包含了基于丢包的带宽估计器:LossBasedBweV2, 其输出的是综合延时估计和丢包估计的带宽估计值,就是最早大家熟知的延时估计和丢包估计两者取其小的逻辑,当然,最新代码实现远比这个表述复杂。
6)ProbeController
GoogCcNetworkController 不直接控制带宽探测,带宽探测的发起、参数和冲突处理等都是由 ProbeController 负责,GoogCcNetworkController 会更新相关状态和通知相关事件的发生,ProbeController 会基于状态和事件来判断是否需要启动带宽探测任务。当然,也有主动请求带宽探测的情况,比如链路经历一次排空后需要立即进行一次带宽探测。
7)AckowledgedBitrateEstimator
深入阅读 GCC 源码就会有一种感觉,ACK 码率满天飞。这是因为,ACK 码率太重要了,它是 GCC 能观测到的唯二带宽,另一个是探测码率(带宽)。作为链路容量最重要的观测值,ACK 码率是各大带宽估计器的基础输入。为了彰显尊贵身份,针对 ACK 码率的估计祭出了贝叶斯估计器,也算是相得益彰。
8)DelayBasedBWE
其实,GCC 真正的扛把子是延迟带宽估计器,看下它后面的小弟就知道了。事实上,GCC 最依仗就是延迟估计带宽,基于丢包的估计带宽可以认为是一种调整。下面的码率关系图也可以看到,延迟估计带宽会作为丢包估计器的一个输入,但是延迟估计器不会参考丢包估计带宽。相比最开始的版本,DelayBasedBWE 也有很大改变,比如卡尔曼滤波没了、引入了基于最小二乘的趋势线判断等,具体细节就不展开了。
2. 整体架构
2.1. 在整体架构中的位置
下图描述的是视频处理逻辑架构,拥塞控制器 Controller 基于丢包估计器和延时估计器的输出计算得到目标码率 target_bitrate,目标码率会传递到带宽分配器 Allocator,Allocator 根据一定算法为编码器和 FEC 分配所需码率。Contoller 会向 Pacer 设置平滑发送码率和链路当前拥塞状态,控制平滑发送模块的行为。Controller 自己不会直接发起带宽探测,而是通过状态和事件驱动 Prober 适时发起带宽探测。
2.2. 各种码流之间的关系
GoogCcNetworkController 对外输出的码流主要有两个:target_bitrate 和 stable_target_bitrate,target_bitrate 由 LossBasedBWE 输出,它是综合丢包估计和延迟估计的估计值,stable_target_bitrate 由 LinkCapacityTracker 输出,是链路容量的一个观测值,因为观测值是真实测量得到的,所以是 stable 的。CongestionWindowPushbackController 基于网络拥塞状态对 target_bitrate 做了进一步调整,输出 pushback_target_bitrate。
ACK 码率是所有估计的基础,对不同估计器的其作用机理不同:
1)对于 LinkCapacityTracker,ACK 码率是估计值的上限,且只会提高估计值,而不会降低估计值。估计值的下限由延迟估计码率决定,延迟估计码率只会降低估计值,而不会提高估计值。
之所以这么设计,是因为 ACK 码率不会超过链路真实容量,如果当前 ACK 码率高于之前链路容量的估计值,完全有理由使用此 ACK 码率作为当前链路容量的估计值(会进行平滑处理);但如果 ACK 码率低于当前链路容量估计值,不能此 ACK 码率来更新链路容量估计值,因为,较低的 ACK 码率可能是由一个低于链路容量发送码率导致。
延迟估计带宽是链路容量的一个估计值,如果发现延迟估计带宽比上一次的估计值低(处于下降趋势中),有理由认为延迟估计带宽是在链路容量的极限附近试探,如果此次延迟估计带宽低于当前链路容量的估计值,那么可以可以用延迟估计带宽更新链路容量估计值。
2)对于 DelayBasedBWE,ACK 码率主要用来控制码率下降或上升的幅度。当链路处于 underusing 或 normal 状态时,会用探测码率直接更新估计值。
3)对于 LossBasedBWE,ACK 码率主要用来生成候选者和辅助调整最佳候选者的带宽,进而影响最终的估计值。
2.3. 重要调用关系
下图展示了以 GoogCcNetworkController 为核心拥塞控制相关的重要类以及它们之间的调用关系。调用方法后的 F 表示是由 TransportFeedback 驱动的调用,调用方法后的 T 表示是由定时器驱动的调用,调用方法后的 U 表示是由 MaybeTriggerOnNetworkChanged 驱动的调用。
2.3.1. PacingController
1)SetCongested
PacingController 在拥塞状态下会停止发送媒体报文(需要继续发送keep-alive报文)。如果未确认的报文的大小超出拥塞窗口的大小,就认为链路处于拥塞状态,未确认报文越多则拥塞越严重。
2)SetPacingRates
GoogCcNetworkController 获得新的带宽估计值,需要设置到 PacingController,用来控制 PacingController 平滑发送速率。包含两个码率:PacingRate 和 PaddingRate,在讲解平滑发送时再详细说明。
3)CreateProbeClusters
如果要发起带宽探测,ProbeController 最终会调用 PacingController 的接口创建带宽探测任务(其实是间接调用,由 RtpTransportControllerSend 完成委托任务)。
2.3.2. BitrateAllocator
GoogCcNetworkController 获得新的带宽估计值,需要通知 BitrateAllocator 重新进行带宽分配。包含两个码率:target_bitrate 和 stable_bitrate,在讲解码率分配时再详细说明。
2.3.3. AcknowledgeBitrateEstimator
1)IncomingPacketFeedbackVector
AcknowledgeBitrateEstimator 基于 TransportFeedback 计算 ACK 码率。其内部使用数据窗口进行采样,每个采样窗口计算一个码率,然后使用贝叶斯算法进行平滑。
2)SetAlrEndedTime
在进行贝叶斯平滑计算时,AcknowledgeBitrateEstimator 会给处于 ALR 状态的样本赋以更低的权重(更大的不确定性),因为 ALR 状态采集的样本不能反映真实的链路带宽。
2.3.4. CongestionWindowPushbackController
CongestionWindowPushbackController 基于 DataWindow 和 OutstandingData 来判断链路拥塞情况,然后根据链路拥塞情况来调整目标码率。
1)SetDataWindow
DataWindow 是基于估计带宽和 RTT 计算的理想拥塞窗口大小。因为 WebRTC 应用在实时音视频场景,其拥塞窗口大小不能设置的太激进,否则可能导致网络管道拥堵,延迟增加。
2)UpdateOutstandingData
OutstandingData 是观测到的未确认数据的大小。
3)UpdatePacingQueue
之所以要获取 PacingQueue 大小,是因为 CongestionWindowPushbackController 支持设置实验参数来决定是否将平滑发送模块队列中缓存的数据也记为 OutstandingData。
2.3.5. ProbeBitrateEstimator
ProbeBitrateEstimator 基于 TransportFeedback 来统计探测带宽。
2.3.6. DelayBasedBWE
DelayBasedBWE 依赖 TransportFeedback 计算报文到达延迟差。
2.3.7. ProbeController
1)Process
需要循环检查 ALR 状态以启动 ALR 带宽探测任务。
2)SetAlrStartTimeMs
设置 ALR 状态开始时间。
3)SetAlrEndedTimeMs
设置 ALR 状态结束时间。
4)RequestProbe
延迟带宽估计器在收到 TransportFeedback 去更新网络使用状态时,如果发现网络使用状态从 underusing 变为 normal,说明网络经历了一次排空,需要调用 RequestProbe 立即启动一次带宽探测。
5)SetEstimatedBitrate
带宽探测完成后,需要将探测结果告知 ProbeController,一来新的带宽可以用来生成后续目标探测带宽,另外,对于设置了 probe_further 参数的带宽探测任务,可能需要创建进一步的带宽探测任务。
2.3.8. AlrDetector
AlrDetector 基于估计带宽去消耗 budget,因此,估计带宽变化需要及时通知 AlrDetector,否则会导致 ALR 状态会判断错误。
2.3.9. SendSideBandwidthEstimation
1)UpdateEstimate
这是一个定时调用。在 LossBasedBWE 还未准备好之前,SendSideBandwidthEstimation 需要通过另一套算法来持续更新估计带宽。LossBasedBWE 好以后,则是获取 LossBasedBWE 估计结果。同时,还需要时刻监视 RTT,一旦 RTT 异常需要采取行动降低带宽。
2)UpdatePropagationRtt
Propagation RTT 是 RttBasedBackoff 所需,用来监控 RTT 异常。
3)SetAcknowledgedRate
ACK 码率会通过 OnRateUpdate 传入 LinkCapacityTracker,通过 SetAcknowledgedBitrate 传入 LossBasedBWE。LinkCapacityTracker 基于 ACK 码率来跟踪链路容量。LossBasedBWE 中 ACK 码率被作为估计值的下限,还有其他很多用途,具体参考相关章节。
4)UpdateDelayBasedEstimate
延迟带宽估计值会设置到 LinkCapacityTracker 作为估计值的下限。
5)UpdateLossbasedEstimate
调用 UpdateBandwidthEstimate 接口将 TransportFeedback 和 延迟估计带宽传入到 LossBasedBWE,LossBasedBWE 输出的估计带宽会选择丢包估计和延迟估计的较小者。
3. 源码分析
3.1. OnTransportPacketsFeedback
收到 TransportFeedback 会通过层层调用到 GoogCcNetworkController,调用流程如下图所示:
TransportFeedback 被用来计算延迟梯度和丢包率,进而更新延迟带宽估计器和丢包带宽估计器。
NetworkControlUpdate GoogCcNetworkController::OnTransportPacketsFeedback(TransportPacketsFeedback report) {// 判断if (report.packet_feedbacks.empty()) {return NetworkControlUpdate();}// 当前已启用congestion_window_pushback_controller_,更新infightif (congestion_window_pushback_controller_) {congestion_window_pushback_controller_->UpdateOutstandingData(report.data_in_flight.bytes());}TimeDelta max_feedback_rtt = TimeDelta::MinusInfinity();TimeDelta min_propagation_rtt = TimeDelta::PlusInfinity();Timestamp max_recv_time = Timestamp::MinusInfinity();// max_recv_time是最及时反馈的数据项std::vector<PacketResult> feedbacks = report.ReceivedWithSendInfo();for (const auto& feedback : feedbacks)max_recv_time = std::max(max_recv_time, feedback.receive_time);for (const auto& feedback : feedbacks) {// 反馈RTT = feedback_ts - send_timeTimeDelta feedback_rtt =report.feedback_time - feedback.sent_packet.send_time;// 相对最后一个报文的偏差,这个偏差表示收到数据后,等待了这么长时间才反馈TimeDelta min_pending_time = max_recv_time - feedback.receive_time;// 真实链路RTT需要减去等待处理时间TimeDelta propagation_rtt = feedback_rtt - min_pending_time;// 获取最大反馈RTTmax_feedback_rtt = std::max(max_feedback_rtt, feedback_rtt);// 获取最小链路RTTmin_propagation_rtt = std::min(min_propagation_rtt, propagation_rtt);}if (max_feedback_rtt.IsFinite()) {// 维护feedback_max_rtts_队列feedback_max_rtts_.push_back(max_feedback_rtt.ms());const size_t kMaxFeedbackRttWindow = 32;if (feedback_max_rtts_.size() > kMaxFeedbackRttWindow)feedback_max_rtts_.pop_front();// 更新链路RTT,用来跟踪RTT over limitbandwidth_estimation_->UpdatePropagationRtt(report.feedback_time,min_propagation_rtt);}// 获取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);}// 更新ALR状态previously_in_alr_ = alr_start_time.has_value();// Ack码率估计acknowledged_bitrate_estimator_->IncomingPacketFeedbackVector(report.SortedByReceiveTime());// 获取ACK码率估计结果auto acknowledged_bitrate = acknowledged_bitrate_estimator_->bitrate();// 丢包带宽估计需要ack码率bandwidth_estimation_->SetAcknowledgedRate(acknowledged_bitrate,report.feedback_time);// 识别带宽探测数据包反馈,计算探测码率for (const auto& feedback : report.SortedByReceiveTime()) {if (feedback.sent_packet.pacing_info.probe_cluster_id !=PacedPacketInfo::kNotAProbe) {probe_bitrate_estimator_->HandleProbeAndEstimateBitrate(feedback);}}// 获取探测到的带宽absl::optional<DataRate> probe_bitrate =probe_bitrate_estimator_->FetchAndResetLastEstimatedBitrate();// 限制探测带宽低于ack码率,这样有助于排空网络管道// kProbeDropThroughputFraction = 0.85if (limit_probes_lower_than_throughput_estimate_ &&probe_bitrate && acknowledged_bitrate) {DataRate limit =std::min(delay_based_bwe_->last_estimate(),*acknowledged_bitrate * kProbeDropThroughputFraction);probe_bitrate = std::max(*probe_bitrate, limit);}NetworkControlUpdate update;bool recovered_from_overuse = false;// 先更新延迟带宽估计器DelayBasedBwe::Result result;result = delay_based_bwe_->IncomingPacketFeedbackVector(report,acknowledged_bitrate,probe_bitrate,estimate_,alr_start_time.has_value());if (result.updated) {if (result.probe) {// 延迟码率是使用探测码率进行更新的,则将探测码率作为目标码率???bandwidth_estimation_->SetSendBitrate(result.target_bitrate,report.feedback_time);}// 调用了SetSendBitrate,还需调用UpdateDelayBasedEstimate进行更新bandwidth_estimation_->UpdateDelayBasedEstimate(report.feedback_time,result.target_bitrate);}// 再更新丢包带宽估计器,因为丢包带宽估计器依赖于延迟带宽估计器的结果bandwidth_estimation_->UpdateLossBasedEstimator(report,result.delay_detector_state,probe_bitrate,alr_start_time.has_value());if (result.updated) {// 丢包带宽估计器得到的才是最终带宽评估结果,需要将评估结果更新到// ProbeController,以防需要进行带宽探测MaybeTriggerOnNetworkChanged(&update, report.feedback_time);}recovered_from_overuse = result.recovered_from_overuse;// 从OverUse恢复,立马触发一次带宽探测if (recovered_from_overuse) {probe_controller_->SetAlrStartTimeMs(alr_start_time);auto probes = probe_controller_->RequestProbe(report.feedback_time);update.probe_cluster_configs.insert(update.probe_cluster_configs.end(),probes.begin(), probes.end());}if (rate_control_settings_.UseCongestionWindow() &&max_feedback_rtt.IsFinite()) {// 根据估计码率和RTT计算并更新current_data_window_UpdateCongestionWindowSize();}if (congestion_window_pushback_controller_ && current_data_window_) {// 使用了congestion_window_pushback_controller_,则更新计算的拥塞窗口大小congestion_window_pushback_controller_->SetDataWindow(*current_data_window_);} else {// 没有使用congestion_window_pushback_controller_,需要将当前计算得到的// 拥塞窗口大小通知给RtpTransportControllerSend,然后根据实际inflight判断// 拥塞状态并设置到Pacer,进而控制Pacer行为。update.congestion_window = current_data_window_;}return update;
}
3.2. OnProcessInterval
GoogCcNetworkController 自己并没有定时器,而是通过 RtpTransportControllerSend 来驱动的。
在 OnProcessInterval 方法中,SendSideBandwidthEstimation 和 ProbeController 需要定时器进行驱动。
NetworkControlUpdate GoogCcNetworkController::OnProcessInterval(ProcessInterval msg) {NetworkControlUpdate update;// 初始化if (initial_config_) {// 重置参数,并设置ProbeController,获取带宽探测Cluster配置update.probe_cluster_configs =ResetConstraints(initial_config_->constraints);// 更新pacing参数update.pacer_config = GetPacingRates(msg.at_time);// 根据配置决定是否启动ALR带宽探测,ALR探测是周期探测if (initial_config_->stream_based_config.requests_alr_probing) {probe_controller_->EnablePeriodicAlrProbing(*initial_config_->stream_based_config.requests_alr_probing);}// 设置最大分配码率,如果需要探测,则添加探测Cluster配置absl::optional<DataRate> total_bitrate =initial_config_->stream_based_config.max_total_allocated_bitrate;if (total_bitrate) {auto probes = probe_controller_->OnMaxTotalAllocatedBitrate(*total_bitrate, msg.at_time);update.probe_cluster_configs.insert(update.probe_cluster_configs.end(),probes.begin(), probes.end());}initial_config_.reset();}// pacer_queue 大小可能被配置为 outstanding dataif (congestion_window_pushback_controller_ && msg.pacer_queue) {congestion_window_pushback_controller_->UpdatePacingQueue(msg.pacer_queue->bytes());}// RTT backoff 监控及启动阶段估计值更新bandwidth_estimation_->UpdateEstimate(msg.at_time);// 获取 ALR 状态absl::optional<int64_t> start_time_ms =alr_detector_->GetApplicationLimitedRegionStartTime();// ProbeController 可能启动了 ALR 带宽探测probe_controller_->SetAlrStartTimeMs(start_time_ms);// 主要用来驱动 ALR 带宽探测auto probes = probe_controller_->Process(msg.at_time);// 可能有带宽探测任务update.probe_cluster_configs.insert(update.probe_cluster_configs.end(),probes.begin(), probes.end());// 根据 RTT 和目标码率更新拥塞窗口大小if (rate_control_settings_.UseCongestionWindow() && !feedback_max_rtts_.empty()) {UpdateCongestionWindowSize();}// 更新拥塞窗口大小if (congestion_window_pushback_controller_ && current_data_window_) {congestion_window_pushback_controller_->SetDataWindow(*current_data_window_);} else {update.congestion_window = current_data_window_;}// 检查状态变化MaybeTriggerOnNetworkChanged(&update, msg.at_time);return update;
}
3.4. MaybeTriggerOnNetworkChanged
网络状态变化,需要及时通知相关利益方,以便执行相应处理动作,包括触发带宽探测、码率重分配、设置平滑发送码率等。
void GoogCcNetworkController::MaybeTriggerOnNetworkChanged(NetworkControlUpdate* update,Timestamp at_time) {// 丢包率uint8_t fraction_loss = bandwidth_estimation_->fraction_loss();// RTTTimeDelta round_trip_time = bandwidth_estimation_->round_trip_time();// 丢包估计目标码率(综合丢包估计和延迟估计)DataRate loss_based_target_rate = bandwidth_estimation_->target_rate();// 丢包估计状态LossBasedState loss_based_state = bandwidth_estimation_->loss_based_state();// 使用丢包估计目标码率初始化 pushback 目标码率DataRate pushback_target_rate = loss_based_target_rate;double cwnd_reduce_ratio = 0.0;if (congestion_window_pushback_controller_) {// 传入丢包估计目标码率,获取根据拥塞状态调整后的 pushback 码率int64_t pushback_rate =congestion_window_pushback_controller_->UpdateTargetBitrate(loss_based_target_rate.bps());// 最小码率约束pushback_rate = std::max<int64_t>(bandwidth_estimation_->GetMinBitrate(),pushback_rate);// 转换为 DataRatepushback_target_rate = DataRate::BitsPerSec(pushback_rate);// 根据拥塞窗口计算后的下调带宽比率if (rate_control_settings_.UseCongestionWindowDropFrameOnly()) {cwnd_reduce_ratio = static_cast<double>(loss_based_target_rate.bps() -pushback_target_rate.bps()) /loss_based_target_rate.bps();}}// 获取稳定目标码率,并用 pushback 目标码率进行了约束DataRate stable_target_rate = bandwidth_estimation_->GetEstimatedLinkCapacity();stable_target_rate = std::min(stable_target_rate, pushback_target_rate);if ((loss_based_target_rate != last_loss_based_target_rate_) || // 带宽估计值变化(loss_based_state != last_loss_base_state_) || // 带宽评估状态变化(fraction_loss != last_estimated_fraction_loss_) || // 丢包率变化(round_trip_time != last_estimated_round_trip_time_) || // RTT变化(pushback_target_rate != last_pushback_target_rate_) || // pushback 码率变化(stable_target_rate != last_stable_target_rate_)) { // 稳定目标码率变化// 更新last_loss_based_target_rate_ = loss_based_target_rate;last_pushback_target_rate_ = pushback_target_rate;last_estimated_fraction_loss_ = fraction_loss;last_estimated_round_trip_time_ = round_trip_time;last_stable_target_rate_ = stable_target_rate;last_loss_base_state_ = loss_based_state;// 设置ALR探测器估计码率alr_detector_->SetEstimatedBitrate(loss_based_target_rate.bps());TimeDelta bwe_period = delay_based_bwe_->GetExpectedBwePeriod();TargetTransferRate target_rate_msg;target_rate_msg.at_time = at_time;if (rate_control_settings_.UseCongestionWindowDropFrameOnly()) {// 如果携带cwnd_reduce_ratio,则目标码率为评估的目标码率target_rate_msg.target_rate = loss_based_target_rate;target_rate_msg.cwnd_reduce_ratio = cwnd_reduce_ratio;} else {// 如果不携带cwnd_reduce_ratio,则目标码率为经拥塞窗口调整后的码率target_rate_msg.target_rate = pushback_target_rate;}target_rate_msg.stable_target_rate = stable_target_rate;target_rate_msg.network_estimate.at_time = at_time;target_rate_msg.network_estimate.round_trip_time = round_trip_time;target_rate_msg.network_estimate.loss_rate_ratio = fraction_loss / 255.0f;target_rate_msg.network_estimate.bwe_period = bwe_period;update->target_rate = target_rate_msg;// 更新带宽探测控制器的估计码率auto probes = probe_controller_->SetEstimatedBitrate(loss_based_target_rate,GetBandwidthLimitedCause(bandwidth_estimation_->loss_based_state(),bandwidth_estimation_->IsRttAboveLimit(),delay_based_bwe_->last_state()),at_time);// 添加带宽探测 Cluste r配置update->probe_cluster_configs.insert(update->probe_cluster_configs.end(),probes.begin(), probes.end());// 更新 Pacing 参数update->pacer_config = GetPacingRates(at_time);}
}
4. 总结
简单总结下 WebRTC 拥塞控制思路。拥塞控制的核心是获取链路的带宽,对于实时音视频通信来说,还要考虑延时指标。因为只有获得了链路的真实带宽,才能确保发送的码率不会超过链路容量,从而避免产生拥塞。那有没有一种办法,在不发送码流的情况下,提前知道链路的真实带宽呢?答案是没有。这个问题貌似变成了一个先有鸡还是先有蛋的问题。理论上是这样的,但实践中,可以采用一个带有负反馈回路的控制算法来打破这个循环魔咒,这就是 WebRTC 拥塞控制的核心思想。
我们回过头来总结 GCC 实现的底层逻辑:设置一个起始码率,按照起始码率开始发送报文,通过 RTCP 收集各种反馈,将反馈导入估计器(延时带宽估计器、丢包带宽估计器以及其他一堆估计器),产生估计值,调整发送码率,按照新的目标码率发送报文,继续收集反馈产生新的估计值,形成循环反馈回路。
这样就完了吗?并没有!如何产生更可靠估计值成了控制算法的关键问题。为了更准确、更可靠的估计链路容量,WebRTC 采用了延时带宽估计器和丢包带宽估计器两个估计器,同时,还引入 RTT、ACK 码率、探测码率、链路容量等一系列估计值来对估计结果进行调整和修正,有一大坨代码都是在解决如果获得更好估计这个问题。如果要深刻理解 WebRTC 是如何解决这个问题的,就必须深入算法和模块细节,比如延迟估计中是如何实现网络状态检测的,丢包估计中是如何实现最优参数搜索的等。理解了必要的细节后,再跳脱出来将这些模块和算法联系起来,将一个个逻辑链条组成前面提到的基于反馈回路的控制算法逻辑。
必须得承认,GCC 算法整体是非常优秀的,主要表现在以下几个方面:
1)良好的顶层架构
模块职责划分清晰,模块接口和模块之间的关系设计合理,使得 GCC 能够很好的融入 WebRTC 整体架构。
2)坚实的理论基础
基于延迟梯度的拥塞控制算法,在学术界有很多研究成果;基于链路固有丢包率属性和二项分布丢包模型,设计精巧的目标函数,通过收集观测样本并搜索最优参数组合,算法整体有很好的数学理论支撑。
3)大量使用成熟算法
基本上每个估计器都有经典算法的加持,比如指数加权移动平均算法、贝叶斯估计算法、最小二乘线性拟合算法等。
不过,GCC 经过多年的演化,也有几个问题值得探讨:
1)GCC 的实现越来越复杂,这种复杂性目前主要体现在子模块内部,但也有向上蔓延的趋势,子模块之间的耦合越来越强。GCC 当前的实现相比他们第一次发表的论文,做了很多改进和优化,甚至一些关键逻辑有了很大变化,导致的现象是分支和判断越来越多,复杂度不断加码,有一种不断往之前算法框架上打补丁的既视感。
2)GCC 里面涉及到一堆估计器,这些估计器的融合,看起来缺少理论基础。比如延迟估计和丢包估计,两者取其小一定是合理的吗?ACK 码率、探测码率、延迟估计码率、丢包估计码率、pushback 码率、链路容量等,这些概念相互交织,相关限制和调整逻辑看似合理,但缺少足够的说服力。
3)引入大量模型参数,导致模型理解困难,实现复杂。比如 LossBasedBweV2::Config,足足有 39 个配置参数,这对于模型调优来说简直就是噩梦。根据奥卡姆剃刀原理,这么复杂的模型可能是脆弱的,不必要的,有必要在更高维度上进行抽象和提炼,并进行适当简化。