高可用和数据库冗余实践

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

By .