slave是单线程的模式,应用relay日志速度较慢(single thread replication)
应用relay日志进行重放
将应用后的relay位置写入到relay-info文件
为了提高磁盘的写入性能,更新relay-info文件并不总是调用fsync操作,其仅是写入操作系统缓存。因此当slave数据库宕机重启之后,relay-info的位置可能还是处于之前的某个点,而slave已经同步了这些数据,故这会导致slave重复去执行这些SQL语句。常见的后果是slave上的replication sql thread服务停止,抛出1062的错误,DBA需要跳过这些语句使得replication服务继续。然而1062的错误并不会导致数据的不一致,因为有唯一键的约束,更需要担心的是那些重复执行,但是没有唯一约束的语句,如UPDATE table SET value = value + 1 WHERE id=xxx。
有经验的DBA可能会说将二进制日志的格式设置为row可以避免上述数据不一致的问题。是的,或许对于UPDATE适用。然而row格式并不意味着其就是幂等的。因为对于INSERT语句,row格式也就显得无能为力。
MySQL 5.5版本引入了参数sync_relay_log_info,其和sync_binlog的工作原理差不多。即在slave重做一个事务后,再对relay-info更新完成后进行fsync操作(这里为了提高性能使用了fdatasync),以此来避免MySQL数据库宕机后导致relay-info不一致的情况。然而,这依然可能导致问题产生,例如当slave重做了日志,但是还没有更新relay-info之前slave数据库宕机了,这会导致会有多一个SQL语句被重复执行。虽然数据不一致的情况可能会减少发生,这依然存在不一致的潜在情况。更重要的是,这对磁盘的fsync要求大为提高,从而对slave的apply性能是一个极大的考验。
Google最先对这个问题提出了解决方案,他们的处理方式是在每次事务commit时将relay-info的位置写入到InnoDB存储引擎的sys_header中。而由于事务在commit时本就会向sys_header写入一些额外的信息,因此这不会带来额外的开销。这个解决方案的目标显然只能特定于InnoDB存储引擎表,并不是一个通用事务存储引擎的解决方案。另外,这个解决方案也不支持XA事务。总体来说虽是一个很好的解决办法,然而还有些欠缺。
MySQL 5.6采用了完全不同的处理方式,其通过参数relay_log_info_repository允许将relay-info的信息存放在表中,而这个表是InnoDB存储引擎(MySQL 5.6.2之前为MyISAM),故可以保证持久性以及replication的安全性(safe-replication)。此外,通过这个方式保证重做二进制日志与更新relay-info是原子的,其实现方式为将重做二进制日志的事务修改为了如下过程:
BEGIN;
redo binary log;
UPDATE slave_relay_log_info SET relay_log_name=xxx, relay_log_pos=xxx ... ;
COMMIT /* xid= */
这种方式的对于MyISAM这类非事务引擎表依然会导致主从不一致,然而这是由于引擎本身就不能保证持久性。而对于其他事务引擎来说,该处理方式可以解决slave宕机重启后主从数据不一致的问题。InnoSQL目前的版本是5.5,其通过移植MySQL 5.6的这部分代码来解决目前MySQL 5.5版本存在的第一个问题。
第二个问题是关于slave单线的工作模式。这种设计架构可以保证主从的一致性,然而因为是单线程的方式(我注意到PostgreSQL也是单线程,至少在9.0版本时这样的),这会导致slave应用二进制日志非常缓慢,特别在主服务器写入压力比较大的情况下。对于想通过Replication机制来实现读写分离就显得比较困难了,因为slave读到的数据可能是之前的数据。在我遇到的大部分情况下,slave大多仅用于数据库的备份,对于查询也只是一些特定时间段的类似报表查询,很少能在一个生产环境中通过Replication来实现“真正”的读写分离(一般需要前端一个中间件来控制)。此外,当主机宕机将slave提升为master,需要等待较长的时间,这在云的环境下可能是不能被接受的情况。
对于这个问题,目前的解决方案有:
pre-fetch
MySQL 5.6多库并行复制
基于row格式的并行复制
pre-fetch的思想是通过预先将relay日志转化为SELECT语句,然后通过多线程的方式去执行SELECT,当slave在应用二进制日志时,因为之前读取的页都已经在缓冲池中,这样可以加快slave重做的速度。这样的工具有mk-slave-prefetch(现已整合到Percona toolkit中),还有通过MySQL binlog API实现的replication-booster-for-mysql。个人更倾向于通过MySQL binlog API来实现,不过貌似现在binlog API还是alpha版本。此外这两个工具都仅支持statement格式的二进制日志格式。淘宝的兄弟们也开发了类似的工具,为了不重复造轮子,他们开发的版本仅支持row格式。
MySQL 5.6的并行复制方案使得可以有多个线程同时应用relay日志。不过其是基于库的并行复制。这样实现的方式应该来说比较简单有效,缺点是需要在库的设计上花一番功夫,对于目前已经在线上跑的库可能就无能为力了(大部分的设计都喜欢单库多表)。
基于row格式的并行复制最早是由淘宝的兄弟开发实现,做到真正的并行性,MariaDB也将基于这种方案来实现并行复制。这个方案的前提是二进制日志需采用row格式。实现的思想是根据二进制日志的每行建立一个哈希表,判断slave重做的行是否在哈希表中,如不在则可以进行并发的应用,否则等待(不知道我理解的是否正确)。这是一个非常非常有创新的想法,并行性比MySQL 5.6的现实方式更好。但缺点是row格式的日志要求过于严格,因为对于博客这类包含大记录格式的应用来说,目前MySQL 5.5的row格式并不理想,其需要记录每行所有列的前项(MySQL 5.6已经解决,仅记录更改的列)。另外,这个方案对于slave或者master宕机之后能否保证数据一致也是需要考虑的问题。
InnoSQL的实现思路希望是可以加快slave的重放日志速度,但是希望是一种更为通用的方式,支持所有的二进制日志格式。为此InnoSQL提出了prefetch + batch commit的设计方案。即通过prefetch来并行的预读数据,通过batch commit来减少fsync的次数,从而加速slave应用二进制的速度。对于batch commit其添加了新的参数batch_commit_count,用来设置每次batch事务的数量,其最终将应用二进制日志合并为下面的方式:
BEGIN;
redo binary log;
UPDATE slave_relay_log_info SET relay_log_name=xxx, relay_pos=xxx ... ;
redo binary log;
UPDATE slave_relay_log_info SET relay_log_name=xxx, relay_pos=xxx ... ;
redo binary log;
UPDATE slave_relay_log_info SET relay_log_name=xxx, relay_pos=xxx ... ;
......
COMMIT; /* xid= */
这样的设计的好处是可以支持各种二进制日志格式,batch方式的执行事务性能可能会比并行的单条执行要来得更为有效。另外,通过将relay-info信息存放到InnoDB表可以保证宕机后数据的一致性。不过对于DDL语句,slave服务器可能会重复执行,不过这不会对slave数据一致产生影响。
第一版本的batch commit已经基本实现,prefetch并没有开发而是直接使用了mk-slave-prefetch。下面是一个非常简单的测试,测试环境为:
Benchmark:sysbench(纯update测试)
DB size:9.1G
InnoDB Buffer Pool:12G
测试是全内存的,其目的在于发现最大写压力情况下从服务器应用速度。测试也没有开启mk-slave-prefetch,因为测试前master和slave数据都已经预热。最终测试结果如下图所示:
将batch_commit_count设置为1即为原MySQL数据库的工作方式,这里比较了原版与每次batch执行500个SQL语句之间的差异。可以发现两次测试master上的结果都差不多,平均每秒的更新在7000 update/s。而slave的应用速度却截然不同。可以发现原MySQL的工作方式下,slave服务器在测试30分钟之后还需要额外的30分钟来完成重放二进制日志,最终达到主从数据的一致性。在测试期间slave的更新速度差不多在3000 update/s(slave的update数值需要除以2,多了一次relay-info表的更新)。而应用batch commit,sysbench测试完成后两边的数据即已经一致。
再来观察下测试过程中磁盘IO的使用率的情况:
可以看到在测试过程中,master服务器上的IO使用率在60%左右。由于原版MySQL slave数据库的工作是单线程的,因此在测试过程中slave的IO利用率在45%左右,当测试在master库上完成后,其IO利用率会有所上升。而开启batch commit功能后,slave服务器上的IO使用率下降为了25%,而这时slave数据库反而能跟上master库。可见并发的更新并不一定比批量执行效率来得高。
最后观察InnoDB的重做日志刷新次数:
可以发现原版本MySQL Replication下,slave上需要更多的日志刷新。而启用batch commit后,fsync的次数下降非常多。
总的来说,这只是一个非常简单的测试。不过其展示了InnoSQL对于slave应用重做日志的改进。更多地测试结果将陆续放出,而补丁也会尽快开放下载。最后祝大家玩得开心。
声明: 此文观点不代表本站立场;转载须要保留原文链接;版权疑问请联系我们。