注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

MySQL从库CrashSafe问题?

MySQL 彭东稳 5288次浏览 已收录 1个评论

从库CrashSafe解决历程?

MySQL数据库的成功离不开其Replicaiton,相对于Oracle DG和Microsoft SQL Server Log Shipping来说,其简单易上手,基本上1,2分钟内根据手册就能完成环境的搭建。然而,随着使用的深入,Replication自身的问题会慢慢显露,其中非crash safe的特性使得许多DBA感到头疼,甚至不能理解其所发问题的原因。简单来说,CrashSafe Replication是指当master/slave任何一个节点发生宕机等意外情况下,服务器重启后master/slave的数据依然能够保证一致性。关于master CrashSafe是如何保证的,可以看MySQL主库CrashSafe与Binlog关系?,下面主要介绍slave CrashSafe是如何保证的。

CrashSafe slave的情况就有些复杂,而这可能是DBA更为常见的问题。例如slave不断的报1062错误,或者发现主从数据不一致(特别是表没有主键的情况)。而这时DBA的选择通常也很无奈,基本就是全库重建了。所以说,当你有运维超过200台以上的MySQL服务器的经验时,就会发现这是一个很大的问题。

导致不能实现CrashSafe slave有两方面的原因,即replication中的SQL thread和IO thread。

在一个从节点上,同步涉及到 2 个线程:把主节点的二进制日志(binary log)复制到本地中继日志(relay log)的 IO 线程,和执行中继日志中的语句的 SQL 线程。每个线程当前的位置都存储在一个文件里:IO 线程存在 master.info 文件,SQL 线程存在 relay-log.info 文件。当从节点重启后可以根据 master.info 文件中记录的位置继续同步主库binlog。

  • master.info是在io_thread向master发送request_dump时被使用,然后写relay log后文件被更新,因此master.info记录代表了io_thread读取的master信息的位置。
  • relay-log.info是在sql_thread读取未执行的event时使用并更新,所以relay-log.info代表的是当前sql_thread执行到的位置。

目前为止,还不错。第一个问题是,这些文件不是每次写入都同步到磁盘:如果发生崩溃,写入到文件中的位置很可能是不准确的。MySQL 5.5 对这个进行了修复:你可以设置 sync_master_info = 1 和 sync_relay_log_info = 1 来确保写入两个日志文件(默认值1000),且在每个事务完成之后同步到磁盘。同步到磁盘是有消耗的,且对磁盘的消耗特别大,但如果服务器有回写缓存(write-back cache)策略,这些设置还是会起到积极作用,可以接受。

但是,等等,尽管设置了 set sync_master_info = 1 和 sync_relay_log_info = 1,还是可能会出现问题。这两个操作不是在一个事务中,一个是数据库操作,一个是文件操作,因此不能达到原子的效果,复制信息是在事务提交后才写到日志文件的。因此,如果在事务提交之后,复制信息更新之前,发生了崩溃,当服务重启的时候,可能会出现复制信息是错的,并且可能会出现一个事务会被执行两次。这个影响取决于这些事务:复制可能还可以正常运行,或者断开,或者导致数据不一致。

MySQL 5.6开始提供了crash-safe slave的功能,意思是说在slave crash后,把slave重新拉起来可以继续从Master进行复制,不会出现复制错误也不会出现数据不一致。MySQL 5.6采用了一种方法,就是将复制信息保存在InnoDB的事务表中,而不是之前的日志文件,来解决这个问题(当 relay_log_info_repository = TABLE 时,会创建表 mysql.slave_relay_log_info,当 master_info_repository = TABLE 时,会创建表 mysql.slave_master_info)。这时两个操作都是数据库操作,在一个事务中就能得到原子性。想法很简单:我们可以把复制信息的更新包含在事务当中,确保它和数据同步/一致。

写入文件:

写入到表:

然而,这并没有看起来那么简单。对于 SQL 线程而言,因为实例会在一个事务提交的同时更新表 slave_relay_log_info,所以它可以很好的工作。但对于 IO 线程而言,表的更新与事务的执行并没有关系,那么实例是如何知道什么时候去更新这个表呢?

答案:它由 sync_master_info 控制,默认值是 10000,表示 IO 线程的位置,只会每提交 10000 个事务更新一次。这明显不利于从节点开启 crash-safe 功能。一个办法是,设置 sync_master_info = 1,但正如前面所说,它会影响性能导致主从延迟变大(这就是为什么默认值不是 1)。

