跳到主要内容

6. 算法 (Algorithm)

6.1. 初始化步骤​

在拥塞控制算法启动的拥塞控制响应阶段开始时, 使用 PRR 的数据发送方 MUST 初始化 PRR 状态.

拥塞控制响应阶段开始的时机完全由拥塞控制算法决定. 例如, 它可以对应快速恢复阶段的开始; 也可以在快速恢复已经进行后, 因检测到重传丢失或原始传输丢失而每轮执行一次降低时发生.

PRR 初始化允许拥塞控制算法 CongCtrlAlg() 将 ssthresh 设置为不同于 FlightSize/2 的值, 例如包括 CUBIC [RFC9438] 的情形.

PRR 初始化中的一个关键步骤是计算恢复飞行大小 (Recovery Flight Size, RecoverFS), 即发送方对当前 PRR 阶段期间可能交付的字节数的估计. 可以将其视为阶段开始时以下值的总和: inflight, 触发恢复的 ACK 中累计确认的字节数, 触发恢复的 ACK 中 SACKed 的字节数, 以及 SND.UNA 与 SND.NXT 之间已经标记为丢失的字节数. RecoverFS 包含丢失, 因为丢失是用启发式方法标记的, 因此在恢复期间, 一些先前标记为丢失的包最终可能仍会被交付而无需重传. PRR 使用 RecoverFS 计算平滑发送速率. 进入快速恢复时, PRR 初始化 RecoverFS, 并且 RecoverFS 在给定快速恢复阶段内保持不变.

PRR 算法初始化步骤的完整序列如下:

ssthresh = CongCtrlAlg()  // Target flight size in recovery
prr_delivered = 0 // Total bytes delivered in recovery
prr_out = 0 // Total bytes sent in recovery
RecoverFS = SND.NXT - SND.UNA // Bytes SACKed before entering recovery

// Bytes SACKed before entering recovery will not be marked as
// delivered during recovery:
RecoverFS -= (bytes SACKed in scoreboard)

// Include selectively ACKed bytes (common case):
RecoverFS += (newly SACKed bytes)

// Include cumulatively acknowledged bytes (rare case):
RecoverFS += (newly cumulatively acknowledged bytes)

6.2. 每个 ACK 的步骤​

在快速恢复开始时或快速恢复期间的每个 ACK 上, PRR 执行以下步骤, 但结束 PRR 阶段的 ACK 除外.

首先, 发送方计算 DeliveredData, 即数据发送方对当前 ACK 表明自上一个收到的 ACK 以来已交付到接收方的总字节数的最佳估计. 使用 SACK 时, DeliveredData 可以精确计算为 SND.UNA 的变化量, 加上记分板中标记为 SACKed 的数据量的有符号变化量. 因此, 在 ACK 前后记分板中都没有 SACKed 序列范围这一特殊情况下, DeliveredData 就是 SND.UNA 的变化量.

在没有 SACK 的恢复中, 每收到一个重复 ACK (即 SND.UNA 未变化), DeliveredData 估计为 1 SMSS. 当 SND.UNA 前进时 (即完整 ACK 或部分 ACK), DeliveredData 是 SND.UNA 的变化量减去此前每个重复 ACK 对应的 1 SMSS. 注意, 在没有 SACK 时, 返回过多重复 ACK 的异常接收方 (如 [Savage99] 所述) 可能试图人为抬高 DeliveredData. 作为缓解措施, 未使用 SACK 时, 如果 PRR 阶段中已交付的总字节数超过进入恢复时估计的未完成数据 (RecoverFS), PRR 不允许继续增加 DeliveredData.

接下来, 发送方计算 inflight, 即数据发送方对网络中在途字节数的最佳估计. 为了计算 inflight, 启用 SACK 且使用 [RFC6675] 丢失检测的连接可以使用 [RFC6675] 规定的 "pipe" 算法. 启用 SACK 且使用 RACK-TLP 丢失检测 [RFC8985] 或其他丢失检测算法的连接 MUST 按如下方式计算 inflight: 从 SND.NXT - SND.UNA 开始, 减去记分板中 SACKed 的字节数, 减去记分板中标记为丢失的字节数, 再加上记分板中那些自标记为丢失后已经被重传的字节数.

对于未启用 SACK 的连接, 发送方 MUST 减去: min(RecoverFS, 快速恢复阶段中此前每个重复 ACK 对应的 1 SMSS). 这里使用 RecoverFS 的 min() 是为了防御异常接收方 [Savage99], 它替代了从 SACK 记分板中减去 SACKed 字节数的做法.

接下来, 发送方计算 SafeACK, 这是一个本地布尔变量, 表示当前 ACK 报告了良好进展. 仅当该 ACK 累计确认新数据且该 ACK 不表明存在进一步丢失时, SafeACK 才为 true. 例如, 触发 "rescue" 重传的 ACK ([RFC6675] 第 4 节, NextSeg() 条件 4) 可能表明存在进一步丢失. 这两个条件共同表明恢复进展良好, 发送方可在适当时更激进地发送, 从而增加 inflight.

最后, 发送方使用 DeliveredData, inflight, SafeACK 和其他 PRR 状态计算 SndCnt. SndCnt 是一个本地变量, 表示响应每个 ACK 应发送多少字节. 随后发送方使用 SndCnt 更新 cwnd.

每个 ACK 的 PRR 算法步骤完整序列如下:

if (DeliveredData is 0)
Return

prr_delivered += DeliveredData
inflight = (estimated amount of in-flight data)
SafeACK = (SND.UNA advanced AND no further losses indicated)

if (inflight > ssthresh) {
// Proportional Rate Reduction
// This uses integer division, rounding up:
#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
out = DIV_ROUND_UP(prr_delivered * ssthresh, RecoverFS)
SndCnt = out - prr_out
} else {
// Use PRR-CRB by default
SndCnt = MAX(prr_delivered - prr_out, DeliveredData)
if (SafeACK) {
// Use PRR-SSRB when recovery is making good progress
SndCnt += SMSS
}
// Try to catch up, as much as allowed
SndCnt = MIN(ssthresh - inflight, SndCnt)
}

if (prr_out is 0 AND SndCnt is 0) {
// Force fast retransmit upon entering recovery
SndCnt = SMSS
}

cwnd = inflight + SndCnt

发送方计算 SndCnt 并用它更新 cwnd 后, 会继续传输更多数据. 注意, 要发送哪些数据的决策, 例如重传缺失数据或发送更多新数据, 超出本文档范围.

6.3. 每次发送的步骤​

在任何数据传输或重传时, PRR 执行以下操作:

prr_out += (data sent)

6.4. 完成步骤​

PRR 阶段在快速恢复完成时结束, 或者在因新的拥塞控制响应阶段而开始新 PRR 阶段之前结束.

完成 PRR 阶段时, PRR 执行以下操作:

cwnd = ssthresh

注意, 将 cwnd 设置为 ssthresh 的这一步在某些情况下可能允许连续突发的一批段进入网络.

鼓励实现使用 pacing 来降低数据流的突发性. 这一鼓励与当前缓解突发性的实践一致, 例如 [PACING], 包括在空闲后重新启动时对突发发送进行 pacing.