站内链接:

主从原理

容灾架构

容灾数据同步和复制文章中我们详细描述了数据同步和复制的几种方式以及 MySQL 使用到异步复制、半同步复制、增强半同步复制等,同时在容灾-策略和架构中我们系统性的阐述了如下几个观点或需求:

  • 为何要对系统使用容灾?
  • 容灾架构的演进是怎样的?

在此文章中我们将开始实际动手进行 MySQL 的主从配置实践。注意,主从配置网络上实际上有两个版本,分别是 GTID 版本和非 GTID 版本,如果没有认真的了解 MySQL 版本演进历史则很容易在这里产生混淆。

主从原理

无论主从配置使用何种配置,主备之间的数据交互方式都是使用相同的原理,通过如下几个角色完成了主端消息到备端的推送和拉取工作:

  • 连接工作线程:处理 SQL 请求并写入数据到磁盘中
  • log dump 线程:监听 binlog 日志变更并 notify 从库有新的记录更新
  • IO 线程:接收主端的消息通知并主动从主端 binlog 中拉取最新的记录
  • binlog 和 relaylog:这两个日志为同步复制数据的存储和中转提供了位置
  1. 下面是最新版本的主备数据同步过程:

mysql-sync-binlog

其同步原理或者流程如下:

  1. 客户端将写入数据的需求交给主节点,主节点先向自身写入数据。
  2. 数据写入完成后,紧接着会再去记录一份Bin-log二进制日志。
  3. 配置主从架构后,主节点上会创建一条专门监听Bin-log日志的log dump线程。
  4. log dump线程监听到日志发生变更时,会通知从节点来拉取数据。
  5. 从节点会有专门的I/O线程用于等待主节点的通知,当收到通知时会去请求一定范围的数据。
  6. 当从节点在主节点上请求到一定数据后,接着会将得到的数据写入到relay-log中继日志。
  7. 从节点上也会有专门负责监听relay-log变更的SQL线程,当日志出现变更时会开始工作。
  8. 中继日志出现变更后,接着会从中读取日志记录,然后解析日志并将数据写入到自身磁盘中。

分布式和主从

注意,在高可用、高性能架构中我们往往会听到数据的分布式存储,但是主从架构并非分布式,换种说法,这两者是两个维度的东西,主从架构考虑的对象是当前系统的所有数据在遭遇可能得伤害时可以快速恢复的问题,所以主从架构中各个节点会同步所有数据,这可能也有历史遗留问题,早些年由于分布式技术还未完善,那时候的主从架构并未考虑数据分片等问题。

但是,所有节点同步所有数据就会产生如下几个致命问题:

  • ① 硬伤:木桶效应,一个主从集群中所有节点的容量,受限于存储容量最低的哪台服务器。解决:提高服务器的配置,分库分表等
  • ② 数据一致性问题:由于同步复制数据的过程是基于网络传输完成的,所以存储延迟性。解决:增强复制方式
  • ③ 脑裂问题:从节点会通过心跳机制,发送网络包来判断主机是否存活,网络故障情况下会产生多主。解决:同网段、网络专线等

术语

  1. server id

    这是用于在主从架构中每一个节点用于识别自身的 ID,所以该值不能相同

  2. bin log

    主从之间 bin-log 起到非常重要的作用,作为主从之间通信的桥梁,里面存储的是主服务器执行的 mysql 命令,在从服务器原样的执行。主要用于从库和从库之间的同步配置,创建一个从库的 bin-log 文件并将从库的 relay-log 信息写入 relay-log 中,从而实现从库和从库的数据同步。

主从复制

非 gtid 配置

主从复制的基本步骤如下:

  • a. 在主库上设置二进制日志记录(binlog)和唯一标识符(server-id)
  • b. 在从库上设置唯一标识符(server-id), 其中 server-id 必须是唯一的
  • c. 在从库上配置连接到主库的权限,并指定要复制的数据库和表, 这是通过下面的步骤 5(建立授权账户)来完成的, 一般情况下是所有库所有表
  • d. 在从库上配置并启动复制进程, 见下方步骤 7
  1. 前期准备工作-主服务器上基本配置
1
2
用户 :bamboo/bamboo
数据库:bamboo
  1. 前期准备工作-从服务器上:
1
2
用户:slavetest/slavetest
数据库:bamboo
  1. 前期准备工作-创建主从服务器上的用户并分配正常的用户权限:
