一、LVS负载均衡集群的缺点

二、Keepalived介绍

三、Keepalived的功能

四、Keepalived工作原理

五、Keepalived组件框架

六、Keepalived的安装

  6.1 高可用集群配置的前提

  6.2 Keepalived配置

  6.3 Keepalived高可用其它服务



一、LVS负载均衡集群的缺点


在之前的一篇博客中介绍了有关LVS的工作原理,详情可参考《LVS工作原理》。LVS能够实现四层负载,能够支持足够大的并发量,但使用LVS负载均衡集群有以下两个缺点。

① 如果调度器(Director)挂了(不可用),将会导致整个系统不可用,从而调度器成为了单点故障(SPOF)。

② 调度器(Director)无法对后端RealServer做健康状态检测。因此,如果后端的某一台RealServer挂了,前端调度器将无法得知,仍然会向该RealServer调度请求,导致服务不可用;另外,如果某一台挂了的RealServer但已经恢复正常并重新提供服务时,前端调度器也无法得知,并将其加入调度队列。

因此,为了能够使前端调度器(Director)能够冗余、使Director能够对后端各RS做健康状态检测,并按需增删RS,需要引入高可用集群的解决方案。以下介绍的是Keepalived高可用软件。



二、Keepalived介绍


Keepalived是VRRP协议的实现,原生设计目的是高可用IPVS服务。此外,Keepalived能够根据配置文件的规则生成IPVS规则,并能够对各RealServer的健康状态进行检测。


Keepalived的高可用功能是通过VRRP协议实现的,VRRP是Virtual Router Redundancy Protocol(虚拟路由器冗余协议)的缩写。VRRP的出现是为了解决静态路由单点故障的问题,当某一节点出现故障时,可以防止导致整个网络不可用。Keepalived除了可以高可用LVS之外,还可以作为其他系统网络服务(Nginx、Haproxy等)的高可用解决方案(Keepalived可通过调用vrrp_script来高可用其它服务,并通过调用vrrp_track来追踪每一个服务)。



三、Keepalived的功能


Keepalived有三个重要功能,如下。

(1) 高可用系统网络服务。

Keepalived可以实现在两台或多台主机之间的故障切换转移。如果在两台主机中都安装了Keepalived,当正常工作时,有一台主机工作为Master角色,另一台主机工作为Backup角色。角色为Master的主机获得所有资源(VIP资源、服务资源)并向用户提供服务,角色为Backup的主机不提供服务而仅作为Master主机的热备。当角色为Master的主机出现故障时,角色为Backup的主机将自动接管Master主机的所有资源(VIP资源、服务资源)并开始工作。当Master主机故障修复完成时,将重新接管原来的资源和工作,而Backup主机则释放Master主机故障时它接管的资源和工作,各自恢复原来的角色。

(2) 实现对LVS集群中各RealServer的健康状态进行检测。

Keepalived可以通过在自身配置文件keepalived.conf中配置LVS集群服务中各台RealServer的IP地址和相关参数,并可以通过网络层、传输层和应用层这三层进行探测各RealServer的健康状态。当有一台或多台RealServer出现故障而无法提供服务时,Keepalived服务可以把出现故障的RealServer从LVS的正常转发队列中移除,保证不影响用户的访问。而当有RealServer故障修复完成时,Keepalived服务可以将其重新加入LVS的正常转发队列中,向用户提供服务。

(3) 管理LVS负载均衡软件。

Keepalived可以读取配置文件,并通过一个更为底层的接口来管理IPVS并生成IPVS规则,这使得LVS的使用更为方便。



四、Keepalived工作原理


前面提到,Keepalived的高可用功能是通过VRRP协议实现的,要了解Keepalived的工作原理,需要先了解VRRP协议的工作原理。


4.1 VRRP工作原理


VRRP,是Virtual Router Redundancy Protocol的缩写,中文名为虚拟路由器冗余协议。VRRP的出现是为了解决静态路由的单点故障问题,早期应用于交换机、路由器等设备中。VRRP协议是通过竞选机制来实现将路由任务交给某一台VRRP路由器的。


