查看原文
其他

一探究竟 | eBay流量管理之重新发现TCP重传

杨胜辉 eBay技术荟 2022-12-29
供稿 | eBay FE Team 

作者 | Victor Yang(杨胜辉)

编辑 | 顾欣怡本文5797字,预计阅读时间14分钟

更多干货请关注“eBay技术荟”公众号


引子

eBay的基础架构FE团队经常需要用wireshark进行网络分析,找到问题的原因。对于时常做网络分析的朋友来说,如果打开wireshark,看到满屏的TCP retransmission,第一感觉是什么?是掉包了,客户端重传了对吧?一定是网络上有问题,比如路由器、防火墙,比如网线、光纤...... 这也确实符合大多数场景,毕竟设计TCP的目标,如果说只有一个,那就是保证可靠的传输。我的包掉了,还能再传一次或者多次,直到传输完成;如果持续失败,放弃传输。有点“先死磕,后认怂”的意思。

而TCP重传,恰恰也跟诸多奇怪的问题有很深的联系。

回顾

让我们先复习一下TCP重传的知识。实际上TCP的重传也有不同的种类,大体上分为两种:1. 超时重传 2. 快速重传。超时重传:数据包如果没有达到接收方,那接收方也没法以回复Ack包的形式向发送方确认“包已经收到”这个事实。发送方为了避免自己陷入“干等”的境地,于是选择在等待某段时间后重新发送同样的数据包。这个等待的时间就是重传超时(retransmission timeout,简称RTO)。这个timeout其实是基于一个计时器,在任何一个数据包发送出去后就开始计时,若在时限内对方回复,这个计时器就清零;若达到时限对方仍未回复,重传操作就会被触发。

当然超时重传也还是有可能陷入丢包的境地,此时发送方一般选择以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给对方)这次无意义的连接。


案发

好了,复习课先上到这里,接下来让我们看看这次的案例:eBay的应用大部分基于微服务进行设计和开发,各种服务组件之间存在着千丝万缕的互相调用的关系,而这种调用又多基于HTTP协议。有一天,负责某一个服务模块的开发团队向基础架构团队报告了一个情况:从他们这个client pool向server VIP发送的请求,遇到了大量的“返回超时”的情况。这是一个应用层的超时设置,如果client无法在1秒钟之内收到返回,就会抛出“超时”的报错。毕竟在内网这种毫秒级网络延迟的环境,大部分的时间开销都在服务端应用程序处理上面,所以1秒钟(1000毫秒)给应用程序,确实算是比较富余。这个应用的1秒超时机制,跟TCP超时重传机制类似,应用也不想“干等”。有没有可能就是服务端应用本身处理很慢,所以导致了超时呢?对于这个问题,开发团队自己也做了初步的诊断。他们发现,如果client绕过VIP去直接访问server,又能恢复正常,即十几毫秒内就收到回复,完全没有超时的问题。如下图所示:

(点击可查看大图)

图的上半部分是client访问LB VIP,需要等待很长时间才能拿到HTTP返回。图下半部分是client直接访问server,结果一切正常,很快就可以拿到结果。所以问题貌似集中到负载均衡(LB)上了。

另外一个强有力的证据是,因为client和server都配置了标准的事务日志,从日志上来看:

1. 对于访问经过了LB的情况:server上的日志显示,这个请求的处理耗费了1703毫秒。如下图:(点击可查看大图)client上的日志显示,这个请求消耗了两千多毫秒,如下图:(点击可查看大图)2. 对于直接访问server的情况,server上的日志显示,很快收到了client发出的请求,server自身也较快(几百毫秒)处理完并回复了。比如这个server日志:(点击可查看大图)对于情况1,很多朋友也许会问:“看起来不是server那头本身处理的耗时很长吗?为什么不查查server上应用代码的bug?” 不过,SRE的同事分析了当时JVM的运行情况,排除了GC等因素的影响,并定位到,服务端的耗时主要是花费在了读取网络数据上面。所以还是回到网络排查的方向上来。从client端看到的现象,结合client和server两方面的应用日志来推断,矛头便更加确定地指向了LB,以及LB前后的网络周边环节。会是LB或者网络层面问题导致了延迟吗?有可能。我们在LB进行了抓包。在wireshark里打开抓包文件,一开始看到的是一帆风顺,全绿,像这样:(点击可查看大图)翻了几页,突然画风一变,全红,像这样:

