高可用和数据库冗余实践
Table of Contents
在真实世界中,任何电子原件或软件系统都会故障。为了在混乱的世界里寻得一丝安心,提出了高可用的概念。高可用 (High availability, HA) 是互联网时代常被提及的词语。在产品设计、开发、发布、交付过程中,通常都会提到一个系统的可用性,提到系统是否保证高可用。本文介绍高可用的意义,而后演示一个系统 (mariadb) 如何通过冗余来保证高可用。
1. 什么是高可用
在描述高可用之前,需要先介绍一些相关概念。
MTTF
MTTF (mean time to failure) 指的是一个原件、或系统模块从开始使用到故障,经历的平均时长。MTTF 的倒数,FIF, 指故障率。
MTTR
MTTR (mean time to repair) 指的是故障开始,到修复为止,经过的时长。其他地方也会出现 MTBF (mean time between failures),是指 \( MTTF + MTTR \) 。
可用性
可用性在不同地方可能有些细微的差别,这里我们使用下面的公式:
\[ availability = \frac{MTTF}{MTTF+MTTR} \]
例如,可用性达到 "一个9",指的是可用性达到 \( 90\% \),每年大概可以故障 36.53 天,或每天 2.4 小时。例如 "三个9",指的是可用性达到 \( 99.9\% \),每年大概可以故障 8.77 小时。
SLA
服务等级协议 (Service Level Agreements),是一个合同或承诺,规定可用性需要达到某个数值,如果没达到,服务提供方需要支付赔偿或承担责任。 SLA 中都会明确责任和义务,指定赔偿数额,标明可用性计算方法。
例如:
如果使用他们的服务的话,这些协议值得花时间看一看。有一点比较有趣:阿里云和腾讯云的 SLA 规定用代金券赔,AWS 会把钱打到信用卡。
根据 SLA,阿里云 ECS 的可用性高于 "三个9",计算周期是月,故障时长肯定不能超过 44 分钟。2022年12月18日的事件里,服务故障超过 12 小时,按照协议要赔当月全部服务费。
高可用
SLA 中规定了可用性高于普通系统水平,就是高可用。如果一个系统没有 SLA,它就不能被称为高可用系统。如果一个系统 SLA 中规定的可用性水平比较低,也不是高可用系统。一般来说,SLA 中规定可用性大于 "三个9" (99.9%),就可以说这个系统是高可用的。
2. 如何实现高可用
一般用冗余模块来保证高可用。在某个模块故障的时候,可以使用冗余模块代替。实际操作中,比较重要的问题是加了冗余到底增加了多少可用性,到底值不值得?我们用一个实例来说明。
假设一个数据库系统,MTTF 是 200000 小时,MTTR是24小时, 如果增加一个冗余,使用两个数据库系统,当一个系统故障时自动切换到另一个上面,这样 MTTF 有什么变化呢?
两个数据库,不可用的情况是:一个库故障后,在运维发现故障并修复之前,另一个库也故障了。
单个库故障率是 \( \frac{1}{MTTF} \),那么两个库中有一个故障的概率是 \( \frac{2}{MTTF} \)。一个库故障的恢复时间是 MTTR,那么在一个库故障后,运维去修复好之前这段时间里,故障的概率是 \( \frac{MTTR}{MTTF} \)。也就是说,一个库故障后,在运维发现故障并修复之前这段时间里,另一个库也故障的概率是:
\[ \frac{MTTR}{MTTF} \times \frac{2}{MTTF} = \frac{2\times MTTR}{MTTF^{2}} \]
新的 MTTF 为:
\begin{split} MTTF_{2} & = \frac{MTTF^{2}}{2\times MTTR} \ & = \frac{200000^{2}}{2\times 24} \ & \cong 830000000 \end{split}大约比单个数据库好了 4000 倍吧。这就是冗余的作用。
3. 数据库冗余实践
我们用一个简单的 MariaDB 数据库来实际操作一遍。其它系统的原理也大致如此。实际工作中 MySQL 更流行,其实两者使用起来差不多。如果需要确保软件上游,给 Oracle 付钱用 MySQL 是比较好的,然而既然你付了钱,为什么不用 oracle 数据库呢。我们没钱的二逼小厂用 MariaDB,或者 Postgres。
我们这里只考虑最简单的高可用,性能并不在考虑范围内。
我们的目标是是两个数据库,平时在 db1 上读写,如果 db1 故障了,就切换到 db2。为了方便,我们这里用 docker 演示。
(1) 下载 docker 镜像
docker pull bitnami/mariadb-galera:10.9.5-debian-11-r2
(2) 创建独立 docker 网络
docker network create mynet
(3) 启动第一个数据库我们新建一个库,在 mynet 网络里面,可以通过域名 db1 访问到这个库。
# <db1> docker run -d --name db1 --net mynet \ -e MARIADB_GALERA_CLUSTER_NAME=my_galera \ -e MARIADB_GALERA_MARIABACKUP_USER=my_mariabackup_user \ -e MARIADB_GALERA_MARIABACKUP_PASSWORD=my_mariabackup_password \ -e MARIADB_ROOT_PASSWORD=my_root_password \ -e MARIADB_GALERA_CLUSTER_BOOTSTRAP=yes \ -e MARIADB_USER=my_user \ -e MARIADB_PASSWORD=my_password \ -e MARIADB_DATABASE=my_database \ -e MARIADB_REPLICATION_USER=my_replication_user \ -e MARIADB_REPLICATION_PASSWORD=my_replication_password \ bitnami/mariadb-galera:10.9.5-debian-11-r2
这一步以后稍等一会,可以使用下面的命令查看日志是否报错:
docker logs -f db1
(4) 添加一个库添加一个库 db2,连接到 db1 上。
docker run -d --name db2 --net mynet \ -e MARIADB_GALERA_CLUSTER_NAME=my_galera \ -e MARIADB_GALERA_CLUSTER_ADDRESS=gcomm://db1:4567,db2:4567 \ -e MARIADB_GALERA_MARIABACKUP_USER=my_mariabackup_user \ -e MARIADB_GALERA_MARIABACKUP_PASSWORD=my_mariabackup_password \ -e MARIADB_ROOT_PASSWORD=my_root_password \ -e MARIADB_REPLICATION_USER=my_replication_user \ -e MARIADB_REPLICATION_PASSWORD=my_replication_password \ bitnami/mariadb-galera:10.9.5-debian-11-r2
(5) 检查集群状态连接一个库,检查集群状态。
docker exec -it db1 /bin/bash # 进来以后 mysql -umy_user -pmy_passowrd
登录数据库后运行:
SHOW GLOBAL STATUS LIKE 'wsrep_%';
检查集群是否有2个节点了。
(5) 使用数据库我们登录db1,创建一点数据。
docker exec -it db1 /bin/bash mysql -umy_user -pmy_password use my_database; # 进入mysql界面后创建表和数据 CREATE TABLE Persons ( PersonID int, LastName varchar(255), FirstName varchar(255), Address varchar(255), City varchar(255) ); insert into Persons (PersonID,LastName,FirstName) values (1, "Dwayne", "Johnson"); insert into Persons (PersonID,LastName,FirstName) values (2, "Taylor", "Swift");
登录 db2 插入一条数据。
docker exec -it db1 /bin/bash mysql -umy_user -pmy_password # 进入mysql界面后插入数据 use my_database; insert into Persons (PersonID,LastName,FirstName) values (3, "Leonardo", "DiCaprio"); # 然后查看数据是不是有3条了 select * from Persons;
(6) 制造意外,关掉一个库
docker stop db1
试试db2还能不能用了:
docker exec -it db1 /bin/bash mysql -umy_user -pmy_password # 进入mysql界面后插入数据 use my_database; insert into Persons (PersonID,LastName,FirstName) values (4, "Vin", "Diesel"); select * from Persons;
(7) 再启动一个库
docker run -d --name db3 --net mynet \ -e MARIADB_GALERA_CLUSTER_NAME=my_galera \ -e MARIADB_GALERA_CLUSTER_ADDRESS=gcomm://db1:4567,db2:4567,db3:4567 \ -e MARIADB_GALERA_MARIABACKUP_USER=my_mariabackup_user \ -e MARIADB_GALERA_MARIABACKUP_PASSWORD=my_mariabackup_password \ -e MARIADB_ROOT_PASSWORD=my_root_password \ -e MARIADB_REPLICATION_USER=my_replication_user \ -e MARIADB_REPLICATION_PASSWORD=my_replication_password \ bitnami/mariadb-galera:10.9.5-debian-11-r2 docker logs -f db3 # 看日志等一会
(8) 登录 db3 查看数据是否同步
docker exec -it db3 /bin/bash
mysql -umy_user -pmy_password
use my_database;
select * from Persons;
(9) 网络配置实际使用过程中,可以用 keepalived 配置把流量转过去。有些网络不支持 keepalived,可以用 sqlproxy 或者其他中间件,甚至 Nginx stream 也可以。
stream {
upstream dbservers {
zone dbservers 64k;
server db1:3306;
server db2:3306 backup;
}
server {
proxy_pass dbservers;
health_check;
}
}
(10) 收拾现场
docker stop db1 db2 db3 docker rm db1 db2 db3 docker rm mynet docker image rm bitnami/mariadb-galera:10.9.5-debian-11-r2