高可用和数据库冗余实践
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