TCP:不丢包的三十六计

TCP之所以称为可靠的传输层协议,一个很重要的原因是TCP力保segments not lost。为了做到不丢包,TCP大体系出现了各种机制和算法;这些设计不仅仅能帮助我们了解TCP协议,更重要的是我们做系统设计很好的借鉴。

站在高处看,不丢包的核心是ack(确认送达)和retransmission(重新发送)。这个大思路很重要,也很通用;但抽象的思路如何具体化如何落地是同样重要的问题。如何ack和retransmission里面的策略可以说是三十六计层出不穷,不同的设计解决的问题和达到的效果也会千差万别。

本文主要依据与TCP相关的3个RFC,将TCP的设计分为3个阶段:累计ack与计时重发,快速重发与恢复,SACK。这三个大阶段中大量更细粒度的设计方案,总的来说上一阶段往往比下一阶段更基础,下一阶段往往是上一阶段的补充。

第一个阶段:序列号、累计确认与超时重发

英文对应:sequence number(seq),cumulative acknowledge,timer-based retransmission
第一个阶段以1981RFC-793正式提出TCP协议为代表。
主要有以下两个设计

基于sequence number的ack机制

RFC-793中表示, “Transmission is made reliable via the use of sequence numbers and
acknowledgments. Conceptually, each octet of data is assigned a
sequence number… “
所有acknowledge的基础是唯一识别的序列号,TCP为了ack也实现了sequence number,也从而需要在连接建立时连接双方互换ISN。

TCP header中有8字节来表示发送和确认的序列号。 4字节代表本次发送的sequence number,和4字节ack sequence number。

发送的序列号代表的是本次segment中第一个byte的序号,注意不是segment编号,不然tcpdump就不会一个segment打印一个seq-number的区间了(可以参考下图tcpdump中的编号)。连接建立的sync包比较特殊,这个时候sync占用一个number代表ISN,同时sync包不包含数据,第一次传输正式数据ISN+1.

累计ack

ACK N代表N之前的数据全部确认收到,下一个尚未收到的字节是N,注意不是本次收到的编号哦。ACK本身并不消耗seq number,也就是说如果不发data只发一个包含ack的数据,sequence number不需要消耗编号。

如图可以看到:红色的ack并未占用seq-number,没有编号出现;第一个绿色的框中的sync占用了一个seq-number,第二个绿色的真实数据包,从相对编号1开始(ISN+1)。

这里核心设计就是cumulative ack/累计ack,也就是当对方收到segment1:10,和segment11:20时(假设一个segment10个字节,即占用是个seq)。并不需要按字节或者segment发ack,只需要发送ack21——最后一个seq+1即可。

一个TCP segment可以存在上千字节,TCP批量发送可能发送大量的segment,一个个回复降低了TCP效率,累计ACK是一个非常巧妙且重要的设计。当然累计ack有自己的“笨拙”,文章下一部分会进行讨论。

基于timer的retransmission

有了ack之后就有ack收不到该“如何处理”的问题了,最基本的方法就是超时重发机制了。

这里的机制在TCP中实际实现就很复杂了,但核心步骤很简单:

  • 当TCP发送一个segment之后,TCP把这个数据放在“retransmission queue”中。

这个queue也就解释了为什么TCP协议有send buffer,而不需要缓存重发的UDP并没有真实buffer只有一个虚拟的buffer来表示size的原因;

  • TCP中有个重要设计叫retransmission-timer开始计时。

只要计时就有计时算法问题,这又是一个能展开对比分析一篇文章的课题。本文不展开,但是可以介绍计时算法主要包括几个部分:1)时间的初始值,2)多次丢失的时间增长速率,3)多次发送的时间间隔;4)退出计时的条件(成功和失败两种情况)。TCP采用的是基于方差平滑平均数的RTO和指数增长间隔的算法。

  • 如果收到的ack中包含这个segment,则把这个数据删除;
  • timer超时没有收到ack,则这个segment会重发并重新进入队列开始计时;