在一组VRRP路由器中,可以有多台物理路由器,但只有一台称为Master的路由器负责路由任务(响应ARP请求、转发发送给网关的数据包),而其他路由器都是Backup角色。Master和Backup是通过竞选机制选举出来的,这种竞选机制是根据优先级的高低来选举,在这一组VRRP路由器中优先级最高的路由器就是Master,其他路由器为Backup。Master路由器需要基于IP组播(默认组播地址为224.0.0.18)的方式发送心跳消息给其它的Backup路由器,告诉其它路由器自己还活着,同时告知Master自己的优先级。而当Master路由器发生故障时,就没法发送心跳消息,这样Backup路由器就检测不到Master的心跳消息了,这时多台Backup路由器就会再通过同样的竞选机制,选举出优先级最高的一台作为新的Master路由器,并接管路由任务(响应ARP请求、转发发送给网关的数据包)。


虚拟路由器由VRID(虚拟路由器标识,0-255),对外部则表现为一个VMAC(Virtual MAC)地址:00-00-5E-00-01-{VRID}和一个VIP(Virtual IP)。


4.2 Keepalived工作原理


Keepalived服务的高可用对之间的通信是基于VRRP协议进行通信的,VRRP是通过竞选机制来确定主、备节点的,主节点的优先级高于备节点。当Keepalived服务正常工作时,主Master节点接管IP资源和服务资源,向用户提供服务,并不断地向备Backup节点发送心跳消息(以组播的方式),用以告知备节点自己还活着,而备节点收到心跳消息后,一方面得知主节点还活着,另一方面得知主节点的优先级仍高于自己,因此不敢“犯上”。当主Master节点出现故障时,就无法发送心跳消息,所以备Backup节点就收不到主节点发来的心跳消息,这时所有备节点就会再此互相发送协议报文进行协商(再次进行竞选),各备节点将自己的优先级通告给所有其他备节点,最后优先级高的备节点则为新的主Master节点,由其调用自身的接管程序接管之前主节点的IP资源和服务资源,向用户提供服务。当先前的主Master节点故障修复完成时,则会再次接管IP资源和服务资源(如果Keepalived工作于抢占模式的话),继续向用户提供服务,而后来接管IP资源和服务资源的备Backup节点会释放接管的资源,再次成为备节点。


导致主、备节点切换的原因不仅仅是主节点发生了故障,也有可能是主Master节点被人为降低优先级(降到比备节点还低),这时备Backup节点会发现主节点的优先级比自己还低,同样会抢占资源并成为主节点。


前面提到,Keepalived主要由三个功能,此处介绍的原理只是其中之一,另外两个功能较为简单,其原理可以参考前面第三点。



五、Keepalived的组件框架


Keepalived的框架可以参考下图(图片来自Keepalived官网)。

wKioL1mD9QrSE125AACUn7rdsas574.gif

Keepalived的组件有控制组件(配置文件分析器)、内存管理、I/O复用和核心组件。其中,核心组件包括VRRP Stack、Checkers、IPVS wrapper、WatchDog和SMTP等,各部分功能如下。

VRRP Stack ==> 是VRRP协议的实现,能够实现Keepalived的高可用性。

Checkers ==> 能够基于网络层(IP)、传输层(TCP)、应用层(HTTP、SSL等)对RealServer进行健康状态检查。

IPVS Checkers ==> 根据配置文件生成IPVS规则并送往IPVS使之生效。

WatchDog ==> 用于监控VRRP Stack和Checkers这两个Keepalived关键组件是否正常工作。

SMTP ==> 是一个SMTP接口,主要用于当VRRP Stack中发生地址流动时,或者当Checkers发现服务上下线并增删服务节点时,能够以邮件方式通知管理员。



六、Keepalived的安装


对于CentOS 6.4+的系统版本,Keepalived程序包已经在base源提供。在CentOS 7.x上使用yum工具安装如下:

# yum -y install keepalived


6.1 高可用集群配置的前提


① 确保各节点时间同步 ==> 基于NTP协议或Chrony协议

