VRRP(虚拟路由冗余协议)常被用于网关高可用,而抢占模式是其中最容易被误解的一环。很多教程只讲配置,很少深入优先级比较和Master选举的代码逻辑。本文用Ruby构建一个简化的VRRP节点模型,用内存队列模拟组播网络,重点展示不同优先级下状态切换的细节。

实现前先明确一个关键点:抢占模式并不是简单的高优先级覆盖,它受到报文间隔、skew_time以及状态机顺序的约束。下面的内容会从协议基础一直走到可运行的Ruby代码。
一、VRRP抢占模式的工作机制
VRRP将一组路由器组成一个虚拟路由器,对外暴露一个虚拟IP地址。组内所有节点运行同样的VRID,某一时刻只有一个节点处于Master状态,负责转发发往虚拟IP的流量,其余节点处于Backup状态并持续监听Master的Advertise报文。协议标准定义优先级范围为1到254,默认值为100,数值越大表示选举优势越明显。0和255是保留值:0表示当前Master主动放弃角色,255表示该节点拥有虚拟IP地址,一般不参与普通选举。
抢占模式是VRRP默认开启的行为。当Backup收到来自Master的Advertise报文后,会提取报文中的优先级字段并与本地优先级比较。如果本地优先级高于报文优先级,Backup会立即或延迟一段时间后切换为Master,并开始发送自己的Advertise报文。即使当前Master运行正常,只要出现更高优先级节点,抢占就会发生。关闭抢占模式后,即便Backup优先级更高,也必须等待当前Master失效才能参与下一次选举。
抢占延迟并不是协议必须实现的参数,但在实际系统中经常需要配置。标准的skew_time计算公式为(256减去优先级)除以256,它用于让高优先级节点在Master失效后更快超时。计算master_down_interval时使用3倍advert间隔加上skew_time。优先级越高,skew_time越短,抢占响应越快。这个细节在后面代码中会体现为定时器的动态调整。
二、用Ruby构建VRRP核心类与状态机
为了让代码可以直接运行,这里不使用真实网络套接字,而是引入一个共享的消息总线对象。两个节点实例通过该总线交换报文,每个报文就是一个简单的哈希结构,包含VRID、发送方优先级、发送方状态以及时间戳。真实环境中这些字段会编码进ICMP或UDP报文,但简化后不影响核心逻辑。
首先定义VrrpNode类。初始化时传入优先级、VRID和消息总线,状态初始为Backup,并启动一个监听线程。状态机包含三个状态:Initialize、Backup和Master。Initialize只在启动瞬间存在,随后根据是否有更高优先级节点决定进入Master或Backup。监听线程不断从总线中取出报文,调用handle_advertise方法处理。
require 'thread'
class VrrpNode
attr_reader :vrid, :priority, :state
def initialize(vrid:, priority: 100, bus:)
@vrid = vrid
@priority = priority
@bus = bus
@state = :initialize
@master_down_interval = 3.0
@last_advert = Time.now
start_listener
end
def start_listener
Thread.new do
loop do
msg = @bus.pop
handle_advertise(msg) if msg[:vrid] == @vrid
end
end
end
def handle_advertise(msg)
return if msg[:sender].equal?(self)
case msg[:type]
when :advertise
process_advert(msg)
when :shutdown
process_shutdown(msg)
end
end
end
上面代码中的线程只负责从总线取出报文并分发。因为多个节点可能同时操作总线,实际项目里需要用Mutex或Queue保证线程安全。这里为了突出VRRP状态机,省略了互斥处理。真实环境里的VRRP报文通过组播发送,每个节点都会收到自己发出的报文,所以代码中通过sender对象判断忽略自身报文。
Advertise报文的发送不是由监听线程负责,而是由状态机触发。节点进入Master状态后会以固定间隔向总线广播Advertise报文。该间隔默认1秒,在测试中可以调小。Master还需维护一个master_down计时器,用于在长时间未收到其他节点报文时确认自身角色是否仍然有效。
三、优先级比较与Master选举逻辑实现
优先级比较需要覆盖三种情况:本地优先级高于对方、本地优先级低于对方、两者相等。前两种情况可以直接决定是否抢占或保持Backup,第三种情况则要引入IP地址比较。真实VRRP在优先级相同时选举拥有较大IP地址的节点为Master。我们的简化模型没有IP地址,可以给每个节点增加一个唯一的比较键,比如节点名称或编号。
process_advert方法的核心分支如下。当本地优先级高于对方时,如果当前不是Master并且抢占模式开启,则切换到Master。如果本地优先级低于对方,并且当前是Master,则需要降级为Backup,同时更新master_down定时器。优先级相等时比较比较键,比较键较大的一方拥有选举优势。
def process_advert(msg)
return if @state == :master && @priority > msg[:priority]
if @priority > msg[:priority]
become_master if @preempt_mode
elsif @priority < msg[:priority]
become_backup if @state == :master
else
if @compare_key > msg[:compare_key]
become_master if @preempt_mode
elsif @compare_key < msg[:compare_key]
become_backup if @state == :master
end
end
@last_advert = Time.now
end
def become_master
return if @state == :master
@state = :master
puts "Node #{@compare_key} becomes Master"
start_advert_timer
end
def become_backup
return if @state == :backup
@state = :backup
puts "Node #{@compare_key} falls back to Backup"
end
代码中的&&表示逻辑与,优先级比较使用了大于和小于号。需要留意一个容易忽略的细节:当本地节点已经是Master且收到优先级相同的报文时,如果比较键较小,也会降级为Backup。这符合VRRP规范,因为选举原则是优先级加IP地址双重比较。
抢占延迟的实现依赖定时器。一个常见的做法是记录收到低优先级报文的时间,然后启动一个延迟定时器,延迟时间可以配置为固定值,也可以使用skew_time。延迟到期后再检查当前是否仍是Backup状态,如果是才执行become_master。这种方式可以避免因网络抖动或瞬时报文导致的频繁角色切换。
四、本地模拟测试与抢占行为观察
测试脚本创建了两个节点,优先级分别为100和200。低优先级节点先启动并成为Master,高优先级节点随后加入。因为抢占模式默认开启,高优先级节点一旦收到低优先级Advertise报文,就会立即抢占。可以通过输出观察状态变化。
bus = Queue.new
node_a = VrrpNode.new(vrid: 1, priority: 100, compare_key: 'A', bus: bus)
node_b = VrrpNode.new(vrid: 1, priority: 200, compare_key: 'B', bus: bus)
node_a.instance_variable_set(:@state, :master)
node_a.send(:start_advert_timer)
sleep 0.5
bus << { vrid: 1, priority: 100, compare_key: 'A', type: :advertise, sender: node_a }
sleep 1.5
puts "Final state A: #{node_a.state}, B: #{node_b.state}"
运行这段代码后,节点B会收到节点A的Advertise报文并发现自身优先级更高,随即切换到Master状态,而节点A会降级到Backup。如果关闭节点B的抢占模式,即便优先级更高,节点A也会继续担任Master,直到自己失效。这个行为可以通过在类中增加@preempt_mode = false来测试。
除了优先级,使用skew_time计算master_down_interval也值得验证。当Master突然停止发送Advertise报文时,所有Backup会启动超时计时器,超时时间等于3倍advert间隔加skew_time。优先级较高的节点skew_time更短,会率先超时成为Master,从而避免多个Backup同时抢占。这个机制保证了在Master失效后,高优先级节点总是更快接管虚拟IP。
最后一个边界情况是优先级0和255。优先级0的报文表示当前Master主动放弃角色,收到该报文后Backup会立即开始选举,而不会等待超时。优先级255一般用于拥有虚拟IP地址的节点,它在所有节点中具有绝对优势,即便优先级不是最高也会被视为Master。实现中可以将这两个值作为特殊分支单独处理,避免与普通优先级混淆。
到这里,一个基于Ruby的简化VRRP抢占模式实现就完成了。虽然省略了真实网络报文编解码和组播细节,但状态机、优先级比较和Master选举的核心逻辑都可以直接运行并观察。理解了这些机制后,再去看Keepalived等成熟实现就不会感到陌生。