同时slow start和congestion avoidance算法会启动,TCP认为网络拥塞进入流量管控阶段(也是大话题了,不展开);

  • 当timer进入临界值(linux中可配置),TCP断开连接,so sad;

第二个阶段:快速重发

fast-retransmission来自于RFC2581, RFC2581的核心是congestion control,包含4种算法:slow start, congestion avoidance, fast retransmit, and
fast recovery
。 据说美国某个主干网时常发生崩溃,直到这个RFC中的4种算法落地才得以解决。
虽然RFC2581修订与于1999年,但是Van Jacobson大神在1988年就提出了这四种算法。

虽说是congestion control算法,但是其中的,fast retransmit不可不谓另一种巧妙的ack设计方法,也是对cumulative ack的一种补充。

快速重发解决空洞

累计重发是一种很好的设计,但是当出现了部分缺失,累计重发就会显得有点笨拙。

如上图所示,如果发送方发了5个segements,但是接收方因为网络原因少接收了某个segment怎么办呢。接收方只能ack 21,因为累计ack是要顺序累计的(ack N代表N之前的都接收到了);这个时候发送方只能进入timer机制继续等待后续segment的timer超时,进入超时重发机制。

这种机制有两个问题:(1)超时重发的时效性不好,至少会浪费一个RTT左右的时间;(2)超发已正确发送的数据

于是Jacobson设计了快速重发的算法:

  • 接收方的算法
    • 当接收方收到乱序数据时(segments out of order),接收方应该立刻多次重复ack缺失片段,例子中应该快速ack 21;
    • 当接收方收到缺失数据后,应该立刻ack下一个seq,例子中应该ack51;
    • 注意:这里的“立刻”是一个信息点,因为TCP的ack往往在时间中采用delayed ack也就是延时到有数据可以和ack一起发送或者延时ack timer超时,这两个条件有一个发生时才ack;
  • 发送方算法
    • 识别:当发送方连续收到ack并超过4次并且其中没有其他ack之后(4次中3次是重复的,第一次被认为是是正常的,所以有些地方会说是3次重复ack);
    • 响应:当发送方识别出loss之后,会跳过上文的timer,会快速发送对应的segment直至收到其他ack,这一过程称为fast recovery
    • 后续:收到ack之后,发送方进入流量管控阶段,这一阶段产生作用的算法是congestion avoidance。

这里可以看出,如果第一阶段的确认核心在于“时间”,这个算法的核心是利用“次数”来传达信息。此外这里有两个关键细节要注意:(1)4次以上;(2)没有其他ack混在里面;这两个条件可以帮助排除其他网络异常造成的多次重复ack,并确认是接收方主动反复发送的ack。其他的一些情况:
1)少数的ack可能是因为网络传输时segment乱序抵达但是没有发生loss;
2)重复的ack可能是网络环境导致重复发送的segments;

Tahoe和Reno

TCP中常见的一个说法是 Tahoe and Reno TCP implementation 其实就和这4个拥塞算法相关。Tahoe和Reno是美国西海岸的度假区,之前去过Tahoe滑雪感觉环境还是蛮好的。

在发生fast-restransmit之后,Tahoe TCP会用slow start来进行管控然后过渡到congestion avoidance 算法,后面的Reno提出省略slow start阶段的方案,Tahoe仅用于timed-retransmission而fast-restranmission直接直接进入congestion avoidance。在fast-retranmission情况下省略更严格从1开始slow-start,是因为重复ack意味着后续的segment依然能穿透网络抵达接收方,网络情况转好,只需要进行拥堵管控即可。

第三个阶段:选择性重发

SACK的RFC其实比fast-retransmission更早,但是其实Jacobson大神(没错又是他)更早的设计了fast-retransmission只是因为这一系列算法相对复杂,完善时间比较长。(有图为证)这里把SACk放在第三阶段,实际时间都在1988年。

SACK的初衷

SACK中的S代表selective,所以很容易猜到是选择性的ack。emm,其实一开始学TCP就可能有人说TCP的ack不能选择性的;没想到它其实做了个选择性的选项- -!