② 确保iptables和selinux不会阻碍

③ 各节点之间可通过主机名互相通信(对Keepalived并非必须) ==> 名称解析服务的解析结果必须与"uname -n"命令的执行结果相同。

④ 各节点之间的root用户可以基于密钥认证的SSH进行通信(对Keepalived并非必须)


6.2 Keepalived配置


主配置文件 ==> /etc/keepalived/keepalived.conf

Unit File ==> /usr/lib/systemd/system/keepalived.service

Unit File的配置文件 ==> /etc/sysconfig/keepalived


Keepalived配置内容块:

GLOBAL CONFIGURATION(全局配置)

VRRPD CONFIGURATION(VRRP配置)

LVS CONFIGURATION(LVS配置)


(1) GLOBAL CONFIGURATION(全局配置)

全局配置框架:

global_defs {
    ...
}


常用配置:

global_defs {
    notification_email        # 邮件通告的收件人(如果有多个收件人须用花括号'{}'括起来)
        {
        admin@example1.com    # postfix服务只能由本机用户发给本机用户
        ...                   # 如果要发送给其它主机上的用户可借助于有提供smtp服务的主机,
        }                       但一般smtp服务器需要申请有互联网的公网IP地址
    notification_email_from admin@example.com    # 邮件通告的发送人
    smtp_server 127.0.0.1     # smtp服务器的IP地址。如果是向外提供服务的smtp服务器,则需要
                                支持正反解
    smtp_connect_timeout 30   # 发送邮件的超时时长
    router_id my_hostname     # 定义当前路由器设备的ID号(不一定要使用主机名)
    vrrp_mcast_group4 224.0.0.18    # 基于VRRP协议的多个虚拟路由器使用的组播地址
                                      group4表示使用ipv4格式地址
}


全局配置示例:

global_defs {
   notification_email {
        root@localhost
   }
   notification_email_from kaadmin@itab.com
   smtp_server 127.0.0.1
   smtp_connect_timeout 30
   router_id node1
   vrrp_mcast_group4 224.0.100.18
}


(2) VRRPD CONFIGURATION(VRRP配置)

虚拟路由配置框架:

vrrp_sync_group GRP_NAME {    # 规定所有的虚拟路由器的主Master节点都必须在同一台主机上
    ...
}
vrrp_instance INST_NAME {     # 配置虚拟路由器实例,可配置多个实例
    ...
}


常用配置:

vrrp_instance <string> {     # 配置虚拟路由器实例
    state MASTER | BACKUP    # 当前节点在此虚拟路由器中的初始状态
    interface ETHERCARD      # VRRP实例工作(绑定)的网络接口
    virtual_router_id 51     # 虚拟路由器ID,范围是0-255
    priority 100             # 当前物理节点在此虚拟路由器中的优先级
    advert_int #             # VRRP通告的时间间隔
    
    authentication {         # 认证机制
        # PASS||AH
        # PASS - Simple Passwd (suggested)
        # AH - IPSEC (not recommended))
        auth_type PASS       # 认证类型,推荐使用简单字符串认证(PASS类型)
        # Password for accessing vrrpd.
        # should be the same for all machines.
        # Only the first eight (8) characters are used. ==> 以下认证密码仅前8位字符有效
        auth_pass 1234       # 认证密码(建议使用随机数作为认证密码)
    }
    
    virtual_ipaddress {      # 定义虚拟IP
        <IPADDR>/<MASK> brd <IPADDR> dev <STRING> scope <SCOPE> label <LABEL>
        192.168.200.17/24 dev eth1
        192.168.200.18/24 dev eth2 label eth2:1
        192.168.200.18/32
    }
    
    track_interface {        # 定义要监控的接口
        eth0
        eth1
        ...
    }
    
    nopreempt                # 工作于非抢占模式(不指定时默认为抢占模式),如果两台路由器
                               性能相差不大时可以考虑工作于非抢占模式
    preempt_delay 300        # 工作于抢占模式时,设定再次上线的节点(原来为主Master节点)
                               应该延迟多长时间再抢占
    
    # 通告脚本定义
    notify_master <STRING>|<QUOTED-STRING>    # 当前节点变成主Master节点时使用的通告脚本
    notify_backup <STRING>|<QUOTED-STRING>    # 当前节点变成备Backup节点时使用的通告脚本
    notify_fault <STRING>|<QUOTED-STRING>     # 当前节点故障时使用的通告脚本
    notify <STRING>|<QUOTED-STRING>           # 以上无论哪一个状态发生变化,都使用的通告脚本
}


