Keepalived与VRRP协议

Keepalived是LVS(Linux Virtual Server)的一个派生项目,前者基于虚拟路由冗余协议模拟路由器的高可用,而后者则是一个主要工作在4层网络中的负载均衡器。Keepalived通过路由冗余保证了LVS的高可用,LVS通过负载均衡保证了后端服务的高可用,两者协同工作,保证了系统不会出现单点故障。在有些场景下,Keepalived也可以脱离LVS单独工作,比如Keepalived与Nginx或HAProxy搭配,实现5层及以上的负载均衡与高可用。

本文介绍了VRRP协议及Keepalived的核心概念,并通过一个实例演示其用法。

VRRP

VRRP(Virtual Router Redundancy Protocol, 虚拟路由冗余协议)是由IETF提出的解决局域网中配置静态网关出现单点失效现象的路由协议。其工作原理是:局域网中多个路由器组成一主多备的冗余结构。同一时间,只有主路由器工作,并定期(默认为1秒)广播自己的状态通告(advertisement),当备用路由器在连续三个通告间隔内收不到主路由器的通告或收到优先级为0的通告时,会重新进行选举,新选举出的主路由器会使用与之前失效的主路由器相同的IP地址与MAC地址。虽然在故障切换的过程中会有短时间的网络状况异常,但整个故障切换是透明的。

概念

在VRRP协议中有几个关键的概念:

  • VRRP路由器(VRRP Router):运行VRRP协议的路由器。
  • 虚拟路由器(Virtual Router):由一个或多个VRRP路由器组成的冗余集群,做为单一的虚拟路由器对外服务。
  • 虚拟路由器编号(Virtual Router ID):也叫做VRID,必须是0~255的数字,同一组虚拟路由器集群需要使用相同的VRID。
  • 虚拟路由器IP地址(Virtual Router IP):也叫做VIP(Virtual IP Address),是虚拟路由器对外服务的IP地址,由主路由器持有。
  • 虚拟物理地址(Virtual MAC address):虚拟路由器的MAC地址会被设置为00-00-5E-00-01-[VRID],主路由器负责对ARP请求用该MAC地址做应答。
  • 优先级(Priority):组成虚拟路由器的每一个路由器都有一个优先级,优先级是1到254之间的数字,数字越大,优先级越高。在选举时,优先级高的路由器会优先被选为主路由器。

初始化、选举与故障恢复

VRRP的控制报文只有一种:VRRP通告(advertisement)。它使用IP多播数据包进行封装,组地址为224.0.0.18,发布范围只限于同一局域网内。

vrrp-packet.png

图一: VRRP通告报文的格式

Source Address为发送广播的地址;Destination Address固定为224.0.0.18;TTL必须为255;Protocol为112;Version一般为2;Type只有ADVERTISEMENT一个可选值;Virtual Rtr ID即VRID,取值区间为1~255;Priority即优先级,取值为0~255,由主路由器发送的报文的优先级必须为255,备用路由器的优先级不能大于254(默认为100),0表示当前的主路由器退出集群,并立即开始选举;Count IP Addrs当前报文中包含的IP地址;Adver Int报文的发送间隔,单位为妙,默认为1秒;后边的校验、IP地址与验证字段就不做说明了。

vrrp-state.png

图二: VRRP路由器的三种状态

一般在配置集群的时候,事先是知道哪一个路由器为主,哪些路由器为从。可以通过设置路由器的初始状态,并设置高优先级,让其一启动就直接成为主路由器,并开始广播状态通告,节省初始化时选举的时间。

集群中的备用路由器会监听主路由器的状态通告广播,如果连续三个通告间隔内收不到主路由器的通告,就认为主路由器发生了故障,会重新选举出新的主路由器。

当一个优先级更高的路由器加入集群后,会强制发起选举并成为主路由器。前主路由器的故障排除之后,重新加入集群中,往往会因为其优先级最高而重获主路由器的角色。

