「回顾2022,展望2023,我正在参与2022年终总结征文大赛活动」
前言
经常会有客户问到一个让人很无奈,却又不得不硬着头皮回答的问题:你们系统的高可用指标能达到几个9?通常情况下,我们会回答4个9,即99.99%,也就是说如果系统每运行100个时间单位,会有1个时间单位无法提供服务,这意味着,系统的年停机时间为8.76个小时。但站在客户的层面,他们会觉得这8.76个小时就是损失。以此引申出一个系统高可用的问题,该如何最大限度的减少系统不可用的时间?
单点往往是系统高可用最大的风险,应该尽量在系统设计的过程中避免单点。应当将服务集群化,这种系统架构的核心准则就是冗余,有了冗余备份,一个节点挂了还有其他备用节点能够顶上。能够保证系统服务的高可用。
当然,仅仅有冗余还不够,每次出现故障需要人工介入恢复势必会增加系统的不可服务时间。所以,要想真正实现高可用,还要搭配另一个杀手锏,就是自动故障转移。
在典型的互联网架构中,就是通过冗余 + 自动故障转移 来保证系统的高可用,目前也有不少成熟的方案,本文会介绍一个主流的数据库高可用方案(keepalived + 双主数据库)。从各个技术概念的理论出发,包含一个双主数据库的搭建示例,可直接跳到实操章节。
数据库高可用方案
KeepAlived介绍
- 常见的高可用软件有Keepalived,HeartBeat RoseHA,二者实现相同的功能,都可以实现服务或者网络的高可用,差别在于:
-
HeartBeat是一个专业的、功能完善的高可用软件,它提供了HA 软件所需的基本功能,比如:心跳检测、资源接管,检测集群中的服务,在集群节点转移共享IP地址的所有者等等。HeartBeat功能强大,但是部署和使用相对比较麻烦。
-
Keepalived主要是通过 虚拟路由冗余 来实现高可用功能,虽然它没有HeartBeat功能强大,但是Keepalived部署和使用非常的简单,所有配置只需要一个配置文件即可以完成。
什么是keepAlived?
- Keepalived起初是为LVS设计的,专门用来监控集群系统中各个服务节点的状态。它根据TCP/IP参考模型的第三、第四层、第五层交换机制检测每个服务节点的状态。
- 如果某个服务器节点出现异常,或者工作出现故障,Keepalived将检测到,并将出现的故障的服务器节点从集群系统中剔除。
- 以上全部是自动完成的,唯一需要人工参与的只是修复出现故障的服务节点。
- 后来Keepalived又加入了VRRP的功能,
VRRP(虚拟路由冗余协议)出现的目的是解决静态路由出现的单点故障问题,通过VRRP可以实现网络不间断的稳定运行,因此Keepalvied 一方面具有服务器状态检测和故障隔离功能,另外一方面也有HA cluster功能。
VRRP协议与工作原理
现实网络环境中,主机间通信都是通过配置静态路由或者默认网关来完成的,而主机之间的路由器一旦发生故障,通信就会失效,因此这种通信模式当中,路由器就成了一个单点瓶颈,为了解决这个问题,就引入了VRRP协议,它是一种主备模式的协议,通过VRRP可以在网络发生故障时透明的进行路由切换而不影响主机之间的数据通信,这其中涉及到两个概念:物理路由器和虚拟路由器。
VRRP可以将多台物理路由器设备虚拟成一个虚拟路由器,这个虚拟路由器通过一个或者多个虚拟IP对外提供服务,在虚拟路由器内部十多个物理路由器协同工作,同一时间只有一台物理路由器对外提供服务。
十台中对外提供的这台物理路由设备被称为:主路由器(Master角色),一般情况下Master是由选举算法产生,它拥有对外提供服务的虚拟IP,其它9个物理路由器不拥有对外的虚拟IP,也不提供对外网络功能,仅仅接收Master的VRRP状态通告信息,这些路由器被统称为BACKUP角色,当主路由器失败时,处于BACKUP角色的备份路由器将重新进行选举,产生一个新的主路由器进入MASTER角色,继续提供对外服务,整个切换对用户来说是完全透明的。
每个虚拟路由器都有一个唯一标识号,称为VRID,一个VRID与一组IP地址构成一个虚拟路由器,在VRRP协议中,所有的报文都是通过IP多播方式发送的,而在一个虚拟路由器中,只有处于Master角色的路由器会一直发送VRRP数据包,处于BACKUP角色的路由器只会接受Master角色发送过来的报文信息,用来监控Master运行状态,一般不会发生BACKUP抢占的情况,除非它的优先级更高,而当MASTER不可用时,BACKUP也就无法收到Master发过来的信息,于是就认定Master出现故障,接着多台BAKCUP就会进行选举,优先级最高的BACKUP将称为新的MASTER,这种选举角色切换非常之快,因而保证了服务的持续可用性。
KeepAlived工作原理
Keepalived作为一个高性能集群软件,它还能实现对集群中服务器运行状态的监控以及故障隔离,Keepalived工作在TCP/IP 参考模型的网络层,传输层和应用层,根据TCP、IP参数模型各层实现的功能,Keepalived运行机制如下:
-
在网络层: Keepalived采用最常见的工作方式是:通过ICMP协议向服务器集群中的每一个节点发送一个ICMP数据包(有点类似Ping的功能),如果某个节点没有返回响应数据包,那么认为该节点发生了故障,Keepalived将报告这个节点失效,并从服务器集群中剔除故障节点。
-
在传输层: Keepalived利用了TCP协议的端口连接和扫描技术来判断集群节点的端口是否正常,比如对于常见的WEB服务器80端口。或者SSH服务22端口,Keepalived一旦在传输层探测到这些端口号没有数据响应和数据返回,就认为这些端口发生异常,然后强制将这些端口所对应的节点从服务器集群中剔除掉。
-
在应用层: 用户可以通过自定义Keepalived的工作方式,例如:可以通过编写程序或者脚本来运行Keepalived,它会根据用户的设定参数检测各种程序或者服务是否允许正常,如果检测结果和用户设定的不一致时,Keepalived将把对应的服务器从服务器集群中剔除。
抢占和非抢占式
keepalive通过组播,单播等方式(自定义),实现keepalive主备推选。工作模式分为抢占和非抢占(通过参数nopreempt来控制)。
- 抢占模式:
主服务正常工作时,虚拟IP会在主设备上,备不提供服务,当主服务优先级低于备的时候,备会自动抢占虚拟IP,顶替掉主节点。也就是说,工作在抢占模式下,不分主备,只管优先级,谁优先级高,谁来提供服务。priority优先级高的那一个在故障恢复后,会自动将VIP地址再次抢占回来!!
- 非抢占模式:
这种方式通过参数nopreempt来控制。不管priority优先级,只要MASTER机器发生故障,VIP资源就会被切换到BACKUP上。并且当MASTER机器恢复后,也不会去将VIP资源抢占回来,直至BACKUP机器发生故障时,才能自动切换回来。
注意事项
- nopreempt这个参数只能用于state为backup的情况,所以在配置的时候要把master和backup的state都设置成backup,这样才会实现keepalived的非抢占模式!
- 当state状态一个为master,一个为backup的时候,加不加nopreempt这个参数都是一样的效果。都是根据priority优先级来决定谁抢占vip资源的,是抢占模式。
- 当state状态都设置成backup,如果不配置nopreempt参数,那么也是看priority优先级决定谁抢占vip资源,也是抢占模式。
- 当state状态都设置成backup,如果配置nopreempt参数,那么就不会去考虑priority优先级了,是非抢占模式!即只有vip当前所在机器发生故障,另一台机器才能接管vip。即使优先级高的那一台机器恢复后也不会主动抢回vip,只能等到对方发生故障,才会将vip切回来。
搭建示例
环境准备
由于是搭建数据库集群,则至少准备两台服务器,没有条件的话,在同一台机器上使用docker启动两个mysql容器用于测试也是可以的。
| 服务器 | 地址 | 用途 |
|---|---|---|
| master1 | 192.168.0.193 | 安装mysql 和keeplived |
| master2 | 192.168.0.247 | 安装mysql 和keeplived |
| VIP | 192.168.0.200 | 虚拟IP,对外提供服务的地址, 此IP一定要是未被使用的 |
启动双数据库
分别在两台服务器的/opt/data/mysql/conf/目录下创建数据库配置文件my.cnf,内容如下:
- 在
master1中创建:
[mysqld]
#启用二进制日志功能
log-bin=mysql-bin
#设置binlog日志格式
binlog_format=mixed
#给服务器分配一个ID编号
server-id=1
#配置中继日志
relay-log=relay-bin
#配置中继日志名
relay-log-index=slave-relay-bin.index
#设置自增字段每次递增的值
auto_increment_increment=2
#用来设定数据库中自动增长的起点
auto_increment_offset=1
#忽略所有复制产生的错误
slave-skip-errors = all
- 在
master2中创建:
[mysqld]
log-bin=mysql-bin
binlog_format=mixed
server-id=2
relay-log=relay-bin
relay-log-index=slave-relay-bin.index
auto_increment_increment=2
auto_increment_offset=2
slave-skip-errors = all
- 分别在两台服务器中启动使用docker 启动 mysql容器,将数据库配置文件挂载进去。
docker run -p 3306:3306 --name mysql \
-v /opt/data/mysql/log:/var/log/mysql \
-v /opt/data/mysql/data:/var/lib/mysql \
-v /opt/data/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:8.0.26
- 进入mysql容器内部测试能否正常登录,会发现报错如下图,是因为数据库默认没有开启远程登录,而且密码的认证方式也不对。
- 进入容器内部,开启远程登录,修改密码认证方式。
1. 直接 mysql -u root 登录,直接enter,无需密码
2. use mysql;
3. 开启远程登录,允许所有连接进来
update user set host='%' where user='root';
4. 查看是否修改成功,并刷新
select host,user,plugin from user;
flush privileges;
5. 修改密码认证方式,刷新
alter user 'root'@'%' identified with mysql_native_password by '123456';
flush privileges;
- 再次进入容器,登录mysql,输入密码正常,记得两个数据库都要改。
MYSQL主从配置
双主数据库,两个节点是互为主从的,就必须满足 mster1 --> master2设置主从同步,同时 master2--->master1 也设置主从同步。
- 分别 进入mysql容器中,创建一个用户,专门用于同步主从库的数据。
1. create user 'repl'@'%' identified with mysql_native_password by '123456';
2. grant all privileges on *.* to 'repl'@'%';
3. flush privileges;
4. flush tables with read lock; #锁表,待同步配置完成在解锁
show master status;查看当前的binlog以及数据所在位置,如果有以下输出,代表配置成功
- 分别开启同步对方
- 在master1上执行
1. unlock tables; # 先解锁,将对方数据同步到自己的数据库中
2. stop slave;
3. 设置slave,注意:以下所有的值,都是另一台slave的配置,log_file和log_pos就是上文中show master status;的结果
change master to master_host='192.168.0.247',
master_user='slave', master_password='replPassWd',
master_log_file='mysql-bin.000001',
master_log_pos=1819;
4. start slave;
- 查看两个进程是否就绪,
show slave status \G;
- 在master2上执行上述相同操作,注意,设置slave的时候,参数都要配置为另一台的。最终需要看到如下结果,则表示配置成功,若有一个不是,则不成功,需要检查操作过程。
- 测试主从同步是否设置成功,在master1数据库中写值,master2中的数据库,也要看到对应同步过来的记录。
安装KeepAlived
注意:如果服务器的操作系统是CentOS,则需要注意,CentOS-8于2021年12月31日停止了源的服务,所以用yum包下载可能会失败,如下解决方案:
1. cd /etc/yum.repos.d
2. mkdir bak
3. cp * bak/
4. sed -i 's/$releasever/8-stream/' CentOS*repo
5. yum update -y
- 执行如下命令安装KeepAlived
1. yum install -y openssl-devel
2. cd /usr/local/src/
3. wget http://www.keepalived.org/software/keepalived-1.3.5.tar
4. tar -zvxf keepalived-1.3.5.tar.gz
5. cd keepalived-1.3.5
6. ./configure --prefix=/usr/local/keepalived
7. yum -y install gcc
8. make && make install
9. 再次执行./configure --prefix=/usr/local/keepalived
- 移动配置文件,设置开机自启动
1. cp /usr/local/src/keepalived-2.0.19/keepalived/etc/init.d/keepalived /etc/rc.d/init.d/
2. cp /usr/local/keepalived/etc/sysconfig/keepalived /etc/sysconfig/
3. mkdir /etc/keepalived/
4. cp /usr/local/keepalived/etc/keepalived/keepalived.conf /etc/keepalived/
5. cp /usr/local/keepalived/sbin/keepalived /usr/sbin/
6. echo "/etc/init.d/keepalived start" >> /etc/rc.local
- 两台服务器上都要执行同样的操作,安装keepalived,并且要保证上述过程都没报错,如有报错,需要检查操作步骤。
KeepAlived配置
第一步: 分别在master1和master2节点上的/etc/keepalived目录中看到有keepalived.conf,这个示例配置文件有参考价值,但是我们需要自己重写一个配置文件。重写配置文件中的内容及作用点如下:
#!/bin/bash
# 1、配置发送事件邮件的通知人邮箱,可忽略
global_defs {
notification_email {
xxxxx@gmail.com
}
script_user root
enable_script_security
notification_email_from xxxxx@gmail.com
smtp_server localhost
smtp_connect_timeout 30
}
# 2、配置检查数据库的脚本文件,并设置检查时间间隔(重要)
vrrp_script check_mysql_status {
script "/etc/keepalived/keepalived_check_mysql.sh"
interval 2 # 执行此check脚本的时间间隔
}
# 3、配置当前实例
vrrp_instance VI_1 {
state BACKUP #初始状态 MASTER|BACKUP, 一旦有其他机器加入,将会举行选举,具有最高优先级的机器将会成为MASTER,所以这个条目的并不重要
interface eth0 #配置虚拟网卡,需要换成自己的网卡
virtual_router_id 101 #虚拟路由ID唯一标识此虚拟设备
priority 100 #数字越大,优先级越高
advert_int 1 #VRRP实例之间发送广播信息的频率
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress { #虚拟IP,就是在准备事项中提到的虚拟IP,需要在同一个网段中
192.168.0.200
}
track_script { #配置检查mysql数据库的方法
check_mysql_status
}
}
第二步: 再编写自定义的数据库切换脚本。KeepAlived做心跳检测,当监测到Master的mysql服务关闭后,就会将vip切换到Backup上(但此时Master的keepalived服务不会被暴力kill)当Master的mysql服务恢复后,就会再次将VIP资源切回来!本示例中,mysql是用docker启动的,所以检查数据库是否存活的脚本有docker相关的操作,keepalived_check_mysql.sh脚本内容如下:
#!/bin/bash
MYSQL_INSTANCE=mysql-1 #你当前节点上mysql的容器名称
USER=root
PWD=XXXXXXX #你的数据库库密码
CMD="mysql -u$USER -p$PWD -e 'show status;'" #检查mysql master状态的命令
LOG_FILE=/etc/keepalived/action.log
# 判断容器是否存活
function isRunning() {
serviceName=$1
container=`docker ps | grep $MYSQL_INSTANCE`
if [ -z "$container" ]; then
return 1
fi
return 0
}
function log() {
msg=$1
echo $(date +'%Y-%m-%d %H:%M:%S') $msg >> $LOG_FILE
}
isRunning $MYSQL_INSTANCE
if [ $? == 1 ]; then
echo $(date +'%Y-%m-%d %H:%M:%S') "$MYSQL_INSTANCE is not running" >> $LOG_FILE
exit 1
fi
# 进入容器,执行命令,检查数据库是否存活
docker exec -t $MYSQL_INSTANCE bash -c "$CMD" >/dev/null 2>&1
if [ $? == 0 ]; then
log "mysql login successfully!"
exit 0
else
let "n=0"
while [ $n -lt 5 ]
do
docker exec -t $MYSQL_INSTANCE bash -c "$CMD" >/dev/null 2>&1
if [ $? == 0 ]; then
log "mysql re-login successfully!"
exit 0
else
let "n=n+1"
fi
sleep 3
done
log "mysql connection faild!"
exit 3
fi
注意,上面的keepalived_check_mysql.sh和keepalived.conf,都需要存在于/etc/keepalived/目录中,两个节点中大配置几乎一样,部分需要修改的地方,在文件的注释中已标注。
检查数据库存活的脚本也要配置执行权限:chmod 744 /etc/keepalived/keepalived_check_mysql.sh
第三步: 启动keepalived服务,systemctl start keepalived.service,启动正常之后,执行ip a show eth0,可以看到当前节点拿到了虚拟IP。
第四步: 通过当前虚拟IP地址访问数据库,目前是哪个节点上的数据库提供服务,那么就可以在ip a中看到此虚拟IP地址,如果停掉当前节点的数据库,又会在另一个节点中,看到当前IP,由于这个虚拟IP是在这两个数据库节点之间切换的,所以也称之为漂移IP。
总结
至此,整个mysql高可用环境的搭建也就结束了,通过这个漂移IP提供数据库服务,但是后续使用过程中一定会出现问题的。
-
一方面是业务层的问题,因为keepalived检查是依赖于配置文件的,无法做到完全平滑的切换,即使是检查时间间隔设置到1秒,如果在这1秒钟,业务代码层有数据库更新操作,那么此次就更新失败回滚,如果处理不好也会影响后续的操作,对执行过程比较敏感的,可能还需要在代码层做数据库操作重试机制,我司在做其中一个项目的时候,就给数据库操作加上了重试机制,参考这篇文章:
-
另一方面,也有可能输双主数据库本身也有不完善的地方,在更新密集的情况下可能会导致数据同步出错等等问题,当前这些问题都可以通过配置某些参数去解决。如果笔者在后续的使用过程中遇到问题,也会在本文补充上来。