ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

网络层可达性:TCP可靠传输的真正基石与排查指南

网络层可达性:TCP可靠传输的真正基石与排查指南

我经常在排查网络问题时跟同事说:先别急着抓TCP的包,先回答一个问题——这个包到底能不能到达对端。很多看起来像传输层故障的症状,比如连接迟迟建不起来、数据传着传着就卡死、重传率居高不下,根子其实在网络层。TCP是教科书里最讲究“可靠”的协议,有重传、有序列号、有流量控制,但这一切都站在一个前提之上:IP层必须把数据包送到对面。网络层“可达”,TCP的“可靠”才有了用武之地;一旦路由不通、下一跳丢失、MTU协商出错,TCP再精密的机制也只能在黑暗中反复挣扎。这篇文章想和你聊聊网络层在TCP通信里的基石作用,以及当“可达性”出问题时,我们怎么快速定位。

1. 一场TCP连接背后,网络层到底在忙什么

1.1 从一次SYN的旅行说起

当你执行一次connect()向某台服务器发起TCP连接时,内核并不只是构造一个TCP报文这么简单。这个SYN段会被交给IP层,封装成一个IP数据包,然后根据目标地址查路由表,确定应该从哪个网卡出去、下一跳是谁。接下来还要经过ARP解析,把下一跳的IP变成MAC地址,封装成二层帧,从物理网线上发出去。数据包到了路由器,路由器同样要做三件事:查路由表、换下一跳、重新封装,一步一步把包转发到目的地。

要理解这件事,最直观的类比是寄信。TCP协议是信件内容,IP协议是信封上的收件人地址,路由器是各个城市的信件分拣中心。每一站都只根据信封上的地址决定下一站往哪里送,至于信里的内容写了什么、有没有写错,分拣中心完全不关心。信能不能到,取决于两件事:第一,信封上的地址是否清晰可辨;第二,途中的分拣中心是否知道怎么往那个方向送。放到网络里,就是IP地址是否配置正确,以及沿途所有路由器是否都有通往目标的路由表项。

1.2 “尽力而为”的网络层与“无中生有”的可靠传输

网络层协议的设计哲学是“尽力而为”。它会把数据包往目标方向送,但不承诺一定送到,也不承诺不丢包、不乱序、不重复。听起来像是偷懒,但恰恰是这种去状态化的设计,才让几十亿设备之间的互联成为可能——网络层不需要为每一条连接维护状态,路由器不需要记住“张三正在和李四通信”,只需要在每一个数据包到达时,机械地查表、转发、走人。这让转发的规模可以做到极大,也让网络的中间节点变得异乎寻常地简单。

TCP则完全不同,它寄居在传输层,是一条端到端连接的维护者。它通过序列号解决乱序,通过ACK确认解决丢包,通过超时重传决定什么时候重发,通过流量控制避免压垮对端,通过拥塞控制适应网络状态。这个机制精密而优雅,但它有一个隐藏假设:数据包总有机会到达对端。如果IP层根本没有一条可达的路径,TCP的所有“可靠”动作都会失去操作对象——你重传一个永远送不到的报文,重传一万次也没有意义。

注意:这里有一个常见的认知误区,初学者容易把TCP的可靠性与网络层的职责混在一起,以为“因为TCP是可靠的,所以网络层不重要”。恰恰相反,网络层的可达性是TCP可靠性的地基。地基塌了,房子再漂亮也立不住。这也是标题想表达的核心意思:“可达”方能“可靠”。

2. 路由与转发:网络层如何决定“可达”

2.1 路由表:可达性的决策源头

每个网络节点——无论是主机还是路由器——都维护着一张路由表。它本质上是一张“目标到哪里,走哪个口,交给谁”的决策表。查表的时候遵循一条关键规则:最长前缀匹配。

举个例子,假设路由表长这样:

root@host:~# ip route show default via 192.168.1.1 dev eth0 10.8.0.0/16 via 10.0.0.254 dev eth1 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100