这个选项推出的原因很简单,cumulative ack面对部分丢失这种情况的确认效率非常低,fast-retransmission更多的是考量了网络阻塞调控只能一次汇报一个丢失的seq;于是正如RFC2018中所说的SACK selective acknowledgement专项解决多个TCP segments丢失的问题

SACK的实现

TCP header中的options

SACK通过两个TCP options来实现,TCP header中预留的Option空位终于有用武之地了。

TCP的header是变长的,最小长度也就是前五行固定内容20字节。前20字节中半个字节的data offset(1代表4字节,最多表示15*4=60bytes)表示TCP header的长度,也就是offset多长之后进入data部分。由于定长的20字节,变长的option部分最多可以占用60-20=40字节

2个SACK相关的options

  1. 启动SACK的SACK-permitted:

启动SACK时这个option必须在连接建立时与SYN一起发送;
该option编号为2,总长度定长为2字节。

  1. 另一个option就是SACK-option确认内容本身
    该option编号为5,并且是变长数据;


SACK和cumulative ACK并不矛盾,相反两者是相互配合一起反映接收情况的。累计ack继续反映连续累计收到的seq,sack具体展示在累计收到seq之后不连续收到的block片段;sack中不连续的内容被补发,累计ack的编号也会相应的前进。一段段不连续的内容就通过区间的左节点和右节点来表示,也就是图中的left/right edge of nth block。

sender和receiver关于SACK的策略

只看options可能抽象,可以举个栗子。SACK的实现sender和receiver各有分工。receiver主要行为是生成SACK options。

sender发送了从1到60的所有数据,receiver只收到了图中绿色片段,丢失了两个segment。这里cumulative ack一直累积到20,所以基于TCP对于累积ack的设计,receiver要发起ack 21。如果这个时候SACK的option打开,那么options中将包含已经接收到的后续片段:61:71, 31:51;

sender的主要行为是解读SACK options并设置重发策略

当发送者解读了sack里的信息之后,会对retransmission queue中的数据进行标记。累计ack的内容直接被移出queue,sacked的内容打标,并对中间遗漏数据进行补发。这个策略可以看出,累计ack和sack的地位是不同的,是确认被接受的唯一标识,sacked只作为辅助。一个原因是客户端可以“返回”也就是receiver sack reneging行为。被SACk的内容如果在计时重发策略里判断超时,则sender会取消所有的sacked标识,并认为receiver发生了sack机制的返回策略,这时retransimission进入第一部分的计时重发。

总结

讲到这里TCP的ack策略就很清晰了:1)唯一确认数据的sequence number是所有ack的基础,2)在此基础上cumulative ack是唯一最终确认的机制,3) 同时sack和fast-retransmission作为cumulative ack的辅助帮助其快速向后确认。

后记

文章写到深夜,再打开微信发现红会相关的文章都被和谐了。

觉得心中有大石,想记录点什么。

2020年的长假期给了我一些时间写作,但2019年末和2020年无异是从巨大的失望甚至绝望中开始的。

所幸失望中也有感动和希望的光芒。韩红团队和很多其他自发帮忙的人们的行为,让我感觉也许无论是暴力伤害医生还是疫情救助慈善组织,可能只是物质发展但是精神文化尚未健全社会的极端的现象。

记得詹青云曾说学法律想帮助推进中国的法治进程。我也常想,自己的微弱力量能为生病的社会做些什么,如果面对失去理性的群众,困难和危险面前是不是也只能以自己肉身相搏?

没找到更好的答案,目前只能更好的自律更加上进,至少我有足够的知识、理性、良知和勇气不要成为“他们”。

最后希望知文明和法治的思想能传播给更多的人,希望国内的教育能更加普及和改善让大家能有更好的思辨能力。希望灾难面前,不会再有人想到为了“自己”可以轻易随便的牺牲别人或者别的生命。

https://juejin.im/post/5e357c4d6fb9a02fef3a7180

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论