一探究竟 | eBay流量管理之重新发现TCP重传
作者 | Victor Yang(杨胜辉)
编辑 | 顾欣怡本文5797字,预计阅读时间14分钟更多干货请关注“eBay技术荟”公众号
引子
eBay的基础架构FE团队经常需要用wireshark进行网络分析,找到问题的原因。对于时常做网络分析的朋友来说,如果打开wireshark,看到满屏的TCP retransmission,第一感觉是什么?是掉包了,客户端重传了对吧?一定是网络上有问题,比如路由器、防火墙,比如网线、光纤...... 这也确实符合大多数场景,毕竟设计TCP的目标,如果说只有一个,那就是保证可靠的传输。我的包掉了,还能再传一次或者多次,直到传输完成;如果持续失败,放弃传输。有点“先死磕,后认怂”的意思。
而TCP重传,恰恰也跟诸多奇怪的问题有很深的联系。回顾
当然超时重传也还是有可能陷入丢包的境地,此时发送方一般选择以RTO为基数的2倍,4倍,8倍......时间这样去尝试多次。
快速重传:上面的超时重传虽然避免了“干等”的尴尬局面,但仍旧带来了另外的问题:“干等”的时间并不短(常见的配置是200毫秒)这段时间被白白地浪费了。快速重传就是解决这个问题的。它的思路如下:发送方一般不会把数据包一个一个地发(这跟滑动窗口的知识有关,请参考后续相关文章),而是连续发送几个数据包。这连续几个数据包也不太会在路上全军覆没,有个别幸存者还是到达了接收方的阵地。我们来看一个例子:
1. 发送方派遣五个兄弟一起去接收方。
2. 路不好走,老二走丢了,而老大、老三、老四、老五到了终点。
3. 接收方连续收到老大、老三、老四、老五,也派出4个Ack包(其中后三个属于DupAck)去通知发送方我这边的接收情况。
4. 这四个Ack具体是指:
第一个Ack是确认老大到了,也就是确认了老大的序列号+载荷之和(也等于老二的序列号)。
第二个Ack属于DupAck,为什么是Dup (duplicate)呢?因为确认号还是上一个,也就是老大的序列号+载荷之和(也等于老二的序列号)。
第三个Ack也属于DupAck,也是老大的序列号+载荷之和(也等于老二的序列号)。
第四个Ack也属于DupAck,同样是老大的序列号+载荷之和(也等于老二的序列号)总之看起来就是,后面三个Ack都一直在呼唤老二(他的序列号)。
5. 发送方赶紧把老二克隆了一遍,把克隆出来的老二再次发送给接收方(发送缓冲区里有所有的数据,使得克隆成为可能)。
6. 接收方这次收到了克隆后的老二,这五兄弟终于凑齐了,于是这次发送任务成功完成。如下图所示:
(点击可查看大图)
如果在第5步,这两个重传的数据包也还是都没有到达,那情况会如何呢?不同的操作系统和TCP配置,其行为会有些差异,但大体上也还是超时重传和快速重传这两者结合,力争数据包都成功传送。对于实在“路太难走导致一直不能发送成功”的情况,其中一方会选择结束(发送RST给对方)这次无意义的连接。案发
(点击可查看大图)
图的上半部分是client访问LB VIP,需要等待很长时间才能拿到HTTP返回。图下半部分是client直接访问server,结果一切正常,很快就可以拿到结果。所以问题貌似集中到负载均衡(LB)上了。另外一个强有力的证据是,因为client和server都配置了标准的事务日志,从日志上来看:
1. 对于访问经过了LB的情况:server上的日志显示,这个请求的处理耗费了1703毫秒。如下图:(点击可查看大图)
看到这个景象,大多数人第一感觉就是“这肯定是有网络丢包了,所以接收方一直在回复DupAck!赶快去查网络问题。”这里说的网络问题特指交换机路由器这些。这句话大部分时候都是对的,但在这里却不是。为什么呢?这跟人生病的例子类似,咳嗽发烧多半是感冒引起,但并不全是。DupAck多半是丢包引起的,但这次不是。在这个纠结点上展开之前,我们先看一下抓包文件展现出来的全貌:第一阶段:连接建立,正常。第二阶段:client开始发送数据包给LB,正常。第三阶段:client连续发送了约70KB数据,目前为止也是正常。第四阶段:LB发送Ack包确认收到了前面约27KB的数据。第五阶段:client继续发送70~91KB的数据。第六阶段:在27KB之后,LB连续发送数十个DupAck,然后client发送一次TCP fast retransmission;这样的情况持续到client把所有应该发的数据包都发完。我们在wireshark里打开expert information看一下汇总信息:(点击可查看大图)
快速重传有8个,DupAck有567个。平均每个快速重传对应70个DupAck。我们看其中一个例子:(点击可查看大图)
在LB给client发送了数十个DupAck之后,client发送了一次快速重传(包号105),然后LB回复Ack(针对包号105),并且LB继续新一轮的数十个DupAck。以此往复。其实这里已经说明了这个案例的特殊性:如果是网络问题导致丢包,那么丢包会是随机现象,不太可能像这样有规律。有规律的现象背后,一般藏着某种正在运行的机制,尽管它尚未被证实。不过,大量的DupAck和重传,确实跟应用层看到的严重的延迟现象对上了。可以说,我们的排查工作有了明显的进展,至少能回答“应用为什么变慢”这个问题了。当然,探究根因的话,接下来就是要回答“为什么有重传”这个问题。在前面的复习部分,我们提到了“快速重传”和“超时重传”。那么这次的重传属于哪一种呢?我们先看一下相关数据包的具体细节,因为只有用这种“放大镜”的方式,我们才能更加清晰地定位到根因。首先,我们看一下在LB回复大量DupAck之前,client的数据包发送情况。(点击可查看大图)
第一个LB DupAck包之前,client发送的最后一个数据包是包号67:序列号91305:表明从握手开始有91304(减去握手阶段的1)字节的TCP载荷数据从client发出了;确认号1:因为LB还没回复HTTP响应,所以client还是保持握手阶段的确认号1。然后看一下第一个LB DupAck包,其包号为68:序列号1:因为LB还没回复应用层的HTTP响应,所以还是保持握手阶段的序列号1;确认号27741:表示LB收到了27740(减去握手阶段的1)字节的数据,而第27741字节之后的数据并没有收到。TCP协议规定:接收方回复的Ack包的确认号=发送方数据包的序列号+TCP载荷字节数。如果接收方回复了DupAck,假设这个DupAck的确认号为N,那么其含义是:我只收到发送方给我的序列号为N之前的数据包,而序列号为N及其之后的数据包,我都没有收到。LB通过DupAck包向client(以及坐在电脑前看着wireshark界面的我们)宣告:“我这边只收到序列号27741之前的数据包,而序列号为27741的数据包,及其之后的数据包,我(LB)都没收到。”这里出现几十次DupAck的原因是,一旦LB认为某个数据包我没有收到(此处是序列号为27741的数据包),那么之后client送过来的每个数据包,LB都无法Ack这些数据包的序列号+TCP载荷字节数。所以虽然Ack包还是要发,但确认号却只能“停留”在丢包处的确认号,这也是DupAck的前半部分Dup (Duplicate的缩写)的由来。差不多长这样(除了27741以外,其他序列号是为举例而编造的):client -> LB: seq 27741 (包掉了)LB -> client: Ack 27741client -> LB: seq 30000LB -> client: dupAck 27741client -> LB: seq 40000LB -> client: dupAck 27741......当然也可能长这样:client -> LB: seq 27741(包掉了)LB -> client: Ack 27741
client -> LB: seq 30000
client -> LB: seq 40000
LB -> client: dupAck 27741 (for seq 30000)
LB -> client: dupAck 27741 (for seq 40000)
......
好了,情况比较清楚了,虽然“丢包”的根因还没找到,但整个排查工作的脉络相对清楚了,即:某处丢包->TCP重传->TCP传输速度下降->应用层超时报错。看起来离成功只剩一步了,即:找到那个丢失的序列号为27741的数据包!只要证明这个数据包丢失了,那我们就可以找到网络部门,让他们彻查网络,以恢复网络可靠性。TCP不丢包了不重传了,速度就上来了,应用就不超时了。逻辑圆满自洽。问题虽难,但不敌我wireshark分析小能手。“好嗨呀,感觉人生已经达到了巅峰”。反转
(点击可查看大图)
这个包是从client发给LB的,它的序列号为27229,载荷为1460字节。wireshark也告诉我们,client将要发的下一个包的序列号将会是28689。而LB回复的Ack(后续同样的Ack就是DupAck)是这样:(点击可查看大图)
我们再看那个最为可疑的数字27741。显然,27741=27229+512。到这里看清楚了吗?这次LB确认的是一件“上衣”(512字节),而余下的“裤子”(另外的1460-512=948字节),LB并没有确认。没有被确认的数据,在client看来,是需要重新发送的。我们先看看正常情况(即每次确认1460字节 )下的数据包交换过程:(点击可查看大图)
然后看一下这次异常交换的过程。client:我发给你从27229开始的1460字节,下一次你懂的,我将要发的是28689开始的数据。LB:我也不知道最近怎么搞的,你这次给我这些数据,我好像只认得前512字节,其他的我认不出来了,先确认这512字节吧!client:怎么回事?只确认前512字节?年轻人不讲武德啊。麻烦了,我为了保证这次发送的TCP载荷依然能用足一个MSS即1460字节,必须把前一个包的后948和下一个包的前512字节,组合在一起,变成一个1460字节的包,再发送给你。不过还好,所有未被确认的数据都还在我的缓存(buffer)里面,没有丢失。不过原先计算好的安排都要改掉了,我的CPU开销很大啊!如下图所示:(点击可查看大图)
这里有一个关键点:确认号是字节级别的,而不是报文级别!说到这里,再回顾前面提到过的现象:“平均每个快速重传对应70个DupAck”,而每次重传都需要客户端把发送缓冲区里面打包好的数据包,挨个拆开,重组成LB想要的样子(512字节的位移的关系)。想必读者朋友们已经清楚为什么应用会超时(超过1秒)了:因为时间都花费在了各种包的分拆、重组上面了。光是客户端想成功发完一个POST请求,都花费了远远超出预期的时长。就如同一辆车频繁熄火,还怎么可能高速行驶呢?我们做一下更详细的图解:(点击可查看大图)
最后再来回顾一下整个过程:1. client发送HTTP POST请求,在TCP层面体现为一系列数据包(30KB以内)给LB,LB转发给server,目前一切正常。client端应用持续的timeout也从TCP数据发送那刻开始计时。2. 大约在client发送数据到30KB左右时,LB回复的Ack包确认的数据不再是数据包分界点的字节数,而是位于中间的某个字节数(这个行为比较罕见)。3. client累积收到超过3个(实际是70多个)这样的DupAck,认为该包丢失。加上这个包的特殊性(是之前某个完整包的一部分),client从缓冲区找出对应的字节数,拼凑上后续包的数据,拼接成一个新的MSS(1460字节)的数据包,而在此处消耗了不少时间。4. LB继续发送类似的“中间确认”包,client继续进行“拆包、重组”的操作,此处持续消耗client时间。5. server一直在等待读取网络数据,无法及时收取完整的POST请求并计算处理,此时已经无法在client预定timeout时限内完成任务,于是client报错。结案与思考
基于目前为止的分析,我们已经取得了充足的证据,并提交给了LB厂商进行进一步分析。厂商也十分配合地进行了排查,并比较少见地确认这是一个bug。当时的回复是这样的:Thank you for the update. I have opened a Bug with the engineering team to investigate the reason for the LB not to send the complete segment. The Bug number is 695668.在bug修复之前,我们也通过修改LB的TCP setting实现了对这一bug的规避,效果也可以说是立竿见影。具体来说就是扩大了TCP receive buffer size,使得缓冲区足够大(你HTTP POST请求大,我缓冲区更大),不至于触发这个数据块操作相关的bug,使得网络还是能愉快地工作下去。好了,这次的技术难题十公里跑完了。感觉如何?可能中间有艰难的部分,可能有不止一次想放弃的时刻,这都是正常的。技术的提升就是在很多个不眠的夜晚你辗转反侧的时候,在你把精神聚成刀光劈开一扇新的大门的时候,在你为了一个小小的问题翻遍大部头的时候,在你为终于攻克了一个难题而欢呼雀跃的时候。当站上一个新的平台,你已经无惧那些曾经困扰你的问题,而眼前看到的将是更加宽广壮丽的风景。
您可能还感兴趣:
Hadoop平台进阶之路 | HDFS NameNode性能优化实践
Hadoop平台进阶之路| 一场PB规模量级的HDFS数据迁移实战
Hadoop平台进阶之路 | eBay Spark测试框架——Woody
从OpenStack到Kubernetes | 如何在大规模产线应用迁移中保证高可用性?
从Druid到ClickHouse | eBay广告平台数据OLAP实战
eBay大量优质职位,等的就是你