这三条路由覆盖了三种典型场景。第一条是默认路由,意思是除了明确知道怎么走的目标,其他所有流量都交给192.168.1.1这位“总代理”。第二条是特定网段路由,10.8.0.0/16这个网段的流量要走内部路由器10.0.0.254。第三条是直连路由,192.168.1.0/24就在本机所在的局域网里,直接通过eth0网卡发送,不需要拐弯。

当系统要访问一个地址,比如10.8.3.4,它会从左往右比对路由表:默认路由匹配所有地址,10.8.0.0/16也匹配这个目标。这时候最长前缀匹配就起作用了——10.8.0.0/16的前缀长度是16位,比默认路由的0位更长,所以最终选择走10.0.0.254。这套规则的直觉也很好理解:路由越具体,就越应该优先被采纳,就像你手里既有一张全国地图,又有一张街区地图,找某个小区时显然应该看街区地图。

如果路由表里既没有匹配的条目,又没有默认路由,内核会直接返回ENETUNREACH,也就是“Network is unreachable”。此时connect()会立刻失败,连一次SYN都发不出去,因为IP层根本没有可用的路径。这是最直接、最彻底的一种“不可达”。

2.2 动态路由:让网络自己知道“怎么走才可达”

在单台主机上,路由表可能很简单,但在大型网络里,路由表往往是“动态学习”来的。OSPF、BGP这类路由协议让路由器之间互相通告自己已知的网络,并根据链路状态或路径属性计算出到达每个目标的最优路径。这样即便某条物理链路中断,路由协议也能通过收敛,让数据包绕道新的路径继续前进。

这里有个值得琢磨的点:TCP只管两个端点之间的事,它不知道也不会关心路径上的路由器发生了什么。如果网络层能在毫秒级收敛到备用路径上,TCP几乎无感;但如果收敛太慢,或者收敛过程中产生了环路,TCP就会表现为丢包、超时、连接中断。所以很多看似是TCP性能的问题,本质是路由收敛问题。你优化一万遍拥塞控制算法,也抵不上让路由协议收敛快一秒钟。

这也说明一个道理:网络层的可靠性,和TCP的可靠性,是两个不同层次的可靠性。TCP负责把一条可能时断时续的链路“打扮”成可靠的字节流;而网络层负责让这个链路本身在绝大多数时间尽量可用。两者是协作关系,不是替代关系。

2.3 从路由到实际发送:ARP与最后一跳

三层路由决定了“发给谁”,但真正的物理发送还需要知道“对方的MAC地址是什么”,这个过程由ARP完成。比如一台主机发现目标IP就在同一网段时,它会广播一个ARP请求:“谁是192.168.1.100?请把你的MAC地址告诉我。”目标回应之后,数据包才能封装成二层帧发出去。如果ARP解析失败,无论TCP层怎么重传,包始终卡在网卡门口,表现就是connect超时,或者局域网上反复广播ARP请求。

我处理过好几个“TCP连接超时”的case,最后查出来都是网关设备ARP表项异常。比如网关换了新硬件,但旧表项还在,或者虚拟机迁移后MAC地址变了,别的主机还在用旧的ARP缓存。这时候抓TCP包看,表现是SYN一直在发,但二层根本送不出去,局域网内的ARP请求满天飞。生活里可以类比成:门牌号(IP地址)是给分拣中心看的,但具体送达还需要确认这栋楼里住的是谁(MAC地址)。地址查到了却找不到人,信永远送不到。

实操心得:遇到connect超时,我习惯同时跑两条命令,一条看路由ip route get <目标IP>,一条看ARP缓存ip neigh show。很多时候问题根本不在这两层,而是更下面的二层链路出了问题。

3. 网络层宣告失败时,TCP为何苦等不放弃

3.1 ICMP:网络层的“退信通知”

当数据包在网络层真的无法送达时,路由器或目标主机通常会产生一个ICMP错误消息回传给源端。这就像邮政系统在信无法投递时,给你退回来一封写着“查无此人”或“地址错误”的退信。

常见的ICMP错误有两类。第一类是目标不可达(Type 3),其中Code 0表示网络不可达,Code 1表示主机不可达,Code 4表示“需要分片但DF标志置位”——这是MTU黑洞问题的元凶,后面单独讲。第二类是超时(Type 11),通常是IP报文头里的TTL字段递减到0,被某台路由器丢弃后产生的,往往意味着路由环路或者traceroute探测超时。

