一次网络超时的排查

云原生实验室

共 8442字,需浏览 17分钟

 ·

2021-10-22 08:55


最近在测试一个分布式组件的时候,发现节点之间会频繁的出现网络传输超时的情况。组件的工作原理如下

  1. 节点 1 向节点 2 发送心跳包,发送之后等待节点 2 的回应
  2. 节点 2 在收到心跳包之后做一些处理,随后发送回复报文给节点 1
  3. 节点 1 收到节点 2 回复的报文并退出等待,一次数据传输结束
  4. 节点 1 如果一段时间内没有收到节点 2 的回应就会发生超时异常

我们遇到的问题就是节点 1 总是在运行一段时间之后(很短,大约几秒钟),发送给节点 2 的数据就无法及时的得到回应,随后节点 1  报出超时异常。问题在于我们用于测试的机器应该都在同一个机房,而我们设置的超时时间为 50ms,同一个机房的节点延迟怎么会超过 50ms 呢?

日志

因为是分布式系统,并且涉及到网络传输,我们基本上没有办法使用 DEBUG 来进行调试,所以日志成为了排解问题的唯一线索。顺便说一句,在写代码的时候日志应该越详细越好,日志除了打印变量数据,还应该附带好详细的上下文信息。

我们在发送数据和接收数据的地方都打上日志,此外应该对所使用的节点的时间进行校对,我们在分析问题时很依赖于日志中记录的时间信息。

我们执行程序获取日志如下

发送端

2019/08/14 15:18:45.703679 node.go:233: 202(me) send AppendEntries to 92
2019/08/14 15:18:45.753927 node.go:259: 202(me) to 92 AppendEntries timeout and retry
2019/08/14 15:18:45.763991 node.go:233: 202(me) send AppendEntries to 92
2019/08/14 15:18:45.814221 node.go:259: 202(me) to 92 AppendEntries timeout and retry
2019/08/14 15:18:45.824193 node.go:233: 202(me) send AppendEntries to 92
2019/08/14 15:18:45.874455 node.go:259: 202(me) to 92 AppendEntries timeout and retry

我们观察以上日志发现确实发送端在发送了心跳 50ms 后因为没有收到响应随即超时了,接下来我们再观察接收端的日志。根据上面的日志我们已经知道了异常发生在 703ms ~ 874ms  之间,所以我们在观察接收端的日志时应该重点关注 703、763、824 这三个发送了心跳的时间点,接收端有如下日志信息:

2019/08/14 15:18:45.655403 connection.go:387: AppendEntries

2019/08/14 15:18:45.655477 connection.go:95: Read Start
2019/08/14 15:18:45.895077 connection.go:99: Read End
2019/08/14 15:18:45.895112 connection.go:100: Read 1 bytes and cost 239ms 239574218ns
2019/08/14 15:18:45.895379 connection.go:387: AppendEntries

2019/08/14 15:18:45.895439 connection.go:95: Read Start
2019/08/14 15:18:45.895452 connection.go:99: Read End
2019/08/14 15:18:45.895464 connection.go:100: Read 1 bytes and cost 0ms 4203ns
2019/08/14 15:18:45.895711 connection.go:387: AppendEntries

2019/08/14 15:18:45.895766 connection.go:95: Read Start
2019/08/14 15:18:45.895779 connection.go:99: Read End
2019/08/14 15:18:45.895791 connection.go:100: Read 1 bytes and cost 0ms 3862ns
2019/08/14 15:18:45.896082 connection.go:387: AppendEntries

观察接收端的日志我们发现在 655ms 的时候接收端接收到最后一次正常的心跳,随后就被阻塞在 read 调用上,下一次读取到心跳信息则是在  895ms,并且随后很快的就读完了三次心跳的数据。很显然,895 远大于  703、763、824,那么是什么原因导致了发送端在这三个时间点发送的报文一直到 200 多毫秒之后才被接收端成功的读取到呢?

tcpdump