(点击可查看大图)

看到这个景象,大多数人第一感觉就是“这肯定是有网络丢包了,所以接收方一直在回复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分析小能手。“好嗨呀,感觉人生已经达到了巅峰”。


反转

但事情就是这么诡异,去翻前面数据包的时候,发现根本就没有那个“序列号为27741”的数据包!有序列号27229的包,也有序列号28689的包,但就是没有位于这两个数中间的27741的包。这个时候,恍惚中有点错觉,该不是到了天涯论坛的文学版块吧,怎么好像是悬疑小说呢:一桩案件的元凶被查出是某某某,结果发现某某某这个人压根不存在。我想静静(这不是一个人名)。我有时候会跑步,这是我众多健身习惯中的一个。跑步过程中会有一个极限区,在这个极限区里面,心肺会遇到很大的压力,这种难受的感觉很容易让人放弃,但如果继续坚持挺过这个极限区,身体就能提升到一个新的平台上继续进行“供能-耗能”的平衡运转,进而,我们的运动能力就得以提升了,可以继续快乐地跑下去。显然,我们的排查进入“极限区”了。一念之间,如果挺过去,就是新世界;如果知难而退,那么我们会留在舒适区,在从平庸到平庸的区间车里轮回。这个序列号27741的包消失了吗?还是说它就在那里,只是我们忽视了它的存在?TCP序列号、payload(载荷)、TCP确认号,一般情况下就是一个A+B=C的关系。但是,确认号必须是序列号+全部载荷吗?它可以是序列号+部分载荷吗?我从网上购买了一套衣服(上衣+裤子),我也收到了全套。但我觉得裤子尺码不对,上衣还挺合身,我可以只确认我收到了上衣(当然裤子还是要退回的),让卖家重新发裤子给我吗?当然可以。其实TCP也是可以这样的。那么,“寻找序列号为27741的包”其实是个伪命题,其实这个“包”并不是独立的一个包(TCP segment),其实它只是某一个TCP包的一部分(前半部分)。我们来看一下包号20的详情:

(点击可查看大图)

这个包是从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,使得网络还是能愉快地工作下去。

好了,这次的技术难题十公里跑完了。感觉如何?可能中间有艰难的部分,可能有不止一次想放弃的时刻,这都是正常的。技术的提升就是在很多个不眠的夜晚你辗转反侧的时候,在你把精神聚成刀光劈开一扇新的大门的时候,在你为了一个小小的问题翻遍大部头的时候,在你为终于攻克了一个难题而欢呼雀跃的时候。当站上一个新的平台,你已经无惧那些曾经困扰你的问题,而眼前看到的将是更加宽广壮丽的风景。

您可能还感兴趣:

分享 | ebay服务器稳定性测试的探索和实践

干货 | eBay的4层软件负载均衡实现

干货 | eBay Feature测试环境上k8s的实践

平台迁移那些事 | 企业级消息系统测试之道

平台迁移那些事 | eBay GC调优策略的实践

平台迁移那些事 | eBay百亿级流量迁移策略

分享 | Spark Skew Join的原理与优化

Hadoop平台进阶之路 | HDFS NameNode性能优化实践

Hadoop平台进阶之路| 一场PB规模量级的HDFS数据迁移实战

Hadoop平台进阶之路 | eBay Spark测试框架——Woody

从OpenStack到Kubernetes | 如何在大规模产线应用迁移中保证高可用性?

从Druid到ClickHouse | eBay广告平台数据OLAP实战

数据之道 | Akka Actor及其在商业智能数据服务中的应用

技术分享|基于图的大规模微服务Trace分析方法与企业实践

超越“双十一” | ebay支付核心账务系统架构演进之路

👇点击阅读原文,一键投递 

    eBay大量优质职位,等的就是你

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存