当前的主路由器可以通过广播优先级为0的状态通告,通知其已经放弃了主路由器的角色,并已经释放被其占用的虚拟路由器的IP地址与MAC地址。此时,高优先级的备用路由器会成为主路由器,并使用虚拟路由器的IP地址与MAC地址开始提供服务。

不论虚拟路由器里边的多个路由器的状态如何变化,虚拟路由器对外的IP地址与MAC地址始终保持不变,由当时的主路由器持有,并对外提供服务。

当主路由器发生故障并无法发送状态通告时,备用路由器判断主路由器发生故障的用时可以通过如下公式计算:

(3 * Advertisement_Interval) + (256 - Priority) / 256

如果Advertisement_Interval为默认值1秒,则备用服务器至少在3秒后才能开始新的选举,选举的过程很快,一般都会远小于1秒,此时网络会有4秒钟左右处于异常状态。计算公式中引入Priority,旨在让高优先级的路由器先发出通知报文,所以建议多个备用路由器的优先级值的差别要大一些。

应用

很多路由器都支持VRRP协议,比如思科基于IOS系统的路由器有对VRRP的支持,开源的家用路由器系统OpenWRT也基于Keepalived提供了对VRRP的支持。

在实际的使用场景中,往往会使用更复杂的集群策略,尽量减少设备闲置造成的浪费。如在下图中,rHrJ互为主备又同时可以提供服务,rE同时为rDrF提供备份。

vrrp-topology.png

图三: VRRP路由器部署拓扑图

VRRP协议虽然主要用于路由器,但其可以推广到一切使用虚拟IP地址与MAC地址来提供服务的冗余高可用集群的场景中。下边要谈到的Keepalived,就是一个开源的VRRP实现,常常被用来解决系统中的单点故障问题。

Keepalived

Keepalived官方对自己的定义是一个用C语言写的路由软件。其主要可以做以下几件事情:

  • 基于VRRP协议的路由器(或服务器)的故障切换
  • VRRP路由器实例的状态变更时,可以运行指定的脚本,脚本可以做很多事情,例如自动主备切换、自动故障修复等工作
  • 内置邮件通知功能,在VRRP路由器实例的状态变更时,发送通知邮件
  • 基于来自LVS项目的IPVS Linux内核模块提供4层网络的负载均衡
  • 负载均衡目标服务器的健康检查及负载均衡池的动态管理,内置了TCP_CHECKHTTP_GETSSL_GETMISC_CHECK四种检查器,其中MISC_CHECK可以运行任何系统脚本

本文只关注与VRRP相关的前三项功能,LVS负载均衡与健康检查涉及的内容太多,会在稍后单写一篇文章来介绍。

架构

keepalived-design.gif

图四: Keepalived架构图

在Keepalived启动后,主进程会启动两个子进程,一个负责VRRP的工作,一个负责负载均衡的健康检查工作,每个子进程都有自己的非阻塞的网络接口(scheduling I/O multiplexer)与事件调度器,这样两者就不会相互影响,尤其可以防止健康检查影响更敏感也更重要的VRRP的工作。

主进程叫做WatchDog,使用进程间通信机制(unix domain socket)监控子进程的工作状态,它每隔5秒向子进程发送一个hello数据包,如果发送失败,主进程首先使用sysV信号来检测子进程,如果检测子进程已死,则重启子进程。

作为一个路由软件,Keepalived与IO的关系非常密切,处于性能方面的考虑,它没有使用现有的线程库(POSIX thread libs),而是实现了自己的事件调度线程模型。所有事件都由该调度器处理,包括主进程的hello检测事件,这样主进程就可以检测到子进程的死循环(deadloop)。

配置