当TCP栈收到这些ICMP消息时,行为会有一些微妙差异。有些错误会被转化为连接的系统错误码,比如connect()可能返回EHOSTUNREACH或者ENETUNREACH。而有些错误,比如MTU相关的ICMP通知,只用于让TCP调整数据包大小,并不会断开连接。也就是说,TCP某种程度上是“看得见”网络层反馈的——前提是,这个反馈能顺利传回来。

3.2 没有退信的世界:被黑洞吞噬的SYN

更麻烦的情况是,中间设备的防火墙策略直接把报文丢弃,并且把ICMP错误消息也一并过滤掉。此时发送端对“不可达”毫不知情,只能靠TCP自身的超时重传机制来兜底。

TCP在首次发送SYN后会启动重传定时器。初始的超时值通常是1秒,后续按指数退避:1秒、2秒、4秒、8秒……同时按系统配置重发若干次SYN。Linux默认的tcp_syn_retries是6,意味着在放弃之前会重传6次。把这几次的时间加起来,整个过程可能超过两分钟。在用户侧看到的现象,往往就是“界面卡住,好一会儿才报连接超时”。

我印象里有一个很典型的case:某个新上线的服务,客户端永远连不上,日志里全是连接超时。抓包一看,客户端一直在发SYN,服务端却毫无响应——既没有回包,也没有ICMP退信。排查到最后发现是安全组的入方向规则忘了放行该端口。TCP在这种场景下就像一个不知道答案又坚持猜谜的孩子:它不知道网络层发生了什么,只知道没收到回应,那就再发一次。这种“盲目坚持”在链路抖动时是美德,在真正的黑洞面前却是灾难。

3.3 长连接中段的无声断裂

还有一类场景更隐蔽:TCP连接已经建好,数据正在传输,此时中间的链路突然断开,但两端的网卡和IP地址都完全正常。这时候TCP一方会持续发送数据并等待ACK,收不到ACK就开始重传,直到超过tcp_retries2阈值才宣告连接失败。整个过程可能持续十几分钟,期间用户感受到的就是“应用卡死,毫无响应”。

这个案例在分布式系统设计里尤其值得警惕。如果业务非常依赖TCP长连接,就必须额外引入应用层心跳机制。TCP自己不会在第一条重传失败时就承认“可能网络不可达了”,它倾向于往好的方向想,习惯再坚持一下。而应用层如果也像TCP一样“乐观”,就会跟着卡死。设计上的应对方法是:应用层定时发送自己的心跳包,连续N次没收到回应就主动重建连接。

4. 网络层暗坑撕裂TCP可靠性的四大场景

4.1 MTU黑洞:能通,但非常慢

MTU(最大传输单元)决定了一个网络接口单次能承载的IP数据包大小。不同链路、不同封装技术的MTU可能完全不一样,常见的是1500字节,但隧道、PPPoE、VXLAN等场景下会更小。问题在于,TCP建立连接时协商的MSS值,通常只参考了本机接口的MTU,却不知道整条路径上每一跳的最小MTU是多少。于是就有了路径MTU发现机制:发送端发送带DF标志、大小递增的IP数据包,如果路径上某个设备无法承载这个大小,就会回一个ICMP“需要分片”的错误,发送端再根据这个错误调小包长。

问题是,很多中间防火墙为了图省事,把所有ICMP都丢了。这等于把退信通知扔进了垃圾桶。发送端发现大包丢了,只能等TCP超时重传,而重传的包依然那么大,继续被丢弃。整条连接的表现就是:TCP吞吐量忽高忽低、单次传输卡顿、想发送大文件时频繁超时。但你ping小包全通,所以很少有人第一时间想到MTU。

我处理过一次“诡异慢连接”就是这种:主备机房间有一段隧道封装,隧道额外占了近100字节开销,导致端到端MTU从1500掉到1400左右。业务方一上传大包就掉速,小包却一切正常。定位的步骤很简单,用带DF标志的ping逐级递减负载:

