从一张拓扑图变成一棵最短路径树,再到路由表的实际生成,IS-IS 的 SPF 计算过程就是一套非常经典的 Dijkstra 算法。很多朋友学路由协议时都卡在这里:LSP 洪泛看懂了、LSDB 也同步了,一到 SPF 计算阶段就被一堆术语绕晕。这篇文章我直接把整个计算过程掰开揉碎,从数据结构到一次完整迭代,再到常见的排障思路,一次性讲清楚。
1. 从一个“选路问题”说起:IS-IS 为什么要用 SPF
1.1 链路状态协议和距离矢量协议的差别
先想一个生活场景:你要开车从北京到上海,导航软件不是靠“听邻居说哪条路近”来给你指路的,而是掌握了一张全国高速地图,然后基于这张地图算出最短路线。IS-IS 就是路由协议里的“导航软件”,它不像 RIP 那样只把路由表转发给邻居(这种就是距离矢量,“我听说去某地经过你那儿比较近”),而是让区域内所有路由器都拿到一张完整的网络拓扑图(LSDB,链路状态数据库),再基于这张图各自独立计算最短路径。
SPF 算法(Shortest Path First,最短路径优先)就是这张拓扑图的“路径规划引擎”。它解决的核心问题是:给定一台路由器知道的全网节点和链路,如何快速、无环地算出自己到达每个目的地的最优路径。这也是链路状态协议和距离矢量协议最本质的分水岭,一个靠全局视野,一个靠邻接互传,二者在计算模型、收敛速度、扩展性上都有巨大差异。
1.2 IS-IS 眼中的“图”:LSP、LSDB 和全网拓扑
IS-IS 路由器之间通过交换 LSP(Link State PDU,链路状态数据单元)来互相通告自己的接口状态、邻居关系和链路开销。每台路由器把这些收到的 LSP 存进自己的 LSDB,这才有了所谓“全网同步的拓扑图”。需要注意,IS-IS 和 OSPF 虽然都是链路状态协议,但它俩的报文封装方式不同,LSP 的组织形式也不同,IS-IS 直接封装在二层,不适合在思科、华为等厂商中简单一概而论。但从 SPF 计算的角度,你只需要知道:LSDB 里有一堆描述链路和节点的记录,SPF 算法就是从这个数据库里把图结构提取出来,再做路径计算。
顺着这个思路继续走,为什么 IS-IS 能保证“所有路由器看到同一张图”?这就得益于 LSP 的泛洪机制和每条 LSP 上都带有的序号、老化时间、校验和。路由器只有在 LSDB 同步完成后才开始 SPF 计算。如果某个 LSP 还没泛洪完成,数据是残缺不全的,算出来的最短路径树也必然是残缺甚至错误的。实际排障中,“LSDB 不同步导致路由黑洞”是非常常见的现场问题,后面我在第 5 章展开聊。
1.3 SPF 算法要解决的三个问题
SPF 算法看似只是“求最短路径”,但落到 IS-IS 这个具体场景,它实际上要回答三个递进的问题:
第一,从当前路由器出发,到每个网段/节点的最短路径是哪一条?这是最基本的目标,Dijkstra 算法通过逐步松弛来做到。第二,如果有多条路径开销完全相同怎么办?这就引出了 ECMP(等价多路径),IS-IS 在生成最短路径树时允许保留多个父节点,然后把这多条下一跳同时放进路由表。第三,拓扑发生变化时如何快速收敛?只重算受影响的部分,而不是动不动全量跑一遍 SPF,于是才有了 I-SPF(增量 SPF)和 PRC(部分路由计算)这些优化手段。
把这三个问题理清楚了,你对 IS-IS SPF 的理解就不会飘在算法表面,而是能落到真实网络的工程需求上。
2. SPF 算法核心拆解:Dijkstra 在 IS-IS 中的落地
2.1 三个关键数据结构:Path、Tent、LSDB
SPF 计算不是空谈理论,它在代码层面的核心是维护两个集合、依赖一张数据库:
- Path 集合:已经计算出最短路径的节点集合,算法开始时只有本路由器自己(根节点),随着迭代不断扩充,直到覆盖全网可达节点。
- Tent 集合:候选节点集合,那些已经通过某个 Path 集合中的节点发现、但尚未确认“这就最短”的节点,先放进 Tent 里候着。
- LSDB:提供边信息。LSP 里写明的邻居关系和开销值,就是 SPF 需要处理的边。
我常用一句话帮助朋友理解:“Path 是定居名单,Tent 是候补名单”。每次迭代从候补名单里挑一个最近的迁入定居名单,然后检查新居民的邻居,看有没有能更新入候选人名单的。整个过程很像一场“最邻近扩展”的选举,谁离根节点近谁先上岸。
这里要特别注意:LSDB 里存储的拓扑信息不一定是完全连接的。非广播多路访问网络(NBMA)下可能出现部分连通,广播网里又引入了伪节点。SPF 算法在迭代前通常会先把图结构建好,凡是邻居状态不是 Up 的、接口被动关闭的、或者开销值为无穷大的,都会在建图时直接过滤掉,不参与计算。
2.2 计算步骤与松弛操作
把 SPF 的迭代过程写成伪代码,大概是这样的:
初始化: Path = { self, cost=0 } Tent = 所有可达邻居 { neighbor, cost=接口开销, parent=self } 循环: if Tent 为空: 结束计算 从 Tent 中选出 cost 最小的节点 X,移入 Path 遍历 X 的所有邻居 Y: 新开销 = X.cost + cost(X,Y) if Y 不在 Path 且 新开销 < Y 在 Tent 中的记录: 更新 Y 的记录(parent = X, cost = 新开销) else if 新开销 == Y 在 Tent 中的记录: 为 Y 增加一个父节点 X(支持 ECMP 时的处理)这个“新开销小于原记录就把父指针更新掉”的操作,在算法上叫“松弛”。松弛操作保证每个节点在进入 Path 集合的那一刻,其记录的 cost 就是从根节点到它的全局最小值。这里蕴含了一个数学前提:边开销非负。IS-IS 的默认开销是正整数,所以 Dijkstra 算法成立。如果谁配了负开销,SPF 可能算出“越走越近”的诡异环路,所以在实际网络中负开销是严格禁止的。
另外提醒一个容易被忽视的小细节:从 Tent 中选“最小 cost 节点”时,如果有多个节点 cost 相同,算法选中哪一个都是正确的,因为最后生成的最短路径树在这几个节点上的开销相同。但对 ECMP 的处理,IS-IS 的做法是记录多个父节点,而不是只选其中一个,否则等价路径就被丢弃了。这个细节我在第 3.4 节再细说。
2.3 度量值为什么这么重要
SPF 计算中“cost”的值直接决定了生成树长什么样,所以度量值设计是 IS-IS 部署里的关键决策点。老式 IS-IS 默认接口开销是 10(所有接口不分带宽一律 10),这在早期链路带宽差距不大的年代够用;但后来千兆、万兆接口出现,全默认 10 的话,SPF 算出来的路径很可能不是真正的高带宽路径。
现在主流做法是启用 wide metric(宽度量),支持 24 位开销值,可以做到接口级精细化调优。你可以在接口下手动设置开销,也可以用自动带宽参考值。比如参考值设为 1000Mbps,那么千兆接口开销是 1,百兆接口开销是 10,SPF 匹配出的最优路径就自然选择了高带宽链路。这就和 Dijkstra 算法里的“权重”概念完全对应了,权重定得合理,算法才算出合理结果。
还需注意 LSP 中通告的开销是单向的:A 到 B 的开销可以不同于 B 到 A 的开销。如果配置时不注意链路两端度量值对称,SPF 就会算出不对称的往返路径,这在某些回程路径敏感的组网里会引发问题。常规做法是保持全网络开销策略一致,自动模式选定后不要局部乱改。
2.4 伪节点是什么:广播网络里的特殊节点
在广播多路访问网络(比如以太网)里,所有路由器两两之间都能互通,如果每台路由器都在 LSP 里描述“我和其余每一台相邻”,那 LSDB 里边的数量就是 N*(N-1)/2,N 大了以后数据量和 SPF 计算量都会爆炸。IS-IS 的解决办法是:在广播网里选举一个 DIS(Designated Intermediate System,指定中间系统),由它生成一个“伪节点”,并通告一条伪节点 LSP,描述这个广播网里连接了哪些路由器。
伪节点在 SPF 里被当作一个普通节点来参与计算,所有真实路由器都只连接到伪节点,开销为 0 或配置的接口开销。这样一来,原本的全网状连接被折叠成了一个星型结构,边的数量从 O(N^2) 降到 O(N)。我打一个比方:这就好比小区里每家每户的快递,不是每户之间都单独互相送,而是统一送到小区快递柜(伪节点),再由柜子分发给各家。这样 SPF 的迭代次数大幅减少,收敛速度自然就快。
这里有一个调试时的关键认知:伪节点 LSP 的生成者是 DIS,而且 DIS 是可以抢占的。一旦 DIS 发生故障或重新选举,伪节点标识就变了,原来基于该伪节点的 SPF 树全部失效,会触发一次全量重算。所以如果广播网内 DIS 频繁震荡,SPF 计算次数会明显上升,全网路由器的 CPU 都会受影响。
3. 一个 SPF 计算实例:从 R1 出发算到全网
3.1 示例拓扑与初始数据
理论讲再多,不如手推一遍。我设计了一个最简单的 4 台路由器拓扑,链路开销标在边上,注意 IS-IS 开销是接口级的,每台路由器对自己接口的开销负责:
| 节点 | 邻居 | 开销 |
|---|---|---|
| R1 | R2 | 10 |
| R1 | R3 | 20 |
| R2 | R3 | 5 |
| R2 | R4 | 10 |
| R3 | R4 | 10 |
假设 LSDB 已经完成同步,每台路由器都清楚全网拓扑。现在以 R1 为根,执行 SPF。
初始阶段,Path 集合只有 {R1(0)},Tent 集合里有 R1 直接相连的两个邻居:R2(10,父 R1) 和 R3(20,父 R1)。变量记录如下:
| 节点 | 当前最优开销 | 父指针 | 所在集合 |
|---|---|---|---|
| R1 | 0 | 无 | Path |
| R2 | 10 | R1 | Tent |
| R3 | 20 | R1 | Tent |
3.2 一步步推演 SPF 迭代过程
第一轮迭代,从 Tent 中选出开销最小的节点。R2 开销是 10,R3 是 20,所以选 R2 进入 Path。
把 R2 移入 Path 后,遍历 R2 的所有邻居:R1、R3、R4。R1 已经在 Path 里,直接跳过,不需要处理。R3 当前在 Tent 中,开销记录是 20,父指针是 R1。但通过 R2 到 R3 的新开销是 10(R2 自身成本)加 5(R2-R3 链路开销)等于 15,小于原来记录的 20。于是松弛发生:R3 在 Tent 里的记录被更新为开销 15、父指针改为 R2。R4 是新发现的节点,之前不在 Tent 里,现在通过 R2 发现,开销为 10(R2)+10(R2-R4)=20,父指针为 R2,加入 Tent。
本轮结束后的状态:
| 节点 | 当前最优开销 | 父指针 | 所在集合 |
|---|---|---|---|
| R1 | 0 | 无 | Path |
| R2 | 10 | R1 | Path |
| R3 | 15 | R2 | Tent |
| R4 | 20 | R2 | Tent |
第二轮迭代,Tent 中有 R3(15) 和 R4(20),最小的是 R3,把 R3 移入 Path。
遍历 R3 的邻居:R1、R2、R4。R1、R2 已在 Path 中,跳过。R4 在 Tent 中,当前记录是开销 20、父指针 R2。通过 R3 到 R4 的新开销为 15(R3 自身成本)加 10(R3-R4 链路开销)等于 25,大于原有的 20。松弛操作的前提是“新开销更小”,这里不满足,所以 R4 的记录保持不变,仍维持父指针 R2、开销 20。
第三轮迭代,Tent 里只剩 R4,开销 20,移入 Path。遍历 R4 邻居:R2、R3,都已在 Path 中,全部跳过。Tent 变空,SPF 计算结束。
最终结果:
| 节点 | 最终最短开销 | 父指针 |
|---|---|---|
| R1 | 0 | 无 |
| R2 | 10 | R1 |
| R3 | 15 | R2 |
| R4 | 20 | R2 |
最短路径树为 R1→R2→R3,以及 R1→R2→R4。这里特别值得注意:R1 和 R3 之间看起来有一条直连链路,开销 20,但 SPF 依然选择了绕行 R2 的路径(总开销 15),因为它更短。这就是最短路径树和物理拓扑不一致的典型例子,很多刚学 SPF 的人会被“明明有直连,为什么要绕路”这个问题卡住,答案是算法只认总开销,不认跳数或“直观路线”。
3.3 从最短路径树到路由表
SPF 算出的最短路径树并不是直接写入路由表的,它还要经过一次“信息提取”的过程。
树上的每个节点对应一台路由器(或伪节点),但真实目标是网段。IS-IS 路由信息其实是 LSP 里附带的:每台路由器在自己的 LSP 中通告它直连的网段和开销。SPF 计算出的是“去往某台路由器的最优路径”,然后结合那台路由器 LSP 里通告的网段开销,得出“去往某个网段的总开销”,最后把这部分信息投递到路由表管理模块。
继续用上面的例子。假设 R3 在它的 LSP 里通告了一个直连网段 10.1.3.0/24,开销为 0(指本机直连网段),那么 R1 到达该网段的总开销就是:R1 到 R3 的最短开销 15,加上 R3 通告的网段开销 0,最终路由就是 10.1.3.0/24,下一跳 R2,开销 15。如果 R4 也通告了 10.1.4.0/24,那么 R1 去 10.1.4.0/24 的开销就是 20,下一跳同样是 R2。
这就是为什么说 SPF 计算结果要先形成一棵“以本机为根的最短路径树”,再根据树上的各节点地址和通告前缀生成路由表。只有链路状态变化了才重新算树,前缀变化时则可以通过 PRC 做局部刷新,不需要重跑整棵树。
3.4 等价路径与 ECMP 怎么处理
如果我把拓扑稍微改一下:把 R1-R3 之间的开销从 20 改成 10,情况就完全不同了。R1 到 R3 的路径有两条:直连开销 10,绕行 R2 的路径开销 15,直连更优,所以树会变成 R1 直连 R3。如果再把 R2-R3 的开销改成 5,同时 R1-R3 也保持 10,那 R1 到 R3 就有两条开销相同的路径了:直连 R3 开销 10,经过 R2 开销也是 10。
这时候 Dijkstra 算法在“从 Tent 中选最小节点”这一步可能先选 R3 或 R2,但最终的结果必须是两条路径都被记录。在实际实现中,IS-IS 会对一个目的地保留多个父指针,并在生成路由时产生多个下一跳,这就是 ECMP。路由表里会出现同一前缀带两个下一跳、开销相同的条目,流量被哈希分摊到两条链路上。
ECMP 是 SPF 一个非常实用的自然延伸:算最短路径树的时候,不死板地“只保留一个父节点”,而是对开销相同的候选全部保留。这样做不仅提高了链路利用率,还在单条链路故障时实现了快速切换,因为另一条等价路径还在树上。调试时注意,ECMP 的数量不是无限的,常见实现支持 4 条或 8 条等价路径,超出后会按一定规则挑选。
4. L1/L2 分层与 SPF 的边界问题
4.1 Level 分级带来的 SPF 区域化
IS-IS 和 OSPF 一样有分层的概念,但叫法不同:Level-1 相当于 OSPF 的非骨干区域,Level-2 相当于骨干区域。一台路由器可以只跑 Level-1、只跑 Level-2,或者同时跑 Level-1/2(这是最普遍的骨干路由器模式)。
分层对 SPF 最大的影响是:路由器不需要为全网计算一棵完整的树。Level-1 路由器只维护本区域内的 LSDB,只计算本区域内的 SPF 树;Level-2 路由器维护整个 Level-2 域(以及通过路由泄露获取的区域信息)。这种设计大幅缩小了单台路由器的计算规模。
对于 Level-1 路由器,会发现一个有趣的机制:它往区域外发流量时,不是去算一条到外部目标的最短路径,而是通过区域内最近的 Level-1/2 路由器转发,并默认把该路由器通告的一条默认路由(缺省路由)作为出口。换句话说,Level-1 的 SPF 树是“区域内精确计算,区域外走默认”,这大大简化了叶节点路由器的存储和计算压力。
4.2 默认路由和路由泄露
上面提到的 Level-1 默认路由,Level-1/2 路由器会通过特定 LSP 通告给 Level-1 区域,告诉区域内部“去外部网络从我这走”。如果一台 Level-1 路由器连接的 Level-1/2 路由器有多台,它就会在 SPF 树里发现多个默认路由的等价出口,然后在路由表里生成多条默认路由,流量可以在这几台 Level-1/2 之间负载均衡。
这里又引入一个精细话题:Level-1/2 路由器把自己学到的 Level-2 路由以“路由泄露”方式注入到 Level-1 区域。如果泄露了明细路由,区域内的 Level-1 路由器就能算出更精确的出口选择,而不是一刀切全走默认路由。但注意:泄露会增加区域内 LSDB 的规模,加大 SPF 计算负担;不泄露虽然简单,但可能造成次优路径(所有外部流量都挤在同一个出口)。
在实际网络中,是否开启路由泄露需要在“计算规模”和“路径最优性”间权衡。我见过不少项目一上来就全量泄露,导致区域内 SPF 树变得非常庞大,收敛变慢;后来又收回去,只泄露部分汇聚路由。SPF 的计算成本不是一个理论参数,它在真实组网里直接影响故障切换速度。
4.3 SPF 触发的真实场景:链路抖动时的防抖机制
SPF 不是任何时刻都在跑,它是由事件触发的。IS-IS 收到新的 LSP,或者本地接口状态变化、邻居断开、开销变更,这些都会触发 SPF 调度。如果网络不稳定,LSP 不断刷新,SPF 也不断重算,那路由器 CPU 就危险了。
所以 SPF 计算前面有一道“防抖”闸门:第一次收到触发事件后,会等待一个很短的时间间隔(比如 50ms),看看还有没有后续事件。如果陆续有新的 LSP 到达,就继续等待并进入指数退避,把间隔逐步拉长到几百毫秒甚至几秒。等到网络安静了,才真正执行 SPF 计算。这有点像是给算法加了一个“聚拢窗口”,把一阵风暴内的事件合并成一次计算。
调试中判断网络是否稳定,一个经典指标就是看 SPF 运行次数。如果一段时间内 SPF 运行次数异常多、且每次运行的间隔都很短,说明网络里有路由震荡源。常见诱因包括:互联接口频繁 up/down、LSP 生成计时器参数不一致导致泛洪循环、等价路径反复切换等。要治本,不是去调大防抖参数把问题掩盖,而应定位到具体链路或邻居,先把物理层和链路层搞定。
5. 实战排障:SPF 异常的典型症状
5.1 路由震荡反复重算
现场最头疼的问题就是“全网路由表在抖”。你登录设备看 SPF 日志,发现同一棵最短路径树在几十秒内被反复重算,每一轮重算后的下一跳都在变化。这时候第一步不是分析算法,而是确认到底哪个 LSP 在频繁刷新。
排查思路是:把最近收到的 LSP 按“源系统 ID”分组,统计刷新次数最多的那一个。找到源头路由器后,去那台设备上检查它产生的 LSP 为什么每隔几秒就更新。常见原因包括:某接口物理层不稳定反复 up/down、等价链路的一端配置了接口开销自动调整而另一端没有、或者该路由器 CPU 过高导致 LSP 重生成超时。把源头搞定,全网 SPF 次数自然就回到正常水平。
另外一个被忽视的原因:时钟不同步。IS-IS 的 LSP 老化时间基于相对时间,如果两台设备的时钟偏斜大,LSP 剩余寿命的判断就会异常,导致未到期就重发新 LSP。所以部署 IS-IS 的网络建议把所有设备纳入统一的时钟同步,这不是可选项,而是稳定性的刚需。
5.2 LSP 不新导致树陈旧
和“频繁重算”反着来的一种症状:网络明明已经发生变化,但路由表迟迟不更新。问题多半出在 LSP 的接收和处理链路,而不是 SPF 本身。登录设备检查 LSDB 中对应路由器的 LSP,如果发现序列号不是最新,或者剩余寿命已经很低却没有被刷新,那就是泛洪环节出了故障。
泛洪问题的常见诱因包括:两台设备之间的 Level-1/Level-2 邻接没有正确建立(两边 Level 配置不一致)、MTU 问题导致大的 LSP 被静默丢弃、以及某台设备的接口下配置了泛洪过滤策略导致特定 LSP 被拦截。SPF 本身算法没错,但它吃的是 LSDB 的“料”,料不新鲜,炖出来的树就一定是过期的。所以排查 SPF 问题时,永远先查 LSDB,再查树上。
5.3 次优路径问题
还有一种情况:路由表里明明有路径,但不是最佳路径。这往往是 SPF 输入数据的问题,而不是算法的问题。最典型的例子就是伪节点参与方式异常。如果广播网中 DIS 发生变化,但老伪节点的 LSP 迟迟没有从 LSDB 中清除,SPF 就会基于一张同时包含新旧伪节点的“残留图”计算结果,生成的路由可能绕路。
另一个次优路径原因是度量值不一致。比如一条链路两端接口的 wide metric 配置不同,A 到 B 的开销是 10,B 到 A 的开销是 100,那么从 A 算 SPF 时,到 B 的路径开销是 10;但从 B 算 SPF 时,到 A 的路径开销是 100。路由表里同一个目的地的开销取决于计算方向,这就会造成严重的路径不对称。解决方法是统一两端接口开销配置,并且通过拓扑检查工具验证双向开销数据。
5.4 常见问题速查表
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| SPF 重算次数过高 | 接口震荡、LSP 反复刷新、等价路径切换 | 查看 SPF 日志,定位高频 LSP 源 |
| 路由更新延迟 | LSP 泛洪不及时、邻接异常、MTU 问题 | 检查 LSDB 中 LSP 序号与剩余寿命 |
| 次优路径 | 双向度量不对称、残留伪节点、路由泄露缺失 | 逐段核对接口开销,检查 DIS 稳定性 |
| 部分路由缺失 | LSP 被过滤、区域边界路由策略错误 | 检查 LSP 过滤策略与 Level 边界配置 |
| 环路 | 负开销配置或路由策略异常 | 检查是否有非法开销值,核验路由策略 |
这张表是我在实际项目中反复验证过的排障路径,基本覆盖了 SPF 阶段 90% 以上的异常场景。记住一句话:SPF 算法本身很成熟,真正出问题的几乎总是它的输入数据——LSDB 的内容和开销信息。
结尾:一点个人体会
SPF 算法看起来只是一段 Dijkstra 的实现,但在 IS-IS 里它被赋予了太多工程细节:伪节点、Level 分层、防抖调度、增量计算、等价路径处理,每一层都让这个经典算法更适应真实网络的复杂性。我每次在只剩三层结构的跨地区项目里,都会先把网络设计和 SPF 计算规模算一遍账,因为链路状态协议最大的敌人不是算法正确性,而是拓扑规模失控导致的收敛缓慢。如果各位能看懂这篇文章里手动推演的那个 4 节点例子,再回头看生产环境里任何一张复杂的 SPF 树,思路都会清晰得多——不过是从一个根节点出发,不断选择最近的邻居扩展,直到覆盖整张图而已。