1
2
3
4
5
6
7
8
9
10
-- 主,该账户会在备端通过change master命令进行主备同步操作
create user bamboo identified by 'bamboo';
GRANT ALL PRIVILEGES ON *. * TO bamboo@localhost identified by 'bamboo' with grant option;
-- 一般是最小权限
GRANT REPLICATION ON *. * TO bamboo@localhost identified by 'bamboo' with grant option;

-- 从,该账户仅仅在备端有用
create user slavetest identified by 'slavetest';
GRANT ALL PRIVILEGES ON *. * TO slavetest@localhost identified by 'slavetest' with grant option;
flush privileges;
  1. 前期准备工作-设置主从服务器上的 my.cnf 文件配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# a. 主
log_bin = mysql-bin
server_id = 1 # 服务器ID是唯一的
binlog-do-db = bamboo # 主从复制涉及的数据库名
expire-logs-days = 7

# 配置完成保存之后重启服务:service mysqld restart

# b. 从
server_id = 2 # 服务器ID,不能重复
replicate-do-db = bamboo # 需要复制的数据库名
slave-skip-errors = 1032,1062,126,1114,1146,1048,1396 #自动跳过的错误代码,以防复制出错被中断
log_error = /var/log/mysql/error.log # 开启错误日志
# 配置完成之后重启服务:/etc/init.d/mysql restart
  1. 主从配置-建立账户授权 slave
1
2
3
4
5
6
-- 在主服务器上建立账户并授权slave:(可以使用bamboo登录)
grant replication slave on *.* to 'bamboo'@'%' identified by 'bamboo';
-- %表示所有的客户端, 获取二进制日志名和偏移量(数据库启动之后从这个点开始进行数据恢复)
show master status;
-- 获取File字段、Position字段信息(mysql-bin.000001 | 2719 | bamboo)

在这里配置完成之后就应该关闭所有应用服务, 然后根据show master status获取 position 信息, 后续备端会根据该信息为起点进行主从数据同步.

  1. 主从配置-开始导出主端数据:
1
2
# 导出bamboo数据库
mysqldump –ubamboo –p bamboo > bamboo.sql
  1. 主从配置-在备端导入数据:

此时主端的所有操作已经完成, 此时主端可以恢复写操作, 然后在备端导入上一个步骤从主端导出的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在从服务器上导入数据库并配置slave
mysqldump –uroot –p admin < test.sql
# 使用bamboo登录, 指定复制使用的用户, 主服务器IP, 端口, 开始执行复制的日志文件和位置, 这里的日志文件和位置由步骤5中获取
stop slave
change master to master_host='172.16.161.253',master_user='bamboo',
master_password='bamboo',master_log_file='mysql-bin.000001',master_log_pos=2719;
# 在从库启动slave
start slave
# 查看状态信息:
show slave status\G;

# 保证下面两项是完全一致的:
# Relay_Master_Log_File: mysql-bin.000001
# Slave_IO_Running: Yes(IO线程)
# Slave_SQL_Running: Yes
# Replicate_Do_DB: bamboo
# Second_behinds_master:0(主从同步延迟)
  1. 进行相关的验证工作

gtid 配置

gtid 相比非 gtid 的主从配置,前者更加的便捷,其中主备两端的用户创建这里就不再阐述,见上一节内容。

  1. 配置my.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# a. 主
log_bin = mysql-bin
server_id = 1 # 服务器ID是唯一的
enforce-gtid-consistency = ON
gtid_mode = ON
binlog-format = ROW

# 配置完成保存之后重启服务:service mysqld restart

# b. 从
server_id = 2 # 服务器ID,不能重复
log_bin = /var/log/mysql/mysql-bin.log
relay-log = /var/log/mysql/mysql-relay-bin.log
enforce-gtid-consistency = ON
gtid_mode = ON
binlog-format = ROW
  1. 从服务器上进行主从复制配置,注意同非 gtid 的区别
1
2
3
4
5
6
7
CHANGE MASTER TO MASTER_HOST='172.16.161.253', MASTER_USER='bamboo', MASTER_PASSWORD='bamboo', MASTER_AUTO_POSITION = 1;

-- 启动复制
start slave

-- 查看复制状态
show slave status\G;

另外, 在配置 GTID 主从复制之前,确保主服务器的所有事务都已经复制到从服务器,避免数据不一致,GTID 仅仅在 MySQL5.6+版本才生效。

server id

