ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

跨机房访问卡顿排查:IP编址与路由转发原理实战

跨机房访问卡顿排查:IP编址与路由转发原理实战

排查一次跨机房访问卡顿的时候,我先怀疑带宽,又怀疑中间设备限速,折腾半天才发现是两台机器掩码配置不一致:一台是 /24,另一台写成了 /23,结果回程流量绕到了默认网关,硬是多跳了两台路由器。这类问题在真实环境中特别常见,而它的根子就在两个词上——互联网中的转发和编址。网际协议这一层做的事情说穿了不复杂:给每台设备一个能定位的地址,再让每个中间节点知道该把这个包往哪个口送出去。前者是编址,后者是转发。这篇文章我会把这两件事从原理到手算、从通配规则到抓包验证完整拆一遍,中间穿插我自己踩过的坑。适合刚学完网络基础但概念还搅成一团的学生,也适合每天配路由却对“为什么这么配”说不清楚的运维和开发。

1. 先把“转发”和“编址”这两件事分清楚

1.1 从寄快递的类比说起:编址是门牌号,转发是分拣中心

我习惯用寄快递来讲这一层。你要把一件东西送到某个城市某个小区某栋楼某户人家,这个“省市区街道门牌”就是编址要解决的问题;而包裹在路上会经过一级一级的分拣中心,每个分拣中心不看你的全名,只看地址里它能识别的那一段,然后决定往哪条干线甩出去,这就是转发。

关键点在于分拣中心并不认识收件人,它也不关心包裹里装的是什么。它只做一件事:拿地址去对照自己手里的路由表,找出一条最合适的出口。所以转发是一个逐跳、无状态的局部决策过程,每一台路由器只对自己的下一跳负责,不负责端到端。这个特性带来两个直接后果:一是单台设备的表项规模可以控制,不用装下整个互联网的完整拓扑;二是某一跳出错时,端到端排查必须一跳一跳地看,不能指望某个中心告诉你全程情况。

编址则相反,它是全局性的约定。你必须保证同一个网络里没有两台设备用同一个地址,否则后果是随机的、间歇性的、极难复现的——两台机器抢同一个地址时,交换机的 MAC 表会在两个端口之间来回翻动,表现出来就是“一会儿能通一会儿不能通”,这种故障能耗掉你一整天。

1.2 为什么这两件事必须拆开看:控制平面与数据平面

很多人学这一章时会混淆“路由”和“转发”。严格讲,路由(routing)是算出路径的过程,靠的是路由协议、静态配置、策略;转发(forwarding)是拿到一个已经到达的包,查表把它从某个接口送出去。前者慢、有状态、跑在控制层面;后者快、无状态、最好能用硬件做,跑在数据层面。

注意:把这两个词混用,在排障时会导致方向性错误。如果现象是“路径选错了”,你要查的是路由层面(为什么这条路由被选中、优先级对不对);如果现象是“包到了但出不去”,你要查的是转发表和二层解析。

分开看的好处是,转发可以被极度优化。现代路由器的转发引擎会把控制平面算好的路由表编译成一张更适合硬件查找的表(常见叫法是 FIB),用专门的结构做前缀匹配,每秒能处理上亿个包。而控制平面可以在后台慢慢收敛,短暂的不一致是允许的。理解这个分层,你就能明白为什么“路由已经收敛了但流量还没恢复”这种看似矛盾的现象是正常的——控制平面把结果写进转发表需要时间,而且写进去之后还有存量会话的问题。

2. 编址:IP 地址到底是怎么被“切”出来的

2.1 从分类地址到 CIDR:一段被浪费掉的地址史

早期的 IP 地址是分类的:A 类以 0 开头,第一字节就是网络号,能容纳一千六百万台主机;B 类前两字节是网络号,容纳六万五千台;C 类前三字节,容纳二百五十四台。这套规则的问题非常明显:一个单位只需要五百个地址,也只能申请一个 B 类,一下子浪费掉六万多个;而真正需要大地址块的组织又只能拿 A 类,A 类总共只有 126 个。

于是有了 CIDR,也就是无类别域间路由。它的核心思想只有一句话:不再用固定的字节边界划分网络,而是用一个斜杠后面的数字,任意指定前多少位是网络前缀。这么一改,地址分配的粒度从“整块”变成了“任意大小”,同时也让路由聚合成为可能——一个运营商手里有一堆连续的 /24,可以对外只通告一条 /16,全球路由表才不会爆炸。

