binlog 可以用来归档,也可以用来做主备同步,但它的内容是什么样的呢?为什么备库执行了 binlog 就可以跟主库保持一致了呢? MySQL 能够成为现下最流行的开源数据库, binlog 功不可没。
一、基本原理
如图 1 所示就是基本的主备切换流程:
在状态 1 中,客户端的读写都直接访问节点 A ,而节点 B 是 A 的备库,只是将 A 的更新都同步过来,到本地执行。这样可以保持节点 B 和 A 的数据是相同的。
当需要切换的时候,就切成状态 2 。这时候客户端读写访问的都是节点 B ,而节点 A 是 B 的备库。
把备库设置成只读:防止切换逻辑有 bug或者误操作。同时readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新的
线程,就拥有超级权限。
再看看 节点 A 到 B B 这条线的内部流程是什么样的 :画出的就是一个 update语句在节点 A 执行,然后同步到节点 B 的完整流程图。
主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog 。
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP 、端口、用户名、密码,以及要从哪个位置开始请求 binlog ,这个位置包含文件名和日志偏移量。
2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和sql_thread 。其中 io_thread 负责与主库建立连接。
3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog ,发给 B 。
4. 备库 B 拿到 binlog 后,写到本地文件,称为中转日志( relay log )。
5. sql_thread 读取中转日志,解析出日志里的命令,并执行。
binlog 里面到底是什么内容,为什么备库拿过去变成relay log可以直接执行?
二、binlog三种格式的对比
1.当 binlog_format=statement 时, binlog 里面记录的就是 SQL 语句的原文
因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a ;而在备库执行这条 SQL 语句的时候,却使用了索引
t_modified 。因此, MySQL 认为这样写是有风险的。即在 statement 模式下还必须记录每条语句在执行的时候的一些相关信息,也就是上下文信息,
2. binlog_format=‘row’ 。 row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event : Table_map 和 Delete_rows 。
binlog 里面记录了这条语句所执行行为之后每一条记录的变化情况。
3.为什么会有 mixed 格式的 binlog ?
row 格式的缺点是,很占空间。比如你用一个 delete 语句删掉 10 万行数据,用 statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog ,就要把这 10 万条记录都写到 binlog 中。
MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog:会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
现在越来越多的场景要求把 MySQL 的 binlog 格式设置成 row:有恢复数据的好处。分别从 delete 、 insert 和 update 这三种 SQL 语句的角度,来看看数据恢复的问题。
即使我执行的是 delete 语句, row 格式的 binlog 也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条 delete 语句以后,发现删错数据了,可以直接把binlog 中记录的 delete 语句转成 insert ,把被错删的数据插入回去就可以恢复了。
如果执行的是 update 语句的话, binlog 里面会记录修改前整行的数据和修改后的整行数据。如果你误执行了 update 语句的话,只需要把这个 event 前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了。
row 格式下,如果你是执行错了 insert 语句呢? insert 语句的 binlog 里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把 insert 语句转成delete 语句
binlog 的特性确保了在备库执行相同的 binlog ,可以得到与主库相同的状态。认为正常情况下主备的数据是一致的。实际生产上使用比较多的是双 M 结构,也就是图 9 所示的主备切换流程。
双 M 结构和 M-S 结构,其实区别只是多了一条线,即:节点 A 和 B之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。
当然,这也存在问题:
业务逻辑在节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B ,节点 B 执行完这条更新语句后也会生成 binlog
那么,如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。
我们可以用下面的逻辑,来解决两个节点间的循环复制的问题:
1. 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
2. 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog ;
3. 每个库在收到从自己的主库发过来的日志后,先判断 server id ,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
如果我们设置了双 M 结构,日志的执行流就会变成这样:
1. 从节点 A 更新的事务, binlog 里面记的都是 A 的 server id ;
2. 传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id ;
3. 再传回给节点 A , A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了。
主备切换可能是一个主动运维动作,比如软件升级、主库所在机器按计划下线等,也可能是被动操作:主库所在机器掉电。
与数据同步有关的时间点主要包括以下三个:
1. 主库 A 执行完成一个事务,写入 binlog ,我们把这个时刻记为 T1;
2. 之后传给备库 B ,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
3. 备库 B 执行完成这个事务,我们把这个时刻记为 T3
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1 。
可以在备库上执行 show slave status 命令,它的返回结果里面会显示:seconds_behind_master ,用于表示当前备库延迟了多少秒,其实就是T3-T1
在网络正常的时候,日志从主库传给备库所需的时间是很短的,即 T2-T1 的值是非常小的。所以,主备延迟的主要来源是备库接收完 binlog 和执行完这个事务之间的时间差,也就是备库消费relay log的速度,比主库生产 binlog的速度要慢
主备延迟的来源:
1)备库所在机器的性能要比主库所在的机器性能差。当备库主机上的多个备库都在争抢资源的时候,就可能会导致主备延迟了。
2)备库的压力大。由于主库直接影响业务,大家使用起来会比较克制,反而忽视了备库的压力控制(主写备读的话):备库上的查询耗费了大量的 CPU 资源解决办法:
3)大事务。主库上必须等事务执行完成才会写入 binlog ,再传给备库。如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。比如不要 一次性地用 delete 语句删除太多数据,或者是达标的DDL
4)备库的并行复制能力
由于主备延迟的存在,所以在主备切换的时候,就相应的有不同的策略。
1.可靠性优先策略
1) 判断备库 B 现在的 seconds_behind_master ,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
2) 把主库 A 改成只读状态,即把 readonly 设置为 true ;
3)判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
4) 把备库 B 改成可读写状态,也就是把 readonly 设置为 false ;
5) 把业务请求切到备库 B 。
但是,这个切换流程中是有不可用时间的。因为在步骤 2 之后,主库 A 和备库 B 都处于readonly 状态,也就是说这时系统处于不可写状态
在这个不可用状态中,比较耗费时间的是步骤 3 ,可能需要耗费好几秒的时间。这也是为什么需要在步骤 1 先做判断,确保 seconds_behind_master 的值足够小。否则,如果一开始主备延迟三十分钟,不做判断直接切换,那么系统不可用时间就会长达30分钟
那能不能先直接切换到备库 B ,但是保持 B 只读呢?这样也不行。
因为,这段时间内,中转日志还没有应用完成,如果直接发起主备切换,客户端查询看不到之前执行完成的事务,会认为有 “ 数据丢失 ” 。即使查询到 “ 暂时丢失数据的状态 ” 也是不能被接受的。
2.可用性优先策略
如果我强行把步骤 4 、 5 调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库B(即直接把业务切到备库中) ,并且让备库 B 可以读写,系统几乎就没有不可用时间了:代价就是可能出现数据不一致的现象
例子:假设有一个表 t ,这个表定义了一个自增主键 id ,初始化数据后,主库和备库上都是 3 行数据。
mysql> CREATE TABLE `t` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`c` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(c) values(1),(2),(3);
接下来,业务人员要继续在表 t 上执行两条插入语句的命令,依次是:
insert into t(c) values(4);
insert into t(c) values(5);
假设,现在主库上其他的数据表有大量的更新,导致主备延迟达到 5 秒。在插入一条 c=4 的语句,发起了主备切换。
下图 是 可用性优先策略,且 binlog_format=mixed 时的切换流程和数据结果
1. 步骤 2 中,主库 A 执行完 insert 语句,插入了一行数据( 4,4 ),之后开始进行主备切换。
2. 步骤 3 中,由于主备之间有 5 秒的延迟,所以备库 B 还没来得及应用 “ 插入 c=4” 这个中转日志,就开始接收客户端 “ 插入 c=5” 的命令。
3. 步骤 4 中,备库 B 插入了一行数据( 4,5 ),并且把这个 binlog 发给主库 A 。此时relaylog还没有执行(因为主备延迟问题),先处理了客户端发来的命令
4. 步骤 5 中,备库 B 执行 “ 插入 c=4” 这个中转日志,插入了一行数据( 5,4 )。而直接在备库 B 执行的 “ 插入 c=5” 这个语句,传到主库 A ,就插入了一行新数据( 5,5 )。
最后的结果就是,主库 A 和备库 B 上出现了两行不一致的数据。
那么,如果我还是用 可用性优先策略,但设置 binlog_format=row ,情况又会怎样呢?
因为 row 格式在记录 binlog 的时候,会记录新插入的行的所有字段值,所以最后只会有一行不一致(即A的(4,4)和B的(4,5))。而且,两边的主备同步的应用线程会报错 duplicate key error 并停止。也就是说,这种情况下,备库 B 的 (5,4) 和主库 A 的 (5,5) 这两行数据,都不会被对方执行。
结论:
1. 使用 row 格式的 binlog 时,数据不一致的问题更容易被发现。而使用 mixed 或者 statement 格式的 binlog 时,数据很可能悄悄地就不一致了。
2.主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,我都建议你使用可靠性优先策略。毕竟对数据服务来说的话,数据的可靠性一般还是要优于可用性的。
如果备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别。备库很可能永远都追不上主库的节奏。
关注的是图中黑色的两个箭头,一个箭头代表了客户端写入主库,另一箭头代表的是备库上 sql_thread 执行中转日志( relay log )
在主库上,影响并发度的原因就是各种锁了。而日志在备库上的执行,就是图中备库上 sql_thread 更新数据 (DATA) 的逻辑。如果是用单线程的话,就会导致备库应用日志不够快,造成主备延迟。
在官方的 5.6 版本之前, MySQL 只支持单线程复制,由此在主库并发高、 TPS 高时就会出现严重的主备延迟问题。
1.
到最新版本的多线程复制,中间的演化经历了好几个版本。所有的多线程复制机制,都是要把图 1 中只有一个线程的 sql_thread ,拆成多个线程。
coordinator 就是原来的 sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务,真正更新日志的,变成了 worker 线程。而 work 线程的个数,就是由参数slave_parallel_workers 决定的。
事务能不能按照轮询的方式分发给各个 worker ,也就是第一个事务分给 worker_1 ,第二个事务发给 worker_2 呢?不行,事务被分发给 worker 以后,不同的 worker 就独立执行了。由于cpu的调度策略(调度线程),很可能第二个事务比第一个事务先执行,那么在主库和备库上的执行顺序相反,会导致主备不一致的问题。
同一个事务的多个更新语句,能不能分给不同的 worker来执行呢?
答案是,也不行。某个事务更新了表 t1 和表 t2 中的各一行,如果这两条更新语句被分到不同 worker 的话,虽然最终的结果是主备一致的,但如果表 t1 执行完成的瞬间,备库上有一个查询,就会看到这个事务 “ 更新了一半的结果 ” ,破坏了事务逻辑的隔离性。
所以, coordinator 在分发的时候,需要满足以下这两个基本要求:
1. 不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker 中。
2. 同一个事务不能被拆开,必须放到同一个 worker 中。
MySQL的5.5,5.6,5.7有不同的并行复制策略,再次不再赘述