tcp 协议曾引入过很多碎点,因此拧巴复杂,今天简单说两个,tlp(tail loss probe) 和 er(early retransmit)。
tlp 考虑的是怕发出去的报文太少或尾丢,没有足够的 ack/sack 数量足以触发快速重传,这种情况下需要主动发送一个报文(新的或旧的)以期待 receiver 回复的 ack 中携带足够的 sack 段触发快速重传。
而 er 考虑的是即使在发出的报文反馈的 ack/sack 不足以触发快速重传时,也不用怕,放宽约束即可在不足 3 个重复确认时触发快速重传。(3 个 dupack 中的 3 涉及到 reordering,但这是另一个话题,以前说过,不再赘述)
一般而言 tlp 和 er 一起使用,让这些特性显得复杂的一逼,如果看代码的话,一层层缩进的 if 混杂着一坨坨屎一样的 &&,& 和 || 就更让人绝望了。
细节中还有细节的细节,tlp 发新数据还是旧数据,若不明白 tlp 的初衷是希望 receiver 反馈 sack block 触发快速重传,很难理解发什么以及发多少的决策,其实发任意 byte 即可,考虑到捎带的有效性,优先发送新数据。
tlp 的 probe 报文大概率也带不回足量的 sack block,否则早就带回了,也不必触发 tlp 就开始快速重传了,所以如果一个 probe 仅仅带回了有但不足的 sack block,er 便接力,让 sender 在松约束下立即开始快速重传。
如果你花了很长时间来 "牢记" 相关 rfc 的约定和代码实现的 trick,并且你真的做到了,你甚至总结出一些非常容易记忆的 1,2,3,4,你也不必因此而感动,因为这些都不是核心本质,这只让 tcp 的形象越来越拧巴。
仔细看 tlp,er 这一坨坨的规定,很容易发现问题的核心在 "3 次 dupack 才能触发快速重传"。所以为了在一些不太易的场景(比如 thin 管道,尾丢)下迎合和满足触发快速重传的条件,不至于跌入 rto,tlp 和 er 才被设计出来,条件苛刻,代码凌乱。
所以你看,tcp 说在长肥管道下有问题,那么在短瘦管道下岂不是也有问题,tlp,er 就是为此而生的。
我常说,tcp 的代码写得像坨屎不是程序员的问题,而是 tcp 的代码根本就写不好,tcp 的特性是一点点小问题的解决 trick 逐渐像贴胶布一样贴出来的,换谁上也写不好看,这个和 openssl 还不一样。即使不说代码,你去看 rfc 也不好看。
rack 似乎是福音,因为它直接解除了 "3 次 dupack 才能触发快速重传" 这个条件,每当有人咨询我重传的问题时,我都会说你去看 rack 好了。
看 rack 之前的快速重传条件:
c
static bool tcp_time_to_recover(struct sock *sk, int flag)
{
struct tcp_sock *tp = tcp_sk(sk);
__u32 packets_out;
/* Trick#1: The loss is proven. */
if (tp->lost_out)
return true;
/* Not-A-Trick#2 : Classic rule... */
if (tcp_dupack_heuristics(tp) > tp->reordering)
return true;
/* Trick#4: It is still not OK... But will it be useful to delay
* recovery more?
*/
packets_out = tp->packets_out;
if (packets_out <= tp->reordering &&
tp->sacked_out >= max_t(__u32, packets_out/2, sysctl_tcp_reordering) &&
!tcp_may_send_now(sk)) {
/* We have nothing to send. This connection is limited
* either by receiver window or by application.
*/
return true;
}
/* If a thin stream is detected, retransmit after first
* received dupack. Employ only if SACK is supported in order
* to avoid possible corner-case series of spurious retransmissions
* Use only if there are no unsent data.
*/
if ((tp->thin_dupack || sysctl_tcp_thin_dupack) &&
tcp_stream_is_thin(tp) && tcp_dupack_heuristics(tp) > 1 &&
tcp_is_sack(tp) && !tcp_send_head(sk))
return true;
/* Trick#6: TCP early retransmit, per RFC5827. To avoid spurious
* retransmissions due to small network reorderings, we implement
* Mitigation A.3 in the RFC and delay the retransmission for a short
* interval if appropriate.
*/
if (tp->do_early_retrans && !tp->retrans_out && tp->sacked_out &&
(tp->packets_out >= (tp->sacked_out + 1) && tp->packets_out < 4) &&
!tcp_may_send_now(sk))
return !tcp_pause_early_retransmit(sk, flag);
return false;
}
再看下 rack 后的:
c
static bool tcp_time_to_recover(struct sock *sk, int flag)
{
struct tcp_sock *tp = tcp_sk(sk);
/* Trick#1: The loss is proven. */
if (tp->lost_out)
return true;
/* Not-A-Trick#2 : Classic rule... */
if (!tcp_is_rack(sk) && tcp_dupack_heuristics(tp) > tp->reordering)
return true;
return false;
}
很多逻辑都收入 rack 逻辑了,这就清爽了许多。
rack 是一个非常自然的丢包探测方案,包括我自己在内的传输圈子内部早在 2013 年之前就都自行设计出了时间序探测算法,后来看了 tlp 和 er 反而有点懵,因为它们是为不合理打补丁,而不是为问题提供方案。
本质上,rack 在跟踪时间序中的传输行为,它跟踪的正是时间戳本身,"更晚传输的报文 p 已经被确认,在 p 之前更早传输的未确认报文 pj 视为丢失",为了给乱序保留余地,rack 保留了一个乱序时间窗口 d,在标记并重传一个 pj 之前会等待一小段时间。乱序时间窗口 d 自然也不必像 reordering 个 dupack 触发 fr 时那般复杂拧巴,根据 "乱序是路径属性而不是协议属性",d 被限制在 minrtt / 4 到 srtt 之间,完全取消了复杂的乱序度的计算。这就把 rack 全部看懂了。
但同样也是时间序,很多人不明白 tcp 的 rto 为什么不能直接是 rtt,至多加上一个不太大的余量,这很合理,超过 rtt + 小余量 的时间收不到 ack,报文就算丢了,但当获得了更官方的解释后,捎带 ack,delay ack,各平台相异的实现等一大坨一大坨的东西就全搅和进来成一锅粥了。
rack 在标记一个报文丢失时与 rto 估算不同,rack 不需要估算,它用一个 recent ack 做参照,人家都被确认了,你们更早发送的有什么理由不被确认,那就是丢了呗。你看,有了 recent ack 参照就不需要再考虑那一坨坨的污秽逻辑了。
最后看一下 rack-tlp,来自 rfc8985,一张图,不说话:
浙江温州皮鞋湿,下雨进水不会胖。