在运行生成树协议的交换网络中,拓扑变化是一件既平常又危险的事情:端口翻动、链路故障、新设备接入都会触发STP重新计算,而每一次重新计算都可能带来几秒到几十秒的转发中断。如果运维人员只能事后靠用户投诉发现网络抖动,故障定位的难度会成倍增加。本文将介绍如何用Ruby编写一套监控程序,实时捕捉生成树拓扑变化事件,把每一次拓扑翻转的桥设备、端口和时间点都记录下来,为网络故障排查提供第一手数据。

理解生成树拓扑变化的底层机制
要监控拓扑变化,必须先知道它以什么形式暴露出来。生成树协议通过BPDU(Bridge Protocol Data Unit)报文在交换机之间传递信息,其中涉及拓扑变化的核心机制是TCN BPDU。当一台交换机的指定端口检测到链路由转发状态进入阻塞或失效状态,或者非指定端口转变为转发状态时,该交换机就会从根端口向根桥方向发送TCN BPDU,上游交换机收到后会回复TCA确认,并将TCN继续向根桥传递,直到根桥收到后发出带TC标志的配置BPDU,通知全网交换机缩短MAC地址表的的老化时间,从而加速重新学习转发路径。
对监控程序而言,这些事件有两个可观察的出口。第一个出口是SNMP Trap:绝大多数交换机在检测到生成树拓扑变化时会发送标准或厂商私有的Trap报文,例如STP拓扑变化Trap中通常携带dot1dStpTopChanges计数器的新值和发生变化的端口号。第二个出口是网桥MIB中的计数器:BRIDGE-MIB定义了dot1dStpTopChanges这个只读计数器,它记录了自设备启动以来拓扑变化发生的总次数,通过定期轮询并比对前后值,就能间接感知拓扑变化事件。
两条路径各有优劣:Trap方式实时性强、无需高频轮询,但要求网络设备正确配置了Trap目标并且SNMP Trap报文采用v2c或v3的团体字与认证信息;轮询方式实现简单、兼容性好,即使老设备不支持Trap也能工作,但实时性受轮询间隔限制。实际部署中往往两者结合,Trap负责实时告警,轮询负责兜底核对。
基于SNMP Trap接收拓扑变化告警的Ruby实现
接收SNMP Trap最常用的Ruby库是net-snmp系列中的snmp gem,它提供了TrapListener类,可以绑定UDP 162端口监听Trap报文。使用前需要通过gem安装依赖:
gem install snmp
下面的代码演示了一个完整的Trap监听器,它会过滤出生成树拓扑变化相关的Trap,并提取出桥MAC地址、端口号和计数器信息。注意标准BRIDGE-MIB中拓扑变化Trap的OID是1.3.6.1.2.1.17.0.2(topologyChange),很多厂商还有私有OID,可以在厂商文档中查询后加入匹配列表:
require 'snmp'
require 'time'
# 生成树拓扑变化相关的Trap OID列表
STP_TRAP_OIDS = [
'1.3.6.1.2.1.17.0.2', # BRIDGE-MIB topologyChange
'1.3.6.1.4.1.9.9.82.0.1' # Cisco私有 STP拓扑变化Trap
].freeze
listener = SNMP::TrapListener.new(host: '0.0.0.0', port: 162) do |manager|
manager.on_trap_v2c do |trap|
next unless STP_TRAP_OIDS.include?(trap.trap_oid.to_s)
puts "[#{Time.now.iso8601}] 检测到拓扑变化"
puts " 来源设备: #{trap.source_ip}"
puts " Trap OID: #{trap.trap_oid}"
# 遍历变量绑定,提取端口与计数器信息
trap.varbind_list.each do |vb|
case vb.name.to_s
when /dot1dStpTopChanges/
puts " 拓扑变化总次数: #{vb.value}"
when /dot1dBasePort/
puts " 变化端口: #{vb.value}"
else
puts " #{vb.name} => #{vb.value}"
end
end
# 此处可以接入告警逻辑:写日志、发邮件、推送消息等
log_topology_change(trap)
end
end
def log_topology_change(trap)
File.open('stp_changes.log', 'a') do |f|
f.puts("#{Time.now.iso8601}|#{trap.source_ip}|#{trap.trap_oid}")
end
end
puts 'Trap监听器已启动,等待拓扑变化事件...'
listener.join代码中有几个细节值得注意。TrapListener默认以阻塞方式运行,调用join后主线程会等待,适合作为独立守护进程部署;如果需要同时处理其他任务,可以把监听器放到独立线程中。其次,不同厂商的Trap变量绑定差异较大,Cisco设备通常携带vlan索引和端口ifIndex,而华为设备可能使用私有MIB节点名,建议先用snmptrapd工具抓一次真实报文,确认字段后再完善解析逻辑。
在交换机侧还需要做对应配置,以Cisco为例,需要开启SNMP Trap并指定接收服务器地址。配置完成后可以手动将某个端口shutdown再no shutdown来触发一次拓扑变化,验证Ruby监听器能否正确收到事件。这种主动制造事件的方式在联调阶段非常高效。
轮询dot1dStpTopChanges计数器的主动监控方案
对于不支持Trap或者Trap链路不稳定的场景,轮询是更稳妥的选择。核心思路是维护每台设备上一次采样到的dot1dStpTopChanges值,一旦本次采样值大于上次值,就判定发生了拓扑变化,差值即为这段时间内变化的次数。下面是一个支持多设备的轮询脚本:
require 'snmp'
DEVICES = [
{ host: '192.168.1.10', community: 'public' },
{ host: '192.168.1.11', community: 'public' }
].freeze
TOP_CHANGES_OID = '1.3.6.1.2.1.17.2.15.0' # dot1dStpTopChanges.0
SYS_NAME_OID = '1.3.6.1.2.1.1.5.0' # sysName 用于标识设备
last_values = Hash.new { |h, k| h[k] = 0 }
loop do
DEVICES.each do |dev|
SNMP::Manager.open(host: dev[:host], community: dev[:community]) do |manager|
response = manager.get([TOP_CHANGES_OID, SYS_NAME_OID])
current = response[TOP_CHANGES_OID].to_i
sysname = response[SYS_NAME_OID].to_s
if last_values[dev[:host]] > 0 && current > last_values[dev[:host]]
diff = current - last_values[dev[:host]]
puts "[#{Time.now}] #{sysname}(#{dev[:host]}) 检测到#{diff}次拓扑变化,累计#{current}次"
end
last_values[dev[:host]] = current
end
rescue SNMP::RequestTimeout, Errno::EHOSTUNREACH => e
warn "[#{Time.now}] 采集#{dev[:host]}失败: #{e.message}"
end
sleep 30 # 每30秒轮询一次
end轮询间隔的设置需要在实时性与设备负载之间权衡。企业接入层交换机的SNMP处理能力有限,间隔低于10秒可能造成CPU升高;而间隔超过5分钟又会错过短暂连续抖动的细节。一般建议30秒到60秒之间。另外要注意计数器回绕问题:如果设备重启,dot1dStpTopChanges会归零,脚本判断条件是current大于last,回绕时不会误报,但可以在检测到sysUpTime(OID 1.3.6.1.2.1.1.3.0)变小的时候主动重置基线。
还有一个容易被忽略的坑:dot1dStpTopChanges属于BRIDGE-MIB,当交换机存在多个VLAN且每个VLAN独立运行STP时,这个MIB默认只反映VLAN 1(或管理VLAN)的实例。要监控所有VLAN的实例,需要结合IEEE8021-Q-BRIDGE-MIB,通过SNMPv3的上下文名(context,例如vlan-100)分别采集各个VLAN的桥实例数据。net-snmp库对context的支持需要确认版本,必要时可以直接在采集脚本中为每个VLAN建立独立的Manager会话。
两种方案的对比与生产环境建议
从架构角度看,Trap方式和轮询方式对应了事件驱动与状态轮询两种经典监控模型。Trap的实时性通常在秒级以内,CPU和带宽开销几乎可以忽略,但可靠性依赖UDP传输,报文丢失后不会重传,且交换机配置变更可能悄悄中断Trap通道。轮询是拉模式,链路状态可验证,丢失一次采样影响有限,但高频轮询对大规模网络的管理平面压力明显。
生产环境中最可靠的做法是双轨并行:Trap负责第一时间的告警推送,轮询每隔几分钟做一次全量核对,当轮询发现计数器增长但期间没有收到对应Trap时,说明Trap链路可能存在问题,此时应触发链路自检告警。同时建议把每一次拓扑变化事件持久化到数据库,字段包括时间戳、设备、端口、VLAN和计数器值,长期积累后可以分析出哪些端口频繁抖动,为更换线缆或光模块提供数据依据。
在工程实现上,还可以把Ruby脚本封装为systemd服务,利用daemons或sidekiq等组件做进程守护,把告警输出对接到Prometheus的Pushgateway或者企业微信、钉钉机器人webhook,让拓扑变化监控真正融入现有运维体系。整体而言,Ruby凭借简洁的语法和成熟的snmp库,足以支撑一套轻量但完整的生成树拓扑监控系统,代码量不大却能在网络故障发生前提前发出预警。