这个演进过程值得多想一层:CIDR 不只是省地址,它同时也是转发能够规模化的前提。如果没有前缀聚合,骨干路由器的表项会是今天的好几倍。所以编址方案的设计,其实一直在为转发服务。这两件事在设计上是互相牵制的。

2.2 子网掩码与网络前缀的手算方法

斜杠后面的数字还有另一种写法,就是子网掩码。两者是一回事,/24 对应 255.255.255.0,/26 对应 255.255.255.192。换算方法记住一点就行:掩码是从左往右连续的 1,/n 表示前 n 位是 1。

手算子网的时候,我常用的是“块大小”法,比背公式快。以第三字节为例,/26 意味着第三字节前两位是 1,剩下的六位是主机位,块大小就是 2 的 6 次方等于 64。也就是说地址会在第三字节按 0、64、128、192 这样的间隔分段。给你一个具体例子:

要把192.168.10.0/24切成四个等长子网,需要多借两位,前缀变成 /26。每个子网大小 64,可用主机 62 台(去掉网络地址和广播地址)。四个子网分别是:

子网网络地址可用范围广播地址
1192.168.10.0/26192.168.10.1 - 192.168.10.62192.168.10.63
2192.168.10.64/26192.168.10.65 - 192.168.10.126192.168.10.127
3192.168.10.128/26192.168.10.129 - 192.168.10.190192.168.10.191
4192.168.10.192/26192.168.10.193 - 192.168.10.254192.168.10.255

再来一个不那么“整齐”的例子,这是实际工作中更常遇到的情况:判断10.20.30.40/22属于哪个网段。先看 /22 的掩码,前 22 位是 1,也就是 255.255.252.0。第三字节是 252,二进制 11111100,块大小等于 256 减 252 等于 4。所以第三字节按 0、4、8、12……分段,30 落在 28 到 31 之间,网络地址就是10.20.28.0,广播地址是10.20.31.255。这种计算我在判断“两台机器到底算不算同网段”时几乎每次都要做一遍,因为很多连通性问题就是掩码写错了。

实操心得:写完子网规划表之后,一定拿两台“一墙之隔”的机器互相 ping 一遍,再拿一台跨子网的机器 ping 一遍。我见过太多次掩码笔误导致的不对称连通,尤其是非 /24 的子网,一旦配错,去程和回程会走完全不同的路径。

2.3 特殊地址与保留段:那些不能随便分配的坑

编址里有几组地址是有约定含义的,配错了不会立刻报错,但会在某个时刻突然咬你一口。我整理成下面这张表,配置之前扫一眼能省不少事:

地址范围含义常见误用后果
0.0.0.0/8本网络,作为源地址时表示“本机尚未确定地址”误配成主机地址会导致回包无法路由
127.0.0.0/8环回地址抓包时看到它别以为是真流量
169.254.0.0/16链路本地地址,通常是自动获取失败后的兜底看到这个地址基本可以断定获取地址的过程有问题
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16私有地址段,不参与公网路由私网地址用于对外通信时会被丢弃或做转换
192.0.2.0/24、198.51.100.0/24、203.0.113.0/24文档示例专用地址你在文档里抄的地址到了真实环境不能直接照用
224.0.0.0/4组播地址误当单播使用会出现“发得出去收不到”

最后那段文档示例地址,我在本文里所有“看起来像公网”的例子都会用它,比如203.0.113.5。这么做是有意为之:这些段永远不会出现在真实公网上,抄进自己的实验环境不会造成地址冲突。养成这个习惯,比随手编一个公网地址要专业得多。

2.4 动态编址与地址转换:地址不够用之后的现实解法

理论上的编址是一个地址对应一台设备,现实里根本不够分。于是有了两条主要出路:一是动态编址,让设备开机时向网络里的服务请求一个临时地址,用租期管理回收;二是地址转换,让内网一大片私有地址共用少数几个对外的地址。

动态编址的价值不只是省地址,更在于把配置从人工变成自动。一台设备换到另一个网段,不用改配置就能直接上网,这个体验上的差别对小规模部署不明显,对成百上千台终端就是灾难级别的差异。但它也带来一个排障难点:地址是动态的,你在日志里看到的那个地址,过一天可能已经属于另一台设备了。所以排查这类环境的问题时,我会先确认租期有多长,再去核对地址的归属时间点,否则很容易冤枉一台无辜的机器。

地址转换解决的问题是地址数量,代价是端到端可达性被破坏。转换设备维护一张转换表,把内网地址加端口映射成对外的地址加端口,回程靠查这张表还原。这就意味着,外部无法主动发起对内网的连接,除非你显式地配置一条静态映射。很多人抱怨“本地服务起好了但外面访问不到”,八成不是服务的问题,而是这个方向性被忽略了。