虚拟路由器实例配置示例:

vrrp_instance VI_1 {
    state MASTER
    interface ens33
    virtual_router_id 6
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 5FfZnxFm
    }   
    virtual_ipaddress {
        192.168.100.66    # VIP地址
    }
    notify_master "/etc/keepalived/notify.sh master"    # 注意要使用双引号
    notify_backup "/etc/keepalived/notify.sh backup"
    notify_fault "/etc/keepalived/notify.sh fault"
}


通知脚本示例:

# vim /etc/keepalived/notify.sh 

#!/bin/bash
#
contact='root@localhost'

notify() {
    mailsubject="$(hostname) to be $1: vip floating"
    mailbody="$(date +'%F %T'): vrrp transition, $(hostname) change to be $1"
    echo $mailbody | mail -s "$mailsubject" $contact
}

case $1 in
master)
    notify master
    ;;
backup)
    notify backup
    ;;
fault)
    notify fault
    ;;
*)
    echo "Usage: $(basename $0) {master|backup|fault}"
    ;;
esac

注意:通告脚本需要有执行权限。

# chmod +x /etc/keepalived/notify.sh


(3) LVS CONFIGURATION(LVS配置)

LVS集群配置框架:

virtual_server_group VSG_NAME {
    ...
}
virtual_server IP port | 
virtual_server fwmark int {
    protocol TCP
	...
    real_server <IPADDR> <PORT> {
        ...
    }
    real_server <IPADDR> <PORT> {
        ...
    }
    ...		
}


常用配置:

virtual_server IP port |        #==> 通过请求报文的目标IP和目标PORT定义集群服务
virtual_server fwmark # {       #==> 通过防火墙标记定义集群服务
    lb_algo rr|wrr|lc|wlc|lblc|sh|dh    # 定义负载均衡调度算法
    delay_loop <INT>            # 定义服务轮询时间间隔,即对后端服务的检测时间间隔
    lb_kind NAT|DR|TUN          # 集群的类型
    persistence_timeout <INT>   # 持久连接时长
    protocol TCP                # 服务协议
    virtualhost <STRING>        # 当后端Web服务器上有多台虚拟主机时,可使用此项来指明要检测
                                  的虚拟主机(带上Host首部)。否则,则只向RIP发出请求,并只
                                  由后端Web服务器上的默认虚拟主机响应
    sorry_server <IPADDR> <PORT>     # 所有RS均故障时,提供say sorry的服务器
    real_server <IPADDR> <PORT> {    # 定义RS
        weight <INT>                 # 权重
        notify_up <STRING>|<QUOTED-STRING>        # 节点上线时调用的通知脚本
        notify_down <STRING>|<QUOTED-STRING>      # 节点离线时调用的通知脚本
        # HTTP_GET|SSL_GET|TCP_CHECK|SMTP_CHECK|MISC_CHECK
        # 支持所有的健康状态检测方式
        
        HTTP_GET|SSL_GET {
            url {
                path <STRING>            # 健康状态检测时请求的资源的URL
                digest <STRING>          # 基于获取的内容摘要码进行健康状态判定
                status_code <INT>        # 基于要求返回的状态码进行健康状态判定
            }
            nb_get_retry <INT>           # 尝试的次数
            delay_before_retry <INT>     # 两次尝试之间的时间间隔
            connect_timeout <INTEGER>    # 连接的超时时长
            connect_ip <IP ADDRESS>      # 向此处指定的地址发测试请求(不指定时则向此前指定的RIP发测试请求)
            connect_port <PORT>          # 项此处指定的PORT发测试请求(不指定时则向此前指定的RS的PORT发测试请求)
            bindto <IP ADDRESS>          # 指定测试请求报文的源IP
            bind_port <PORT>             # 指定测试请求报文的源PORT
        }
        TCP_CHECK {
            # 除了不能定义url,其它的配置同HTTP_GET|SSL_GET
        }
    }
}


