4. 最佳主时钟算法(BMCA):选举过程、优先级比较与实现细节

好,咱们来聊聊 BMCA。这名字听着挺唬人,说白了就是 TSN 时钟领域的一场“选拔赛”。在网络里,谁当老大(主时钟),谁当小弟(从时钟),全凭它说了算。我记得刚接触 那会儿,总觉得 BMCA 也就是比大小,后来在项目里被一个优先级问题折腾了两天,才彻底搞明白——这算法,比你想的讲究。

4.1 选举什么时候触发?

不是时时刻刻都在选老大。选一次,管一阵。那么什么时候重选呢?主要就这几种情况:

  • 新设备加入:有人插队,当然要看看它有没有资格当老大。
  • 现有主时钟挂了:老大失联了,存活广播 Announce 超时,群龙无首,赶紧重选。
  • 收到更优的设备信息:有个节点突然广播说“我比现在的主时钟更好”,那也得重新比划比划。
  • 端口状态变化:比如某台交换机的链路断了又恢复,端口状态变了好几次,也得触发选举。
我的小经验:Announce 报文的发送间隔默认是 125ms。别调得太频繁,否则网络里全是选票广播,反而容易丢帧。

4.2 BMCA 的状态机——时钟端口的五个身份

每个端口都处在下面五种状态之一。说白了,就是给端口定了个职称:

端口状态 含义 能不能参与选举
Master 本端口输出的时钟就是主时钟,发给所有从端口 是
Slave 本端口同步到别的主时钟上,自己的时钟不对外广播 是
Passive 本端口既不发送主时钟,也不同步到别人,但还活着,只是旁听 是
Disabled 端口被管理员关了,或者硬件故障 否
Faulty 端口出错了,比如 CRC 错误太多或链路闪断 否

实际选举只在 Master、Slave、Passive 三个状态之间玩。说白了,选举就是一场“谁能当 Master,谁该做 Slave,谁乖乖去 Passive”的游戏。

4.3 优先级比较——谁才是真正的老大?

好,选举开始了。BMCA 不只看一个指标,它看一套组合拳。每次比较都会依次对比下面这四个属性,一旦分出胜负,后面的就不看了。

  1. priority1:管理员可以手动改,数值越小越牛。范围 0~255。
  2. clockClass:时钟的等级,反映时钟的溯源能力。比如跟北斗/GPS 同步的时钟,class 就很小;本地普通晶振,class 就很大。数值越小越准。
  3. clockAccuracy:时钟的精度,还是数值越小越准。
  4. priority2:又是一个管理员可配的参数,跟 priority1 一样,数值越小越优先。做冗余备份时用得多。
  5. clockIdentity(后备):如果前面四项都一样,就用这个 64 位的全球唯一 ID 做决胜局,再一样的概率约等于零。

每次 Announce 报文里都带着这些字段。收到报文的节点会把自己的这一套跟对方比一遍。看看谁数值更小,谁就胜出。

4.4 具体怎么选?——BMCA 的执行逻辑

我画了个流程图,更直观些:

BMCA 选举核心流程 选举触发 收集所有端口的 Announce 信息 优先级比较(元组对比) 优选出最佳候选者(Best Master) 选定主时钟 / 更新端口状态 避坑提示 同一条链上不同端口的 priority1/2 必须一致

你看,从左到右走一遍:触发 -> 收集 -> 比较 -> 决策 -> 结果。每一步都不多余。实际咱们工程上经常在这个过程里丢帧,原因就是 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 会自动修复一切配置错误。如果 priority1 配错了,或者时钟等级(class)读错了,BMCA 会坚定地选出一个错误的主时钟——而且它不会报错,只会默默同步。

嗯,这就是 BMCA 从触发到选举再到状态变更的完整过程。我个人觉得,它不算复杂,但确实容易在工程细节上摔跟头。尤其是优先级比较时的那个元组顺序,很多人背不下来,而在调试时偏偏就忘了某一位的比较规则。所以我在代码里都喜欢把那五步的注释写进去,每补一个字段就看一眼。时间允许的话,还能写个单元测试,把几个已知的边界案例跑一遍。