我们使用 tcpdump 工具来获取网络报文信息,从而了解数据的传输情况。因为我们的组件使用 6300 端口进行数据传输,所以在两个节点上面都要执行如下命令来抓取通过 6300 端口的报文

$ tcpdump tcp port 6300

我在上一步执行程序的时候已经打开了 tcpdump 命令,所以现在我们已经可以观察 tcpdump  的输出信息了。首先观察发送端发送的信息,同样我们只关注 703ms ~ 874ms  之间的信息。该时间段的信息如下,需要注意有一些网络数据报文与我们要关注的内容无关,直接忽略即可。

15:18:45.643425 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 525:546, ack 164, win 114, options [nop,nop,TS val 3450638986 ecr 3259647687], length 21
15:18:45.664930 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [P.], seq 164:170, ack 546, win 115, options [nop,nop,TS val 3259647744 ecr 3450638986], length 6
15:18:45.703607 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 546:567, ack 170, win 114, options [nop,nop,TS val 3450639047 ecr 3259647744], length 21
15:18:45.763909 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 567:588, ack 170, win 114, options [nop,nop,TS val 3450639107 ecr 3259647744], length 21
15:18:45.784473 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647863 ecr 3450638986,nop,nop,sack 1 {567:588}], length 0
15:18:45.824084 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 588:609, ack 170, win 114, options [nop,nop,TS val 3450639167 ecr 3259647863], length 21
15:18:45.840677 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647920 ecr 3450638986,nop,nop,sack 1 {567:609}], length 0
15:18:45.869809 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [P.], seq 170:183, ack 546, win 115, options [nop,nop,TS val 3259647948 ecr 3450638986,nop,nop,sack 1 {567:609}], length 13
15:18:45.870435 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 609:615, ack 183, win 114, options [nop,nop,TS val 3450639213 ecr 3259647948], length 6
15:18:45.884354 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 615:636, ack 183, win 114, options [nop,nop,TS val 3450639227 ecr 3259647948], length 21
15:18:45.886702 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647966 ecr 3450638986,nop,nop,sack 1 {567:615}], length 0
15:18:45.886732 IP lin-21-34-202.bmc-grx > 172.21.3.92.57619: Flags [P.], seq 546:567, ack 183, win 114, options [nop,nop,TS val 3450639230 ecr 3259647948], length 21
15:18:45.902699 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647981 ecr 3450638986,nop,nop,sack 1 {567:636}], length 0
15:18:45.903892 IP 172.21.3.92.57619 > lin-21-34-202.bmc-grx: Flags [.], ack 636, win 115, options [nop,nop,TS val 3259647983 ecr 3450639230], length 0

其中 lin-21-34-202 是节点 1,172.21.3.92.57619 是节点 2,即心跳是由 lin-21-34-202 发送给 172.21.3.92 的。Flags 中的 P 代表 PUSH, . 代表 ACK。

已知我们心跳的长度为 12,由上面的报文以及前面的日志我们可以推测出来,第一行的信息代表了最后一次节点 1 给节点 2 发送的正常心跳信息。

随后节点 1 发送了 seq 为 546:567567:588 的两条心跳包,这就是第 3 第 4 行的信息。奇怪的是在第 5 行所代表的节点 2 回复的 ACK 中,只表示收到了 567:588 的的数据包,没有 546:567。第 6 第 7 行表示了发送的第三个心跳包 588:609 以及相应的 ACK 567:609

节点 1 发现节点 2 迟迟不回复 546:567 的 ACK,就对 546:567 进行了重发,这就是第 12 行的信息。这一次节点 2 终于回复了 ACK(第 14 行),我们可以认为 546:567 确定被发送到节点 2 了。

随后我们再看一下接收端的报文信息