LVS集群配置示例:

virtual_server 192.168.10.7 80 {
    delay_loop 6
    lb_algo rr
    lb_kind DR
    persistence_timeout 50
    protocol TCP
    sorry_server 127.0.0.1 80

    real_server 192.168.10.11 80 {
        weight 1
        HTTP_GET {
            url {
              path /
              status_code 200
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }
    }
    real_server 192.168.10.12 80 {
        weight 1
        HTTP_GET {
            url {
              path /
              status_code 200
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }
    }
}


6.2 Keepalived高可用其它服务


(1) vrrp_script、track_script

vrrp_script ==> 用于自定义一个资源(或服务)监控脚本;vrrp实例能够将脚本的状态返回值作为判断一个资源(或服务)是否可用的依据。需要公共定义,可被多个vrrp实例调用,因此要定义在vrrp实例之外。

track_script ==> 用于调用vrrp_script定义的脚本去监控资源(或服务),并能够在监控过程中,一旦发现脚本状态返回值为成功时,则使节点的优先级不变或升高,当发现脚本状态返回值为是失败时,则使节点的优先级降低。而当前节点就可以通过向外通告自身优先级来完成节点角色(主或备)的切换。


(2) 使用示例1 ==> 当检测到/etc/keepalived/down文件存在时,则进行地址漂移

vrrp_script chk_down {
    script "[[ -f /etc/keepalived/down ]] && exit 1 || exit 0"
    # 当检测到down文件存在时(成功),定义为失败(状态码为1);而将检测不到down文件存在时(失败),反而定义为成功(状态码为0)
    interval 2    # 每隔2秒执行脚本检测一次
    weight -5	  # 一旦脚本运行失败,则执行该项(将优先级降低5)
}

vrrp_instance INST_NAME {
    ...
    track_script {
        chk_down    # 调用已定义的vrrp_script
    }
    ...
}


(3) 使用示例2 ==> 监控具体的服务进程

vrrp_script chk_httpd {
    script "killall -0 httpd"    # 当httpd进程运行不正常时,则脚本运行失败(状态码为1);
                                   当httpd进程运行正常时,则脚本运行成功(状态码为0)
    interval 2
    weight -5     # 一旦脚本运行失败,则执行该项(将优先级降低5)
}

vrrp_instance VI_1 {
    ...
    track_script {
        chk_httpd
    }
    ...
}

注意:

(1) killall命令使用-0选项时只起探测作用,不会真正杀死进程。

(2) 调用的脚本中使用的命令必须为已经安装的命令。本示例中的vrrp脚本只有在主机上必须有killall命令时才能执行,如果没有killall命令,则可使用yum安装psmisc。


问题:如何改进该脚本?

vrrp_script所实现的监控功能应该更为完整,例如可以在检测不到资源(或服务)时,不要立即调低优先级,而先尝试重启服务,并规定尝试几次,两次尝试之间的时间间隔。当然,在vrrp_script中所实现的功能也可由前面所提到的通告脚本来实现,也就是可以将前面的通告脚本示例再改进一下。改进后的脚本如下。

#!/bin/bash
#
contact='root@localhost'

notify() {
	mailsubject="$(hostname) to be $1: vip floating"
	mailbody="$(date +'%F %T'): vrrp transition, $(hostname) change to be $1"
	echo $mailbody | mail -s "$mailsubject" $contact
}

case $1 in
master)
	notify master
	systemctl start httpd.service      # 保证服务是启动的
	;;
backup)
	notify backup
	systemctl restart httpd.service    # 当切换为备节点时,重启一次服务
	;;
fault)
	notify fault
	systemctl restart httpd.service    # 当节点发生错误时,重启一次服务
	;;
*)
	echo "Usage: $(basename $0) {master|backup|fault}"
	;;
esac