PRR算法(Proportional Rate Reduction)決定在丟包恢復(Loss Recovery)期間,對應於每個ACK報文,可發送的報文數量。目的是:1)快速平穩的從Loss中恢復;2)恢復之後擁塞窗口收斂與ssthresh。主要是爲了解決Linux內核之前採用的恢復算法Rate-halving存在的一些弊端:
- 在恢復階段,爲防止burst發送,內核將擁塞窗口設置爲pipe+1,然而,如果由於應用程序沒有數據可發送,最早將導致擁塞窗口降低爲1,即使僅丟失了一個報文。
- 在恢復之後,將擁塞窗口降低太多(一半甚至更低),但是當前內核的默認Cubic算法將ssthresh降至之前擁塞窗口的70%,太低的擁塞窗口將造成性能損失。
- ACK報文的丟失,將導致更少報文的發送。
- 在丟失多個報文時,容易觸發RTO超時。
PRR由兩個部分組成。一是當網絡中報文數量(pipe)大於ssthresh時,通常是丟失較少報文時的恢復開始階段的情況,根據ACK報文的到達成比例的降低擁塞窗口。例如對於Cubic算法,此部分通過每接收到10個ACK確認報文(10報文被對端所接收),發送7個報文的方式,將擁塞窗口降低30%(等於Cubic設置的ssthresh值)。
第二個部分是,如果網絡中報文數量(pipe)小於ssthresh,通常是丟失多個報文或者應用程序在恢復階段沒有數據可發送的情況,阻止擁塞窗口的降低。RFC6937中定義了兩種Reduction Bound算法:Conservative Reduction Bound (CRB)和Slow Start Reduction Bound (SSRB),前者嚴格遵守報文守恆機制;而後者類似SlowStart,比CRB更具侵略性,對於每個接收到的ACK報文,SSRB允許額外多發送一個數據報文。
PRR初始化
在進入TCP_CA_Recovery或者TCP_CA_CWR擁塞狀態時,調用函數tcp_init_cwnd_reduction初始化PRR相關參數。prr_delivered記錄在進入Recovery/CWR狀態後接收端收到的報文數量,而變量prr_out用於統計進入Recovery之後,發送的報文數量。
snd_ssthresh爲擁塞算法(默認Cubic,參見函數bictcp_recalc_ssthresh)計算的ssthresh值,最終,擁塞窗口將收斂與此值。
static void tcp_init_cwnd_reduction(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
...
tp->prior_cwnd = tp->snd_cwnd;
tp->prr_delivered = 0;
tp->prr_out = 0;
tp->snd_ssthresh = inet_csk(sk)->icsk_ca_ops->ssthresh(sk);
tcp_ecn_queue_cwr(tp);
PRR更新cwnd
如下函數tcp_cwnd_reduction,如果pipe大於snd_ssthresh,即delta小於零,執行PRR算法的第一個成比例部分,如下爲RFC6937給出的算法:
RecoverFS = snd.nxt-snd.una // FlightSize at the start of recovery
On every ACK during recovery compute:
DeliveredData = change_in(snd.una) + change_in(SACKd)
prr_delivered += DeliveredData
pipe = (RFC 6675 pipe algorithm)
if (pipe > ssthresh) {
// Proportional Rate Reduction
sndcnt = CEIL(prr_delivered * ssthresh / RecoverFS) - prr_out
}
內核中使用進入Recovery時的擁塞窗口prior_cwnd表示算法中的RecoverFS的值,變量dividend首先增加了prior_cwnd-1的值,在除去prior_cwnd,達到了算法中CEIL的效果。此階段需要等比例的減小擁塞窗口,比例爲:snd_ssthresh/prior_cwnd。
void tcp_cwnd_reduction(struct sock *sk, int newly_acked_sacked, int flag)
{
struct tcp_sock *tp = tcp_sk(sk);
int sndcnt = 0;
int delta = tp->snd_ssthresh - tcp_packets_in_flight(tp);
if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd))
return;
tp->prr_delivered += newly_acked_sacked;
if (delta < 0) {
u64 dividend = (u64)tp->snd_ssthresh * tp->prr_delivered + tp->prior_cwnd - 1;
sndcnt = div_u64(dividend, tp->prior_cwnd) - tp->prr_out;
} else if ((flag & (FLAG_RETRANS_DATA_ACKED | FLAG_LOST_RETRANS)) ==
FLAG_RETRANS_DATA_ACKED) {
sndcnt = min_t(int, delta,
max_t(int, tp->prr_delivered - tp->prr_out,
newly_acked_sacked) + 1);
} else {
sndcnt = min(delta, newly_acked_sacked);
}
/* Force a fast retransmit upon entering fast recovery */
/* 在進入快速恢復階段時,強制發送至少一個報文(此時prr_out爲零)。 */
sndcnt = max(sndcnt, (tp->prr_out ? 0 : 1));
tp->snd_cwnd = tcp_packets_in_flight(tp) + sndcnt;
如果當前ACK報文確認了之前重傳的數據,並且沒有進一步標記新的重傳數據的丟失,使用PRR算法的第二部分計算髮送數量。
RFC6937中定義的PRR算法的第二部分如下(else部分,linux使用PRR-SSRB部分)。如果由於應用程序缺少報文發送將導致prr_delivered的值大於prr_out的值,內核取其與新確認報文數量newly_acked_sacked值,兩者之間的最大值,另外再加上1(MSS),最後,取以上結果和delta之間的較小值。此階段增加擁塞窗口,趨近於ssthresh。
相比於PRR-CRB,SSRB的加1操作,並非嚴格遵守報文守恆原則,
if (pipe > ssthresh) {
...
} else {
// Two versions of the Reduction Bound
if (conservative) { // PRR-CRB
limit = prr_delivered - prr_out
} else { // PRR-SSRB
limit = MAX(prr_delivered - prr_out, DeliveredData) + MSS
}
// Attempt to catch up, as permitted by limit
sndcnt = MIN(ssthresh - pipe, limit)
}
最後,如果當前ACK報文沒有確認重傳數據,確認的爲正常數據,或者確認了新的重傳數據的丟失,發送數量設定類似於PRR-CRB,不同點在於內核使用delta與newly_acked_sacked之間的最小值。而PRR-CRB使用的是delta與prr_delivered - tp->prr_out的差值之間的最小值。此種情況下,報文發送數量不像PRR-SSRB激進,原因是一方面丟失報文較少;或者另一方面,丟失報文較多,網絡擁塞嚴重。
sndcnt = min(delta, newly_acked_sacked);
以上函數tcp_cwnd_reduction在tcp_ack處理完成ACK報文之後,在函數tcp_cong_control中調用。注意BBR擁塞算法(實現了自身的cong_control控制)不使用PRR。
static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked,
int flag, const struct rate_sample *rs)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
if (icsk->icsk_ca_ops->cong_control) {
icsk->icsk_ca_ops->cong_control(sk, rs);
return;
}
if (tcp_in_cwnd_reduction(sk)) {
/* Reduce cwnd if state mandates */
tcp_cwnd_reduction(sk, acked_sacked, flag);
更新prr_out
變量prr_out記錄CWR/Recovery擁塞狀態發送的報文數量,內核使用函數tcp_in_cwnd_reduction判斷套接口是否處於CWR/Recovery擁塞狀態。
static inline bool tcp_in_cwnd_reduction(const struct sock *sk)
{
return (TCPF_CA_CWR | TCPF_CA_Recovery) &
(1 << inet_csk(sk)->icsk_ca_state);
}
如下TCP報文發送函數tcp_write_xmit,如果套接口處於CWR/Recovery擁塞狀態,增加prr_out的值。
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
...
if (likely(sent_pkts)) {
if (tcp_in_cwnd_reduction(sk))
tp->prr_out += sent_pkts;
如下TCP報文重傳函數tcp_xmit_retransmit_queue,如果套接口處於CWR/Recovery擁塞狀態,增加prr_out的值。
void tcp_xmit_retransmit_queue(struct sock *sk)
{
...
rtx_head = tcp_rtx_queue_head(sk);
skb = tp->retransmit_skb_hint ?: rtx_head;
max_segs = tcp_tso_segs(sk, tcp_current_mss(sk));
skb_rbtree_walk_from(skb) {
if (tcp_retransmit_skb(sk, skb, segs))
return;
NET_ADD_STATS(sock_net(sk), mib_idx, tcp_skb_pcount(skb));
if (tcp_in_cwnd_reduction(sk))
tp->prr_out += tcp_skb_pcount(skb);
內核版本 5.0