读写分离

读写分离

随着业务的不断发展,用户数量和并发量不断上升。这时如果仅靠单个数据库实例来支撑所有访问压力,几乎是在 自寻死路。以产品库为例,可能库中包含了几万种商品,并且每天新增几十种,而产品库每天的访问了可能有几亿甚至几十亿次。数据库读的压力太大,单台mysql实例扛不住,此时大部分Mysql DBA就会将数据库设置成 读写分离状态。也就是一个Master节点(主库)对应多个Salve节点(从库)。可以将slave节点的数据理解为master节点数据的全量备份。

Master 与 Slave 分离

master节点接收用户的写请求,并写入到本地二进制文件(binary log)中。slave通过一个IO线程与Master建立连接,发送Binlog dump指令。Master会将Binlog数据推送给slaveslave将接收到的Binlog保存到本地的中继日志(relay log)中,最后,slave通过另一个线程SQL thread应用本地的relay log,将数据同步到slave库中。

当更新数据时,应用将数据写入master主库,主库将数据同步给多个slave从库。当查询数据时,应用选择某个slave节点读取数据。当更新数据时,应用将数据写入master主库,主库将数据同步给多个slave从库。当查询数据时,应用选择某个slave节点读取数据。

读写分离的挑战

  • 主从数据同步延迟问题:因为数据是从master节点通过网络同步给多个slave节点,因此必然存在延迟。因此有可能出现我们在master节点中已经插入了数据,但是从slave节点却读取不到的问题。对于一些强一致性的业务场景,要求插入后必须能读取到,因此对于这种情况,我们需要提供一种方式,让读请求也可以走主库,而主库上的数据必然是最新的。

  • 事务问题:如果一个事务中同时包含了读请求(select)和写请求(insert),如果读请求走从库,写请求走主库,由于跨了多个库,那么本地事务已经无法控制,属于分布式事务的范畴。而分布式事务非常复杂且效率较低。因此对于读写分离,目前主流的做法是,事务中的所有sql统一都走主库,由于只涉及到一个库,本地事务就可以搞定。

  • 感知集群信息变更:如果访问的数据库集群信息变更了,例如主从切换了,写流量就要到新的主库上;又例如增加了从库数量,流量需要可以打到新的从库上;又或者某个从库延迟或者失败率比较高,应该将这个从库进行隔离,读流量尽量打到正常的从库上。

主从路由

基本路由路功能主要是解决,在读写分离的情况下,如何实现一些基本的路由功能,这个过程通常可以通过下图进行描述:

主库路由策略

主要是判断出来sql是读还是写sql,将读sql到从库上去执行,写sql去主库上执行 write语句:insert、update、delete、create、alter、truncate… query语句:select、show、desc、explain…

有的时候,对于一些强一致性的场景,需要写入后,必须能读取到数据。由于主从同步存在延迟,可能会出现主库写入,而从库查不到的情况。这次时候,我们需要使用强制走主库的功能。具体实现上有2种方案:hintAPI

hint,就是开发人员在sql上做一些特殊的标记,数据库中间件识别到这个标记,就知道这个sql需要走主库,如:

/*master*/select * from table_xx

这里的/master/就是一个hint,表示需要走主库。不同的数据库中间件强制走主库的hint可能不同,例如zebrahint/zebra:w+/hint到底是什么样是无所谓的,其作用仅仅就是一个标记而已。之所以将hint写在//中,是因为这是标准的sql注释语法。即使数据库中间件未能识别这个hint,也不会导致sql语法错误。

api方式主要是通过代码的方式来添加sql走主库的标识,hint通常只能加在某个sql上。如果我们希望多个sql同时都走主库,也不希望加hint,则可以通过api的方式,其内部主要利用语言的thread local线程上下文特性,如:

ForceMasterHelper.forceMaster()
    //…执行多条sql
ForceMasterHelper.clear()

