跳到主要内容

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.