15:18:45.654608 IP 172.21.34.202.bmc-grx > lin-21-3-92.57619: Flags [P.], seq 525:546, ack 164, win 114, options [nop,nop,TS val 3450638986 ecr 3259647687], length 21
15:18:45.655249 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [P.], seq 164:170, ack 546, win 115, options [nop,nop,TS val 3259647744 ecr 3450638986], length 6
15:18:45.774317 IP 172.21.34.202.bmc-grx > lin-21-3-92.57619: Flags [P.], seq 567:588, ack 170, win 114, options [nop,nop,TS val 3450639107 ecr 3259647744], length 21
15:18:45.774348 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647863 ecr 3450638986,nop,nop,sack 1 {567:588}], length 0
15:18:45.831770 IP 172.21.34.202.bmc-grx > lin-21-3-92.57619: Flags [P.], seq 588:609, ack 170, win 114, options [nop,nop,TS val 3450639167 ecr 3259647863], length 21
15:18:45.831795 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647920 ecr 3450638986,nop,nop,sack 1 {567:609}], length 0
15:18:45.859801 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [P.], seq 170:183, ack 546, win 115, options [nop,nop,TS val 3259647948 ecr 3450638986,nop,nop,sack 1 {567:609}], length 13
15:18:45.877852 IP 172.21.34.202.bmc-grx > lin-21-3-92.57619: Flags [P.], seq 609:615, ack 183, win 114, options [nop,nop,TS val 3450639213 ecr 3259647948], length 6
15:18:45.877870 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647966 ecr 3450638986,nop,nop,sack 1 {567:615}], length 0
15:18:45.892884 IP 172.21.34.202.bmc-grx > lin-21-3-92.57619: Flags [P.], seq 615:636, ack 183, win 114, options [nop,nop,TS val 3450639227 ecr 3259647948], length 21
15:18:45.892901 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [.], ack 546, win 115, options [nop,nop,TS val 3259647981 ecr 3450638986,nop,nop,sack 1 {567:636}], length 0
15:18:45.894971 IP 172.21.34.202.bmc-grx > lin-21-3-92.57619: Flags [P.], seq 546:567, ack 183, win 114, options [nop,nop,TS val 3450639230 ecr 3259647948], length 21
15:18:45.894989 IP lin-21-3-92.57619 > 172.21.34.202.bmc-grx: Flags [.], ack 636, win 115, options [nop,nop,TS val 3259647983 ecr 3450639230], length 0

接收端的报文基本上就是发送端的镜像,但是有趣的是接收端的报文只有 13 条,而发送端则发送了 14 条报文。仔细对比就发现发送端的 seq 546:567 在接收端不存在,看来这个报文在网络传输中丢失了没能成功到达节点 2。节点 2 在节点 1 再次重传后才最终获取了 546:567 的报文信息,即接收端的第 12 行,此时时间点为 15:18:45.894971,最后节点 2 返回给节点 1 ACK。

分析

观察上面接收端收到重传数据的时间为 15:18:45.894971,日志中接收端用户态成功读取到报文的时间为 15:18:45.895077,那结果就明显了。

节点 1 发送给节点 2 的第一个心跳包丢失了,因为 TCP  协议中数据的有序性,所以虽然后面的数据成功到达也不能将这些数据返回给用户态程序。一直到节点 1 因为没能得到 ACK 而进行超时重传,节点 2  最终获取了心跳包 1,此时数据已经完整可以返回给用户态程序。这一顿操作也导致这部分数据从节点 1 的用户态到节点 2 的用户态之间产生了 894 - 703 约 200ms 的延迟,因而节点 1 产生了超时。

解决

咦,为什么同一个机房的两个机器之间会丢包呢,难道路由器出问题了?经过检查之后发现是我们自己搞错了,在申请机器的时候我们第一次申请的机器在南京,第二次申请的机器在杭州,所以这两个机器其实并不是在一个机房并且之间隔了接近  300km,产生丢包的现象也就可以理解了。虽然问题产生的原因很囧,但是这次耗时两天的问题排查还是让我很有收获的,切换了负载之后问题也就成功解决了。

原文链接:https://www.nosuchfield.com/2019/08/14/Network-timeout-troubleshooting/


你可能还喜欢

点击下方图片即可阅读

WireGuard 全互联模式终极指南(上)!

云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!



点击 "阅读原文" 获取更好的阅读体验!


发现朋友圈变“安静”了吗?

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报