注意:转换设备的转换表是有容量限制和老化时间的。并发连接数一多,或者空闲连接被过早老化,都会导致“连接莫名断开”。排查这类问题时,看一眼转换表的使用率和老化配置,比一直盯着应用日志效率高得多。

3. 转发:路由器拿到一个包之后到底做了什么

3.1 转发表、路由表、FIB,三个容易混的名词

这三个词经常被混着用,但含义有细微差别。路由表是控制平面维护的“全部候选路径”,里面可能有同一个目的地的好几条路由,带着不同的优先级和度量值;再经过优选算法,每个目的地只保留最优的一条,生成的结果写进转发表;转发表再被编译成硬件友好的形式,就是 FIB。

对你实际排障来说,最需要分清的是“表里有”和“表里被选中”这两件事。有时候你明明配了一条路由,show出来也在,但流量就是不按它走——那多半是没被优选,或者在写入转发表的时候被别的条目盖住了。所以我会养成一个习惯:不只查路由表,还要查实际生效的转发表,两者不一致就是问题所在。

在常见的操作系统上,这个区别也体现得很清楚。家用或办公主机通常只有一个默认网关,本质上就是一条0.0.0.0/0的路由,所有不认识的都往那儿扔。当你在一台机器上同时接了有线和无线,两条默认路由就会打架,谁被选中取决于度量值的比较。这就是为什么有些人插上网线之后,某些访问突然变慢或者不通了——不是网线的问题,是出口被换了。

3.2 最长前缀匹配:为什么不是“精确匹配优先”

这是整个转发环节里最精妙的一条规则。路由表里可能同时存在多条能匹配同一个目的地址的路由,选择办法是:在所有匹配项里,挑前缀最长的那一条。

假设路由表是这样的:

0.0.0.0/0 -> 默认网关 10.0.0.0/8 -> 出口A 10.20.0.0/16 -> 出口B 10.20.30.0/24 -> 出口C

一个目的地址是10.20.30.7的包进来,四条路由全部匹配,但 /24 最长,走出口 C。如果目的地址换成10.20.9.9,只有前三条匹配,最长的是 /16,走出口 B。

为什么这么设计?因为前缀越长,描述的范围越具体,说明这条路由的“知情程度”越高。默认路由是所有设备都能给的最粗糙的信息,而一条 /24 说明配置者对这个网段有直接了解。用最长前缀匹配,就等于让最了解情况的节点说话。

这条规则还有一个常被低估的作用:它让默认路由可以安全存在。没有这条规则,只要表里有一条 /0,所有精确路由都会失效,整个转发就没法工作了。

3.3 一次完整转发的逐跳推演

我把一次跨网段访问拆开走一遍,这个过程看起来平淡,但每一步都可能出问题。设定:源主机192.168.1.10/24,网关192.168.1.1,目的地203.0.113.5(文档示例地址)。

第一步,源主机判断目的地是否同网段。它用自己的掩码算出203.0.113.5不在192.168.1.0/24里,所以必须交给网关,不能直接发。

第二步,源主机查自己的路由表,找网关地址对应的那条路由的出口接口。

第三步,源主机需要把包封装成二层帧。问题来了:它知道网关的 IP,但不知道网关的 MAC。于是先发一个地址解析请求广播出去,网关回应自己的 MAC,缓存起来备用。

第四步,封装完成,源 IP 和目的 IP 都不变,源 MAC 是主机自己,目的 MAC 是网关。包从接口发出。

第五步,网关收到包,剥掉二层头,看目的 IP,查自己的转发表。查到了下一跳,于是 TTL 减一,重新计算头部校验和,再用下一跳的 MAC 重新封装发出去。注意:IP 地址自始至终没变,变的是二层头和 TTL。

第六步,这个动作逐跳重复,直到最后一台路由器发现目的地址就在自己直连的网段里,直接做地址解析并发给目标主机。目标主机回应时,走的是另一套路由,未必是原路返回。

这里最值得记住的是第五步里“IP 不变、二层变”这件事。很多抓包新手看到不同网段的抓包里 MAC 地址变了,会以为包被改写了。实际上三层地址是端到端保持的,变化只是链路层的重新封装。理解这一点,你就能看懂为什么在两端同时抓包,IP 层字段一致而链路层完全不同。

3.4 下一跳解析:链路层那一脚油门

