2026/10/24 20:38:12

从LSA到SPF:OSPF链路状态路由原理、区域设计与排障

从LSA到SPF:OSPF链路状态路由原理、区域设计与排障 简介《RFC2328中文版》是一份面向网络管理员、运维工程师及路由协议学习者的 OSPF V2 中文技术文档用于系统理解 OSPF 的连接状态路由机制、最短路径树计算与区域化设计。内容围绕协议概述、常用术语、连接状态数据库组织、SPF 算法、区域与骨干区域、区域间路由、LSA 类型与老化、认证安全及配置调试等展开并配有连接状态数据库示例与清晰目录便于按章节查阅和对照实验。包内共 1 个 PDF 文件压缩包约 1.93MB适合作为 RFC 原文的辅助阅读材料或软件/插件环境中的路由配置参考。已有 431 人学习可帮助读者建立从拓扑表示、路由计算到多区域部署的完整知识框架提升 OSPF 网络设计与故障分析能力。1. 一条 LSA 背后的分歧OSPF 为什么不愿意相信邻居第一次抓到 OSPF 的数据库描述包很容易产生误判以为邻居之间在交换路由。打开连接状态数据库才会发现里面存储的并不是路由而是一条条“我这台路由器连着谁、开销多少”的拓扑记录。更反直觉的是同一区域里每台路由器的这份数据库几乎完全一致包括它一辈子不会转发一个包过去的链路。这个设计绕开了距离矢量协议最根本的毛病邻居说“我到某网段三跳”你只能信因为你手里没有第二份数据可以对照也不知道它背后是不是绕了个环。OSPF 换了个思路——每台路由器都拿到完整地图自己算。RFC2328 这份中文版讲的就是这张地图怎么画、怎么同步、链路抖动时怎么不出错。2. 连接状态数据库为什么每台路由器都存一份全网的图2.1 LSA把一条链路压缩成一条可比较的记录OSPF 的数据库里没有“路由”这个东西只有 LSA连接状态宣告。一条 LSA 描述的不是“到某地怎么走”而是“我这条接口连到了谁、代价是多少”。这个视角转换是整个协议的基础拓扑是客观事实路由是主观计算结果事实可以同步计算结果不能同步。LSA 头部是定长的 20 字节字段布局如下字段长度作用LS age16 bit从生成开始的秒数上限 MaxAgeOptions8 bit能力位如是否支持外部路由LS type8 bit1 到 5决定后面的正文怎么解析Link State ID32 bit含义随类型变化见下文Advertising Router32 bit生成者的 Router IDLS sequence number32 bit同一 LSA 的版本号LS checksum16 bit覆盖除 LS age 外的全部内容length16 bit整条 LSA 的字节数为什么非要定长、非要这么排因为洪泛过程中路由器要频繁做两件事判断“这条 LSA 我是不是已经有了、哪个更新”以及“我有没有收到过同一条”。前者需要把两条 LSA 逐字段比较后者需要给每条 LSA 建索引。定长头部加三元组(LS type, Link State ID, Advertising Router)正好让 LSDB 可以当成一张哈希表来查同一份拓扑的不同版本落在同一个桶里只比较序列号就能定胜负。Link State ID 的取值很能说明这个协议的务实Router-LSA 里它是生成者自己的 Router ID一台路由器在区域内只有一条 Router-LSANetwork-LSA 里它是 DR 那个接口的 IP 地址因为 DR 用这个地址标识这个广播网Summary-LSA 里它是被汇总的目的网络地址或者 ASBR 的 Router IDAS-external-LSA 里它是外部目的网络地址。同一个字段在不同类型下装不同的东西省下了一个“扩展 ID”字段代价是解析时必须先看类型。还有一个细节值得注意校验和刻意排除了 LS age。因为 LS age 每秒都在涨如果校验和把它算进去每台路由器每秒都要重算一遍全库校验和这个开销在 1998 年的路由器上不可接受。把 age 摘出去意味着一条 LSA 从生到死校验和只算一次拓扑内容不变则校验和不变。这种“把变量隔离出去”的手法在后面判断新旧时会再次出现。2.2 DijkstraOSPF 的图为什么有两类顶点拿着 LSDB 跑最短路径树时最容易出错的地方是把图建错。OSPF 的图不是“路由器连路由器”而是“路由器顶点 网络顶点”的二部结构。原因在广播网络一个以太网段上挂了 10 台路由器如果按点对点的方式两两建边需要 45 条边用 Network-LSA 建一个虚拟的网络顶点只需要 10 条边。# 简化版区域内部 SPF顶点用 (类型, 标识) 表示 # (r, router_id) 是路由器顶点(n, dr_ip) 是传输网络顶点 def spf_in_area(lsdb, root, area): dist {root: 0} parents {root: []} # 等值多路径同一距离允许多个父节点 in_tree set() cand [(0, root)] while cand: cand.sort(keylambda x: x[0]) cost, v cand.pop(0) if v in in_tree: continue in_tree.add(v) for link, link_cost, nbr in expand(lsdb, v, area): if nbr in in_tree: continue nd cost link_cost if nbr not in dist or nd dist[nbr]: dist[nbr] nd parents[nbr] [v] cand.append((nd, nbr)) elif nd dist[nbr]: parents[nbr].append(v) # 代价相同保留为等值路径 return dist, parentsexpand负责从顶点取出邻接关系如果 v 是路由器顶点就遍历它 Router-LSA 里的 linktype 1 指向另一台路由器点对点type 2 指向网络顶点连到 DRtype 3 指向末梢网段type 4 是虚链路。如果 v 是网络顶点就从 Network-LSA 的“连接路由器列表”里取出所有挂在上面的路由器代价为 0——因为网络顶点本身不产生开销开销记在路由器接口上。dist记录到每个顶点的最小代价parents记录前驱in_tree保证每个顶点只出队一次。三个参数里最容易被忽略的是parents用列表而不是单值一旦某个顶点被发现存在第二条等价路径代码不是覆盖而是追加。这就是等值多路径ECMP在算法层面的落点——不需要额外的负载均衡模块SPF 本身就把多条路径都算出来了路由表只需要把parents链回溯成下一跳列表。末梢网段type 3 link的处理是个例外它不进候选列表直接在当前顶点的距离上加上 link cost 就生成一条路由。因为末梢网段后面不可能再挂别的路由器让它在树里占一个节点纯属浪费。2.3 外部路由为什么不参与 SPFAS-external-LSA 描述的是“自治系统外面有个网段我从某个 ASBR 那儿听来的”。它不参与最短路径树计算只在树算完之后作为叶子挂上去。计算时先看 LSA 里的 forwarding address如果非 0就去路由表里查这个地址的最短路径如果是 0就查 Advertising Router 的最短路径。拿到这条路径的代价之后再按外部度量类型决定怎么加Type 1 的外部代价和内部代价可以直接相加也就是说外部路径的优劣会随着内部拓扑变化而变化Type 2 只比较外部代价内部代价不参与比较只在外部代价相等时才用内部代价打破平局。默认是 Type 2因为绝大多数场景下“绕一圈进 ASBR”和“直连 ASBR”的差别比起外部链路本身的代价可以忽略。这个“外部不参与 SPF”的划分本质是把路由域切成两个计算层次域内是全局最优域外是近似最优。它带来一个后果——外部路由天然可能产生环路因为重分发是人为配置的协议管不了。RFC2328 只能规定 ASBR 不要把学到的外部路由再以同样的身份发回去剩下的靠人。3. 区域划分为什么骨干区域必须是 Area 0 而不能是任意一个3.1 不分区的代价一次抖动全网重算设想一个不分区域的自治系统200 台路由器、500 条链路。每条链路的状态变化都会产生一条新 LSA洪泛到全网 200 台路由器每台都要把这条 LSA 放进 LSDB然后重新跑一次 200 个顶点、500 条边的 Dijkstra。链路抖动频繁的核心层这个开销足以让 CPU 直接跑满。区域Area就是给这个开销设一个天花板。一个区域内的路由器只同步本区域的 LSA只对本区域的拓扑跑 SPF。区域外的世界对它来说是一堆“某网段距离某 ABR 多少代价”的摘要不需要知道细节。区域边界路由器ABR站在两个区域中间一边是完整的 LSDB一边是摘要。这个设计的收益很直接链路抖动的影响半径被限制在区域内区域外的路由器连 LSDB 都不会变。但代价同样直接——抽象必然丢信息丢信息就可能算出次优路径。比如区域 A 里有两台 ABR 都能到区域 B 的某个网段一台代价 10 一台代价 100区域 A 内部的路由器只知道“通过 ABR1 到那个网段是 15通过 ABR2 是 105”它选 ABR1这是对的但如果那个网段其实离 ABR2 更近只是摘要里的数字被 ABR 用自己的视角写死了选出来的路径就不是全局最优。OSPF 接受这个代价因为“次优但稳定”比“最优但天天重算”划算。3.2 ABR 的两副面孔一边跑 SPF一边做距离矢量ABR 的行为最容易让人困惑它在区域内是标准的链路状态路由器跑 SPF在区域间却干着距离矢量的活——把自己知道的网段加上自己的代价打包成 Summary-LSA 发出去。这意味着区域之间的路由传播本质上是“摘要的摘要”一跳一跳传和 RIP 传路由没有本质区别。既然像距离矢量就要防环。RFC2328 的防环手段不是水平分割而是一条硬性拓扑约束所有非骨干区域必须直连骨干区域区域间的摘要只能沿着骨干区域流动。ABR 从非骨干区域学到的 Summary-LSA 不会回灌到骨干从骨干学到的才会发往非骨干。这条规则把整个自治系统的区域间拓扑强制成星形星形没有环。# ABR 生成区域间摘要的简化逻辑 def generate_summaries(abr, backbone, non_backbone_areas): out [] for area in non_backbone_areas: for net in area.reachable_networks(): # 只汇总本区域内可达的网段不汇总从其他区域学来的 if net.learned_from ! intra_area: continue ls SummaryLSA( ls_type3, link_state_idnet.prefix, adv_routerabr.router_id, masknet.mask, metricnet.cost_from_abr abr.cost_to_area(area), ) out.append(ls) # 发往骨干区域 return out这段代码里两个参数决定了摘要的正确性。net.learned_from ! intra_area是防环的核心只有真正长在这个区域里的网段才配被汇总出去从别的区域学来的摘要一律不发否则就形成“A 告诉 B、B 又告诉 A”的回灌。abr.cost_to_area(area)则决定了摘要里的度量值——它是 ABR 到那个区域出口的代价加上区域内到网段的代价这个数字是 ABR 自己的视角下游路由器只能全盘接受。mask字段支持汇总如果配置了区域范围汇总多条明细会合并成一条更短前缀的 Summary-LSA度量为明细中最小的那个。汇总的副作用是明细里被抑制的网络如果出现故障汇总路由不会消失流量会被黑洞掉。这也是血泪经验——汇总配得越狠故障时的排查难度越高。3.3 虚链路与存根区域两个补丁级设计现实网络经常不按教科书长。收购、历史遗留、机房调整都可能导致某个区域没能直连骨干。虚链路Virtual Link就是为此准备的两台 ABR 之间穿过一个传输区域建立一条逻辑上属于骨干区域的邻接让断开的区域重新“看起来”接在 Area 0 上。这条链路的实现方式很有意思——它不是一个隧道不会封装任何数据包。两台 ABR 只是把 Hello 包按普通 IP 包发到对方穿过传输区域然后在本地把它们当成骨干区域的接口来处理。所以虚链路的代价等于穿过传输区域的最短路径代价而这条路径在传输区域的拓扑变化时会跟着变。用过的人大概都踩过这个坑底层区域一抖虚链路跟着断骨干区域的邻接重建SPF 重算连锁反应比预想的严重。存根区域Stub Area是另一个方向的补丁。如果一个区域里只有一台出口路由器它根本不需要知道自治系统外面有哪些网段——知道了也只有一个出口。于是规则改成AS-external-LSA 不进这个区域ABR 用一条 Type 3 的默认路由0.0.0.0/0代替。LSDB 体积瞬间小一个数量级区域内的低端设备也能扛住。这两个设计放在一起看能读出同一个思路区域的边界不是地理概念而是信息边界可以协商、可以搬动。但拓扑上那个星形结构不能破因为一旦破了防环的前提就没了。虚链路只是把“直连”换成了“逻辑直连”没有改变星形本身。4. 邻接与状态机为什么邻居会卡在 ExStart 不动4.1 八个状态每个状态在等一样东西邻居状态机是 OSPF 里最像玄学的部分因为状态名字看不出它到底在等什么。八个状态的实质如下状态在等什么进入条件Down什么都没收到初始状态AttemptNBMA 网络上门动去联系对方NBMA 且手工配置邻居Init收到 Hello但对方邻居列表里没有我收到单向 Hello2-Way双方互相看见Hello 里包含我的 Router IDExStart协商主从和 DD 序列号决定要建邻接Exchange交换 DD 包互相描述自己的 LSDB 摘要主从确定Loading补发 LSR把缺的 LSA 要过来DD 交换完成且还有缺失Full两边 LSDB 一致所有请求都被应答卡在 Init 说明只有一方能收到 Hello典型的单向可达或者访问控制把回程挡了。卡在 2-Way 有两种情况一种是广播网络上两台都不是 DR/BDR 的路由器协议本来就不让它们建邻接这是正常状态另一种是该建邻接却没建需要去看 DR 选举结果。卡在 ExStart 和 Exchange八成是 MTU 不一致——DD 包带着完整的 LSA 头部信息包体会随 MTU 变大一端发得出去另一端接不进来双方就一直停在“我先说”的循环里。4.2 DD 协商I/M/MS 三个位怎么定主从DD 包里有三个标志位I 位表示这是第一个 DD 包M 位表示后面还有MS 位表示“我是主”。主从协商的规则简单粗暴Router ID 大的当主主负责递增 DD 序列号从必须跟着主的序列号走。# DD 主从协商的简化模型 def handle_dd(nbr, pkt, my_router_id): if pkt.ms and pkt.seq nbr.dd_seq: # 对方自称主且序列号和我记录的一致 if my_router_id nbr.router_id: # 我 ID 更大应该我当主用对方的序列号继续 nbr.is_master True nbr.dd_seq pkt.seq return become_master else: nbr.is_master False nbr.dd_seq pkt.seq return accept_slave if not pkt.ms: # 对方是从来应答序列号必须回显我发的那个 if pkt.seq ! nbr.dd_seq: return drop_mismatch nbr.dd_seq 1 return send_next_dd这段逻辑的关键在于nbr.dd_seq这个变量它是整个交换过程的单调序列号主每发一个 DD 就加一从必须原样回显。参数my_router_id只参与一次比较之后就固定在nbr.is_master里。pkt.ms and pkt.seq nbr.dd_seq这个条件同时检查了两件事——“对方声称是主”和“对方的序列号接得上我这边”任何一项不满足都会掉进 Drop 分支这就是邻居反复在 ExStart 和 Down 之间跳的常见原因。一旦进入 Exchange两台路由器互发 DD 包包里装的是自己 LSDB 里每条 LSA 的头部摘要20 字节不是完整 LSA。对方比对之后把“你有我没有”或者“你的比我的新”的那些记下来在 Loading 阶段用 LSR 逐条去要。这个“先对齐目录再取内容”的设计让同步的传输量从“全库”降到“差集”在带宽紧张的时代是刚需。4.3 DR 与 BDR为什么选举要等一个 DeadInterval广播网络上一共有 N 台路由器如果两两建邻接邻接数是 N(N-1)/2LSDB 同步的洪泛量是 O(N²)。用一个指定路由器DR做中心所有路由器只和 DR、BDR 建邻接邻接数降到 N洪泛可以顺着 DR 集中转发。选举规则先比 Hello 包里的 priority0 表示不参选大的赢priority 相同比 Router ID大的赢。BDR 是 DR 的热备DR 挂了 BDR 直接顶上不需要重新选举加重新同步收敛时间少一个数量级。有个细节初看很奇怪选举不是立刻进行的要等一个 Wait Timer长度等于 Router DeadInterval广播网默认 40 秒。原因是如果不等先启的路由器会自封为 DR后启的即使优先级更高也只能当 BDR而 OSPF 的 DR 是不可抢占的——除非 DR 挂掉否则没人能把它挤下来。等一个 Dead 间隔就是给同一网段的所有路由器一个“同时看到彼此”的窗口。这个不可抢占的设计是刻意的抢占意味着 DR 变更意味着全网重新同步代价远大于“让一个优先级低的路由器当 DR”。这也解释了一个常见困惑——把 priority 调高之后邻居没变化不是配置没生效是要等当前 DR 重启或链路断开。5. 洪泛与老化LSAck 重传机制和五条排查记录5.1 没有广播确认靠什么保证每条 LSA 都送到OSPF 跑在 IP 之上IP 不保证送达也不保证顺序。要让一条 LSA 可靠地传遍全网协议用了最朴素的办法逐条确认加重传。收到 LSU 的路由器做三件事。第一对包里的每条 LSA 判定新旧只有比本地新的才接受。第二接受的 LSA 进 LSDB然后从除接收接口之外的所有接口洪泛出去——但有一个例外邻居处于 Exchange 或 Loading 状态时不发因为它正在同步发了会打乱 DD 序列。第三回一个 LSAck逐条确认收到了哪些 LSA。发出去没收到确认的进重传列表按 RxmtInterval默认 5 秒重发直到收到 LSAck 或者邻接断开。这个重传列表是跟着邻居走的不是跟着接口走的所以一个邻接断了列表里属于它的条目直接清掉。注意LSAck 可以逐条确认也可以用一个包一次确认多条甚至可以对还没收到过的 LSA 做“预确认”。这不是 bug是规范允许的优化——如果一条 LSA 已经从别的邻居那里收到并入库再收到同一条时可以直接回确认不需要再走一遍判定。这套机制里最妙的是老化。LSA 有一个 LS age 字段每秒加一到 MaxAge3600 秒时被特殊处理不删除而是以 MaxAge 状态重新洪泛一次让全网都知道它死了然后才从 LSDB 里移除。为什么不用超时删除因为如果各路由器各自计时删除删除时刻不一致就会出现“我这儿还有你没有”的短暂不一致此时如果有新的 LSA 到达可能被误判为旧版本。用“提前老化 洪泛”这个显式动作把删除变成一个全网可见的事件。还有一个常被忽略的机制正常情况下即使拓扑不变LSA 也会在 LSRefreshTime1800 秒时被重新生成一次序列号加一重新洪泛。这个周期性刷新看起来是浪费实际是给“LSA 意外丢失”兜底——万一某台路由器的 LSDB 里少了这条下一次刷新会自动补上。两害相权选了带宽。5.2 判定较新序列号、校验和、年龄的三级比较整个洪泛机制的正确性压在一个函数上给定两条同三元组的 LSA谁更新MAX_AGE_DIFF 900 # 年龄差超过这个值认为小的那条是新的 SEQ_INITIAL 0x80000001 # 序列号起点 def seq_gt(a, b): # 序列号空间是 0x80000001..0x7fffffff 的线性空间按有符号比较 diff (a - b) 0xFFFFFFFF return 0 diff 0x80000000 def lsa_is_newer(a, b): if a.seq ! b.seq: return seq_gt(a.seq, b.seq) # 一级序列号 if a.checksum ! b.checksum: return a.checksum b.checksum # 二级校验和大的新 if abs(a.age - b.age) MAX_AGE_DIFF: return a.age b.age # 三级年龄差够大小的新 return False # 无法判定视为相同三级比较的顺序不能换。序列号是第一优先因为它是人为递增的版本号最能反映“谁后生成”。校验和是第二优先用于序列号相同时的兜底——两条序列号相同的 LSA 内容不同说明有人重启后序列号回绕了此时校验和大的被认为是新的这个规则纯属约定没有物理意义。年龄是第三优先且必须带MAX_AGE_DIFF这个门槛如果两条 LSA 内容完全一样只是年龄差了几秒说明是同一份数据在不同时间点被看到不能因为“你的比我的新 3 秒”就互相覆盖否则洪泛会无限循环。seq_gt里那个 0xFFFFFFFF和0x80000000的判断处理的是序列号回绕。序列号从 0x80000001 涨到 0x7FFFFFFF 之后再涨就回到 0x80000001如果按无符号数直接比大小回绕那一刻会出现“新版本反而更小”的翻车。用有符号差值判断把整个空间当成一个环来比较前提是两条 LSA 的版本差距不超过半个空间——这在实践中成立因为一条 LSA 要涨 20 亿个版本得刷新几万年。5.3 五条排查记录现象一邻居停在 Init一直不往上走。原因通常是单向可达——我能收到你的 Hello但我的 Hello 到不了你。常见于两端掩码不一致一端 /24 一端 /30广播地址对不上、或者中间有策略只放行了一个方向。解决办法是把两端接口的掩码、区域号、Hello/Dead 定时器逐项对齐然后用抓包工具确认对方 Hello 的邻居列表里到底有没有我的 Router ID。现象二邻居卡在 ExStart日志里反复出现 DD 重传。九成是 MTU 不一致。DD 包在 Exchange 阶段会逐渐变大一端能发 1500 字节另一端接口 MTU 只有 1400大包直接被丢双方就一直停在“我发了你没回”。用show ip ospf interface对比两端 MTU或者在接口上把 MTU 调成一致。这类问题的恶心之处在于小包能通、ping 能通只有 OSPF 的 DD 包过不去光看连通性完全看不出问题。现象三邻居卡在 LoadingLSR 发出去没回应。说明 DD 阶段已经谈完了但请求的 LSA 拿不到。可能是对方 LSDB 里那条 LSA 刚好被老化删除了也可能是 LSR 包被中间设备丢弃。这种情况一般会自己恢复——重传几次之后要么拿到要么邻接超时重建。如果反复出现要去看是不是有大量 LSA 在同时老化比如某台路由器重启导致序列号回绕。现象四邻居在 Full 和 Down 之间反复跳。典型的 Dead 定时器到期。可能是链路本身在闪断也可能是 CPU 过高导致 Hello 包发不出去Hello 是周期性发送优先级低于数据转发时会被饿死。还有一个隐蔽原因单向链路。一端认为邻接正常另一端收不到 Hello40 秒后单方面断开然后重建周而复始。现象五区域内路由正常区域间路由缺失。先确认这个区域是不是直连骨干。非骨干区域如果没连 Area 0又没有配虚链路ABR 根本不会给它发 Summary-LSA。其次看 ABR 上有没有产生 Type 3——如果 ABR 同时是 ASBR还要检查有没有配no-summary之类的抑制。最后看区域间路由的度量如果两条 Summary-LSA 的代价相同但路径不同可能被等值多路径吞掉路由表里看不出来源。6. 设计取舍OSPF 把复杂度花在了哪些地方把整份文档读完会发现 OSPF 的复杂度分布很不均匀洪泛和状态机占了绝大部分篇幅而真正算路由的 SPF 只有一章。这个分布本身就说明了设计者的判断——在一个会丢包、会抖动、会有设备重启的网络上让所有路由器对同一份拓扑达成一致比算出最短路难得多。几处取舍值得单独拎出来看取舍点选择换来了什么付出了什么拓扑同步全量 LSDB不做增量摘要每台路由器都能独立验证路径内存和洪泛带宽随规模线性增长可靠性逐条 LSAck 加定时重传不依赖链路层可靠性状态机复杂重传列表要跟着邻接走规模划分区域区域间用摘要SPF 范围可控区域间路径可能次优且需要星形拓扑DR 选举不可抢占等一个 Dead 间隔避免频繁重建邻接调优后要等 40 秒才生效老化周期刷新加显式 MaxAge 洪泛LSA 丢失能自愈稳定网络也有固定洪泛开销这些选择里有几条是有争议的。TOS 支持按业务类型分别计算路由在规范里留了完整的字段和计算流程但几乎所有实现都不做因为维护多棵 SPF 树的收益远小于复杂度。认证机制在规范里只给了简单的明文和校验和两种安全性按今天的标准看很弱但它保证了和旧实现的互通。我自己的习惯是每次看到一个“为什么不设计成 XX 那样”的疑问先去找规范里对应的约束条款——大多数看起来奇怪的设计背后都有一条硬约束在撑着。比如区域必须连骨干初看是历史包袱读懂了摘要防环就明白它是必要条件比如 LSA 要周期刷新初看是浪费带宽想清楚“删除时刻不一致”的后果就知道它是唯一解。从那以后我每次排查 OSPF 问题都强制走一遍“先看邻接状态、再看 LSDB 差异、最后看 SPF 结果”这个顺序不再一上来就抓包。这份 RFC2328 中文版放在手边最大的价值不是让你背下字段布局而是当你想改一个定时器、想加一条汇总、想动一个区域边界时能翻到对应的章节看清楚这个动作会牵动哪些状态机。希望帮到你。本文还有配套的精品资源点击获取