还有一个更加优雅的解决方法,那就是设置 relay_log_recovery = ON,但它要求重启 MySQL 服务。这个设置确保当 MySQL 服务启动时,会从表 slave_relay_log_info 恢复出最新的 IO 线程的位置。因此,你甚至不需要为了从节点要开启 crash-safe 功能而去把 IO 线程信息存储到表里面。换句话说,没有必要再去设置 master_info_repository = TABLE。

最后再说一下,一旦设置了 relay_log_info_repository = TABLE,因为这个表会在每个事务提交之后更新,所以 sync_relay_log_info 的设置是什么就无关紧要了。因此,你可以安全地把它从配置文件中删除。CrashSafe是运维人员不能忽略的一点,否则DBA将忙于处理这些异常状况导致的slave服务停止的情形。

一些相关参数的说明:

master_info_repository

master_info_repository决定了slave的master status是存储在master.info还是slave_master_info表。

sync_master_info

和master_info_repository相关的参数sync_master_info

  • master_info_repository=FILE ,如果sync_master_info大于0,每sync_master_info个event会把master.info同步到磁盘。如果sync_master_info=0,则MySQL不会主动进行同步,而是由系统来管理同步。
  • master_info_repository=TABLE ,如果sync_master_info大于0,每sync_master_info个event更新slave_master_info表。如果sync_master_info=0,则MySQL不会更新slave_master_info表。

relay_log_info_repository

relay_log_info_repository决定了slave的relay log信息是存储在relay-log.info还是存储在slave_relay_log_info表。

sync_relay_log_info

  • sync_relay_log_info=FILE ,如果sync_relay_log_info大于0,每sync_relay_log_info个事务会把relay-log.info同步到磁盘。如果sync_relay_log_info=0,则MySQL不会主动进行同步,而是由系统来管理同步。
  • sync_relay_log_info=TABLE ,如果slave_relay_log_info表是支持事务的表,则每个事务都会更新slave_relay_log_info表。如果slave_relay_log_info不支持事务,那么sync_relay_log_info>0时会在sync_relay_log_info个event后更新,在sync_relay_log_info=0时,MySQL不会更新slave_relay_log_info表。

所以到这里我们基本弄清楚了 master_info 和 relay_log_info 的来龙去脉。下面我们看一下这些信息被存储在TABLE中和FILE中的区别在哪?实际上这个区别可以认为是文件系统和数据库事务的区别。

MySQL从库CrashSafe问题?

master info会通过mi->flush_info方法更新到FILE或者TABLE,relay log info通过rli->flush_info更新FILE或TABLE。

在FILE的实现中,实际上每次执行flush_io_cache都会写文件,我们读文件可以看到已经被更新了,但是新的写入并不会立马同步到磁盘,在sync_period(也就是参数sync_master_info/sync_relay_log_info)次后MySQL会通过fdatasync写到磁盘。那么这样就存在的问题就在于如果系统宕机,那么还没有持久化到磁盘的信息就会丢失,当然我们可以把sync_period设置为1,那么每次写操作都会进行sync,但是系统的sync操作并不是一个高效的行为,这样势必会牺牲一些性能。

而TABLE方式是依赖数据库事务实现的,如果relay log表支持事务,上述relay log进行flush_info操作会在事务commit时一同提交,这里就可以保证relay log info中存储的点和当前真正执行事务的点是一致的。如果服务宕掉或者系统宕机,如果设置了relay_log_recover=1参数,recovery时就会创建新的relay log,重新初始化sql_thread和io_thread从真正执行的点开始执行,从而保证复制的正确。

到这里,我们可以看出 master_info_repository 和 relay_log_info_repository 默认值被设置为TABLE(默认innodb),主要还是从安全的方面考虑,依赖数据库的事务,可以保证 relay_log_info_repository 存储的点和数据库真正执行的点一致,这一点FILE方式是无法做到的。同时在性能上,和文件系统的直接交互会增加io,而写table是直接写内存中的buffer pool,在性能上也能有一定的提升。

注意到上面 sync_period 在 master info 和 relay log info 中分别是代表了 event 和 transaction,这是因为io_thread是按照 event 来一个一个读取后 flush_info,而sql_thread虽然也是以 event 为单位执行的,但是会等待事务 commit 时才会 flush_info,否则如果事务没有提交已经刷新了 relay log info,然后recovery时会把未 commit 的事务 rollback 。此时重新从 relay log info 的 pos 开始执行就会出错。

两种基于CrashSafe的复制方式区别?

首先都设置为TABLE是必要的,对性能有提升作用。

  • 基于binlog的复制

在基于binlog文件位置的复制下,如何要保证crash safe slave?如果没有开启gtid,那就应该slave_relay_log_info的存储引擎支持事务,然后开启relay_log_recover即可。

