TCP CUBIC is becoming the default in Linux Kernel and the Rate Halving technique for congestion avoidance was becoming the default in Linux Kernel. So, there was a mismatch between these two. Because Rate Halving was good for TCP Reno, Tahoe, and NewReno. It is good for those TCPs only which reduce the cwnd by half when packet loss occurs. But TCP Cubic reduces the cwnd by 30%, unlike other TCPs. Thus, Rate Halving and CUBIC couldn’t fit as default in Linux Kernel. This was the motivation of Google and they invented a new algorithm known as Proportional Rate Reduction. This is covered in RFC 6937.
Proportional Rate Reduction:
PRR calculates the number of new packets to be sent when an ACK arrives using some equations. Some new terms are coined for these equations, the new terminologies are as follows:
1. sndcnt (read as ‘send count’): the amount of data to be sent on the arrival of a DupACK. This is the number of new packets that the sender sends on the arrival of a new ACK. Unlike Fast Recovery, it is not equal to 2 and unlike Rate Halving it does not send 1 new packet on the arrival of two DUP-ACKs. It can send any number of new packets which is calculated using a formula.
2. RecoverFS: the value of pipe at the beginning of the recovery phase
3. prr_delivered: the amount of data delivered ‘during’ the recovery phase. This is the count of accumulated data delivered so far to the receiver ever since the recovery phase has begun. This is equal to the number of DUP-ACKs arrived during the recovery phase. How many packets are delivered before the recovery phase is not important? It only counts packets delivered after packet loss.
4. prr_out: the amount of data transmitted ‘during’ the recovery phase. This counts the accumulated number of new packets sent by the sender once it has entered the recovery phase.
5. DeliveredData: the amount of data delivered as indicated by an ACK packet (look at sequence numbers). Don’t confuse this with “prr_delivered”. DeliveredData indicates the number of packets delivered by this DUP_ACK. It can be 1 or more if ACKs arrive out-of-order.
6. limit: data sending limit (calculated in PRR algorithm). This can be considered as the threshold. It is used inside the equation and calculated in intermediary steps.
7. MSS: Maximum Segment Size. This is the typical size of one segment. Since we will be using the granularity of all terms as segments, MSS becomes 1.
Mode of Proportional Rate Reduction:
Proportional Rate Reduction (PRR) has two modes, which are as follows:
- Conservative Reduction Bound (CRB)
- Slow Start Reduction Bound (SSRB) [this one is enabled by default in Linux]
We can only use one mode at a time, both modes can’t be used simultaneously.
Working of PRR:
When the recovery phase begins, the pipe value is greater than cwnd. But there can be the case when SACK blocks confirm the packet loss and pipe value becomes lesser than cwnd. All these cases are handled differently by the PRR algorithm.
CASE 1: PRR (Case: pipe > ssthresh)
When sender enters the recovery phase:
pipe = 10 segments, RecoverFS = 10 segments ssthresh = 7 segments,
cwnd is calculated by PRR:
cwnd = pipe + sndcnt sndcnt = CEIL(p_d * ssthresh / RecoverFS) - p_o, {p_d=prr_delivered, p_o=prr_out}
Step 1: one DupACK comes, pipe=10-1=9, p_d=1, p_o=0, (Pipe reduces by one). DupACK tells that 1 packet was delivered. So, p_d=1, no new packet has been sent yet, so p_o=0
sndcnt= CEIL(1*7/10)-0 = CEIL(0.7)-0 = 1 sender sends one new packet and pipe becomes =9+1=10 again.
Step 2: one DupACK comes, pipe=10-1=9, p_d=2, p_o=1, (Pipe reduces by one). DupACK tells that one packet was delivered, so p_d=2 (cumulative). One new packet has been sent yet, so, p_o=1.
sndcnt= CEIL(2*7/10)-1 = CEIL(1.4)-1= 1 sender sends one new packet and pipe becomes 9 + 1 = 10 again.
This is how it continues.
Below is the code implementation for Case 1:
// pipe>cwnd, PRR // PRR, case-1 #include <bits/stdc++.h> using namespace std;
// utility function void prr( float pipe, float cwnd, float recoverFS,
float ssthresh)
{ int i;
float prr_delivered = 0, prr_out = 0, snd_cnt;
for (i = 1;; i++) {
// one DUP-ACK arrived
pipe -= 1;
// terminating condition.
if (pipe < ssthresh)
break ;
// Calculation
cout << "Iteration " << i << " => "
<< "pipe= " << pipe;
prr_delivered = i;
cout << " p_d= " << prr_delivered;
snd_cnt = ceil (prr_delivered * ssthresh / recoverFS)
- prr_out;
cout << " snd_cnt= " << snd_cnt;
cout << " p_o= " << prr_out << "\n" ;
prr_out += snd_cnt;
cwnd = pipe + snd_cnt;
pipe = cwnd;
}
} // main function int main()
{ float pipe, cwnd, recoverFS, ssthresh;
pipe = 10;
ssthresh = 7;
cwnd = pipe;
recoverFS = pipe;
prr(pipe, cwnd, recoverFS, ssthresh);
} |
Iteration 1 => pipe= 9 p_d= 1 snd_cnt= 1 p_o= 0 Iteration 2 => pipe= 9 p_d= 2 snd_cnt= 1 p_o= 1 Iteration 3 => pipe= 9 p_d= 3 snd_cnt= 1 p_o= 2 Iteration 4 => pipe= 9 p_d= 4 snd_cnt= 0 p_o= 3 Iteration 5 => pipe= 8 p_d= 5 snd_cnt= 1 p_o= 3 Iteration 6 => pipe= 8 p_d= 6 snd_cnt= 1 p_o= 4 Iteration 7 => pipe= 8 p_d= 7 snd_cnt= 0 p_o= 5 Iteration 8 => pipe= 7 p_d= 8 snd_cnt= 1 p_o= 5 Iteration 9 => pipe= 7 p_d= 9 snd_cnt= 1 p_o= 6 Iteration 10 => pipe= 7 p_d= 10 snd_cnt= 0 p_o= 7
CASE 2: PRR (Case: pipe ≤ ssthresh, with SSRB)
When recovery phase begins:
pipe = 4 segments RecoverFS = 10 segments ssthresh = 5 segments
cwnd is calculated by PRR:
cwnd = pipe + sndcnt sndcnt = MIN (ssthresh - pipe, limit) and limit = MAX (p_d - p_o, DeliveredData) + MSS
Step 1: One DupACK comes, pipe reduces by 1, pipe=4-1=3, DupACK confirms that 1 packet got delivered, so p_d=1, no new packet is sent yet, so p_o=0
limit = MAX(1-0, 1)+1 = 1+1 = 2 sndcnt= MIN(5-3, 2) = MIN(2, 2) = 2 So, the sender sends 2 new packets. Thus, pipe=3+2=5
Step 2: One DupACK comes, pipe reduces by 1, pipe=5-1=4, DupACK confirms that 1 packet got delivered, so p_d=2 (cumulative) one new packet is sent yet, so p_o=1
limit = MAX(2-1, 1)+1 = 1+1 = 2 sndcnt= MIN(5-4, 2) = MIN(1, 2) = 1 So, the sender sends 1 new packet. Thus, pipe=4+1=5
This is how it continues.
Implementation:
Below is the code implementation for CASE 2:
// pipe<cwnd with Slow Start Reduction Bound, PRR #include <bits/stdc++.h> using namespace std;
// utility function void prr_ssrb( float pipe, float cwnd, float recoverFS,
float ssthresh)
{ int i;
float prr_delivered = 0, prr_out = 0, limit, snd_cnt;
for (i = 1; i < 10; i++) {
// one DUP-ACK arrived
pipe -= 1;
// Calculation
cout << "Iteration " << i << " => pipe= " << pipe;
prr_delivered = i;
cout << " p_d= " << prr_delivered;
cout << " p_o= " << prr_out;
limit = max(prr_delivered - prr_out, float (1)) + 1;
cout << " limit= " << limit;
snd_cnt = min(ssthresh - pipe, limit);
cout << " snd_cnt= " << snd_cnt << "\n" ;
prr_out += snd_cnt;
cwnd = pipe + snd_cnt;
pipe = cwnd;
// terminating condition.
if (pipe > ssthresh)
break ;
}
} // main function int main()
{ float pipe, cwnd, recoverFS, ssthresh;
pipe = 4, recoverFS = 10, ssthresh = 5;
prr_ssrb(pipe, cwnd, recoverFS, ssthresh);
} |
Iteration 1 => pipe= 3 p_d= 1 p_o= 0 limit= 2 snd_cnt= 2 Iteration 2 => pipe= 4 p_d= 2 p_o= 2 limit= 2 snd_cnt= 1 Iteration 3 => pipe= 4 p_d= 3 p_o= 3 limit= 2 snd_cnt= 1 Iteration 4 => pipe= 4 p_d= 4 p_o= 4 limit= 2 snd_cnt= 1 Iteration 5 => pipe= 4 p_d= 5 p_o= 5 limit= 2 snd_cnt= 1 Iteration 6 => pipe= 4 p_d= 6 p_o= 6 limit= 2 snd_cnt= 1 Iteration 7 => pipe= 4 p_d= 7 p_o= 7 limit= 2 snd_cnt= 1 Iteration 8 => pipe= 4 p_d= 8 p_o= 8 limit= 2 snd_cnt= 1 Iteration 9 => pipe= 4 p_d= 9 p_o= 9 limit= 2 snd_cnt= 1
CASE 3: PRR (Case: pipe ≤ ssthresh, with CRB)
Initially when sender enters recovery phase:
pipe = 4 segments RecoverFS = 10 segments ssthresh = 5 segments
cwnd is calculated by PRR:
cwnd = pipe + sndcnt sndcnt = MIN (ssthresh - pipe, limit), and limit = p_d - p_o
Step 1: One DupACK arrives, pipe reduces by 1, pipe=4-1=3, DupACK confirms that one packet got delivered, so p_d=1, no new packet is sent yet, so p_o=0.
limit = 1-0 = 1, sndcnt = MIN(5-3, 1) = MIN(2, 1) = 1 Sender sends one new packet, thus pipe = 3+1 = 4
Step 2: One DupACK arrives, pipe reduces by 1,
pipe= 4-1=3, p_d=2, p_o=1 limit = 2-1 = 1 sndcnt = MIN(5-3, 1) = MIN(2, 1) = 1 Sender sends one new packet, thus pipe = 3+1 = 4
This is how it continues.
Implementation:
Below is the code implementation for CASE 3:
// pipe<cwnd with Conservative Reduction Bound, PRR #include <bits/stdc++.h> using namespace std;
// utility function void prr_crb( float pipe, float cwnd, float recoverFS,
float ssthresh)
{ int i;
float prr_delivered = 0, prr_out = 0, limit, snd_cnt;
for (i = 1; i < 10; i++) {
// one DUP-ACK arrived
pipe -= 1;
// Calculation
cout << "Iteration " << i << " => pipe= " << pipe;
prr_delivered = i;
cout << " p_d= " << prr_delivered;
cout << " p_o= " << prr_out;
limit = prr_delivered - prr_out;
cout << " limit= " << limit;
snd_cnt = min(ssthresh - pipe, limit);
cout << " snd_cnt= " << snd_cnt << "\n" ;
prr_out += snd_cnt;
cwnd = pipe + snd_cnt;
pipe = cwnd;
// terminating condition.
if (pipe > ssthresh)
break ;
}
} // main function int main()
{ float pipe = 4, cwnd, recoverFS = 10, ssthresh = 5;
prr_crb(pipe, cwnd, recoverFS, ssthresh);
} |
Iteration 1 => pipe= 3 p_d= 1 p_o= 0 limit= 1 snd_cnt= 1 Iteration 2 => pipe= 3 p_d= 2 p_o= 1 limit= 1 snd_cnt= 1 Iteration 3 => pipe= 3 p_d= 3 p_o= 2 limit= 1 snd_cnt= 1 Iteration 4 => pipe= 3 p_d= 4 p_o= 3 limit= 1 snd_cnt= 1 Iteration 5 => pipe= 3 p_d= 5 p_o= 4 limit= 1 snd_cnt= 1 Iteration 6 => pipe= 3 p_d= 6 p_o= 5 limit= 1 snd_cnt= 1 Iteration 7 => pipe= 3 p_d= 7 p_o= 6 limit= 1 snd_cnt= 1 Iteration 8 => pipe= 3 p_d= 8 p_o= 7 limit= 1 snd_cnt= 1 Iteration 9 => pipe= 3 p_d= 9 p_o= 8 limit= 1 snd_cnt= 1