api标识范围内执行的sql,都会走主库。具体API到底应该是什么样,如何使用,也是由相应的数据库中间件来决定的。特别的,对于一些特殊的sql,例如select last_insert_id;或者select @@identity等,这类sql总是需要走主库。这些sql是要获得最后一个插入记录的id,插入操作只可能发生在主库上。

从库路由策略

通常在一个集群中,只会有一个master,但是有多个slave。当判断是一个读请求时,如何判断选择哪个slave。一些简单的选择策略包括:随机选择(random),按照权重进行选择(weight),或者轮训(round-robin),等。特别的,对于一些跨IDC(数据中心)部署的数据库集群,通常需要有就近路由的策略。

当然一个数据中心内,可能会部署多个slave,也需要进行选择,因此就近路由通常和一些基本的路由策略结合使用。另外,对于就近路由,通常也会有一个层级,例如同机房、同中心、同区域、跨区域等。

高可用保障

数据库中间件除了需要具备上述提到的读写分离功能来访问底层的数据库集群。也需要一套支持高可用、动态扩展的体系:

  • HA的角度来说,例如主库宕机了,那么应该从从库选择一个作为新的主库。开源的MHA可以帮助我们完成这个事;然而,MHA只能在主库宕机的情况下,完成主从切换,对于仅仅是一个从库宕机的情况下,MHA通常是无能为力的。因此,通常都会在MHA进行改造,使其支持更多的HA能力要求。

  • Scalable角度来说,例如读qps实在太高,需要加一些从库,来分担读流量。

事实上,无论是HA,还是Scalable,对于数据库中间件(不论是proxy或者smart-client)来说,只是配置信息发生了变更。因此,通常我们会将所有的配置变更信息写到一个配置中心,然后配置心中监听这个配置的变更,例如主从切换,只需要把最新的主从信息设置到配置中心;增加从库,把新从库ipport等信息放到配置中心。数据库中间件通过对这些配置信息变更进行监听,当配置发生变更时,实时的应用最新的配置信息即可。

一个简化的数据库中间件的高可用架构通常如下所示:

配置中心

监控服务对集群进行监控,当发生变更时,将变更的信息push到配置中心中,数据库中间件(proxysmart-client)接收到配置变更,应用最新的配置。而整个过程,对于业务代码基本是无感知的。 对于配置中心的选择,有很多,例如百度的disconf、阿里的diamond、点评开源的lion、携程开源的apollo等,也可以使用etcd、consul。通常如果没有历史包袱的话,建议使用携程开源的apollo。 特别需要注意的一点是,通常监控服务监控到集群信息变更,推送到配置中心,再到数据库中间件,必然存在一些延迟。对于一些场景,例如主从切换,没有办法做到彻底的业务无感知。当然,对于多个从库中,某个从库宕机的情况下,是可以做到业务无感知的。例如,某个从库失败,数据库中间件,自动从其他正常的从库进行重试。

端兜底

上图中的HA方案强依赖于配置中心,如果某个数据库集群上建立了很多库,这个集群发生变更时,将会存在大量的配置信息需要推送。又或者,如果数据库集群是多机房部署的,在某个机房整体宕机的情况下(例如光纤被挖断了,或者机房宕机演练),也会存在大量的配置信息需要推送。如果配置中心,推送有延迟,业务会有非常明显的感知。 因此,通常我们会在客户端进行一些轻量级的HA保障。例如,根据数据库返回异常的sqlstatevendor code,判断异常的严重级别,确定数据库实例能否正常提供服务,如果不能正常提供服务,则自动将其进行隔离,并启动异步线程进行检测数据库实例是否恢复。 最后,很多数据库中间件,也会提供一些限流和降级的功能,计算sql的唯一标识(有些称之为sql指纹),对于一些烂sql,导致数据库压力变大的情况,可以实时的进行拦截,直接抛出异常,不让这些sql打到后端数据库上去。

客户端兜底