这样可行的原因是,relay_log_info_repository = TABLE时,apply event和更新relay_log_info表的操作被包含在同一个事务里,innodb要么让它们同时生效,要么同时不生效,保证位点信息和已经应用的事务精确匹配。同时relay_log_recovery = ON时,会抛弃master_log_info中记录的复制位点,根据relay_log_info的执行位置重新从Master获取binlog,这就回避了由于未同步刷盘导致的binlog文件接受位置和实际不一致以及relay log文件被截断的问题。

在同时使用MTS(multi-threaded slave)时,为保证crash safe slave基于binlog文件位置的复制还需要设置 sync_relay_log=1,因为MySQL在Crash恢复时必须先通过读取relay log补齐MTS导致的事务空洞。

  • 基于5.6 GTID的复制

上面的设置并不适用于基于5.6 GTID的复制。在基于GTID的复制下,crash的Slave重启后,从binlog中解析的gtid_executed决定了要apply哪些binlog记录(5.6 GTID需要开启binlog来确定当前执行到的GTID值),所以binlog必须和InnoDB存储引擎的数据保持一致。要做到这一点,需要把sync_binlog和innodb_flush_log_at_trx_commit都设置为1,即所谓的”双1″。

另外MySQL启动时,会从relay log文件中获取已接收的GTIDs并更新Retrieved_Gtid_Set。由于relay log文件可能不完整,所以需要抛弃已接收的relay log文件。因此relay_log_recovery = ON也是必须的。

这样,对于基于5.6 GTID的复制,保证crash safe slave的设置就是下面这样(小于MySQL 5.7版本设置)。

关于如何设置以确保crash safe slave,官方文档有明确记载,见 17.3.2 Handling an Unexpected Halt of a Replication Slave。

“双1″是必要的,否则crash的Slave重启后,可能会重复应用binlog event也可能会遗漏应用binlog event(#70659)。其中遗漏应用binlog event的情况更可怕,因为Slave在不触发SQL错误的情况下就默默的和Master不一致了。

  • MySQL 5.7针对的Slave CrashSafe改进(基于GTID复制)

如果是MySQL 5.7开启了GTID,就不需要太关心master info和relay log info是记录table还是file了,因为不会重复执行同一个事务。

由于MySQL 5.7可以关闭log_slave_updates(不需要记录binlog)参数了,MySQL会将已执行的GTIDs实时记录到系统表mysql.gtid_executed中,mysql.gtid_executed是和用户事务一起提交的,因此可以保证和实际数据的一致。

如果是异常宕机,宕机后丢失了master info或者relay log info,io_thread会dump到重复的和sql_thread可能会重复执行同一个事务,这样就会导致复制出错。但是如果开启了gtid就可以避免这个问题,虽然io_thread和sql_thread的动作是一样的,但是如果事务的gtid是已经存在,ev->apply_event时就会忽略这个事务。因此不会重复执行同一个事务。

  • MTS下特有的问题

在同时使用MTS(slave_parallel_workers > 1)时,即使按上面crash safe slave的要求设置了基于GTID的复制,Slave crash后再重启还是会导致复制中断。

通过强制杀掉MySQL所在虚机的方式模拟Slave宕机,然后再启动MySQL,MySQL日志中有如下错误消息:

启动slave时也会报错

出现这种现象的原因在于,relay_log_recovery=1 且 slave_parallel_workers>1的情况下,MySQL启动时会进入MTS Group恢复流程,即读取relay log,尝试填补由于多线程复制导致的gap。然后relay log文件由于不是实时刷新的,在relay log文件中找不到gap对应的relay log记录(覆盖了gap的relay log起始和结束位置分别被称为低水位和高水位,低水位点即slave_relay_log_info.Relay_log_pos的值)就会报这个错。

实际上,在GTID模式下,slave在apply event的时候可以跳过重复事件,所以可以安全的从低水位点应用日志,没必要解析relay log文件。 这看上去是一个bug,于是提交了一个bug报告#83713,目前还没有收到回复。

作为回避方法,可以通过清除relay log文件,跳过这个错误。执行步骤如下:

在这里,单纯的调reset slave不能把状态清理干净,内部的Relay_log_info.inited标志位仍然处于未被初始化状态,此时调用start slave仍然会失败。因此需要补一刀change master。

<参考>

与MySQL传统复制相比,GTID有哪些独特的复制姿势?

MySQL 5.6从库开启CrashSafe功能

从源码入手,详解参数master_info_repository和relay_log_info_repository


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)or分享 (0)
关于作者:

您必须 登录 才能发表评论!

(1)个小伙伴在吐槽