P发生时需要在C和A中进行权衡。即使在同一个业务系统中,有些业务需要保证C(即使需要保证C,但是也有一些策略在保证C的前提下不断提高A),但是有些业务对数据一致性要求不那么高(查询、不造成资损等操作),可以优先保证A。
case1:P发生时保证A(这也是大多数业务采取的策略)
ADHA(database HA),从名字就能知道这个是干啥的, 一句话说明就是检测到DB有故障,并及时切换到备库。如下图是所用的MySQL架构,假如在时间点T2,Master binlog的位点为120, 备库因为延迟,位点还处在100,如果这时候发生分区,ADHA会在第一时间保证业务的高可用性,把Slave开写,提供给业务访问了,但是这时候新的主库(也就是图中的Slave)其实是缺失了位点为100-120的更新的,所以这部分的数据一致性是没法保证的, 等Master起来后,首先会把它回滚到位点100, 这样就与Slave开写前的数据保持一致了,也就是说分区两侧的状态在这个位点是完全一致的,这为最终两侧的状态一致打下了基础, 然后ADHA再把100-120的更新, 在Slave上重跑一把, 以便补齐之前缺失的更新,达到数据的最终一致。
这时候有同学肯定要问, Slave 在T2就开写了,并且写到位点150了, 这时候再把100-120的更新追加到Slave,能保证数据的一致性吗?线上这么重要的系统,就这么放弃了数据一致性,是不是疯了啊。
反正我第一次了解ADHA切换机制的时候, 是这么认为的, 但是后来仔细了解了一下, 发现很多情况下这个担忧是多余的,因为我们线上的MySQL binlog都是row 模式, 而不是statement模式,比如有一个Table A (id,value,gmt_modified)三个字段组成的,我一个delete语句删除了一万条记录, binlog记录的不是一条delete 的逻辑SQL,而是会有一万条binlog记录,并且每条记录都是包含全字段的,不需要记录上下文信息的。 对于幂等的操作肯定是没有问题的, 我们来看下最容易让人疑惑的Update操作, 因为直观上看,Update最容易出现最后写入者胜的问题,假设我在位点100-120之间,有个update A set value =3 where id =100, 那么实际上在binlog里,实际上记录的是全字段的, 即我生成回放的SQL是这样的:update A set value =3 where id =100 and value =2 and gmt_modified=Tn) ,如果Slave 在位点回补之前, 已经有一个更新操作了update A set blance =3 where id = 100,那么在回放的时候,执行update A set value =3 where id =100 and value =2 and gmt_modified=Tx 是不能成功的, 因为value=2的值已经没有了,所以不会把2 重新更新上去的, 不存在更新丢失的问题。
当然在一些场景下,还是会无法达到数据一致性的情况: 下面的一个case,说明了因为ADHA无法保证事务级别的回滚和回补,所导致的数据不一致, 比如一个事务由(insert Table A; Update Table B; insert Table C)组成,若该事务处在100-120之间, 且Master回滚成功
在Slave回放的时候,insert都成功了,但是由于Table B的这条记录已经被更新了,再update的时候, 结果是Row affected 0,MySQL也认为他更新成功了,事务也就标识成功了,但是脏数据也就产生了, 对于业务来说这个已经返回成功的事务失去了ACID中的A了。
还有一些特殊的场景会导致产生脏数据,比如重复主键的处理,这里就不过多列举了,从我们实际的切换统计中, 99%的情况, 切换后都没有数据不一致的情况, 另外我们在进入分区后,是有额外记录便于后面分区恢复的操作信息的,分区结束后我们还可以通过全量校验,增量校验来找出不一致的数据,总之我们通过可以种种补偿机制,达到数据最终一致。
这其实是基于我们的考量,对于数据一致性的要求做了业务分级:互联网的应用,比如商品评论, 比如互动话题等, 这些数据一致性的要求不那么强的,丢几条数据可能用户都感受不到的, 尽可能的高可用是我们可以追求的目标,相应的我们采取了这种胆大心细的做法, 对于库存,账务操作,或者余额宝这类的业务, 如果有数据不一致产生, 就容易产生超卖或者资损,或者说当出现分区的情况下,谁也不知道一笔脏数据意味着什么,这样的应用我们还能不能类似ADHA这样的方式搞呢?还记得之前工行大机挂掉, 为什么宁愿长时间的停服务, 也不愿意把异地容灾的Standby激活呢?
case2:P发生时根据业务类型在C和A中进行权衡
所以第二个case就来聊聊我们支付宝的场景,在这个情况下,我们在CA权衡中,会更多的倾向于C,重点考虑如何保证数据的可靠性,如何保证提供的数据的一致性,如何保证数据不可丢,在满足以上条件的前提下,再去考虑可用性与扩展性。
我们的方案可以套用上面这张常见的图来说明,通过atuofo工具检测到分区出现,自动推送到failover模式,进入分区状态,即进入状态S2,限制某些操作,同时DBA会尽力恢复主库, 来消除分区,当分区结束后,完成分区修复,恢复数据一致性,并补偿分区期间发生的错误(如果有的话),重新进入正常模式S。 这么说可能还是比较抽象,大家会问S2到底是什么样的状态呢?
我们拿账务层的操作类型来看,对于查询类的业务,我们有读库或者备库可以继续提供访问,对于更新的业务,我们再细分一下:一种是非余额的,其他的属性的变动, 这块非常少, 可以忽略不计, 另外一种是网关类的支付请求,对客户来说, 支付前后的余额是没有变化的,影响的只是中间账户,所以这类的可用性可以继续保持, 可以通过failover库继续服务(failover库可以理解为结构完全一致, 但是是数据是空的镜像数据库,能够保证业务往前走), 第三种就是客户是对余额更新来完成付款的,这部分的约束是不能触动的,所以我们为了保证数据的强一致,只好把这部分功能降级了,即牺牲掉这部分的A。
实际上也不是丧失全部的A,在保证C的前提下有很多方法不断的提高A。比如。用户可以使用其他的支付方式, 比如网银来继续完成支付,这样可用率就提高了,进一步, 如果通过黑白名单的方式,只限制部分的变动账户的记账,还可以继续提升可用率, 或者说是不是还可以想象一下, 类似于ATM的补偿问题一样,设定一个限额,只要没超出这个限额, 也可以让他完成余额支付? 总之办法总是比问题多,虽然永远无法达到100%, 但是总能不断的向他看齐。