一、什么是数据一致性?
在分布式环境中,在数据有多分副本的情况下,如果网络、服务器或者软件出现故障,会导致部分副本写入成功,部分副本写入失败。这种情况造成了各个副本之间的数据不一致,数据内容冲突。 在实际生产应用场景中,数据不一致的情况有很多,数据库、中间件、分布式系统交互,每个环节都有可能会产生数据不一致,轻则影响系统功能可用性,重则会带来一定的资金损失。
二、一致性模型
在分布式系统中要解决的一个重要问题就是数据的复制,复制在提高系统性能和可用性的同时,也带来了诸多的应用问题。在我们的日常开发经验中,相信很多开发人员都遇到过这样的问题:在mysql主从集群模式下,采用读写分离架构来解决读多写少的业务场景,假设客户端C1将系统中的一个值K由V1更新为V2,但客户端C2无法立即读取到K的最新值,需要在一段时间之后才能读取到,这很正常,因为数据库之间数据复制存在延时。
这种情况下,我们如何解决从主延迟的读写问题?假如我们要求数据写入时,整个集群要复制完成,才能对外提供服务?这样做是可以,但是意味着要牺牲可用性,起码在数据复制期间,集群是服务处于不可用状态的,如有大量客户端写入,系统的整体性能将会急剧下降。因此,如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡的,于是就有了一致性模型。一致性模型本质上是进程与数据存储的约定:如果进程遵循某些规则,那么进程对数据的读写操作都是可预期的。
1 . 线性一致性:线性一致性是对一致性要求最高的一致性模型,就现有技术是不可能实现的。因为它要求所有操作都实时同步,在分布式系统中要做到全局完全一致时钟现有技术是做不到的。首先通信是必然有延迟的,一旦有延迟,时钟的同步就没法做到一致。当然不排除以后新的技术能够做到,但目前而言线性一致性是无法实现的。
顺序一致性:顺序一致性使用的是逻辑时钟来作为分布式系统中的全局时钟,进而所有进程也有了一个统一的参考系对读写操作进行排序,因此所有进程看到的数据读写操作顺序也是一样的。
因果一致性:因果一致性进一步弱化了顺序一致性中对读写操作顺序的约束,仅保证有因果关系的读写操作有序,没有因果关系的读写操作(并发事件)则不做保证。也就是说如果是无因果关系的数据操作不同进程看到的值是有可能是不一样,而有因果关系的数据操作不同进程看到的值保证是一样的。
最终一致性:最终一致性是更加弱化的一致性模型,因果一致性起码还保证了有因果关系的数据不同进程读取到的值保证是一样的,而最终一致性只保证所有副本的数据最终在某个时刻会保持一致。这是大型分布式系统构建使用最多的一种一致性模型。
而以客户端为中心的一致性包含了四种子模型:
单调读一致性:如果一个进程读取数据项 x 的值,那么该进程对于 x 后续的所有读操作要么读取到第一次读取的值要么读取到更新的值。即保证客户端不会读取到旧值。
单调写一致性:一个进程对数据项 x 的写操作必须在该进程对 x 执行任何后续写操作之前完成。即保证客户端的写操作是串行的。
读写一致性:一个进程对数据项 x 执行一次写操作的结果总是会被该进程对 x 执行的后续读操作看见。即保证客户端能读到自己最新写入的值。
写读一致性:同一个进程对数据项 x 执行的读操作之后的写操作,保证发生在与 x 读取值相同或比之更新的值上。即保证客户端对一个数据项的写操作是基于该客户端最新读取的值。
单调写一致性 | 读写一致性 | 写读一致性 | 单调读一致性 | |
---|---|---|---|---|
线性一致性 | √ | √ | √ | √ |
顺序一致性 | √ | √ | √ | √ |
因果一致性 | √ | × | × | √ |
最终一致性 | × | × | × | × |
参考《分布式系统原理与范型》
三、分布式基石CPA定理
说完一致性模式,分布式环境下的系统实战,我们就会想到Eric Brewer大神提出的著名CAP定理,所谓的CPA定理指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C):指的是在分布式系统中的所有数据备份,在同一时刻是否同样的值。即当一个进程修改了某个数据后,其他进程读取该数据能读取到最新的值,但并不是所有系统都可以做到这一点。例如,在一些并非严格要求一致性的系统中,后来的进程得到的数据可能还是修改之前的数据,或者需要等待一定时间后才能得到修改 之后的数据,这被成为“弱一致性”,最经典的应用就是DNS系统。当用户修改了DNS配置后,往往不会马上在全网更新,必定会有一个延迟,这个延迟被称为 “不一致窗口”,它的长度取决于系统的负载、冗余的个数等因素。但对于某些系统而言,一旦写入,后面读取的一定是修改后的数据,如zookeeper事务写入,这被称为 “强一致性”。
可用性(A):指的是在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。即系统总是能够为用户提供连续的服务能力。当用户发出请求是,系统能给出响应(成功或者失败),而且是立即给出响应,而不是等待其他事情完成才响应。如果需要等待某件事情完成才响应,那么“可用性”就不存在了。
分区容忍性(P):任何一个分布式计算系统都是由多个节点组成的。在正常情况下,节点与节点之间的通信是正常的。但是在某些情况下,节点之间的通信会断开,这种断开形成“Partition”。在分布式计算的实现中,Partition是很常见的,因为节点不可能永远不出故障,尤其是对于跨物理地区的 海量存储系统而言,而容错性则可以保证如果只是系统中的部分节点不可用,那么相关的操作仍旧能够正常完成。
四、CAP定理的应用思考
CAP定理强调的是分布式系统中,CA、CP、AP仅能存在其中一种选择,不可能存在CAP共存的情况。为什么三者不可得兼呢?我们可以从下图所示来分析一下,假设分布式系统A集群由一个master节点和两个replica节点组成,master负责数据写入,再分别将数据同步给replica节点。
从图中我们可以看到,分布式系统中保留可用性(A),在replica节点数据不一致、部分replica节点故障的情况下,存活的节点仍能对外提供服务能力,及时响应客户端请求,当然前提是业务能够容忍在一段时间内节点间数据不一致,因为网络是不可靠的;保留一致性(C),当数据数据发生写入时,如部分replica节点发生故障或网络故障时,系统集群将停止对外提供服务的能力,直到更新的数据能成功同步给到所有replica节点为止;保留分区容忍性(P):当部分replica节点发生故障或网络故障时,存活的节点仍能对外提供服务能力。我们可以发现,可用性(A)和分区容忍性(P)看起来貌似有点相似,都是在特定情况下仍能对外提供服务能力,但分布式系统如果没有了分区容忍性(P),那就直接退化到单体系统了。所以P是分布式系统构建必备的条件,一般在做分布式系统搭建的时候,都是在AP和CP架构中选型。下面我们来比较下当前比较流行的两个注册中心应用系统zookeeper和eureka的架构设计。
1、CP架构
zookeeper是一个分布式协调服务,它为分布式应用程序提供了统一命名服务、状态同步服务、集群管理、分布式应用配置管理等解决方案。2011年,阿里巴巴开源Dubbo分布式服务,ZooKeeper 作为Dubbo其推荐的注册中心,后来在国内,在业界诸君的努力实践下,Dubbo + ZooKeeper 的典型的服务化方案成就了 ZooKeeper 作为注册中心的声名。zookeeper基于zab一致性协议来实现消息广播和崩溃恢复,该协议的核心算法是fast paxos,其强调的是一致性(C),在事务消息写入时,为尽最大努力保证数据视图一致性,当集群(官方推荐2N+1节点集群)过半数节点写入成功时才能提交该事务,崩溃恢复选主也是类似,少数服从多数,过半数投票成功才算选主成功。当然ZooKeeper为了保证在集群脑裂分区(P)情况下数据一致性(C),其放弃了可用性(C),所以在集群部分节点或者网络发生故障,master离线的时候,集群将进行崩溃恢复选注,在选主期间,整个集群对外是不可用的,一直持续到选主完成,主从数据同步成功为止。而当跨机房部署zookeeper集群提供注册中心服务时,当部分机房网络故障发生分区时,由于舍弃了可用性(C),也会存在网络分区“孤岛”机房服务间连通性受影响的情况。这些场景都是由CP架构选型所决定的,至于作为注册中心服务CP架构好与不好不是本次的讨论主题,有兴趣可参考阿里的技术文章《阿里巴巴为什么不使用zookeeper做服务发现?》http://jm.taobao.org/2018/06/13/%E5%81%9A%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%EF%BC%9F/
2、AP架构
Eureka是由Netflix 公司开源的、用于实现服务注册和发现的应用服务,目前作为Spring Cloud 全家桶中作为核心子项目二次封装使用。Eureka天生就是为服务注册中心而设计的产品,其采用了 C-S 的设计架构,Eureka Server 作为服务注册功能的服务器,其他微服务提供者,使用 Eureka 的客户端连接到 Eureka Server,并维持心跳连接。
在CAP原则中,Eureka是典型的AP架构原则,整个系统架构设计都是围绕这高可用来做,并且大量使用缓存机制来保证系统的SLA。
在Eureka集群中,如果某台服务器宕机,客户端请求会自动切换到新的Eureka节点;当宕机的服务器重新恢复后,Eureka会再次将其纳入到服务器集群管理之中;而对于客户端来说,所有要做的无非是同步一些新的服务注册信息而已。所以,再也不用担心有“掉队”的服务器恢复以后,会从Eureka服务器集群中剔除出去的风险了。Eureka甚至被设计用来应付范围更广的网络分割故障,并实现“0”宕机维护需求。当网络分割故障发生时,每个Eureka节点,会持续的对外提供服务,其接收新的服务注册同时将它们提供给下游的服务发现请求。这样一来,就可以实现在同一个子网中,新发布的服务仍然可以被发现与访问。
正常配置下,Eureka内置了心跳服务,用于淘汰一些“濒死”的服务器;如果在Eureka中注册的服务,它的“心跳”变得迟缓时,Eureka会将其整个剔除出管理范围。这是个很好的功能,但是当网络分割故障发生时,这也是非常危险的;因为,那些因为网络问题(注:心跳慢被剔除了)而被剔除出去的服务器本身是很”健康“的,只是因为网络分割故障把Eureka集群分割成了独立的子网而不能互访而已。幸运的是,Netflix考虑到了这个缺陷。如果Eureka服务节点在短时间里丢失了大量的心跳连接(注:可能发生了网络故障),那么这个Eureka节点会进入”自我保护模式“,同时保留那些“心跳死亡“的服务注册信息不过期。此时,这个Eureka节点对于新的服务还能提供注册服务,对于”死亡“的仍然保留,以防还有客户端向其发起请求。当网络故障恢复后,这个Eureka节点会退出”自我保护模式“。所以Eureka的哲学是,同时保留”好数据“与”坏数据“总比丢掉任何”好数据“要更好,所以这种模式在实践中非常有效。
Eureka还有客户端缓存功能(注:Eureka分为客户端程序与服务器端程序两个部分,客户端程序负责向外提供注册与发现服务接口)。所以即便Eureka集群中所有节点都失效,或者发生网络分割故障导致客户端不能访问任何一台Eureka服务器;Eureka服务的消费者仍然可以通过Eureka客户端缓存来获取现有的服务注册信息。甚至最极端的环境下,所有正常的Eureka节点都不对请求产生相应,也没有更好的服务器解决方案来解决这种问题时,Eureka的客户端缓存机制,消费者服务仍然可以通过Eureka客户端查询与获取注册服务信息。
五、结语
数据一致性是一个很大的话题,从一致性模式到CAP定理,再延伸到BASE定理,强调的核心都是围绕着数据怎么在分布式环境中达成共识。当我们在做分布式系统建设、应用开发的时候,对一致性理论的理解和应用,可以帮助我们更快地去掌握一个系统的架构设计,寻找到更合适的系统架构之路。