能ping通,TCP就一定能连通吗?
文章持续更新,可以微信搜一搜「小白 debug」第一时间阅读,回复【面试】获面试题集。本文已经收录在 GitHub https://github.com/xiaobaiTech/golangFamily , 有大厂面试完整考点和成长路线,欢迎 Star。
平时,我们想要知道,自己的机器到目的机器之间,网络通不通,一般会执行ping 命令。
一般对于状况良好的网络来说,你能看到它对应的loss
丢包率为0%
,也就是所谓的能 ping 通。如果看到丢包率100%
,也就是ping 不通。


那么问题来了,假设我能ping通某台机器,那这时候如果我改用TCP 协议去发数据到目的机器,也一定能通吗?
或者换个问法,ping 和 tcp 协议走的网络路径是一样的吗?
这时候第一反应就是不一定,因为 ping 完之后中间链路里的某个路由器可能会挂了(断电了),再用 TCP 去连就会走别的路径。
也没错。但假设,中间链路没发生任何变化呢?
我先直接说答案。
不一定,走的网络路径还是有可能是不同的。
今天就来聊聊为什么。
我之前写过一篇《断网了,还能 ping 通 127.0.0.1 吗?》,里面提到过ping 数据包和 tcp 数据包的区别。

我们知道网络是分层的,每一层都有对应协议。

而这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。
不管是 ping(用了 ICMP 协议)还是 tcp 本质上都是基于网络层 IP 协议的数据包,而到了物理层,都是二进制 01 串,都走网卡发出去了。
如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢?
我们就从路由这个话题聊起吧。
网络路径
在我们的想象中,当我们想在两台机器之间传输数据。本机和目的机器之间会建立一条连接,像一条管道一样,数据从这头到那头。这条管道其实是我们为了方便理解而抽象出来的概念。
实际上,我们将数据包从本地网卡发出之后,会经过各种路由器(或者交换机),才能到达目的机器。
这些路由器数量众多,相互之间可以互连,连起来之后就像是一张大网,所以叫**"网络"**可以说是非常的形象。

考虑到交换机有的功能,路由器基本上都支持,所以我们这边只讨论路由器。
那么现在问题来了,路由器收到数据后,怎么知道应该走哪条路径,传给哪个路由器?
路径由什么决定?
在上面的那么大一张网络中,随便一个路由器都有可能走任何一个路径,将数据发到另外一个路由器上,
但路由和路由之间距离,带宽啥的可能都不同。
于是就很需要知道,两点之间走哪条路才是最优路径。
于是问题就变成了这样一个图状结构。每条边都带有成本或权重,算这上面任意两点的最短距离。

这时候想必大家回忆压不住要上来了。
这题我熟,这就是大学时候刷的Dijkstra 算法。菊花厂的 OJ 笔试题集里也经常出现,现在终于明白为什么他们家的笔试题里图类题目比别的大厂貌似要多一些了吧,因为菊花厂就是搞通信的,做路由器的老玩家了。
路由表的生成
基于Dijkstra 算法,封装出了一个新的协议,OSPF 协议(Open Shortest Path First, 开放最短路径优先)。
有了 OSPF,路由器就得到了网络图里自己到其他点之间的最短距离,于是就知道了数据包要到某个点,该走哪条最优路径。
将这些信息汇成一张表,也就是我们常说的路由表。
路由表里记录了到什么 IP 需要走什么端口,以及走这条路径的成本(metric
)。
可以通过 route
命令查看到。

路由表决定数据包路径
数据包在发送的过程中,会在网络层加入目标地址 IP。
路由器会根据这个IP跟路由表去做匹配。
然后路由表,会告诉路由器,什么样的消息该转发到什么端口。
举个例子。