路由表里写的是“下一跳的 IP”,但实际发送需要的是“下一跳的 MAC”。这中间的转换靠地址解析完成,本质是一个广播询问加单播应答的过程。解析结果会被缓存,缓存有过期时间,过期后重新询问。

这里有个细节很多人不知道:路由器在转发时,查的是下一跳的地址,不是最终目的地的地址。这意味着在一条长路径上,每一跳都在解析“邻居”的地址,而不是在做端到端的地址解析。这个设计让解析范围被限制在本地链路内,不会造成全局广播风暴。

缓存失效的瞬间会带来短暂的排队,流量大时表现为微小的抖动。还有一种情况是地址发生了迁移——某台设备换了网卡或者被替换,MAC 变了但 IP 没变,这时候缓存里还是旧 MAC,包就会发到一个不存在的目的地。表现是“IP 能 ping 通但服务连不上”或者“间歇性丢包几十秒后自己好了”。遇到这种,清一下解析缓存往往立刻见效。

4. 动手验证:把转发和编址过程“看”出来

4.1 用命令行观察本机编址与路由

概念讲再多不如自己看一眼。我通常按这个顺序走一遍,五分钟就能把本机的编址和转发策略摸清楚。

# 看本机地址、掩码、接口状态 ip addr show # 看路由表,重点看默认路由和直连网段 ip route show # 看内核对某个目的地址实际会选哪条路、从哪个口出去 ip route get 203.0.113.5

第三条命令是我最推荐的,它直接告诉你内核的决策结果,包括源地址选择和出口接口。很多“为什么走这个口”的争议,一条命令就能终结。

在另一个常见系统上,对应的命令是route print和tracert。前者输出格式不太一样,但看的东西是一样的:网络目标、掩码、网关、接口、度量值。度量值这一列要多留意,多网卡环境下它就是决定胜负的关键。

# Windows 环境下查看路由表与路径 route print tracert 203.0.113.5

4.2 抓包看一次真实的转发:TTL 与二层头的变化

想看转发过程,抓包是最直观的。在一台中间设备或者两端同时抓,用带链路层头和序号的方式:

tcpdump -i eth0 -nn -e icmp

-e会把链路层头打出来,你能看到源和目的 MAC。然后在另一端发起 ping,对照两边看:

  • IP 层的源和目的在整个路径上保持不变;
  • TTL 每经过一台路由器减一;
  • 二层 MAC 每跳都变,变成“上一跳出口”和“下一跳入口”;
  • 头部校验和会随着 TTL 变化被重新计算。

我拿这套观察做过一次很典型的实验:在同一条路径上抓包,第一跳的 TTL 是 64,到目标端抓已经是 61,中间正好三台设备。这个数字对不上,就说明路径和你想的不一样,这时候用traceroute逐跳看,能立刻定位到哪一跳开始偏离预期。

顺带说一个观察技巧:如果 TTL 在网络中间突然出现两种不同的初始值,说明存在多条路径或者负载分担。这在排查“为什么有时快有时慢”时非常有用,因为不同路径的拥塞状况可能完全不同。

4.3 分片与 MTU:一次被忽略的转发失败

这个坑我印象很深。现象是:小包能通,大包不通;网页打不开但 ping 得通。第一反应是应用层问题,查了半天,最后是路径上某一跳的 MTU 比两端都小,而那一跳又不愿意回送“需要分片”的提示。

链路层的最大传输单元通常是 1500 字节,减去 IP 头 20 字节和 ICMP 头 8 字节,能承载的数据是 1472 字节。想验证路径 MTU,可以用不允许分片的探测包:

# 发送 1472 字节数据且禁止分片,总长度正好 1500 ping -M do -s 1472 203.0.113.5 # 如果失败,逐步减小,找到能通过的最大值 ping -M do -s 1400 203.0.113.5

-M do的含义是禁止分片,一旦超过路径 MTU 就会直接失败而不是默默切开。这个探测方法的逻辑很朴素:从理论最大值往下试,第一个能通的尺寸加上 28 字节就是路径的实际 MTU。

提示:隧道封装会额外占用字节数,一条 1500 的链路套上封装之后,可用载荷会明显变小。如果你的环境里有隧道,把 MTU 调小一点通常能一次性解决很多“大包不通”的疑难问题。

5. 常见问题与排查技巧实录

5.1 问题速查表

下面这张表是我这些年攒下来的,基本都是“现象对得上、原因就那几种”的高频组合:

