很多人都知道交换网络的生成树协议,然而很少人真正理解它,正如CCIE路由与交换考试指南中的对应章节所叙述的那样。学习一个网络协议,一定要很深入的理解才能学有所用,否则只能是过眼云烟。生成树协议的要旨在于对付交换网的环路引发的广播风暴,有人可能会问,网管自己把网络用线缆联成了环,怎样还要靠协议来擦屁股?实际上连成环是为了提供冗余,这样就不会出现单点故障,生成树协议所要做的就是在稳定状态下将一个有环的交换机所构成的图裁减成一棵树,当这棵树的某处出现单点故障时,生成树协议可以自动闭合先前裁减掉的路径,重新生成一棵树,简单点说,以上就是生成树协议的全部。
最好的学习资料当然不是google到的,而是IEEE 802.1d的原始文档,该文档的第58页开始讲述STP,总结起来就是三部分:
1.STP的操作原语
2.STP协议的操作
3.一系列的定时器
802.1d文档所提供的操作原语如下:
8.6.9 Designated Port selection
8.6.9.1 Purpose
To determine, for each Port, whether the Port should be the Designated Port for the LAN to which it is attached.
8.6.9.2 Use
As part of the Configuration Update procedure (8.6.7).
8.6.9.3 Procedure
The procedure to Become Designated Port (8.6.10) shall be invoked for each Port that
a) Has already been selected as the Designated Port for the LAN to which it is attached, i.e., the value of the Designated Bridge and Designated Port parameters held for the Port are the same as that of the Bridge Identifier and the Port Identifier for that Port respectively, or for which
b) The Designated Root parameter recorded for the Bridge differs from that recorded for the Port (note that this procedure follows root selection), or
c) The Bridge offers a Path of lower cost to the Root for the LAN to which the Port is attached, i.e., the Root Path Cost recorded by the Bridge is less than the Designated Cost recorded for the Port, or
d) The Bridge offers a Path of equal cost to the Root, and the Bridge’s Bridge Identifier denotes a Bridge of higher priority than that recorded as the Designated Bridge for that Port, or
e) The Bridge offers a Path of equal cost to the Root, and the Bridge is the Designated Bridge for the LAN to which the Port is attached, and the Port Identifier of the Port is of higher priority than that recorded as the Designated Port.
首先给出名称,接下来是目的,然后是触发时机,最后是整个操作原语的执行过程。类似的操作原语还有很多,列举了所有的操作原语之后,802.1d文档给出的是STP协议的操作,举一例如下:
8.7.4 Message Age Timer expiry
a) Step 1. The procedure to Become Designated Port (8.6.10) is used for the Port for which Message Age Timer has expired.
b) Step 2. The Configuration Update procedure (8.6.7) is used. # 此处8.6.7正是一个操作原语
c) Step 3. The Port State Selection procedure (8.6.11) is used.
d) Step 4. If the Bridge is selected as the Root following Configuration Update, then
1) Step 1. The Max Age, Hello Time, and Forward Delay parameters held by the Bridge are set to the values of the Bridge Max Age,Bridge Hello Time, and Bridge Forward Delay parameters.
2) Step 2. The Topology Change Detection procedure (8.6.14) is used.
3) Step 3. The Topology Change Notification Timer (8.5.4.2) is stopped.
4) Step 4. The Configuration BPDU Generation procedure (8.6.4) is used and the Hello Timer is started.
有了这些操作原语和协议操作,还有什么难的吗?不需要太精通C语言的人应该可以写出代码了,用java来写代码应该更方便些吧。以上的操作原语和协议操作展示其实就是一系列的流程图,把流程图翻译成代码即可。以下举一个例子展示一些MaxAge在根端口超时后的执行流程:
按照以上流程,如果一个交换机超过MaxAge时间在根端口收不到Hello,那么它会尝试在该交换机上选出一个新的根端口,如选不出,那么说明该交换机很可能和根已经彻底断链了,那么它应该宣称自己是根,重新开启一次根选举。实际上是在blocking状态的端口中选举新的根端口,因为既然处于blocking状态,那么就说明它和上游存在环路,此时将它闭合即可。前面说“该交换机很可能和根已经彻底断链”,这只是说它在上游方向和根断链,而在其指定端口的下游方向,依然可能和根连通。见下图:
选举出新根端口的情形如下:
上面仅仅是给出一个典型的新拓扑收敛的例子,照着802.1d的IEEE文档,你可以全部搞定整个STP协议,如果你懒得这么做,你还有一个选择,那就是看Linux内核的源码,不过还是最好看文档,因为看代码的话,很容易迷失在很不经常进入的异常流,好在Linux还有unlikely为你分担这部分的忧愁。Linux处理MaxAge超时的代码如下:
1.一个交换机只有根端口才能接收和发送BPDU(blocking端口只能接收,但不发送,见3)
2.一个交换机的指定端口发送BPDU
3.一个交换机的blocking端口接收对端指定端口发送的BPDU并执行更新操作,但不转发该BPDU
4.指定端口是相对于网段来讲的,而不是相对于交换机来讲的
5.根端口连接上游,指定端口连接下游
6.第3点中的更新操作指的是端口类型的更新以及端口状态的更新
来看下一个问题,对于STP,它在数学上如何被证明所生成的树是最小生成树。这个可以抛开网络环境,仅仅根据最小生成树算法来证明,STP的第一步选取根桥在最小生成树算法中是不存在的,它代表最小生成树算法的前置操作“任意选取连通图中的一个节点”。在STP中,生成树基本分为以下几步骤:
1.所有交换机群中选取根桥
2.每一个交换机选取一个根端口连接上游
3.每一个交换机选取一个或多个指定端口连接下游
4.每一个交换机中将2和3中的同时落选者设置为blocking
最后的一个问题就是为何STP中存在如此多的定时器,一句话概括那就是为了“以时间的流逝来洗涤旧迹”,这是鲁迅文章中的一句话,我超级喜欢,后来发现计算机中的很多定时器概念和定时器操作都可以套用这句话,STP中最关键的计时器如下:
1.Message Age Timer:设置一个BPDU消息超时时间,收到BPDU后重置,过期后执行协议操作Message Age Timer expiry;
2.Forward Delay Timer:为了洗涤旧迹,超时后端口状态更新到它本应该立即处于的状态。
对于定时器1,前面已经详细描述了它的行为,下面说一下Forward Delay Timer,为了这个定时器,STP引入了两个端口状态:listening和learning。这两个状态使得端口不会直接进入forwarding状态,为何不能直接进入呢?那是因为有“旧迹”需要“洗涤”,这个旧迹就是交换机中老的端口映射表,也就是交换机的地址表,由于blocking状态要变为forwarding,等于说打开了一条新路,而按照此前的地址表并不能寻址到这条新路,直接forwarding可能还会导致环路,因此需要等待老的地址表过期,那么就要等待Forward Delay这个时间。一般而言只要检测到拓扑改变,交换机就会把地址表的超时时间设置为Forward Delay,这样可以保证地址表快速过期以学习新的拓扑。那么怎样检测拓扑改变呢?IEEE 802.1d里面说的很清楚,有专门的操作原语和协议操作,举例来讲,比如说当一个交换机被选成了新的根节点时。因此listening和learning状态的含义如下:
listening:网络拓扑处于动荡之中,可能会引发某个交换机检测到拓扑改变而在其根端口发送BPDU,也可能导致将要进入forwarding状态的端口重新进入blocking(注意,整个STP的运行时分布式的),因此此状态需要持续Forward Delay。此状态下端口角色随时可能改变。
learning:拓扑逐渐变得稳定,然而旧迹未完全过期,可能构成暂时的环路,因此需要等待Forward Delay,此时的地址表过期时间已经设置成了Forward Delay,而这是在其前一个状态listending时拓扑更新被检测到时设置的,可以保证此阶段完毕后,老地址表均已过期,而新的地址表此时已经可以学习了。
在一个交换机的MaxAge过期后,它可能将一个blocking端口设置为forwarding,而此时可能也有别的交换机检测到了此事件的发生,进而引发的根交换机的选举,而此时候选根交换机会触发拓扑改变操作原语,接下来的很短时间内,该消息将会到达这个交换机群的每一个交换机,设置收到该BPDU的交换机将地址表的超时时间设置为很短的Forward Delay,最终可能会引发一系列的选举根端口,选举指定端口等连锁反应,等Forward Delay时间过后,网络稳定,分布式选举已完成,地址表已然过期,此时便开始了一个新的learn过程。如果相类比一下类似的机制,请参考Linux内核实现的两阶段的无睡眠的RCU锁。
一个交换机群的收敛到底需要多久呢?IEEE文档给出了以下的建议配置,这些值并不是胡乱猜测的,而是经验值,因此,如果你将MaxAge配置成了2,将Forward Delay配置成了1,那也没关系,只是可能造成环路,然而在STP下,环路永远都是暂时的,因为它早晚会切断某些分支的,所以说,环路并不是很重要的情况下,为了使得收敛更快,自己可以配置这些值
Parameter
Recommended or default value
Fixed value
Range
Bridge Hello Time
2.0
—
1.0–10.0
Bridge Max Age
20.0
—
6.0–40.0
Bridge Forward Delay
15.0
—
4.0–30.0
Hold Time
—
标准STP协议之外,人们想出了多种优化协议,特别是Cisco。然而如果你彻底理解了标准的STP,你会发现这些优化的协议是如此的简单以至于你会认为它们仅仅是配置层面的。在实现上,你为了不让STP耽搁在某个你已经知道结果的状态之上太久,那么你就要亲口告诉它,因此这些优化无不是引入新的配置项,新的状态来保存那些“你已经知道的事实”,因此便不需要STP来无条件耽搁那么久等待一个某种情况下绝对不可能发生的事件上了。
个人觉得,IEEE的文档写的要比RFC更加规范,你看着它基本就能完成自己的实现了,如果能为Netfilter整理出这样的一篇文档,如此抽象,如此和系统无关,那么我觉得将其移植到BSD系统就不是什么难事了吧。
2.我认为“指定端口”这个词用得或者翻译的不好,用“派送者”则比较好,类似物流系统,下游的数据统一由派送者来派送
3.本文不包含“开销”的计算方法,因为这些都被说烂了。
最好的学习资料当然不是google到的,而是IEEE 802.1d的原始文档,该文档的第58页开始讲述STP,总结起来就是三部分:
1.STP的操作原语
2.STP协议的操作
3.一系列的定时器
802.1d文档所提供的操作原语如下:
8.6.9 Designated Port selection
8.6.9.1 Purpose
To determine, for each Port, whether the Port should be the Designated Port for the LAN to which it is attached.
8.6.9.2 Use
As part of the Configuration Update procedure (8.6.7).
8.6.9.3 Procedure
The procedure to Become Designated Port (8.6.10) shall be invoked for each Port that
a) Has already been selected as the Designated Port for the LAN to which it is attached, i.e., the value of the Designated Bridge and Designated Port parameters held for the Port are the same as that of the Bridge Identifier and the Port Identifier for that Port respectively, or for which
b) The Designated Root parameter recorded for the Bridge differs from that recorded for the Port (note that this procedure follows root selection), or
c) The Bridge offers a Path of lower cost to the Root for the LAN to which the Port is attached, i.e., the Root Path Cost recorded by the Bridge is less than the Designated Cost recorded for the Port, or
d) The Bridge offers a Path of equal cost to the Root, and the Bridge’s Bridge Identifier denotes a Bridge of higher priority than that recorded as the Designated Bridge for that Port, or
e) The Bridge offers a Path of equal cost to the Root, and the Bridge is the Designated Bridge for the LAN to which the Port is attached, and the Port Identifier of the Port is of higher priority than that recorded as the Designated Port.
首先给出名称,接下来是目的,然后是触发时机,最后是整个操作原语的执行过程。类似的操作原语还有很多,列举了所有的操作原语之后,802.1d文档给出的是STP协议的操作,举一例如下:
8.7.4 Message Age Timer expiry
a) Step 1. The procedure to Become Designated Port (8.6.10) is used for the Port for which Message Age Timer has expired.
b) Step 2. The Configuration Update procedure (8.6.7) is used. # 此处8.6.7正是一个操作原语
c) Step 3. The Port State Selection procedure (8.6.11) is used.
d) Step 4. If the Bridge is selected as the Root following Configuration Update, then
1) Step 1. The Max Age, Hello Time, and Forward Delay parameters held by the Bridge are set to the values of the Bridge Max Age,Bridge Hello Time, and Bridge Forward Delay parameters.
2) Step 2. The Topology Change Detection procedure (8.6.14) is used.
3) Step 3. The Topology Change Notification Timer (8.5.4.2) is stopped.
4) Step 4. The Configuration BPDU Generation procedure (8.6.4) is used and the Hello Timer is started.
有了这些操作原语和协议操作,还有什么难的吗?不需要太精通C语言的人应该可以写出代码了,用java来写代码应该更方便些吧。以上的操作原语和协议操作展示其实就是一系列的流程图,把流程图翻译成代码即可。以下举一个例子展示一些MaxAge在根端口超时后的执行流程:
按照以上流程,如果一个交换机超过MaxAge时间在根端口收不到Hello,那么它会尝试在该交换机上选出一个新的根端口,如选不出,那么说明该交换机很可能和根已经彻底断链了,那么它应该宣称自己是根,重新开启一次根选举。实际上是在blocking状态的端口中选举新的根端口,因为既然处于blocking状态,那么就说明它和上游存在环路,此时将它闭合即可。前面说“该交换机很可能和根已经彻底断链”,这只是说它在上游方向和根断链,而在其指定端口的下游方向,依然可能和根连通。见下图:
选举出新根端口的情形如下:
上面仅仅是给出一个典型的新拓扑收敛的例子,照着802.1d的IEEE文档,你可以全部搞定整个STP协议,如果你懒得这么做,你还有一个选择,那就是看Linux内核的源码,不过还是最好看文档,因为看代码的话,很容易迷失在很不经常进入的异常流,好在Linux还有unlikely为你分担这部分的忧愁。Linux处理MaxAge超时的代码如下:
static void br_message_age_timer_expired(unsigned long arg)
{
struct net_bridge_port *p = (struct net_bridge_port *) arg;
struct net_bridge *br = p->br;
const bridge_id *id = &p->designated_bridge;
int was_root;
if (p->state == BR_STATE_DISABLED)
return;
/*
* According to the spec, the message age timer cannot be
* running when we are the root bridge. So.. this was_root
* check is redundant. I'm leaving it in for now, though.
*/
spin_lock(&br->lock);
if (p->state == BR_STATE_DISABLED)
goto unlock;
was_root = br_is_root_bridge(br);
br_become_designated_port(p); //Become Designated Port原语
br_configuration_update(br); //Configuration Update原语
br_port_state_selection(br); //Port State selection原语
if (br_is_root_bridge(br) && !was_root)
br_become_root_bridge(br);
unlock:
spin_unlock(&br->lock);
}
可以清晰地看到几个关键原语操作的执行,类似的还有br_received_config_bpdu函数,它实现了一个交换机端口接收到一个BPDU时的操作。总体来讲,对于STP,有以下关键点:
1.一个交换机只有根端口才能接收和发送BPDU(blocking端口只能接收,但不发送,见3)
2.一个交换机的指定端口发送BPDU
3.一个交换机的blocking端口接收对端指定端口发送的BPDU并执行更新操作,但不转发该BPDU
4.指定端口是相对于网段来讲的,而不是相对于交换机来讲的
5.根端口连接上游,指定端口连接下游
6.第3点中的更新操作指的是端口类型的更新以及端口状态的更新
来看下一个问题,对于STP,它在数学上如何被证明所生成的树是最小生成树。这个可以抛开网络环境,仅仅根据最小生成树算法来证明,STP的第一步选取根桥在最小生成树算法中是不存在的,它代表最小生成树算法的前置操作“任意选取连通图中的一个节点”。在STP中,生成树基本分为以下几步骤:
1.所有交换机群中选取根桥
2.每一个交换机选取一个根端口连接上游
3.每一个交换机选取一个或多个指定端口连接下游
4.每一个交换机中将2和3中的同时落选者设置为blocking
最后的一个问题就是为何STP中存在如此多的定时器,一句话概括那就是为了“以时间的流逝来洗涤旧迹”,这是鲁迅文章中的一句话,我超级喜欢,后来发现计算机中的很多定时器概念和定时器操作都可以套用这句话,STP中最关键的计时器如下:
1.Message Age Timer:设置一个BPDU消息超时时间,收到BPDU后重置,过期后执行协议操作Message Age Timer expiry;
2.Forward Delay Timer:为了洗涤旧迹,超时后端口状态更新到它本应该立即处于的状态。
对于定时器1,前面已经详细描述了它的行为,下面说一下Forward Delay Timer,为了这个定时器,STP引入了两个端口状态:listening和learning。这两个状态使得端口不会直接进入forwarding状态,为何不能直接进入呢?那是因为有“旧迹”需要“洗涤”,这个旧迹就是交换机中老的端口映射表,也就是交换机的地址表,由于blocking状态要变为forwarding,等于说打开了一条新路,而按照此前的地址表并不能寻址到这条新路,直接forwarding可能还会导致环路,因此需要等待老的地址表过期,那么就要等待Forward Delay这个时间。一般而言只要检测到拓扑改变,交换机就会把地址表的超时时间设置为Forward Delay,这样可以保证地址表快速过期以学习新的拓扑。那么怎样检测拓扑改变呢?IEEE 802.1d里面说的很清楚,有专门的操作原语和协议操作,举例来讲,比如说当一个交换机被选成了新的根节点时。因此listening和learning状态的含义如下:
listening:网络拓扑处于动荡之中,可能会引发某个交换机检测到拓扑改变而在其根端口发送BPDU,也可能导致将要进入forwarding状态的端口重新进入blocking(注意,整个STP的运行时分布式的),因此此状态需要持续Forward Delay。此状态下端口角色随时可能改变。
learning:拓扑逐渐变得稳定,然而旧迹未完全过期,可能构成暂时的环路,因此需要等待Forward Delay,此时的地址表过期时间已经设置成了Forward Delay,而这是在其前一个状态listending时拓扑更新被检测到时设置的,可以保证此阶段完毕后,老地址表均已过期,而新的地址表此时已经可以学习了。
在一个交换机的MaxAge过期后,它可能将一个blocking端口设置为forwarding,而此时可能也有别的交换机检测到了此事件的发生,进而引发的根交换机的选举,而此时候选根交换机会触发拓扑改变操作原语,接下来的很短时间内,该消息将会到达这个交换机群的每一个交换机,设置收到该BPDU的交换机将地址表的超时时间设置为很短的Forward Delay,最终可能会引发一系列的选举根端口,选举指定端口等连锁反应,等Forward Delay时间过后,网络稳定,分布式选举已完成,地址表已然过期,此时便开始了一个新的learn过程。如果相类比一下类似的机制,请参考Linux内核实现的两阶段的无睡眠的RCU锁。
一个交换机群的收敛到底需要多久呢?IEEE文档给出了以下的建议配置,这些值并不是胡乱猜测的,而是经验值,因此,如果你将MaxAge配置成了2,将Forward Delay配置成了1,那也没关系,只是可能造成环路,然而在STP下,环路永远都是暂时的,因为它早晚会切断某些分支的,所以说,环路并不是很重要的情况下,为了使得收敛更快,自己可以配置这些值
Parameter
Recommended or default value
Fixed value
Range
Bridge Hello Time
2.0
—
1.0–10.0
Bridge Max Age
20.0
—
6.0–40.0
Bridge Forward Delay
15.0
—
4.0–30.0
Hold Time
—
标准STP协议之外,人们想出了多种优化协议,特别是Cisco。然而如果你彻底理解了标准的STP,你会发现这些优化的协议是如此的简单以至于你会认为它们仅仅是配置层面的。在实现上,你为了不让STP耽搁在某个你已经知道结果的状态之上太久,那么你就要亲口告诉它,因此这些优化无不是引入新的配置项,新的状态来保存那些“你已经知道的事实”,因此便不需要STP来无条件耽搁那么久等待一个某种情况下绝对不可能发生的事件上了。
个人觉得,IEEE的文档写的要比RFC更加规范,你看着它基本就能完成自己的实现了,如果能为Netfilter整理出这样的一篇文档,如此抽象,如此和系统无关,那么我觉得将其移植到BSD系统就不是什么难事了吧。
注解:
1.任何端口的up/down都应该能被立即检测出来,在Linux实现上,这是通过内核的notify观察者模式机制实现的;2.我认为“指定端口”这个词用得或者翻译的不好,用“派送者”则比较好,类似物流系统,下游的数据统一由派送者来派送
3.本文不包含“开销”的计算方法,因为这些都被说烂了。