# 尝试1472字节负载,对应MTU 1500 ping -M do -s 1472 目标IP # 如果上面不通,依次降到1400、1372、1300... ping -M do -s 1372 目标IP

当-s 1372能通而-s 1373不通时,说明端到端有效MTU就是1400(1372 + 20字节IP头 + 8字节ICMP头)。找到根因后,最稳妥的解法是在隧道设备的入接口启用TCP MSS clamping,把TCP SYN报文里的MSS值强制设为路径MTU减去40字节,也就是1360,让TCP从一开始就不用超大包。

4.2 路由环路与TTL耗尽

路由环路是动态路由收敛期间或者静态路由配置错误时容易出现的问题。比如路由器A认为去某个目标应该交给B,B又认为应该交给A,于是数据包就在两台路由器之间来回转圈,直到IP头里的TTL递减到0,才被某台路由器丢弃,并产生一个ICMP超时消息。

TCP在这个场景里的表现依然是重传、反复超时。排查线索主要来自traceroute:你会发现探测包到某个节点后,后面每一跳都超时,或者路径上反复出现同一跳的IP地址。环路问题必须回到路由配置层面去解决,比如修正静态路由、检查OSPF区域划分、确认BGP路由过滤策略。单纯盯着TCP的传输行为,永远找不出答案。

4.3 非对称路由与乱序重传

有些网络环境下,去程和回程走的不是同一条物理路径,这叫非对称路由。在多数场景下TCP可以容忍,因为TCP只关心数据最终能否到达。但非对称路由往往伴随着路径延迟差异,比如去程30毫秒、回程80毫秒,或者去程走A路、回程走B路且两路带宽差异悬殊,这就可能让数据包乱序到达。

乱序是TCP比较尴尬的局面。接收端收到一个序号靠后的包,却还没收到序号靠前的包,会立刻返回重复ACK;发送端看到重复ACK会触发快速重传。如果乱序频繁,重传率就会虚高,吞吐量上不去。抓包时能看到明显的特征:正常的数据流序号按顺序递增,乱序场景里则会出现大量跳号包和重复ACK。应对思路通常是优化路由策略,让来回路径尽量一致,或者在应用层允许适度的乱序容忍。

4.4 拥塞丢包:TCP如何“翻译”网络层的信号

TCP的拥塞控制完全依赖网络层的间接信号。当某个路由器或链路的流量逼近容量上限,路由器队列开始溢出,后续到达的数据包就会被直接丢弃。TCP观察到丢包,就推断网络发生了拥塞,然后主动削减发送窗口,避免把网络压垮。

这套机制把网络层的问题(丢包)翻译成传输层的策略(退让),是一种相当优雅的解耦。但这种翻译也有代价:网络层的任何丢包,无论是拥塞、链路误码还是策略丢弃,都会被TCP当成拥塞信号并降低速率。这就解释了为什么某些高误码链路下的TCP性能异常差——它把信号噪声混淆成了拥塞灾难。

5. 从“不可达”到“排障”:一套实用的网络层体检流程

5.1 ping与traceroute的正确打开方式

Ping是可达性体检的第一项。除了看通不通,更要看RTT的均值和抖动。如果RTT抖动很大,说明路径上存在排队或链路不稳定。我习惯用ping带时间戳连续跑一段时间,观察是否出现周期性丢包,这往往对应着网络设备的队列周期性溢出。

traceroute则是第二项关键检查。它利用IP报文头里的TTL字段,从1开始递增发送探测包,让每一跳路由器产生ICMP超时消息,从而还原整条转发路径。通过观察每一跳的延迟,可以大致定位瓶颈在哪一段。如果某跳延迟突然暴涨到几百毫秒,那一段链路多半有问题。

小技巧:Linux默认的traceroute使用UDP探测,如果中间防火墙只放行ICMP,很多结果会被过滤掉。这时候改用traceroute -I发送ICMP探测,或者用traceroute -T -p 443直接发TCP SYN探测,效果往往立竿见影。

5.2 面向连接的排查三板斧