假设 A 要发消息到 D。也就是192.168.0.105/24
要发消息到192.168.1.11/24
。
那么 A 会把消息经发到路由器。
路由器已知目的地 IP192.168.1.11/24
,去跟路由表做匹配,发现192.168.1.0/24
, 就在 e2 端口,那么就会把消息从 e2 端口发出,(可能还会经过交换机)最后把消息打到目的机器。
当然,如果路由表里找不到,那就打到默认网关吧,也就是从 e1 口发出,发到 IP192.0.2.1
。这个路由器的路由表不知道该去哪,说不定其他路由器知道。
路由表的匹配规则
上面的例子里,是只匹配上了路由表里的一项,所以只能是它了。
但是,条条大路通罗马。实际上能到目的地的路径肯定有很多。
如果路由表里有很多项都被匹配上了,会怎么选?
如果多个路由项都能到目的地,那就优先选匹配长度更长的那个。比如,还是目的地192.168.1.11
,发现路由表里的192.168.1.0/24 和 192.168.0.0/16都能匹配上,但明显前者匹配长度更长,所以最后会走 192.168.1.0/24对应的转发端口。
但如果两个表项的匹配长度都一样呢?
那就会看生成这个路由表项的协议是啥,选优先级高的,优先级越高也就是所谓的管理距离(AD,AdministrativeDistance)越小。比如说优先选手动配的静态(static)路由,次优选OSPF动态学习过来的表项。
如果还是相同,就看度量值 metrics,其实也就是路径成本 cost,成本越小,越容易被选中。
路由器能选的路线有很多,但按道理,最优的只有"一条",所以到这里为止,我们都可以认为,对于同一个目的地,ping 和 TCP 走的路径是相同的。
但是。
**如果连路径成本都一样呢?**也就是说有多条最优路径呢。
那就都用。
这也就是所谓的等价多路径,ECMP(Equal Cost MultiPath)。
我们可以通过traceroute
看下链路是否存在等价多路径的情况。

可以看到,中间某几行,有好几个 IP,也就是说这一跳里同时可以选好几个目的机器,说明这段路径支持 ECMP。
ECMP 有什么用
利用等价多路径,我们可以增加链路带宽。
举个例子。

从 A 点到 B 点,如果这两条路径成本不同,带宽都是1千兆
。那数据包肯定就选成本低的那条路了,如果这条路出故障了,就走下面那条路。但不管怎么样,同一时间,只用到了一条路径。另外一条闲置就有些浪费了,有没有办法可以利用起来呢?
有,将它们两条路径的成本设置成一样,那它们就成了等价路由,然后中间的路由器开启ECMP特性,就可以同时利用这两条链路了。带宽就从原来的1千兆
变成了2千兆
。数据就可以在两条路径中随意选择了。

但这也带来了另外一个问题。加剧了数据包乱序。
原来我只使用一条网络路径,数据依次发出,如无意外,也是依次到达。
现在两个数据包走两条路径,先发的数据包可能后到。这就乱序了。
那么问题又又来了。
乱序会有什么问题?
对于我们最最最常使用的 TCP 协议来说,它是个可靠性网络的协议,这里提到的可靠,不仅是保证数据要能送到目的地,还要保证数据顺序要跟原来发送端的一样。
实现也很简单,TCP 为每个数据包(segment)做上编号。数据到了接收端后,根据数据包编号发现是乱序数据包,就会扔到乱序队列中对数据包进行排序。如果前面的数据包还没到,哪怕后面的数据包先到了,也得在乱序队列中一直等,到齐后才能被上层拿到。
举个例子,发送端发出三个数据包,编号1,2,3
,假设在传输层2和3
先到了,1
还没到。那此时应用层是没办法拿到2和3
的数据包的,必须得等1
来了之后,应用层才能一次性拿到这三个包。因为这三个包原来可能表示的是一个完整的消息,少了 1, 那么消息就不完整,应用层拿到了也毫无意义。
像这种,由于前面的数据丢失导致后面的数据没办法及时给到应用层的现象,就是我们常说的TCP 队头阻塞。

乱序发生时2和3
需要待在乱序队列中,而乱序队列其实用的也是接收缓冲区的内存,而接收缓冲区是有大小限制的。通过下面的命令可以看到接收缓冲区的大小。
# 查看接收缓冲区
$ sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096(min) 87380(default) 6291456(max)
# 缓冲区会在min和max之间动态调整
乱序的情况越多,接收缓冲区的内存就被占用的越多,对应的接收窗口就会变小,那正常能收的数据就变少了,网络吞吐就变差了,也就是性能变差了。
因此,我们需要尽量保证所有同一个 TCP 连接下的所有 TCP 包都走相同路径,这样才能最大程度避免丢包。
ECMP 的路径选择策略
当初开启 ECMP 就是为了提升性能,现在反而加重了乱序,降低了 TCP 传输性能。
这怎么能忍。
为了解决这个问题,我们需要有一个合理的路径选择策略。为了避免同一个连接里的数据包乱序,我们需要保证同一个连接里的数据包,都走同样的路径。
这好办。我们可以通过连接的五元组(发送方的IP和端口,接收方的IP和端口,以及通信协议)信息定位到唯一一条连接。