现象优先怀疑快速验证方法
能 ping 通网关,ping 不通外部本机路由表缺默认路由,或网关未开启转发看路由表里有没有0.0.0.0/0,看网关的转发开关
同网段内互 ping 不通掩码不一致,一方认为同网段一方认为跨网段双方各算一遍网络地址,比对结果
部分地址能通部分不通路由表存在更长前缀的冲突条目逐条比对前缀长度,看目标落在哪个块里
连接建立后很快断开转换表老化时间过短,或表项容量不足看转换表使用率与超时配置
换网卡后间歇性丢包地址解析缓存里还是旧记录清缓存后重新测试
大包不通小包通路径 MTU 不匹配,分片被丢弃用禁止分片的方式逐级试探包长

5.2 几个反直觉的坑

第一个坑:掩码不是越“大”越好。有人觉得配 /16 比 /24 更“宽松”,能通更多机器。实际上掩码决定的是“哪些地址算邻居”,配大了会把本来不该直连的地址当成同网段,于是不做转发直接本地解析,结果自然是失败。这个错误在小规模网络里很少见,一旦上了规模,特别是多个部门共用一个地址段时,特别容易出。

第二个坑:路由“看起来是对的”不等于生效。前面说过路由表和转发表可能不一致,还有一种情况是策略路由的优先级高于普通路由。你以为按目的地址查表,实际上规则先按源地址或者端口匹配了一遍。这种环境里,改路由怎么都不生效,得先看策略规则的顺序。

第三个坑:地址转换之后,日志里的地址不是真实的。应用日志记录的是看到的源地址,而在转换之后这个地址是被改写过的。两端日志一对,发现源地址对不上,就会误判成攻击或者串包。跨转换设备排查时,一定要先确认“我在哪一侧看到的地址”。

第四个坑:同一个地址出现在两个地方不一定马上冲突。如果两个位置之间没有二层连通,地址重复可以相安无事很久,直到某天网络被拉通,冲突才突然爆发,而且表现极其诡异。所以地址规划这件事,宁可一开始就做一张全局表,也别指望事后补救。

5.3 排障的固定动作

我自己的排障顺序基本固定,写下来供参考:

  1. 确认本机编址:地址、掩码、网关三件套,缺一个都不行。掩码算一遍网络地址,确认和网关同网段。
  2. 看路由决策:用系统提供的查询命令,看内核实际会选哪条路、从哪个口出。这一步比看静态配置表可靠。
  3. 逐跳缩小范围:从近到远,先 ping 网关,再 ping 最近的一跳,一层层往外。只要能通到某一跳,问题就在那一跳之后。
  4. 抓包确认方向:请求发出去了吗?回应回来了吗?是单方向不通还是双向不通?这一条能砍掉一半的猜测。
  5. 查中间设备的特殊配置:转换、策略、限速、隧道,这些都会改变“理论路径”和“实际路径”的对应关系。

有一个经验值得单独说:排障时先做能一次性排除大范围的动作。比如先确认是不是全网都这样,还是一台机器这样;先确认是不是只有大包不行,还是所有包都不行。这类信息一秒钟就能拿到,但能把排查空间砍掉一大半。反过来,一上来就钻进某个细节,很容易在错误的方向上耗掉半天。

实操心得:把每次排查的结论记下来,尤其是“现象 → 原因”的对应关系。网络问题的模式化程度很高,攒够二三十条之后,你会发现新问题里有相当一部分是旧问题的变体。

另外补充一点关于 IPv6 的观察:编址从 32 位扩到 128 位之后,地址不够用的问题基本解决了,但转发侧的思路并没有变,仍然是最长前缀匹配、逐跳决策。变化主要在两件事上:一是地址配置更依赖自动发现,手工配的场景少了很多;二是很多环境是双栈并存,于是“为什么这台机器走了 IPv6 而我以为它走 IPv4”成了新的常见问题。排查双栈环境时,先确认实际用的是哪个协议族,比直接看路由表更有效——我遇到过一次,应用日志里的目标地址是 IPv4 写法,实际连接却走了 IPv6,两个协议族的路由策略完全不同,白查了半天。

最后分享一个小习惯:在自己的实验环境里,我总会留一个网段专门用来做破坏性实验,掩码、网关、路由想怎么改就怎么改,改坏了直接重置。真实环境里不敢试的东西,先在实验网段里跑一遍,心里才有底。编址和转发这两件事,看十遍文档不如亲手配错一次——配错之后那种“原来是这样挂的”的体感,是任何教程都给不了的。

RELATED READING

延伸阅读

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