4. 最佳主时钟算法(BMCA):选举过程、优先级比较与实现细节
好,咱们来聊聊 BMCA。这名字听着挺唬人,说白了就是 TSN 时钟领域的一场“选拔赛”。在网络里,谁当老大(主时钟),谁当小弟(从时钟),全凭它说了算。我记得刚接触 那会儿,总觉得 BMCA 也就是比大小,后来在项目里被一个优先级问题折腾了两天,才彻底搞明白——这算法,比你想的讲究。
4.1 选举什么时候触发?
不是时时刻刻都在选老大。选一次,管一阵。那么什么时候重选呢?主要就这几种情况:
- 新设备加入:有人插队,当然要看看它有没有资格当老大。
- 现有主时钟挂了:老大失联了,存活广播
Announce超时,群龙无首,赶紧重选。 - 收到更优的设备信息:有个节点突然广播说“我比现在的主时钟更好”,那也得重新比划比划。
- 端口状态变化:比如某台交换机的链路断了又恢复,端口状态变了好几次,也得触发选举。
4.2 BMCA 的状态机——时钟端口的五个身份
每个端口都处在下面五种状态之一。说白了,就是给端口定了个职称:
| 端口状态 | 含义 | 能不能参与选举 |
|---|---|---|
| Master | 本端口输出的时钟就是主时钟,发给所有从端口 | 是 |
| Slave | 本端口同步到别的主时钟上,自己的时钟不对外广播 | 是 |
| Passive | 本端口既不发送主时钟,也不同步到别人,但还活着,只是旁听 | 是 |
| Disabled | 端口被管理员关了,或者硬件故障 | 否 |
| Faulty | 端口出错了,比如 CRC 错误太多或链路闪断 | 否 |
实际选举只在 Master、Slave、Passive 三个状态之间玩。说白了,选举就是一场“谁能当 Master,谁该做 Slave,谁乖乖去 Passive”的游戏。
4.3 优先级比较——谁才是真正的老大?
好,选举开始了。BMCA 不只看一个指标,它看一套组合拳。每次比较都会依次对比下面这四个属性,一旦分出胜负,后面的就不看了。
- priority1:管理员可以手动改,数值越小越牛。范围 0~255。
- clockClass:时钟的等级,反映时钟的溯源能力。比如跟北斗/GPS 同步的时钟,class 就很小;本地普通晶振,class 就很大。数值越小越准。
- clockAccuracy:时钟的精度,还是数值越小越准。
- priority2:又是一个管理员可配的参数,跟 priority1 一样,数值越小越优先。做冗余备份时用得多。
- clockIdentity(后备):如果前面四项都一样,就用这个 64 位的全球唯一 ID 做决胜局,再一样的概率约等于零。
每次 Announce 报文里都带着这些字段。收到报文的节点会把自己的这一套跟对方比一遍。看看谁数值更小,谁就胜出。
4.4 具体怎么选?——BMCA 的执行逻辑
我画了个流程图,更直观些:
你看,从左到右走一遍:触发 -> 收集 -> 比较 -> 决策 -> 结果。每一步都不多余。实际咱们工程上经常在这个过程里丢帧,原因就是 Announce 报文的 接收超时 设置得太紧,有个别丢包就触发重选,网络反而更不稳定。
priority1 最有话语权。我曾经见过一个项目,两颗 GPS 授时交换机放一台,结果 priority1 没配好,两个都是 128,结果每半分钟互换一次 Master,整个网络抖动全乱了。后来把一台改成了 priority1=128,另一台改成 priority1=129,世界清净了。
4.5 实现细节——代码里怎么写?
搞嵌入式的话,BMCA 一般都在协议栈里跑。我习惯自己写一个比较结构体,方便移植和调试。
// 定义一个用于 BMCA 比较的元组结构体
typedef struct {
uint8_t priority1; // 用户可配
uint8_t clockClass; // 从时钟模块读取
uint8_t clockAccuracy; // 从时钟模块读取
uint8_t priority2; // 用户可配
uint64_t clockIdentity; // 设备出厂唯一 ID
} BMCA_AnnounceData;
// 比较函数:返回值 1 表示 a 优于 b, 0 表示 b 优于 a
int BMCA_IsBetter(BMCA_AnnounceData *a, BMCA_AnnounceData *b) {
if (a->priority1 != b->priority1)
return (a->priority1 < b->priority1) ? 1 : 0;
if (a->clockClass != b->clockClass)
return (a->clockClass < b->clockClass) ? 1 : 0;
if (a->clockAccuracy != b->clockAccuracy)
return (a->clockAccuracy < b->clockAccuracy) ? 1 : 0;
if (a->priority2 != b->priority2)
return (a->priority2 < b->priority2) ? 1 : 0;
// 最后用 clockIdentity 硬比
return (a->clockIdentity < b->clockIdentity) ? 1 : 0;
}
这个比较函数,你想想看,其实就是一层层 if。我习惯把 priority1 和 priority2 直接在启动时从配置表里读出来,而不是写死在代码里。这样现场调试时直接在网管界面上改,不用重新编译固件。
4.6 避坑指南——我踩过的几个雷
- Announce 超时别设太短:我曾经把超时改成 10ms,想加快选举速度,结果一把 Announce 丢包,络就震荡三天。后来改回 125ms 加三次重试兜底,稳了。
- 环形拓扑要小心:环网里如果不用 STP 或者 RSTP 做链路冗余,Announce 报文会在环里死循环,导致 BMCA 永远在比较,永远不稳定。解决办法是配合 RSTP 让逻辑树无环。
- 优先级不要全设成一样:同一个链路上,如果两台交换机 priority1、priority2 全相同,最后就用 clockIdentity 硬比。虽然也能选出主时钟,但万一有一台重启,ID 不变,选举结果不变;一旦另一台重启,ID 改变了,结果就变了。这会导致逻辑上的摇摆。所以最好配一个从网管改 priority2 的习惯。
- 双主时钟问题:少数设备可能在隔离的两条链路上同时当选主时钟,合并链路后会爆炸。我建议在固件里加一个“当选主时钟后立即检查相邻端口是否也有主时钟”的校验逻辑。
嗯,这就是 BMCA 从触发到选举再到状态变更的完整过程。我个人觉得,它不算复杂,但确实容易在工程细节上摔跟头。尤其是优先级比较时的那个元组顺序,很多人背不下来,而在调试时偏偏就忘了某一位的比较规则。所以我在代码里都喜欢把那五步的注释写进去,每补一个字段就看一眼。时间允许的话,还能写个单元测试,把几个已知的边界案例跑一遍。