当TCP建联失败或性能异常,我通常按这个顺序查:

  1. ip route get <目标IP>:确认本机路由是否可达。如果返回unreachable,说明系统路由表就没有这条路,TCP根本没机会发包。
  2. ping -M do -s <大小> <目标IP>:带DF标志探测路径MTU,逐步增大payload,找到最大可用值。专门对付MTU黑洞。
  3. tcpdump -i any host <目标IP> and tcp:抓包确认SYN是否有去无回,观察是否存在大量重传和乱序ACK。

如果SYN有去无回,大概率是中间网络策略丢包或者对端防火墙没放行;如果SYN发出后收到了ICMP unreachable,那就是路由或目标主机本身不可达;如果SYN能通但握手后吞吐异常,重点查MTU、链路丢包和拥塞。

还可以用一条命令快速看TCP整体健康状况:

netstat -s | grep -iE 'retrans|out-of-order|reorder|syn'

重传数、乱序数、SYN数的异常增长,会直接告诉你TCP层正在承受什么压力。

5.3 手把手复现一次“网络层不可达”

想真正理解网络层对TCP的影响,可以自己搭一个最小实验。Linux的网络命名空间非常适合干这件事,不需要任何物理设备。首先创建两个隔离的网络空间,用veth虚拟网线连起来:

# 创建两个netns,并用veth连接 ip netns add a ip netns add b ip link add veth-a type veth peer name veth-b ip link set veth-a netns a ip link set veth-b netns b # 配置IP并启用接口 ip netns exec a ip addr add 10.0.0.1/24 dev veth-a ip netns exec b ip addr add 10.0.0.2/24 dev veth-b ip netns exec a ip link set veth-a up ip netns exec b ip link set veth-b up

此时在netns a里执行nc 10.0.0.2 8080,可以看到正常的三次握手完成。关掉veth-b接口:

ip netns exec b ip link set veth-b down

再发起连接,你会发现连接立即卡住,SYN发出去就没有回音,和真实网络中防火墙丢弃SYN的效果完全一致。如果还想看得更细,可以在netns a里同时跑tcpdump,观察SYN重传的指数退避过程。这种实验比背十遍TCP状态转移图都管用——眼见为实。

6. 网络层问题定位速查表

现象可能的网络层根因快速检查手段解决方向
TCP建联长时间超时路由不可达、防火墙丢包、ARP失败ip route get、抓包看SYN修正路由、放行端口、排查ARP表
connect()立即报No route to host路由表缺失、目标网段无路ip route show、ip route get补静态路由或默认网关
数据能通但速度极慢MTU黑洞、路径MTU小于接口MTUping -M do -s逐级探测调低MTU、配置TCP MSS clamping
traceroute到某跳后全部超时防火墙屏蔽探测报文或链路黑洞换-I或-T探测方式结合两端抓包确认丢包位置
重传率高、大量重复ACK链路丢包、非对称路径、拥塞netstat -s、tcpdump统计优化链路、调整路由、更换拥塞控制算法
长连接固定周期内断开中间设备超时回收、链路闪断查看两端日志、抓包确认RST来源应用层心跳、调整保活参数

这张表基于我在线上环境里反复踩过的经验,不一定覆盖所有情况,但能覆盖绝大多数“TCP看起来有问题”的普通场景。每次遇到诡异连接问题,先对着表走一遍,往往能快速找到突破口。

最后再分享一个小体会。有一次我花了一整天排查一个“加密隧道偶发断开”的问题,抓包、调内核参数、换拥塞控制算法,全都没用。最后无意中在隧道两端各做了一次traceroute,发现中间有一段物理链路每天固定时段出现RTT剧烈抖动,原来是链路本身在高峰期拥塞,跟TCP和隧道协议一点关系都没有。那件事之后我养成了一个习惯:凡是处理网络问题,先确认“可达”这个前提——路径通不通、稳不稳、中间有没有坏节点——再谈TCP内部的那些精妙机制。毕竟,只有“可达”,才谈得上“可靠”;不可达的世界里,再优秀的可靠协议也只是一个徒劳的守望者。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进