多平台融合——数据库HA(一)
需求背景:在K8S集群中创建的微服务要使用浪潮云超融合上的数据库资源,为减少两个平台之间的耦合度,同时确保系统整体的可用性和安全性,做如下设计:
1、建立数据库主从集群,并将超融合中mariadb数据库视为主数据库,负责原平台的写入和读取业务,同时为其建立HA,确保主库的高可用性;
2、在K8S容器中建立多副本的mariadb数据库,并将其视为从数据库,与主数据库建立主从复制机制,确保数据的一致性。所有K8S集群上的微服务要访问超融合上的数据资源都通过从数据库访问。这样微服务中的数据或结构修改,不会污染到超融合平台上的数据库和应用系统,超融合上的数据更新又能及时同步到K8S集群中,减少了平台间的耦合度。
3、通过防火墙和istio网关设置两平台间服务通信及访问策略,确保两平台的安全性。
本篇只记录超融合上数据库HA的建立过程,其他内容后续再做记录。
数据库HA 前期准备
超融合上原有数据库为mariadb 使用docker创建,操作系统为ubuntu,虚拟机地址为21.12.3.81
1、在超融合中为其创建备份虚拟机可用地址为21.12.3.66(IP地址根据本单位实际使用情况分配),在虚拟机上安装docker 、docker-compose
2、为两虚拟机安装keepalived,配置虚拟地址,此处我使用的虚拟地址为21.12.3.75
配置/etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
state MASTER
interface ens16
virtual_router_id 71
priority 101
advert_int 1
authentication {
auth_type PASS
auth_pass txgm2m85331919
}
virtual_ipaddress {
10.12.3.75
}
}
// 备份虚拟机上 priority 设置比101小就行,默认虚拟地址会飘到priority 值大的虚拟机上。
重启keepalived 服务,使用ip addr 命令查看虚拟地址是否起来
ip addr
2: ens16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:16:3e:8e:33:8c brd ff:ff:ff:ff:ff:ff
inet 21.12.3.81/24 brd 21.12.3.255 scope global ens16
valid_lft forever preferred_lft forever
inet 21.12.3.75/32 scope global ens16
valid_lft forever preferred_lft forever
inet6 fe80::216:3eff:fe8e:338c/64 scope link
valid_lft forever preferred_lft forever
3、在备份虚拟机上安装并启动mariadb
创建docker-company.yaml
version: '3.1'
services:
mariadb:
image: docker.m.daocloud.io/library/mariadb:10.5
container_name: "mariadb-master"
restart: always
environment:
MYSQL_USER: "root"
MYSQL_PASSWORD: "12345678"
MYSQL_ROOT_PASSWORD: "12345678"
MYSQL_REPLICATION_MODE: "master"
MYSQL_REPLICATION_USER: "repluser"
MYSQL_REPLICATION_PASSWORD: "12345678"
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --server-id=1 --log-bin=/var/log/mysql/mysql-bin --expire_logs_days=10
ports:
- "63306:3306"
volumes:
- /datas/mariadb:/var/lib/mysql
- /home/shell/mariadb/log:/var/log/mysql
使用docker-compose up -d 运行,并查看容器运行情况
sudo docker ps
[sudo] password for shell:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
600ce3e8bf25 docker.m.daocloud.io/library/mariadb:10.5 "docker-entrypoint.s…" 6 days ago Up 6 days 0.0.0.0:63306->3306/tcp, [::]:63306->3306/tcp mariadb-slave
配置mariadb主从复制
虚拟机中的mariadb HA,是通过配置主从复制实现数据同步的。
K8S中的数据库从库副本连接主库地址设置为21.12.3.75 ,这样只要两个虚拟机不是同时挂,就能保证主数据库的联通性。
配置21.12.3.81 数据库的 /etc/mysql/my.cnf --> 映射本地~/mariadb/conf/my.cnf
[mysqld]
server-id = 1 # 这里的ID必须是唯一的,如果你在集群中设置多个服务器,每个服务器需要不同的ID
log-bin = mysql-bin # 启用二进制日志,并指定日志的前缀
expire_logs_days = 10 # 可选:设置二进制日志的过期天数
sudo docker stop mariadb-master
sudo docker start mariadb-master
重启数据库 (这里的my.cnf是挂载到虚拟机本地目录的,如果不是挂载到本地,这种修改是不会生效的)
登录mariadb,创建主从复制账号,并授权
CREATE USER 'repluser'@'%' IDENTIFIED BY '12345678';
GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%';
FLUSH PRIVILEGES;
查看主数据库binlog运行状态
show master status;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000003 | 385 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.02 sec)
配置21.12.3.66 数据库
登录mariadb,配置从数据库连接主数据库信息
CHANGE MASTER TO
MASTER_HOST='21.12.3.81',
MASTER_PORT=63306,
MASTER_USER='repluser',
MASTER_PASSWORD='12345678',
MASTER_LOG_FILE='mysql-bin.000003', //这里的值就为主数据库状态查询到的File值
MASTER_LOG_POS=385; //这里的值就为主数据库状态查询到的Position值
启动slave进程,并查看slave状态
start slave;
show slave status;
// 如果能看到Slave has read all relay log; waiting for more updates这样的信息说明主从复制正常工作了
//如果上面的MASTER_LOG_FILE或MASTER_LOG_POS值配置不对,会在状态中看到类似Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'这样的信息
可通过查询主数据库状态后,使用如下重新配置
stop slave;
change master to MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=385;
start slave;
show slave status;
截止到现在HA中的主从同步已经建立,两个虚拟机中的数据库可以同步了,当一个虚拟机宕机,虚拟地址21.12.3.75会主动由21.12.3.81漂移到21.12.3.66上,由另外一台虚拟机上上的数据库持续工作,替代主数据库角色。但是要让K8S集群中的从数据库实现平滑切换,还需要解决一个问题就是从数据库的binglog pos值自动配置问题。起初我的思路是监控81上的pos值变化,如果有变化即时修改66上master的状态值,后来查了资料发现这个方法行不通。因为pos值也就是登录数据库通过show master status;查询到的值是当前记录到binlog文件中最后截止位置的值。这个值只有在数据库处于master模式,并且主动在其数据库中修改动作才会有记录,记录到binlog中,pos值才会有相应变化,也就是说在slave模式下同步master的动作行为过程中,从数据库的同步行为是不记录binlog的,即便从数据库此时设置了master模式pos值也不会变化,只有在从数据库中操作时从数据库才记录binlog,这样刚才的主数据库宕机的场景,从数据库的pos的值是和主数据库的pos值不一致的,如果K8S集群中的从数据库不更改pos的值和此时虚拟机中接管master角色的从数据库pos值一致,就会报错“Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'” 找不到要同步binlog文件的起始点。主从同步就会失败。并且还有一点是show master status看到的状态值是不能直接修改的,所以在虚拟机上同步master角色的pos的值行不通。
HA之间同步pos值行不通,那就试试,让K8S主动识别并修改从库连接主库的pos值,通过
stop slave;
change master to MASTER_LOG_FILE='mysql-bin.*****', MASTER_LOG_POS=****;
start slave;
这个命令在K8S从数据库中主动修改,也可实现HA切换后的平滑过渡。
这里需要解决两点:1、自动获取当前binlog状态值;2、主动同步并修改从数据库中的binlog状态值。
自动获取当前binlog状态值
在HA两虚拟机中安装inotify-tools、expect
编写脚本实现主库中的binlog pos值变化跟踪,并将其值发送到从数据库虚拟机上
因为使用docker 命令需要用到授权 ,存在交互式输入密码的问题,除了在主从数据库虚拟机上打通ssh的免密登录外,还使用了expect交互批处理命令
//文件名getbinlogstatus
#! /usr/bin/expect
set timeout 20
spawn sudo docker exec -it mariadb-master mysql -uroot -p12345678 -e "SHOW MASTER STATUS\\G"
expect {
"*shell:" { send "passwordpassword\r"; exp_continue } #passwordpassword是shell账号的密码
"*password:" { send "passwordpassword\r" }
}
expect eof
chmod +x getbinlogstatus
//脚本文件binlog-sync.sh
#!/bin/bash
exit_loop() {
echo "monitor stop"
exit 0
}
trap exit_loop 2
# ctrl+c 退出程序
BINLOG_DIR="/home/shell/mariadb/log"
REMOTE_SERVER="21.12.3.66"
while true; do
echo 'passwordpassword' | sudo -S inotifywait -rm $BINLOG_DIR -e create,modify | while read path action file; do
if [[ $file == mysql-bin.* ]]; then
# echo "log changed: $file"
/home/shell/mariadb/getbinlogstatus > /home/shell/mariadb/pos/mysql-bin.pos
scp /home/shell/mariadb/pos/mysql-bin.pos shell@$REMOTE_SERVER:~/mariadb/pos/mysql-bin-master.pos
# /home/shell/mariadb/scpfiles $file $REMOTE_SERVER
fi
done
done
chmod +x binlog-sync.sh
./binlog-sync.sh 运行程序
然后用navicat等客户端软件登录mariadb对数据库操作,修改或添加数据,看21.12.3.66中 /home/shell/mariadb/pos/目录中是否有mysql-bin-master.pos文件,打开文件查看内容是否和21.12.3.81中show master status;查询到的内容一致,一致说明脚本正常工作。
配置HA中从数据库虚拟机中运行脚本
//脚本文件getbinlogstatus
#! /usr/bin/expect
set timeout 20
spawn sudo docker exec -it mariadb-slave mysql -uroot -p12345678 -e "SHOW MASTER STATUS\\G"
expect {
"*shell:" { send "passwordpassword\r"; exp_continue }
"*password:" { send "passwordpassword\r" }
}
expect eof
//脚本文件nc-monitor.sh
#!/bin/bash
exit_loop() {
echo "monitor stop"
exit 0
}
trap exit_loop 2
# ctrl+c 退出
i=0
j=0
while true; do
if nc -zv 21.12.3.81 63306; then
if [ $i -eq 0 ]; then
echo "21.12.3.81 畅通,复制slave状态"
/home/shell/mariadb/getbinlogstatus > /home/shell/mariadb/pos/mysql-bin-slave-end.pos
i=1
fi
j=0
else
i=0
if [ $j -eq 0 ] && [ -f "/home/shell/mariadb/pos/mysql-bin-master.pos" ]; then
echo "21.12.3.81 不通,复制master状态"
cp /home/shell811127/mariadb/pos/mysql-bin-master.pos /home/shell/mariadb/pos/mysql-bin-master-end.pos
j=1
fi
fi
sleep 1
done
chmod +x nc-monitor.sh getbinlogstatus
./nc-monitor.sh 运行脚本,关闭21.12.3.81上的数据库(当然这里是你的业务运行暂停数据库服务的前提下,进行的测试),可以发现21.12.3.66 下有:
shell@bigdatasvcdbs366:~/mariadb/pos$ ls
mysql-bin-master-end.pos mysql-bin-master.pos mysql-bin-slave-end.pos
就说明脚本正常工作了。这里nc-monitor.sh脚本主要是记录主数据库宕机时最后的pos值和主数据恢复后HA中从数据库的最后的pos值。K8S中从数据库同步的pos的值就为这两种状态的pos值,以确保同步数据时,在宕机和K8S检测到宕机中间没有数据断档。
然后使用golang编写一个读取pos的守护进程,放在21.12.3.66上,便于K8S从数据库容器获取binglog的pos值,可通过http或grpc读取。这样就可以解决虚拟机主从切换,K8S从数据库连接同步的平滑切换。
核心代码如下供大家参考
// ms_type = 主从类型(master,slave)
func (r *binlogRepo) ReadBLS(ctx context.Context, ms_type string) (*biz.BLStatus, error) {
var file_path string
if ms_type == "master" {
file_path = r.data.binlogconf.MPosfile
} else if ms_type == "slave" {
file_path = r.data.binlogconf.SPosfile
} else {
return nil, errors.New(500, "NO_TYPE_ERR", "未设置主从类型")
}
fo := FilesOper{}
lines, err := fo.ReadFileAllContext(file_path)
if err != nil {
return nil, err
}
var file, position string
for _, line := range lines {
idx_file := strings.Index(line, "File:")
if idx_file != -1 {
tmp := strings.Split(line, ":")
if len(tmp) == 2 {
file = strings.ReplaceAll(tmp[1], " ", "")
} else {
return nil, errors.New(500, "POS_FILE_ERROR", "pos文件损坏!")
}
}
idx_pos := strings.Index(line, "Position:")
if idx_pos != -1 {
tmp := strings.Split(line, ":")
if len(tmp) == 2 {
position = strings.ReplaceAll(tmp[1], " ", "")
} else {
return nil, errors.New(500, "POS_FILE_ERROR", "pos文件损坏!")
}
}
}
if len(file) > 0 && len(position) > 0 {
return &biz.BLStatus{
file,
position,
}, nil
}
return nil, errors.New(404, "GET_POS_ERROR", "pos信息获取失败!")
}
func (r *binlogRepo) CheckHealth(ctx context.Context, ms_type string) (bool, error) {
var addr string
var port int64
if ms_type == "master" {
addr = r.data.binlogconf.MAddr
port = r.data.binlogconf.MPort
} else if ms_type == "slave" {
addr = r.data.binlogconf.SAddr
port = r.data.binlogconf.SPort
} else {
return false, errors.New(500, "NO_TYPE_ERR", "未设置主从类型")
}
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port))
if err != nil {
return true, nil
}
defer listener.Close()
return false, nil
}
func (uc *BinlogUsecase) GetBinlogStatus(ctx context.Context) (*BLStatus, error) {
master_mysql_svc_status, err := uc.repo.CheckHealth(ctx, "master")
if err != nil {
log.Errorf(err.Error())
}
slave_mysql_svc_status, err := uc.repo.CheckHealth(ctx, "slave")
if err != nil {
log.Errorf(err.Error())
}
if master_mysql_svc_status {
bl_status, err := uc.repo.ReadBLS(context.Background(), "master")
if err != nil {
log.Errorf("读取binlog pos 文件失败!%s", err.Error())
return nil, errors.New(500, "READ_POS_ERR", fmt.Sprintf("读取binlog pos 文件失败!%s", err.Error()))
}
return bl_status, nil
} else if !master_mysql_svc_status && slave_mysql_svc_status {
bl_status, err := uc.repo.ReadBLS(context.Background(), "slave")
if err != nil {
log.Errorf("读取binlog pos 文件失败!%s", err.Error())
return nil, errors.New(500, "READ_POS_ERR", fmt.Sprintf("读取binlog pos 文件失败!%s", err.Error()))
}
log.Errorf("主数据库库服务中断!")
return bl_status, nil
} else {
log.Errorf("数据库HA服务中断!")
return nil, errors.New(500, "MYSQL_SVC_ERR", "数据库HA服务中断!")
}