复制概述
*mysql支持两种复制方式:基于行的复制和基于语句的复制,这是由binlog的类型决定的,binlog有三种模式,基于行,基于语句和两种都有的混合模式。这两种模式各有优缺点,一般来讲,基于语句的复制模式更加灵活,可以适应例如主备库表的列顺序不一致等问题,同时一般来讲需要消耗的网络资源更少,但是缺点比如说由于在备库上仅有一个重放线程(在5.6版本后开始支持多线程复制),因而语句的重放是串行的,这可能会导致问题比如说有些语句在主库上是并行执行并且操控的是同一份数据,同时语句重放需要解析优化等也会消耗一些时间;而基于行的复制相对来说像是以空间换时间,并且可靠性更高,像前面所说的并行问题对于基于行来说不是问题,但是一般来说基于行的会消耗更多的空间,比如说更新一个表的一整列,对于基于语句来说仅一句话,但是基于行的会有很多行。还有更多的不同后面会提及。
*复制的作用:1.可以把数据分布在不同的地区,我觉得这对于提高各个地区的查询响应速度有帮助。2.可以负载均衡,例如可以把读的压力分配给多个备库上(通常读操作也占所有操作中的绝大部分),但是将写操作分开通常不是很好,因为这会导致数据不同步的问题。3.备库的存在也对备份的来源多提供了一份选择。4.当出现故障时,可以在备库间切换或者主备间切换或者主主间切换,从而减少损失。
*复制分为三个步骤:1.主库的更改写入binlog(这里有个问题就是主库将binlog发向备库时binlog是一定要写入了磁盘吗,如果是的话如果binlog_sync设置的比较大或者没设置那岂不是备库延迟非常大)。2.备库将主库的日志复制到自己的relay log 上。3.备库读取relaylog的事件并进行重放。需要注意的是binlog中事务记录的顺序是按照提交的时间进行排列的,而非事务开始的时间;另外binlog 的发出是主库主动通知备库的,而非备库通过轮询这种高消耗动作完成;所有这三步其实就是三个线程。
复制配置
*配置一个简单的主从复制总的来说分为三步:1.创建复制账号。通过如下语句赋给用户主从复制权限:grant replication slave,replicationclient on *.* to ‘repl’@’192.168.0.%’ identified by ‘password’;这个语句的意思就是在这台服务器上给从’192.168.0.%’ 这些ip地址连接的repl这个账号以replication slave,replication client这两个权限,其中第一个权限即可完成复制,第二权限主要是让账号拥有查看和管理的权限比如说show master status;show slave status;等。
2.配置主库和备库。对于主库重要的主要是在my.cnf文件设置log bin文件名和server id,这两项最好显式修改因为如果不修改就会使用默认值,log bin会使用服务器名,如果之后进行了主备切换之类的会造成问题,server id默认使用1,如果网络中存在两个一样server id服务器识别会出现问题。设置后注意需要重启服务器。对于备库这两个参数同样需要设置,另外还有些参数比如relay log指定中继日志位置,log slave updates决定备库是否在binlog记录重放的事件,sync master info决定多少个事务间隔会刷新master info,一般来讲为了保证备库崩溃后能更好的从master info重启就需要设置为1,除非你想自己去查找最后执行事务并用change master to重新同步,sync relay log决定多少个事务间隔刷新relay log到硬盘,sync relay log info决定多少事务间隔刷新relay log info,skip slave start让备库重启后不会自动开始复制,备库崩溃后重启自动复制会造成更大损坏,read only让备库处于只读状态,不过对于超级权限用户仍然可以修改。
3.启动复制。在备库服务器输入命令change master to master_host=’server1’,master_user=’repl’,master_password=’password’,master_log_file=’mysql-bin.000001’,master_log_pos=0;这个语句的含义是用repl用户登录server1服务器申请进行复制,复制开始位置为000001binlog的0offset位置。设置好了后通过start slave启动复制,如果要停止复制用stop slave,这会回滚最后一个事务然后记录执行到的日志位置然后安全停止复制,可以通过reset slave删除master info信息,需要注意的是这个命令仅仅删除硬盘信息,如果内存中还有该信息重启后仍然是使用内存中信息重启,如果要完全删除可以使用reset slave all或者change master to重新设置位置,关于reset slave的解析可以看reset master和reset slave解析。
*可以通过show master status监控当前主库最新写入日志的位置,用show slavestatus监控备库复制线程,重放线程的运行状况,以及当前读取到主库binlog的位置,以及自己重放到的事件的位置,以及备库跟主库执行的延时。
*如果备库没有数据或者跟主库数据不一致,则需要备份。获取备份的方法有:1.冷备份,即关闭主库,把数据复制到备库,可以使用的方法有ssh或者netcat,区别就是前者加密,注意这里有个技巧就是直接复制数据的语句通过管道重定向到ssh命令,这样可以节省主库两次硬盘读和备库两次硬盘读。
2.使用热备份,冷备份需要关闭数据库这在实际生产环境中应该是不现实的,对于全innodb表的数据库可以使用mysqldump来转运数据,对于innodb表mysqldum有singletransaction选项,会在开始复制前加一个begin保证读取的数据一致性,这里还有个选项master data可以在复制的数据前加一个change master to来给备库直接确定开始复制位置从而不需要自己查找设置了,关于这两个选项更详细的解析可以看mysqldump的master-data和single-transaction选项解析。至于为什么一定要是innodb表我的思考是其他的表比如myisam不支持事务和mvcc,如果用mysqldump的话需要锁住全部表,这样跟关闭数据库好像没什么区别了。
3.使用备份或者快照,使用它们需要注意的一点就是备份的开始时间点仍然在binlog中存在。
复制的原理
*这里继续两种复制模式的优缺点:基于语句主要是更灵活,对于主备上表定义不同,但数据类型兼容,列顺序不同等情况仍然可以修改,这也让在备库上修改schema更容易,基于语句也更好定位问题发生的位置;缺点主要是受一些因素影响,比如插入的数据为当前时间,这会造成主备不一致,另外在使用触发器或者存储过程时有很多bug。基于行的优点主要是几乎所有场景都可以比较完美的处理,对一些比较复杂的查询但实际只更改一点数据的语句效果很好,当找不到修改行时会停止,而基于语句的不会,能够提醒问题的出现是一个优秀的特性;缺点主要是数据量一般比较大,并且重放过程就像一个黑盒子,你搞不清楚当前的重放到底处理的是啥语句,在干些啥,从而给定位问题造成了困难。
*对于备库,它仍然可以将数据复制给他的备库,只要设置了log slave updates,不过存在一个问题就是备库的备库的日志位置跟主备库的日志位置一般都是不相同的,这对主备切换造成了一定的困难,切换时需要保证主库和该备库数据是同步的,我猜想可以按顺序先将主备同步,然后在让备备同步,最后再进行切换。
*复制过滤器可以允许你过滤一部分binlog日志。在主库上可以用binlog do db和binlog ignore db来控制过滤,备库可以用replicate do db和replicate ignore db来控制,这些选项有个问题就是会根据当前默认数据库进行过滤,如use test;delete from sakila.film;不会过滤该delete语句。另外还可以关闭sql log bin来暂时停止写入binlog,所有这些过滤设置都会导致数据永久丢失且无法恢复,因此需要慎重考虑。
复制拓扑
*所有拓扑都应该遵循一个备库只能拥有一个主库,每个备库只能有一个server id的规则。
*一主多备。简单的拓扑,但是能够满足大部分需求:1.为不同的角色使用不同的备库。2.选用一个备库当作待用主库,以备主备切换。3.可以用于灾难恢复。4.可以将一个备库用作测试。对于这种结构需要关注多个备库的延迟,对于延迟特别严重的需要进行一些处理。另外就是虽然多个备库可以缓解读压力,但是对于写压力仍然在主库上,并且随着备库的增多,主库需要增加更多的io线程来发送binlog给备库,这在扩展时需要注意。
*主动-主动模式下的主主复制。这种将写入分配给了两台服务器,很容易造成冲突从而导致数据不一致。比如说主键自增冲突,两台主库同时插入一行,结果两台主库的行都会使用同一个主键,解决这个问题是设置auto increment increment和auto increment offset,给主备库主键设置一个offset来避免冲突。此外还有其他一些问题,比如两台服务器同时对一些数据进行操作,执行复制时可能因为两边语句执行顺序不一致导致问题。
*主动-被动模式下的主主复制。这种复制解决了主动主动模式下的冲突问题。其中一台服务器可以写入,而另一台只读。这种模式下切换角色,进行故障转移等都很方便。这里有个细节就是,主动的语句复制到被动一方,被动一方执行后悔写入自己的binlog,这时主动一方又会读取被动一方的binlog,mysql在这里会忽略binlog中serverid跟自己相同的语句,这也是为什么要显式设置server id的原因。
*环形结构。由三台主库相互顺序连接形成。这个结构出现问题的几率大于上一个模式,同时存在着一个问题就是如果其中一个主库崩溃了,那么这个主库所造成的修改的语句会在仍然存活的两个主库上无限循环。
*分发主库。前面提到,如果备库数量过大,会使主库存在过多io线程进行binlog发送,这里有个解决方法就是设置一个分发主库,分发主库不存储数据,只存储日志,它接受主库传来的日志,然后将所有表都设置为blackhole引擎,这个引擎会吞噬所有数据而不进行实际操作,因而不耗什么时间,这样做实际上就是将主库的写入功能和分发日志功能分离以减轻主库压力。不过blackhole引擎仍然存在一些bug,比如有时会忘记将自增id写入binlog,使用需要慎重。
*树或金字塔模式。这种模式也可以减轻主库的分发压力,将分发的压力下分给备库,不过存在一个问题就是如果中间层的备库出现问题,那么下面的备库都会跟着出现问题,对恢复造成困难。
*部分数据备库(数据分离)。考虑写入压力特别大的情况,由于备库是单线程复制,这可能会造成很大的延迟。可以将主库的数据库分别分给每个备库,这样每个备库只拥有一部数据库,这减轻了主库的分发压力,每个线程只需传输一部分binlog,也减轻了每个备库的读取和重放压力,只用处理一部分binlog。
*功能分离。比如可以将应用的OLTP查询和OLAP查询分配到不同的备库上,备库间存储相同的数据,但是使用不同的设置,参数,硬件,存储引擎等。
*模拟多主库复制。虽然说mysql备库不支持多主库复制,但是可以通过一些技巧来达成伪多主库复制。一种方法是定时切换备库的主库,这适用于负载较低的情况。一种是使用主主结构并且配合blackhole引擎表。
复制和容量规划
*对于备库的扩展不是简单的线性问题。考虑一种情况工作负载为20%写和80%的读。读写的工作量相同,并且服务器能够承受1000次每秒的查询。此时将负载加倍,按照常理应该增加两个备库来均分1600次的查询,但实际上这是不够的,因为写的负载也增加了,每个备库都有400次的写负载,因此只能承担600次的读负载,因而需要三台备库才能完成这个任务,并且随着负载的增加,备库的增加越发的多。
*确定备库的复制容量。备库使用了5%和95%的复制容量在表现上没有什么不同。可以通过观察延迟曲线来获取备库复制容量是否达到饱和,不过这不是很准确,因为会受比如说一个大事务的影响,在这个大事务刚结束时可能延迟很大,但是过一会儿就恢复正常了。一种办法是认为的将备库停止复制一段时间,然后让备库进行追赶,根据追赶的情况跟追赶所用的时间可以估计出复制容量使用的比例。
复制管理和维护
*测量备库延迟。尽管show slavestatus中seconds behind master显示了备库的延迟,不过不太准确。这个记录的是将备库时间戳与日志中事件时间戳相减得到的,如果复制线程出错或者没有运行,那么这个值会为null或者0;大事务会导致延迟波动,如上面所说的;非直接连接的服务器间延迟可能叠加,比如主库和备库的备库之间的延迟。准确测量延迟的办法可以通过heartbeat record来实现,它会每秒在主库更新一次时间戳,将备库获得时的时间戳相减即可得到延迟。
*确定主备是否一致。mysql提供了checksum table来校验主备表是否一致,不过在复制进行时不可用。一个比较好的工具是pt-table-checksum,他会通过insert into ..select将查询结果插入一个表中,然后复制给备库同样进行一次然后比较表的结果看是否一直,可以无需把主备的表都锁上。使用方法:pt-table-checksum –replicate=test.checksum
*如果主备库数据出现不一致,可以根据错误是否严重决定是否通过备份恢复,如果不太严重可以找出不一致的数据,通过mysqldump转储数据并导入到备库解决,不过这会导致相应的表被锁住。一个更好的办法是pt-table-sync,他会结合上面的checksum工具进行一致性检查然后同步不一致的主备数据。
改变主库
*计划内提升。大致步骤如下:1.停止主库上的写操作,最好关闭所有客户端程序。2.通过设置read only来停止活跃的写入,不过read only不能禁止超级用户写入。通过flush tables wity readlock可以让任何用户都不可写入。flush tables with read lock作用是将所有脏页刷新到硬盘并关闭所有表并给所有表加上读锁。实际上分为三个步骤:给全局加上读锁避免更新;刷新并关闭所有表缓存,如果存在查询或者更新正在运行那么会进入waiting for flush tables状态等待;加全局commit锁保证数据一致性。第一步和第三步的两个锁都属于MDL锁,即元数据锁,该锁可以保证事务正在操作表时,表不会被DDL语句修改删除等。3.选择一个备库作为主库(一般选择复制最接近主库的那个),并确保它已经跟上主库。4.确保主备库数据一致。5.新主库上stop slave。6.新主库上重置master info信息。7.show master status记录新主库binlog坐标。8.确保其他备库赶上。9.将客户端脸上新主库。10.每个备库change master to。
*计划外提升。主要是处理主库崩溃的情况。1.这种情况看主库是否可以重启还是彻底挂了,如果可以重启可以尝试禁止主库更新然后看看它的binlog是否可以用,如果可以用那么然备库跟上主库的binlog,如果无法重启那么就选取备库中日志坐标最接近主库的那个备库当作新主库。2.让其他备库跟上新主库。3.执行前面的5~7。4.比较主备库的日志坐标。5.执行前面的9~10步。
*在主主模式间交换角色。一般来讲分为以下几步:1.停止主动一方的写入。2.通过FTWRL并且set read only=1停止更新并保证数据一致性。3.在主动一方show master status记录binlog位置。4.在被动一方根据上一步位置执行select master pos wait()执行被动一方跟上主动一方。5.被动一方set read only=0变成主动一方。6.修改客户端配置,使其写入主动一方。
复制的问题和解决方案
*日志损坏。当主库日志损坏时,备库会因为读取不到目标偏移量的语句而停止,需要找出主库正常的首个语句的偏移量然后备库change master to重新定位。当备库日志损坏时通过最后执行的日志位置(execmaster log pos)找到主库对应的位置然后change master to重新定位。
日志损坏的类型:1.数据改变,但sql仍然有效,这种损坏不容易找出来,不过5.6版本后给binlog带上了checksum解决了这个问题。2.sql语句错误,这种语句中都是些错乱的数据。3.数据遗漏,mysqlbinlog无法处理这种日志会崩溃退出,这种可以通过十六进制查看器来找到日志的边界。
*对于非事务表崩溃会造成数据损坏,或者不使用stop slave关闭服务器也会导致问题。出现问题可以通过pt-table-sync来找到问题并同步数据,不过这建立在数据表仍然可用的基础上(表仍然可以打开结构仍然正确)。如果不行就只能重新导入该表数据或者使用备份恢复了。
*对于事务和非事务表混合的事务,如果在主库进行了回滚,事务表会回滚修改,而非事务表会永久保留修改,并且这个事务的日志仍然写入binlog,mysql在这里的处理是会给语句加上一句rollback,从而备库回放时能够保证结果一致(除非你中途kill了这个事务0)。
*不确定语句可能会导致主备不一致。比如说使用limit的update语句,主备上的顺序不一样会导致被更新的行不一致;replace或者insert ignore可能会在主备库上使用不同的索引;information_schema表中一些数据在主备库上可能不一致,对这些数据的操作存在不确定性。
*当备库的server id相同时会导致问题。备库可能会缓慢的进行正确的复制,也可能无法正确复制,因为两个备库间会存在竞争,他们会不断的与主库建立连接然后断开,这也可能会导致日志丢失或者重复执行事件,也会因为竞争导致主库开销增大。
*当备库崩溃或者重启时临时表也会造成一些问题。因为临时表一般保存在内存中,重启后临时表就会消失,这对后面依赖临时表的语句造成影响。可以通过保留一个专用数据库保存临时表,将每个连接的id拼接到表名上(模拟临时表仅连接可见的特性),然后在使用完后将其删除(模拟连接关闭后临时表消失特性),这个工作的完成可以通过showprocesslist获取活跃的连接id,然后清理库中非活跃的临时表。
*关于innodb_locks_unsafe_for_binlog,设置这个参数会关闭innodb的间隙锁,间隙锁是为了防止幻读而出现的,间隙锁锁住了范围查询内的所有间隙,例如查询id>4的查询它会把大于4的id行插入都阻塞,从而防止两次范围查询间有新的行插入导致幻读,关于间隙锁的解析可以看innodb下的记录锁,间隙锁,next-key锁。
*当出现过大的复制延迟的时候,可以通过pt-query-digest工具分析备库上的慢查询日志,找出是哪一些查询导致的问题还是写入负载太大导致的问题,如果是查询导致的问题可以看是否可以通过拆分查询等方式来加快速度,如果是负载过大,可以通过预读的方式(在早于重放事务的时候将事务修改为select语句将需要涉及的页预读进内存),从而提高实际重放时的速度,这样可以间接弥补单线程的限制,不过当备库是io密集型或者使用的不是innodb引擎或者插入查询占大多数的时候作用不大(插入本来就会有预读机制)。也可以通过设置innodb flush log at trx commit,delay key write,innodb locks unsafe for binlog等参数来减少刷新脏页到硬盘的次数,不过这也会导致一些安全隐患。
*当带宽不足的时候(尽管大部分时候带宽都不会成为瓶颈),可以通过对复制进行压缩来缓解,设置slave compressed protocol会用zlib引擎压缩连接,一般可以压缩到原大小的三分之一,付出的代价是主备库的cpu资源。
复制的高级特性
*半同步复制。半同步复制在提交过程中增加了一个延迟,在提交事务后,只有在备库收到了该事务的二进制日志才会给客户端进行查询结束的反馈。这会给客户端查询体验增加一些延迟,不过一般不是问题,因为相对于写入硬盘的时间,通过网络传输一些日志的时延不算什么。需要注意的是半同步复制不会阻塞事务的提交,而是阻塞给客户端的反馈(感觉这个用处不是很大,因为不管怎么样如果传输给备库失败那么用这么办法也不会给备库变出日志来);当备库一直没有回应已收到事件主库会超时并转化成正常的异步复制模式。
*在5.6版本后,mysql复制有了更多特性。事务复制状态,崩溃后也不会导致元数据失去同步。二进制日志有了checksum值。备库延迟复制。实现了多线程复制。