一、问题
1.1、 场景(高可用)
在高可用(HA)系统中,当联系2个节点的"心跳线"断开时,本来为一整体、动作协调的HA系统,就分裂成为2个独立的个体。由于相互失去了联系,都以为是对方出了故障。两个节点上的HA软件像"裂脑人"一样,争抢"共享资源"、争起"应用服务",就会发生严重后果——或者共享资源被瓜分、2边"服务"都起不来了;或者2边"服务"都起来了,但同时读写"共享存储",导致数据损坏(常见如数据库轮询着的联机日志出错)。
1.2、 场景2(主备模式)
假设节点A和B组成主备关系,A为备用节点,B为主节点,那么当在图标红叉位置发生网络故障时,节点A接收不到节点B的组播通知,将抢占虚拟IP。这时出现的后果就是节点A和节点B均拥有虚拟IP,就可能导致了脑裂
1.3、脑裂产生的原因
- 高可用服务器对之间心跳线链路发生故障,导致无法正常通信。
- 因心跳线坏了(包括断了,老化)。
- 因网卡及相关驱动坏了,ip配置及冲突问题(网卡直连)
- 因心跳线间连接的设备故障(网卡及交换机)
- 因仲裁的机器出问题(采用仲裁的方案)。
- 高可用服务器上开启了 iptables防火墙阻挡了心跳消息传输。
- 高可用服务器上心跳网卡地址等信息配置不正确,导致发送心跳失败。
- 其他服务配置不当等原因,如心跳方式不同,心跳广插冲突、软件Bug等。
- 提示: Keepalived配置里同一 VRRP实例如果 virtual_router_id两端参数配置不一致也会导致裂脑问题发生。
- 服务器网线松动等网络故障
1.4、对付HA系统"裂脑"的对策,目前达成共识的的大概有以下几条
-
添加冗余的心跳线,例如:双线条线(心跳线也HA),尽量减少"裂脑"发生几率;
-
启用磁盘锁。
- 正在服务一方锁住共享磁盘,“裂脑"发生时,让对方完全"抢不走"共享磁盘资源。但使用锁磁盘也会有一个不小的问题,如果占用共享盘的一方不主动"解锁”,另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃,就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在HA中设计了"智能"锁。即:正在服务的一方只在发现心跳线全部断开(察觉不到对端)时才启用磁盘锁。平时就不上锁了。
-
设置仲裁机制。例如设置参考IP(如网关IP),当心跳线完全断开时,2个节点都各自ping一下参考IP,不通则表明断点就出在本端。不仅"心跳"、还兼对外"服务"的本端网络链路断了,即使启动(或继续)应用服务也没有用了,那就主动放弃竞争,让能够ping通参考IP的一端去起服务。更保险一些,ping不通参考IP的一方干脆就自我重启,以彻底释放有可能还占用着的那些共享资源。
1.5、思考
为了解决上面的问题,每个节点应该定时去判断自身的应用服务状态及自身网络状态。
当自身应用服务异常、或无法ping通网关,则认为自身出现故障,停止keepalived服务。
二、解决问题
2.1、通过vrrp_script及track_script实现
在keepalived的配置文件最前面加入以下代码,定义一个跟踪脚本:
# 定义一个名称为check_local的检查脚本
vrrp_script check_local {
#shell脚本的路径
script "/usr/local/keepalived/bin/check_local.sh"
#运行间隔
interval 5
}
再在vrrp_instance配置中加入以下代码使用上面定义的检测脚本
track_script {
check_local
}
check_local.sh 检测规则是
- 自身web服务故障(超时或者http返回状态不是200)
- 无法ping通网关
- 产生以上任何一个问题,停止keepalived实例
check_local.sh 缺陷
- 当停止掉keepalived服务,那么当故障恢复后,keepalived是无法自动恢复的。
所以我们需要一个定时任务脚本,当障恢复后,keepalived重启。这个脚本要加入到cron任务中。
2.2、定时任务脚本,当障恢复后,keepalived重启
在每个节点运行shell脚本(check_service.sh)检测本机的服务是否正常
- 如果本地服务连续三次检测失败,停止掉本机的keepalived, 如此虚拟IP自动转移到备用机器之上
- 如果本地服务连接三次检测成功,但keepalived没有启动,则启动之,以达到故障恢复之目的。
- 这个可以解本机或是网关偶尔出现一次故障,但是被我们关掉的keepalived问题
check_keepalived.sh
#!/bin/bash
MYSQL=mysql
MYSQL_USER=root
MYSQL_PASSWORD=Fwfk~2021
maxfails=3
fails=0
success=0
while [ 1 ]
do
# 当mysql 连接不上的时候
$MYSQL -u $MYSQL_USER -p$MYSQL_PASSWORD -e "show status;" >/dev/null 2>&1
if [ $? -ne 0 ] ; then
# 错误次数&