一、图抽象

        5
   u ────────  v
   │╲         │╲
  2│ ╲2      2│ ╲1
   │  ╲       │  ╲
   x ── w ─── y ── z
      3    1     2
G = (N, E)
   N = 路由器集合 {u, v, w, x, y, z}
   E = 链路集合
   c(x, y) = 链路 (x,y) 的开销;若不直接相连则为 ∞

开销可以是什么

  • 常数 1(此时最短路 = 最少跳数)
  • 与带宽成反比(OSPF 默认开销 = 参考带宽 / 链路带宽
  • 与拥塞程度相关(动态,但会导致震荡,见第三节)
  • 由管理员任意指定(策略

路由的目标:找出从源到目的的最小开销路径。

分类的三个维度

维度 选项 A 选项 B
信息范围 全局(link-state):每个节点知道完整拓扑 分布式(distance-vector):只知道邻居
变化频率 静态:路由很少改变 动态:定期或事件驱动地更新
是否感知负载 负载敏感:开销随拥塞变化 负载不敏感今天的互联网基本都是这一类

二、链路状态算法(Dijkstra)

2.1 前提

每个节点都知道完整的网络拓扑和所有链路开销。

怎么知道的?通过链路状态广播:每个节点向全网泛洪自己与邻居的连接信息。所有节点因此拥有相同的拓扑数据库,各自独立运行 Dijkstra。

2.2 算法

初始化:
   N' = {u}                      ← 已确定最短路的节点集合
   对所有节点 v:
       若 v 是 u 的邻居: D(v) = c(u,v),  p(v) = u
       否则:            D(v) = ∞

循环:
   从 N' 之外找出 D(w) 最小的节点 w
   把 w 加入 N'
   ⭐ 对 w 的每个不在 N' 中的邻居 v:
       D(v) = min( D(v),  D(w) + c(w,v) )
       若更新了,则 p(v) = w
直到 N' 包含所有节点

核心思想(贪心)每一步都把当前已知距离最小的节点"确定下来",然后用它去松弛它的邻居。

2.3 完整手算示例 ⭐

用上面那张图,从 u 出发。

链路开销:

c(u,v)=5  c(u,x)=2  c(u,w)=2(经x路径)  实际直连:u-v=5, u-x=2
c(x,w)=3  c(v,w)=2  c(v,y)=1  c(w,y)=1  c(y,z)=2

为清晰起见,重列直连关系:

u–v: 5     u–x: 2
v–w: 2     v–y: 1
x–w: 3
w–y: 1
y–z: 2

迭代表

N' D(v),p(v) D(w),p(w) D(x),p(x) D(y),p(y) D(z),p(z)
0 {u} 5,u 2,u
1 {u,x} 5,u 5,x
2 {u,x,v} 5,x 6,v
3 {u,x,v,w} 6,v
4 {u,x,v,w,y} 8,y
5 全部

逐步解释

步 0: 初始化。u 的邻居 v(5)、x(2),其余 ∞
      ⭐ 最小的是 x(2) → 加入 N'

步 1: 用 x 松弛它的邻居 w:
         D(w) = min(∞, D(x)+c(x,w)) = min(∞, 2+3) = 5, p(w)=x
      ⭐ 现在 N' 外最小的是 v(5) 和 w(5),任选 v

步 2: 用 v 松弛邻居 w 和 y:
         D(w) = min(5, D(v)+c(v,w)) = min(5, 5+2) = 5  (不变)
         D(y) = min(∞, D(v)+c(v,y)) = min(∞, 5+1) = 6, p(y)=v
      ⭐ 最小的是 w(5)

步 3: 用 w 松弛邻居 y:
         D(y) = min(6, D(w)+c(w,y)) = min(6, 5+1) = 6  (不变)
      ⭐ 最小的是 y(6)

步 4: 用 y 松弛邻居 z:
         D(z) = min(∞, 6+2) = 8, p(z)=y
      ⭐ 最小的是 z(8)

步 5: 全部完成

从前驱指针回溯出最短路径树

   u
   ├── x (2)
   │   └── w (5)
   └── v (5)
       └── y (6)
           └── z (8)

u 的转发表

目的 最短路径 ⭐ 输出链路(第一跳)
v u→v (u,v)
w u→x→w (u,x)
x u→x (u,x)
y u→v→y (u,v)
z u→v→y→z (u,v)

⚠️ 注意转发表只记录第一跳,不记录整条路径——因为转发是逐跳的(第 2 讲)。

2.4 复杂度

朴素实现:每轮扫描找最小值 O(n) ,共 n 轮  →  O(n²)
用优先队列(堆):                          →  O(E log n)

2.5 震荡问题 ⭐

如果链路开销随负载变化(负载敏感路由),会发生什么?

初始:三条流分别从 x、y、z 流向目的地 w,链路开销 = 承载的流量

      1        1
  x ────▶ w ◀──── y
      ╲       ╱
       ╲ 0   ╱ 1        (数字为当前开销)
        ╲   ╱
          z

① 所有人发现顺时针方向拥塞 → 全部切到逆时针
② 逆时针路径现在拥塞了,顺时针空了 → 全部切回来
③ ⭐ 无限震荡,流量在两条路径间来回摆动

后果:路由不稳定、分组大量乱序、TCP 性能崩溃。

缓解手段

  1. 不用负载敏感的开销(今天互联网的实际做法——OSPF 开销基于带宽而非负载)
  2. 让各路由器的更新时刻去同步化(加随机抖动)
  3. 平滑开销变化(EWMA)

📌 一个重要的工程结论:**「让路由自动避开拥塞」听起来很美,但在实践中会引入正反馈震荡。**互联网选择了让路由保持稳定,把拥塞问题交给传输层的拥塞控制(第 15、16 讲)——又一次「智能在边缘」


三、距离向量算法(Bellman-Ford)

3.1 Bellman-Ford 方程

⭐ d_x(y) = min over v { c(x,v) + d_v(y) }

  d_x(y) = 从 x 到 y 的最小开销
  v 遍历 x 的所有邻居

含义:「我到 y 的最短距离,等于(我到某个邻居的开销 + 那个邻居到 y 的最短距离)在所有邻居上取最小。」

⚠️ 注意 x 并不知道整个拓扑,它只知道:

  • 到各邻居的直连开销 c(x,v)
  • 各邻居告诉它的距离向量 D_v = [d_v(y) for all y]

3.2 算法特点:分布式、异步、自终止

每个节点:
   等待某个事件(邻居的距离向量变了 / 自己的链路开销变了)
   ↓
   用 Bellman-Ford 方程重算自己的距离向量
   ↓
   ⭐ 若自己的距离向量【发生了变化】,通知所有邻居
   ↓
   回到等待

三个性质

  • 迭代(iterative):反复进行直到没有变化
  • 异步(asynchronous):⭐ 节点之间不需要同步,各干各的
  • 自终止(self-terminating):没有变化时自然停止,不需要"结束"信号

📌 这是一个非常优雅的分布式算法:没有中央协调,没有全局时钟,没有终止检测,但它会收敛到正确答案。

3.3 手算示例

      2        1
  x ─────  y ─────  z
   ╲              ╱
    ╲──── 7 ─────╱

初始(每个节点只知道直连邻居)

x 的表:  d_x(x)=0  d_x(y)=2  d_x(z)=7
y 的表:  d_y(x)=2  d_y(y)=0  d_y(z)=1
z 的表:  d_z(x)=7  d_z(y)=1  d_z(z)=0

第 1 轮交换后,x 收到 y 和 z 的向量

d_x(z) = min{ c(x,y) + d_y(z),   c(x,z) + d_z(z) }
       = min{ 2 + 1,             7 + 0 }
       = min{ 3, 7 }
       = 3   ⭐ 从 7 降到 3,下一跳改为 y

同理 z 也会发现经 y 到 x 更近:

d_z(x) = min{ c(z,y) + d_y(x),   c(z,x) + d_x(x) }
       = min{ 1 + 2,             7 + 0 }
       = 3   ⭐

第 2 轮:没有节点的向量发生变化 → 收敛

最终

x: d(y)=2 [下一跳 y]   d(z)=3 [下一跳 y]
y: d(x)=2 [下一跳 x]   d(z)=1 [下一跳 z]
z: d(x)=3 [下一跳 y]   d(y)=1 [下一跳 y]

3.4 链路开销变化

好消息传得快(Good news travels fast)

      4              1
  x ─────  y ─────  z

c(x,y) 从 4 降到 1:

t0: y 检测到变化,更新自己的表,通知邻居
t1: z 收到 y 的更新,更新自己的表
t2: 收敛完成
⭐ 只用了 2 轮

坏消息传得慢(Bad news travels slow)—— 无穷计数问题 ⭐⭐

      4              1
  x ─────  y ─────  z
      ↑
   开销从 4 涨到 60

初始状态:d_y(x) = 4(直连),d_z(x) = 5(经 y)。

t0: y 检测到 c(y,x) 变成 60
    y 计算: d_y(x) = min{ 60 + 0,  1 + d_z(x) }
                   = min{ 60,      1 + 5 }
                   = 6  ⭐ 经 z 走!

    ⚠️ 但 z 到 x 的路径【本来就是经过 y 的】!
       y 不知道这一点 —— 它只看到一个数字 5

t1: z 收到 y 说"我到 x 是 6"
    z 计算: d_z(x) = min{ 50+0,  1 + 6 } = 7

t2: y 收到 z 说 7
    d_y(x) = min{ 60,  1 + 7 } = 8

t3: z → 9
t4: y → 10
    ...
⭐ 一直数到 d = 51 时,y 才发现直接走 60 的链路更划算
   (因为 1 + 50 = 51 > ... 实际到 y 算出 60 < 1+d_z(x) 时才停)

⚠️ 需要【44 轮迭代】才收敛

根本原因:⭐ y 不知道 z 到 x 的路径是经过 y 自己的。这叫路由环路,而 DV 算法的信息是「一个距离数字」,丢失了路径信息

3.5 毒性逆转(Poisoned Reverse)

做法

⭐ 如果 z 到 x 的路径要经过 y,
   那么 z 就告诉 y:「我到 x 的距离是 ∞」

这样 y 就永远不会试图经 z 到 x。

回到上面的例子

z 告诉 y: d_z(x) = ∞
t0: y 计算 d_y(x) = min{ 60,  1 + ∞ } = 60  ⭐ 立刻正确
t1: y 通知 z,z 更新为 61
⭐ 2 轮收敛

⚠️ 但毒性逆转不能解决所有环路!

它只能消除两个节点之间的环路。三个或更多节点构成的环路仍然会产生无穷计数

    x ─── y
     ╲   ╱
      ╲ ╱
       z

若 A→B→C→A 形成环,毒性逆转无能为力。

其他缓解手段

手段 说明
定义"无穷"为一个小数 RIP 中 16 跳即视为不可达,把收敛时间上限压到可接受范围
水平分割(Split Horizon) 不把从某接口学到的路由再从该接口通告出去
触发更新 变化时立即通告,不等周期定时器
抑制计时器(Holddown) 收到坏消息后一段时间内不接受更好的路由

📌 RIP 的最大跳数是 15,正是为了限制无穷计数的持续时间——这也直接限制了 RIP 只能用在小型网络里。


四、LS 与 DV 全面对比

链路状态(LS) 距离向量(DV)
知道什么 完整拓扑 只知道邻居和它们报的距离
算法 Dijkstra Bellman-Ford
通信方式 向全网泛洪链路状态 只与邻居交换距离向量
报文复杂度 O(nE),每个节点广播 只在邻居间,但收敛前轮数可能很多
收敛速度 O(n²) 或 O(E log n),快且确定 可能很慢(无穷计数)
健壮性 节点只广播自己链路的开销,一个节点出错影响有限,各节点独立算 一个节点报错的距离会被传播和放大,因为每个节点的计算依赖邻居的结果
内存 需存完整拓扑 只需存距离向量
代表协议 OSPF、IS-IS RIP、EIGRP、BGP(路径向量)

健壮性差异是最重要的一条

LS: 若某路由器广播了错误的链路开销,其他节点会看到这个错误,
    但它们【各自独立】计算,错误的影响局限在涉及该链路的路径上

DV: 若某路由器报告了错误的距离,邻居会基于它计算,
    然后把结果再传给下一层邻居 —— ⭐ 错误会像病毒一样扩散全网

📌 这解释了为什么大型网络内部普遍使用 OSPF/IS-IS(LS 类)。而 BGP(第 21 讲)虽然是 DV 的变体,但它携带完整的 AS 路径——正是为了解决 DV 丢失路径信息导致环路的问题。


五、例题(Worked Example)

题目:给定网络

        1
   A ────── B
   │╲       │
  4│ ╲2    3│
   │  ╲     │
   D ── C ──┘
      5

链路:A–B=1, A–C=2, A–D=4, B–C=3, C–D=5

(a) 用 Dijkstra 从 A 出发,写出完整迭代表。 (b) 给出 A 的转发表。 (c) 若使用 DV,写出 A 的初始距离向量和收敛后的距离向量。

解答

(a) Dijkstra 从 A 出发

N' D(B),p(B) D(C),p(C) D(D),p(D)
0 {A} 1,A 2,A 4,A
1 {A,B} 2,A 4,A
2 {A,B,C} 4,A
3 {A,B,C,D}

逐步说明

步 0: D(B)=1, D(C)=2, D(D)=4。最小是 B(1) → 加入
步 1: 用 B 松弛 C:  D(C) = min(2, 1+3) = 2  (不变)
      最小是 C(2) → 加入
步 2: 用 C 松弛 D:  D(D) = min(4, 2+5) = 4  (不变)
      最小是 D(4) → 加入
步 3: 完成

(b) A 的转发表

目的 最短路径 开销 输出链路
B A→B 1 (A,B)
C A→C 2 (A,C)
D A→D 4 (A,D)

⭐ 本例中三条最短路径都是直连——因为直连开销都不大于绕行开销。

(c) DV

A 的初始距离向量(只知道直连):

D_A = [ A:0, B:1, C:2, D:4 ]

第 1 轮,A 收到邻居的向量:

D_B = [A:1, B:0, C:3, D:∞]
D_C = [A:2, B:3, C:0, D:5]
D_D = [A:4, B:∞, C:5, D:0]

d_A(B) = min{ c(A,B)+0, c(A,C)+3, c(A,D)+∞ } = min{1, 5, ∞} = 1
d_A(C) = min{ c(A,B)+3, c(A,C)+0, c(A,D)+5 } = min{4, 2, 9} = 2
d_A(D) = min{ c(A,B)+∞, c(A,C)+5, c(A,D)+0 } = min{∞, 7, 4} = 4

没有变化 → 已收敛

D_A = [ A:0, B:1, C:2, D:4 ]      与 Dijkstra 结果一致 ✅

📌 两种算法必然给出相同的最短路径(前提是拓扑稳定且开销一致)。它们的差别不在结果,而在如何得到结果——以及在网络变化和故障时的行为


六、随堂自测

  1. 路由算法的三个分类维度是什么?今天的互联网属于哪一类?
  2. Dijkstra 每一步做什么?为什么它是贪心的?
  3. 为什么转发表只记录第一跳而不记录整条路径?
  4. 什么是路由震荡?为什么互联网不用负载敏感的路由开销?
  5. 写出 Bellman-Ford 方程并解释每一项的含义。
  6. DV 算法的三个性质是什么?「自终止」是什么意思?
  7. ⭐ 用一个具体例子说明无穷计数问题的成因。
  8. 毒性逆转能解决所有环路吗?为什么?
  9. LS 和 DV 在健壮性上的根本差异是什么?

七、本讲要点回顾

  • 路由 = 在带权图上找最短路径。互联网使用负载不敏感的静态开销(OSPF 按带宽)。
  • Dijkstra:贪心,每步确定一个最近节点并松弛其邻居。O(n²) 或 O(E log n)。
  • 负载敏感的开销会导致震荡——互联网选择保持路由稳定,把拥塞交给传输层处理。
  • Bellman-Fordd_x(y) = min_v { c(x,v) + d_v(y) }分布式、异步、自终止
  • 好消息传得快,坏消息传得慢:无穷计数的根因是 DV 只传距离、丢失了路径信息
  • 毒性逆转只能消除两节点环路,三节点以上仍会出问题;RIP 用「16 = 无穷」限制损害。
  • LS 更健壮:错误影响局限;DV 的错误会在全网扩散
  • 代表协议:OSPF/IS-IS 是 LS,RIP 是 DV,BGP 是携带完整路径的 DV 变体

八、自测答案

1.信息范围:全局(链路状态)vs 分布式(距离向量);② 变化频率:静态 vs 动态;③ 是否感知负载:负载敏感 vs 负载不敏感。今天的互联网属于分布式/全局混合(域内用 LS,域间用路径向量)、动态、且负载不敏感

2. 每一步:从尚未确定的节点中选出当前 D 值最小的节点 w,把它加入已确定集合 N’,然后用 w 去松弛它所有还不在 N’ 中的邻居(D(v) = min(D(v), D(w)+c(w,v)))。它是贪心的,因为它假设当前 D 值最小的节点的距离已经是最终答案——这个假设在所有边权非负时可以被证明成立(若存在更短路径,那条路径必须经过某个 D 值更大的节点,加上非负边权后只会更大,矛盾)。

3. 因为转发是逐跳的(第 2 讲):每台路由器只负责把分组交给下一跳,下一跳会用它自己的转发表继续决策。记录整条路径既无必要(下游路由器不看你的决定),也不可行(会让转发表巨大,且路径变化时要更新所有上游)。

4. 路由震荡指流量在多条路径间来回摆动、路由表反复改变的现象。成因:若链路开销随负载变化,则「所有人避开拥塞路径」会让原本拥塞的路径变空、原本空闲的路径变拥塞,形成正反馈。互联网不用负载敏感开销,是因为震荡会造成路由不稳定、分组大量乱序、TCP 性能崩溃——保持路由稳定、把拥塞交给端系统的拥塞控制处理,是更好的分工

5. d_x(y) = min over v { c(x,v) + d_v(y) }d_x(y):x 到 y 的最小开销;v:遍历 x 的所有直连邻居;c(x,v):x 到邻居 v 的直连链路开销(x 自己知道);d_v(y):邻居 v 到 y 的最小开销(由 v 告诉 x)。整个式子的含义:我到目的地的最短距离,等于「到某个邻居的代价 + 那个邻居到目的地的最短距离」在所有邻居上的最小值。

6. 迭代:反复计算直到不再变化;异步:各节点不需要同步执行,谁收到更新谁就算,没有全局时钟或轮次;自终止:当没有任何节点的距离向量发生变化时,就不再有更新报文发出,算法自然停止——不需要任何"算法结束"的检测或信号

7. 见正文 x–y–z 例子:c(x,y) 从 4 涨到 60 时,y 计算 d_y(x) = min{60, 1 + d_z(x)} = min{60, 1+5} = 6,选择经 z。但 z 到 x 的路径本来就是经过 y 的——y 看到的只是数字 5,不知道这条路径的具体走法。于是形成环路,两者交替加 1 慢慢往上数,直到数值超过 60 才收敛,共需 44 轮。根因是 DV 报文只携带距离,丢失了路径信息。

8. 不能。毒性逆转只能消除两个节点之间的环路(z 告诉 y「我经过你,所以我到 x 是 ∞」)。当环路涉及三个或更多节点时(A→B→C→A),每一对节点之间的关系都是"我没经过你",毒性逆转不会触发,无穷计数依然发生。这就是 RIP 必须额外用「16 跳 = 无穷」来兜底、并配合水平分割与抑制计时器的原因。

9. LS:每个节点只广播自己直连链路的状态,所有节点拥有相同的拓扑数据库并各自独立运行 Dijkstra。若某节点广播了错误信息,其他节点会看到这个错误,但错误的影响局限在涉及该链路的路径计算上,且不会被"加工放大"。DV:每个节点的距离向量是基于邻居报告的结果计算出来的,并且这个结果会继续传给下一层邻居。因此一个错误的距离值会被逐级采纳、累加、传播——错误在网络中扩散并放大。这是大型网络内部普遍采用 LS 类协议的核心原因。