一旦涉及主从同步就需要有一个唯一识别码来确定数据库实例, 防止在链式主从, 多主多从拓扑中进行无线循环. 那么应该怎么生成server id呢? 其格式有什么默认的规则呢?

  • 随机数
  • 时间戳
  • IP 地址+端口
  • 集中分配, 例如管理中心自增 ID

对于小型的主从架构, 一般手动定义两个不同的整型数即可, 比如 1.3 节的主从配置中的server_id值. 我们可以通过服务器查看server_id的值:

1
show variables like '%server_id%';

除了server id之外, mysql5.6 复制引入了server uuid的概念, 一般这个值是自动创建, 不会存在重复的可能, 但是如果直接拷贝数据库 data 目录到从机器的时候会碰到此问题, 尤其是直接对虚拟机环境进行克隆(这是常见的操作), 此时会碰到问题:

1
2
3
4
# error 1
Fatal error: The slave I/O thread stops because master and slave have equal MySQL server ids;
# error 2
Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work.

此时就需要手动修改 server uuid 或者重新初始化数据库信息才可以解决该问题.

读写分离

读写分离是指在主从复制的基础上,将读和写分离到不同的服务器上,以提高数据库的并发能力和读取速度。在读写分离中,写操作只在主数据库上执行,读操作则可以在多个从数据库上执行,以此分担主数据库的压力。关于读写分离导致的数据不一致问题见下面第四章节内容.

主主复制

高可用架构

在第一章已经讲解了主从复制的原理以及配置, 但是主从复制有如下特点:

  • 性能上(优): 通过读写分离提高了吞吐量(QPS), 从库为高可用, 有一定的读可用性
  • 可用性(差): 主库为单点, 一旦主库挂了则无法进行写入操作

为了解决该问题, 一般使用主备架构而非主从架构, 一旦主库挂了就会自动启用备库, 这样是实现了服务的高可用, 一个主备架构除了要搭建双主复制之外, 还需要搭配第三方故障转移高可用方案, 比如 keepalive 等, 这就是 DBA 和运维专业领域. 下面简单的介绍几种高可用架构:

  • a. 主从复制, 非高可用
  • b. 主备 + 第三方故障转移, 双主热备高可用
  • c. 高可用复合架构, 将主备和主从结合在一起, 实现高可用和高性能架构, 当然这个也需要第三方故障转移来实现, Mysql 集群就是使用该方案

数据一致性

读写延时

为了提高网站的性能, 我们对数据库设置读写分离之后, 常常会碰到数据延时或数据不一致问题, 这个在低延迟场景下一般不会出现, 当然, 若是主从服务之前的同步机制出现异常, 那就是生产运维事故了. 那么, 对于数据延迟问题, 我们有什么解决方案呢?

  1. 全同步或半同步复制, 具体见 1.1 节介绍, 不过该方式对生产环境要求非常高, 否则可能造成主数据库的事务积累
  2. 前端伪缓存, 即在提交一个表单并保存成功并返回之后前端直接从本地浏览器缓存中读取刚刚提交的数据并展示, 而不是通过另外一个请求从读数据库中获取数据, 但是这个方法还是有风险的. 该机制有点类似本地缓存, 但是在写入公共数据的时候容易造成数据的生产者所见页面和数据的消费者所见页面不一致问题, 因为数据存储在生产者本地缓存中
  3. 使用临时缓存机制, 在数据写入主数据库成功之后设置缓存, 后续的读请求都会直接走缓存中的数据

缓存不一致

4.1 节我们引入缓存来解决主从数据不一致问题, 但是在删除缓存时可能产生不一致问题:

  • a. 缓存数据和写数据库数据已删除, 此时读数据库仍然存在数据
  • b. 极限情况下, 在 request1 更新数据库完成准备更新缓存的时候, 一个 request2 读请求已经从数据库读取数据, 此时可能会发生缓存中的数据先被 request1 删除然后又被 request2 更新的问题
  • c. 在操作数据库成功, 但是写入缓存失败的时候

那么, 如何解决缓存不一致问题呢?

  • 缓存标记法(后台): 在 request1 请求到达时会对缓存进行标记, 此时 request2 请求会进入主数据库进行数据读取操作
  • 消息队列重试操作: 对于写入数据库成功, 更新缓存失败的场景, 引入消息队列进行重试操作
  • 缓存过期时间: 每个缓存的时间比较短, 这样即使有不一致问题也控制了影响范围, 算是一直无奈的做法
  • 分布式锁来更新缓存数据, 避免因并发导致的缓存数据错误问题

参考: