接上一篇文章(关于分布式系统数据一致性的那些事),继续更新一些关于分布式系统数据一致性方面的知识。
1
之前写过一篇文章(如何不宕机实现数据库迁移),介绍如何用“双写法”实现数据迁移。从根本上讲,这个方法的关键在于,service需要把一条数据同时写入两个数据库并且确保数据一致性。当时,采用的方法是spring的ChainedTransactionManager,但是理论上讲,这种方法并不能完全确保数据一致,有可能出现写第一个数据库commit,写第二个数据库rollback的情况。
基于此,如果我们要保证数据强一致性,则可以采用XA分布式事务(两阶段提交)。两阶段提交除了数据库本身(扮演participant的角色),还增加了一个coordinator的角色(also known as:transaction manager),用以保存各个participants是否提交等状态信息。协调者可以是和应用程序同一个进程(如tomcat),也可以是单独的一个进程或服务(如MSDTC)。回到编码实现层面,java developer可以直接使用spring JTA,其对分布式事务有一个很好的封装。
但是,大家可能也都知道,两阶段提交有一些显著的问题,如性能差和coordinator单点故障。coordinator单点故障这个问题,也许我们可以通过一些高可用的方案来解决,比如后面会提到的共识算法。但是性能差的问题,由于“两阶段”提交的本质,个人认为不容易解决,除非我们采用其它方案,放弃两阶段提交,后面会提到一个典型的例子。
2
接下来,我们看一个典型例子:一个银行转账系统,用户A转100元给用户B。
如果所有用户信息都放在一个数据库的一张表里面,则可以直接利用单机数据库的事务(ACID)来保证这次转账正常work。
但是,对于某些系统,由于用户量太大,我们需要分库分表,把用户信息这张表拆分成几张表来存(用户A和用户B存在于两个不同的数据库),那么,在这种情况下,要完成这一次转账操作并且确保操作的原子性,则需要分布式事务来保证,这就和上面数据库迁移的场景就有些类似了。所以,同样的,一种方案是,采用XA分布式事务。
然而,对于用户量巨大的系统,XA分布式事务一般来说都不是首选的方案,我们常常会考虑其它的方案,以提高系统的性能、高可用性、容错性和用户使用体验等。在《Designing Data-Intensive Application》这本书里面,提到了一种解决这个问题的方案,如下:
这个方案,规避了两阶段提交的使用;但是,不像分布式事务的强一致性保证,它只能保证最终一致性。同时,书中提到了关于最终一致性的一个解释:一致性包括“时效性”和“正确性”两个方面,“正确性”是都必须保证的,最终一致性只是在“时效性”方面会差一点,导致的现象可能是,在某些情况下,用户A账户的钱先扣,用户B账户的后扣。“正确性”如何保证呢?使用唯一requst ID来严格保证一次转账请求只会被执行一次,抑制重复处理,保证整个端到端的幂等性。
其实这类方案的理论原则就是之前提到过的:at least once delivery + idempotent + auto automatic dead letter reprocessing。
3
一般来讲,当数据量达到一定级别时,我们可以采用partition的方式(分库分表等)来避免单机处理能力限制;为了避免单点故障,我们可以采用replication的方式来保证高可用。例如,传统的数据库,一般都有master-slave模式架构,master负责读写数据,slave负责备份,master发生故障时,可以切换到slave,继续提供服务,但是这种切换很多都是手动的,并且slave可能会有丢失一部分master上写入的数据的风险。其实,对于一些大型的数据系统集群,依赖于手动容错切换是不现实的,我们往往需要依赖“能够自动切换”的分布式容错共识算法(consensus algorithm)。
大家可能都知道,一些常见的开源组件,比如:zookeeper,rabbitmq,etcd等,都支持集群部署以保证高可用性(high availability),那么,它们都是怎么实现节点故障容错并保证数据一致性的呢?其实,它们都实现了一种分布式容错共识算法(zab,raft,Viewstamped Replication,paxos等),zookeeper实现的是zab算法,rabbitmq和etcd实现的是raft算法。这些算法的关键点都是基于法定人数(quorum)的投票机制,投票会应用在领导人选举和日志复制(确保某条日志commit)的时候,quorum的现实依据就是少数服从多数原则。
但是,另外一个非常流行的开源组件kafka,采用了不同于quorum的一种机制ISR(in-sync replicas)来达成共识。在容错性方面,如果要容忍2个节点错误,quorum需要5个节点,而Kafka只需要3个节点,基于这种配置,kafka的成本更低,并且吞吐量更高(quorum需要复制日志到5个节点,而kafka只需要3个节点),这也是kafka给出的为什么没有采用quorum的原因。
同样的,也有一些关系型数据库(YugabyteDB)开始采用分布式容错共识算法来实现高可用,see:https://blog.yugabyte.com/how-does-the-raft-consensus-based-replication-protocol-work-in-yugabyte-db/
References