Keepalived只有唯一一个配置文件keepalived.conf,一般在/etc//etc/keepalived/路径下,其内容包括三个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 全局配置,主要是关于邮件通知的内容
global_defs {
notification_email {
email
email
}
notification_email_from email
smtp_server host
smtp_connect_timeout num
lvs_id string
}
# VRRP实例配置
vrrp_instance string {
state MASTER|BACKUP
interface string
mcast_src_ip @IP
lvs_sync_daemon_interface string
virtual_router_id num
priority num
advert_int num
smtp_alert authentication {
auth_type PASS|AH
auth_pass string
}
virtual_ipaddress {
@IP
@IP
@IP
}
virtual_ipaddress_excluded {
@IP
@IP
@IP
}
notify_master /path_to_script/script_master.sh
(or notify_master “/path_to_script/script_master.sh <arg_list>”)
notify_backup /path_to_script/script_backup.sh
(or notify_backup “/path_to_script/script_backup.sh <arg_list>”)
notify_fault /path_to_script/script_fault.sh
(or notify_fault “/path_to_script/script_fault.sh <arg_list>”)
}
# 同一组内的VRRP实例会保持状态同步
vrrp_sync_group string {
group {
string
string
}
notify_master /path_to_script/script_master.sh
(or notify_master “/path_to_script/script_master.sh <arg_list>”)
notify_backup /path_to_script/script_backup.sh
(or notify_backup “/path_to_script/script_backup.sh <arg_list>”)
notify_fault /path_to_script/script_fault.sh
(or notify_fault “/path_to_script/script_fault.sh <arg_list>”)
}
# 负载均衡配置
virtual_server (@IP PORT)|(fwmark num) {
delay_loop num
lb_algo rr|wrr|lc|wlc|sh|dh|lblc
lb_kind NAT|DR|TUN
(nat_mask @IP)
persistence_timeout num
persistence_granularity @IP
virtualhost string
protocol TCP|UDP
sorry_server @IP PORT
real_server @IP PORT {
weight num
TCP_CHECK {
connect_port num
connect_timeout num
}
}
real_server @IP PORT {
weight num
MISC_CHECK {
misc_path /path_to_script/script.sh
(or misc_path “/path_to_script/script.sh <arg_list>”)
}
}
real_server @IP PORT {
weight num
HTTP_GET|SSL_GET {
url {
path alphanum
digest alphanum
}
connect_port num
connect_timeout num
nb_get_retry num
delay_before_retry num
}
}
}

实验

这里使用虚拟机实验一下keepalived的VRRP故障切换功能,网络拓扑如下图:

lab-topology.png

图五: 实验网络拓扑图

实验的脚本可以在https://github.com/sean-liang/labs/tree/master/keepalived找到,实验机器需要支持硬件虚拟化、64位CPU及操作系统、4GB内存,并需要安装VirtualBoxVagrant

router1router2client目录下分别运行vagrant up命令,在router1router2命令行中应该可以看到如下的keepalived日志,说明keepalived的VRRP集群运行成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# router1
==> default: Starting VRRP child process, pid=2552
==> default: Registering Kernel netlink reflector
==> default: Registering Kernel netlink command channel
==> default: Registering gratuitous ARP shared channel
==> default: Opening file '/vagrant/keepalived.conf'.
==> default: Configuration is using : 60584 Bytes
==> default: Using LinkWatch kernel netlink reflector...
==> default: VRRP_Instance(VI) Transition to MASTER STATE
==> default: VRRP_Instance(VI) Entering MASTER STATE
# router2
==> default: Starting VRRP child process, pid=2552
==> default: Registering Kernel netlink reflector
==> default: Registering Kernel netlink command channel
==> default: Registering gratuitous ARP shared channel
==> default: Opening file '/vagrant/keepalived.conf'.
==> default: Configuration is using : 60584 Bytes
==> default: Using LinkWatch kernel netlink reflector...
==> default: VRRP_Instance(VI) Entering BACKUP STATE

如果此时在router1中使用ip addr查看网络状态,可以得到如下的内容:

1
2
3
4
5
6
7
8
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:30:43:02 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.10/24 brd 192.168.33.255 scope global eth1
valid_lft forever preferred_lft forever
inet 192.168.33.254/32 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fe30:4302/64 scope link
valid_lft forever preferred_lft forever

