背景
我们某个数据服务向位于公网的阿里云的LogHub(可以理解为一个Kafka)投递消息,投递的客户端使用的是阿里云提供的Java SDK,看其源码发现其使用http批量投递消息的方式向远端投递。
其内部逻辑大概如下
public void send(msg) {
try {
//正常发送
http.send(msg);
} catch (e) {
//遇到异常后重发
http.reSend(msg);
}
}
偶然间发现投递后的数据有重复的,遂排查之,最后结合tcp协议抓包分析到了,就是因为网络带宽限制,发送的第一次请求,实际已经到达服务端,服务端已经写入Loghub服务,但是由于“** 相关ack并没有返回给客户端,所以客户端的tcp重试机制,重试的时候服务器的socket因为某种原因已经关闭,就给正阻塞在read的客户端发送了rst包,导致客户端抛异常 **”, 所以客户端认为发送失败,进而执行了重传逻辑,进行了第二次的发送,导致了消息写入MQ写了两次而出现了重复消息。
第一次请求
分为"5"和"6"两个ip,下面是详细的过程解释:
第一个包:5向6发起三次握手,发送一个syn
第二个包:6向5发送第二个握手包,发送一个syn+ack
第三个包:5向6发送确认包,连接由此建立
第四个包:连接建立后,开始发送post的http请求,这里的第四与第五个包实际上是一个http包来的,因为发送窗口限制,被切成了两个包分两次发送,一个包被wireshark识别成了tcp,另一个被识别成http。实际上http的header是在第四个包里的
第五个包:如上,里面是消息体
第六个包:当"ip-5"发送了第四与第五个包后,期待收到ack确认,但是"ip-6"收到了包,也发送了两个ack,但这两个ack都在网络中丢失了(待会解释为什么这么说),这里有些奇怪,不知道为什么明明没有收到第四个包的ack确认包而跳过第四个包直接重发了第五个包。这个重发包也就是第六个包,可以看到第六个包与第五个包的length一样(都是647)
第七个包:站在"ip-6"的角度想,连接建立后,我收到了两个包,一个tcp,一个http,然后我也都回了两个ack确认,但我并不知道这两个ack都在网络中丢失了,而且在我发出两个ack之后的一段时间内一直没有收到任何数据,所以我觉得这个socket开着浪费,我就关掉了这个连接(这里关掉socket的原因还有待商榷,也可能是服务端程序异常导致关闭这个socket)。但是这时候客户端又发过来一个包(也就是第六个包,而且这个时候客户端还在阻塞在read方法等待服务器回tcp包),当这个包到达操作系统内核后,内核发现该链接已经关闭了,所以内核直接向客户端发送了rst标志位的包来reset连接。reset包也就是第七个包,可以看到第七个包的ack为1066可以计算并推理得到服务端实际上是接收到了包,并返回了ack,只不过客户端没有收到这两个ack而已。
站在"ip-5"的角度想,连接建立后,我发送了两个包,没有一个收到ack,所以我迫不及待的重传了http那个包,发出去后不久,我就莫名其妙地接到了一个reset的包。
第二次请求
因为这边sdk在发送完毕后得到了rst,所以抛出Connection reset的异常,然后执行了重传逻辑,所以就有了这第二次的请求。
以下是第二次请求的情况:
还是根据最后一位ip分为ip5和ip6,以下是对各个包的分析
前三个包(36263,36272,36273):tcp三次握手
第四个包(36274):跟第一次请求一样,把一次http请求包切成了两个包,分两次发送
第五个包(36275):同上
第六个包(36278):这次可以看到,在ip5在发送出两个包后,ip6回复了两个ack并顺利到达了ip5端,第六个包是第一个ack包
第七个包(36279):第七个包是第二个ack包
第八个包(36284):因为一次http连接包括请求和响应,第八个包是ip6的响应包,包很小,一下就能发完
第九个包(36285):ip5在接到响应后,对ip6的ack确认
第十个包(36286):本次http连接完毕,没有数据要发送了,开始tcp四次挥手,这里由ip6首先发起发送了一个fin包
第十一个包(36287):在课本当中,tcp断开要四个包,但在实际当中,为了更快的断开tcp连接,如果当被断开方接到fin包后发现自己也没有业务数据要发送了,那么就会把课本当中的第二个ack包和第三个fin包合并到一起发送,这里就是这种情况
一下三个包,全都是我个人根据我知道的知识做出的推断,并且提供了我的依据,供参考
第十二个包(36289):我认为是ip6对于她自己发送的fin包没有接到回应,进行超时重传的包,这里的依据是从包的长度,包携带的ack number,包的seq都与她发送的第一个fin-ack包相同来判断的
第十三个包(36290):我认为是对第十二个包的确认,虽然第十三个包的从ack number上、wireshark提示的dup ack 36287#1都显示也可能第十三个包是对第十个包的确认,但我并不这么觉得。因为我是在ip5上抓的包,所以只要是ip5上发出的包我就一定能抓到,再加上按照tcp的协议来说,ip6发过一个包来,ip5肯定要回一个ack确认包。这里奇怪的是ip5只发送了一个只包含ACK的包,而没有FIN,这里可能就跟具体的操作系统的tcp实现有关吧。
第十四个包(36299):我认为是当ip6发送出第十个包后,等待超时后重传了第十二个包,发出后,ip5发送的第十一个包到达了ip6(ps:这种情况我觉得是可能会出现的,但我并不确定这种情况下,ip6的tcp层会对这个迟到的包怎么处理),然后ip6对第十一个包进行了ack确认,这个ack也就是第十四个包。依据是:单单按照第十四个包的ack number的角度来看,她确认的可能是第十三个包(因为第十三个包是一个ACK包,确认号等于本身的seq值)和第十一个包(因为第十一个包是fin-ack包,她的确认号等于本身的seq+1),但是TCP中不会对一个确认包进行确认,所以他只可能确认的是第十一个包,也就是第十一个包最后是到达了ip6的
结束。
上文中提到的一些TCP的规则,我整理成了另一篇文章,TCP协议的seq、ack的计算与实际中tcp断开的优化