然后对五元组信息生成哈希键,让同一个哈希键的数据走同一条路径,问题就完美解决了。


TCP 和 Ping 走的网络路径一样吗
现在我们回到文章开头的问题。
对于同样的发送端和接收端,TCP 和 Ping 走的网络路径一样吗?
不一定一样,因为五元组里的信息里有一项是通信协议。ping 用的是ICMP 协议,跟TCP 协议不同,并且 ping 不需要用到端口,所以五元组不同,生成的哈希键不同,通过 ECMP 选择到的路径也可能不同。

同样都用 TCP 协议,数据包走的网络路径一样吗
还是同样的发送端和接收端,同样是 TCP 协议,不同 TCP 连接走的网络路径是一样的吗?
跟上面的问题一样,其实还是五元组的问题,同样都是 TCP 协议,对于同样的发送端和接收端,他们的 IP 和接收端的端口肯定是一样的,但发送方的端口是可以随时变化的,因此通过 ECMP 走的路径也可能不同。

但问题又来了。
我知道这个有什么用呢?我做业务开发,又没有设置网络路由的权限。
利用这个知识点排查问题
对于业务开发,这绝对不是个没用的知识点。
如果某天,你发现,你能 ping 通目的机器,但用 TCP 去连,却偶尔连不上目的机器。而且两端机器都挺空闲,没什么性能上的瓶颈。实在走投无路了。
你就可以想想,会不会是网络中用到了ECMP
,其中一条链路有问题导致的。

排查方法也很简单。
你是知道本机的 IP 以及目的机器的 IP 和端口号的,也知道自己用的是 TCP 连接。
只要你在报错的时候打印下错误信息,你就知道了发送端的端口号了。
这样五元组是啥你就知道了。
下一步就是指定发送端的端口号重新发起 TCP 请求,同样的五元组,走同样的路径,按理说如果链路有问题,就肯定会复现。
如果不想改自己的代码,你可以用nc 命令指定客户端端口看下能不能正常建立 TCP 连接。
nc -p 6666 baidu.com 80
-p 6666
是指定发出请求的客户端端口是6666
,后面跟着的是连接的域名和80 端口。

假设用了6666端口
的五元组去连接总是失败,改用6667或其他端口
却能成功,你可以带着这个信息去找找负责网络的同事。
总结
- 路由器可以通过 OSPF 协议生成路由表,利用数据包里的 IP 地址去跟路由表做匹配,选择最优路径后进行转发。
- 当路由表一个都匹配不上时会走默认网关。当匹配上多个的时候,会先看匹配长度,如果一样就看管理距离,还一样就看路径成本。如果连路径成本都一样,那等价路径。如果路由开启了 ECMP,那就可以同时利用这几条路径做传输。
- ECMP 可以提高链路带宽,同时利用五元组做哈希键进行路径选择,保证了同一条连接的数据包走同一条路径,减少了乱序的情况。
- 可以通过 traceroute 命令查看到链路上是否有用到 ECMP 的情况。
- 开启了 ECMP 的网络链路中,TCP 和 ping 命令可能走的路径不同,甚至同样是 TCP,不同连接之间,走的路径也不同,因此出现了连接时好时坏的问题,实在是走投无路了,可以考虑下是不是跟 ECMP 有关。
- 当然,遇到问题多怀疑自己,要相信绝大部分时候真的跟 ECMP 无关。
参考资料
《网络排查案例课》 ——极客时间
最后
兄弟们。
按照惯例,我应该在这里唯唯诺诺的求大家叫我两声靓仔的。
但我今天不想。
因为越是这样,评论区里叫我 diao 毛的兄弟就越多。
上海快 40° 的天气,你们竟然能说出如此冰冷的话。
但是。
只要你们还能给我文章右下角来个点赞和在看的话。
这口气,我还能忍。
别说了,一起在知识的海洋里呛水吧
关注公众号:【小白 debug】
不满足于在留言区说骚话?
加我,我们建了个划水吹牛皮群,在群里,你可以跟你下次跳槽可能遇到的同事或面试官聊点有意思的话题。就超!开!心!