可以看到虚拟路由器的IP地址192.168.33.254/32现在绑定在router1eth1上,同样的方式查看router2,应该不会看到有该IP地址的绑定。

client中使用命令ping -D 192.168.33.254,可以ping通虚拟路由器的地址,此时使用virtualbox管理界面强制关闭router1,并观察ping的结果,可以看到:

1
2
3
4
5
6
[1511076396.123243] 64 bytes from 192.168.33.254: icmp_seq=10 ttl=64 time=0.316 ms
[1511076397.124273] 64 bytes from 192.168.33.254: icmp_seq=11 ttl=64 time=0.349 ms
[1511076398.125779] 64 bytes from 192.168.33.254: icmp_seq=12 ttl=64 time=0.367 ms
[1511076402.138889] 64 bytes from 192.168.33.254: icmp_seq=16 ttl=64 time=0.651 ms
[1511076403.140476] 64 bytes from 192.168.33.254: icmp_seq=17 ttl=64 time=0.502 ms
[1511076404.139745] 64 bytes from 192.168.33.254: icmp_seq=18 ttl=64 time=0.480 ms

ping是每隔一秒发送一个数据包,在第3条与第4条之间,有4秒的延迟,故障切换就是在这时发生的。如果此时看router2的日志,可以看到:

1
2
==> default: VRRP_Instance(VI) Transition to MASTER STATE
==> default: VRRP_Instance(VI) Entering MASTER STATE

查看router2的网络状态,可以看到虚拟路由器的IP地址已经被绑定到了eth1上:

1
2
3
4
5
6
7
8
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:d6:f5:c7 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.20/24 brd 192.168.33.255 scope global eth1
valid_lft forever preferred_lft forever
inet 192.168.33.254/32 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fed6:f5c7/64 scope link
valid_lft forever preferred_lft forever

使用vagrant up再次启动router1,并运行命令sudo keepalived -nlP -f /vagrant/keepalived.conf启动keepalived,可以看到router1再次成为主路由器,它们的日志为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# router1
Starting VRRP child process, pid=980
Registering Kernel netlink reflector
Registering Kernel netlink command channel
Registering gratuitous ARP shared channel
Opening file '/vagrant/keepalived.conf'.
Configuration is using : 60584 Bytes
Using LinkWatch kernel netlink reflector...
VRRP_Instance(VI) IPSEC-AH : seq_num sync
VRRP_Instance(VI) Transition to MASTER STATE
VRRP_Instance(VI) Received lower prio advert, forcing new election
VRRP_Instance(VI) IPSEC-AH : Syncing seq_num - Increment seq
VRRP_Instance(VI) Entering MASTER STATE
# router2
==> default: VRRP_Instance(VI) Received higher prio advert
==> default: VRRP_Instance(VI) IPSEC-AH : Syncing seq_num - Decrement seq
==> default: VRRP_Instance(VI) Entering BACKUP STATE

从下边ping的结果,可以看到,重新选举有2秒的延迟:

1
2
3
4
5
6
[1511078462.829092] 64 bytes from 192.168.33.254: icmp_seq=2 ttl=64 time=0.292 ms
[1511078463.828161] 64 bytes from 192.168.33.254: icmp_seq=3 ttl=64 time=0.360 ms
[1511078464.827925] 64 bytes from 192.168.33.254: icmp_seq=4 ttl=64 time=0.447 ms
[1511078466.839188] 64 bytes from 192.168.33.254: icmp_seq=6 ttl=64 time=0.580 ms
[1511078467.840747] 64 bytes from 192.168.33.254: icmp_seq=7 ttl=64 time=0.318 ms
[1511078468.843448] 64 bytes from 192.168.33.254: icmp_seq=8 ttl=64 time=0.777 ms

以上实验,验证了VRRP选举的逻辑,以及根据公式计算出来的4秒左右的故障恢复时间。另使用SSH模拟TCP连接的客户端,在故障切换之后,SSH的连接断开,需要重新连接。

参考