VRRP(虚拟路由冗余协议)常用于双机网关场景,主路由器故障时备路由器根据优先级接管虚拟IP。但很多实际组网中,主路由器的上行链路故障并不会导致VRRP协议本身中断,接口仍可能保持转发状态,此时如果不用对象跟踪,虚拟IP仍停留在主设备上,造成黑洞。用Ruby实现一个轻量级跟踪器,可以让自定义逻辑更贴合业务网络。下面从对象跟踪的价值、接口状态采集、路由表检查和优先级计算几个部分展开。

一、VRRP对象跟踪要解决的真实问题
固定优先级是VRRP常见配置方式。两台设备都配置VRRP后,优先级高的一方成为Master,低的一方保持Backup。只有当Master完全宕机或者VRRP心跳报文丢失时,Backup才会触发抢占。然而网络故障并不总是表现为整机宕机,更多时候是部分链路异常。例如Master的上行接口down了,但下行接口和VRRP心跳仍然正常,这时Master会继续通告自己拥有虚拟IP,而实际上已经无法把流量转发到外网。业务流量到达Master后无法出去,造成黑洞。
对象跟踪就是在VRRP实例之外增加额外的健康检查维度。可以跟踪某个接口的物理或运行状态,也可以跟踪路由表中是否存在某条关键路由,比如默认路由或到核心网段的路由。一旦跟踪对象失效,就把本机VRRP优先级降低一个幅度。如果降低后的优先级低于备机,备机就会抢占成为新的Master,从而恢复业务。跟踪对象的状态采集通常由独立进程或脚本完成,Ruby脚本可以作为其中的数据源,灵活实现比固定配置更精细的切换策略。
二、用Ruby读取接口运行状态
Linux系统中,网络接口的运行状态可以直接从sysfs读取,路径为/sys/class/net/接口名/operstate。这个文件的内容不是简单的字符串,常见值为up、down、unknown。相比执行ip link show再解析输出,读取sysfs开销更低,适合高频轮询。Ruby的File.read可以轻松完成。如果文件不存在,说明接口本身不存在,应视为不可用。以下是一个判断接口是否可用的辅助方法。
def interface_up?(name)
operstate = "/sys/class/net/#{name}/operstate"
return false unless File.exist?(operstate)
File.read(operstate).strip == 'up'
end该方法只判断运行状态是否为up。有些场景下管理员可能更关心物理链路状态,可以改读/sys/class/net/接口名/carrier,1表示有载波,0表示无载波。对于聚合接口或隧道接口,operstate可能短暂处于unknown,需要结合重试机制,避免因瞬时抖动误判。通常建议连续两次采样都失败才确认接口故障,降低误切换概率。
如果需要监控多个接口,可以把接口名放入数组,使用reject找出所有不可用接口。这种写法比逐个if判断更简洁,也方便统计失败数量。Ruby的块语法写起来很自然,代码可读性也更好。实际项目中还可以把接口名放在配置文件中,由初始化方法读取,方便后续扩展。
三、路由表跟踪的实现思路
接口up并不意味着路由一定正常。动态路由协议可能在接口up后还没有完成收敛,或者上游设备撤销了某条路由,导致本机路由表中缺少到达目标网段的表项。因此跟踪路由表条目比单纯跟踪接口更接近真实转发能力。常见需要跟踪的是默认路由、管理网段路由、核心业务网段路由。如果这些路由消失,说明本机已经无法将流量送到目标网络,应该让出Master角色。
在Linux上检查路由是否存在有两种常用方式:解析/proc/net/route,或者执行ip route show 目标前缀。前者不需要外部命令,但解析格式比较复杂;后者调用系统命令,输出直观,适合脚本场景。Ruby中可以使用Open3.capture3安全地执行命令并捕获标准输出和退出状态,避免shell注入和重定向符号带来的转义问题。下面代码封装了一个route_exists?方法。
require 'open3'
def route_exists?(prefix)
stdout, _stderr, status = Open3.capture3('ip', 'route', 'show', prefix)
status.success? && !stdout.strip.empty?
end
puts route_exists?('default')
puts route_exists?('10.0.0.0/8')Open3.capture3会把参数作为独立argv传递,避免前缀中包含空格或特殊字符时被shell解释。status.success?表示命令成功执行,再结合stdout是否为空判断路由是否存在。如果只是命令执行成功但输出为空,说明没有匹配路由,仍然不可用。使用这种方式不需要处理shell重定向,代码更健壮。如果目标系统没有ip命令,可以退回到解析/proc/net/route,但为保持简单,本文采用ip命令方案。
四、整合为可运行的VRRP跟踪类
将接口状态检查和路由表检查合并到一个类中,每次轮询统计失败对象数量,再根据惩罚幅度计算新的优先级。VRRP优先级范围通常是1到254,数值越大越优先。假设基础优先级为100,每个失败对象扣40分,如果两个对象同时失效,优先级降到20,低于备用设备的默认值100,备机就会抢占。实际部署中惩罚值需要根据双方配置计算,保证Master失效后一定能低于Backup。
下面给出完整的VrrpTracker类。它初始化时接收需要跟踪的接口列表和路由前缀列表,提供update_priority方法进行一次完整检查并更新状态。代码中没有引入额外依赖,只使用标准库中的open3和file操作,适合在最小化安装的Linux系统上运行。
require 'open3'
class VrrpTracker
attr_reader :base_priority, :current_priority, :state
def initialize(interfaces: [], routes: [], base_priority: 100, down_penalty: 50)
@interfaces = interfaces
@routes = routes
@base_priority = base_priority
@down_penalty = down_penalty
@current_priority = base_priority
@state = 'BACKUP'
end
def check_interfaces
@interfaces.reject { |iface| interface_up?(iface) }
end
def check_routes
@routes.reject { |route| route_exists?(route) }
end
def update_priority
failed_count = check_interfaces.size + check_routes.size
penalty = failed_count * @down_penalty
@current_priority = [@base_priority - penalty, 0].max
@state = if @current_priority < 100
'BACKUP'
else
'MASTER'
end
end
private
def interface_up?(name)
operstate = "/sys/class/net/#{name}/operstate"
return false unless File.exist?(operstate)
File.read(operstate).strip == 'up'
end
def route_exists?(prefix)
stdout, _stderr, status = Open3.capture3('ip', 'route', 'show', prefix)
status.success? && !stdout.strip.empty?
end
end
tracker = VrrpTracker.new(
interfaces: ['eth0', 'eth1'],
routes: ['default', '10.0.0.0/8'],
base_priority: 100,
down_penalty: 40
)
tracker.update_priority
puts "current priority: #{tracker.current_priority}"
puts "state: #{tracker.state}"update_priority中把失败对象数量乘以惩罚值,再从基础优先级中扣除。使用[计算值, 0].max防止优先级低于0。状态切换的条件可以按需调整,比如当优先级低于80就降为BACKUP,而不一定非要低于100。此外可以在状态变化时打印日志或执行自定义脚本,例如调用keepalived的通知脚本。轮询间隔建议设置为1到3秒,过短会增加系统负载,过长则影响切换速度。
五、测试方法与注意事项
在测试环境中,可以用ip link set eth0 down和ip link set eth0 up模拟接口故障与恢复,用ip route del default和ip route add default via 网关模拟路由丢失和恢复。每次修改后运行tracker.update_priority,观察current_priority和state的变化是否符合预期。如果当前用户没有读取sysfs或执行ip命令的权限,需要以root身份运行或配置sudoers。
需要注意不同Linux发行版的网络接口命名和sysfs路径可能存在差异,容器环境中/sys可能只读或缺失。对于容器化部署,建议通过环境变量传入接口名和路由前缀,避免硬编码。如果系统使用NetworkManager或其他网络管理工具,operstate的变化可能存在一定延迟,需要适当增加重试次数。另外VRRP实际宣告和抢占动作仍然需要由支持VRRP的守护进程完成,Ruby跟踪器负责计算优先级并通知该进程,两者通过文件或信号交互。
整体来看,Ruby实现VRRP对象跟踪并不复杂,关键在于把接口状态和路由表状态抽象成统一的可检查对象,再用惩罚机制影响优先级。这种思路可以方便地扩展到更多跟踪维度,例如ping探测、TCP端口检查或自定义脚本输出。把跟踪逻辑从VRRP守护进程中独立出来,也让测试和维护都更加直观。

