分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。
分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。
本文介绍该定理。它其实很好懂,而且是显而易见的。下面的内容主要参考了 Michael Whittaker 的文章。
一、分布式系统的三个指标
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
-
Consistency
-
Availability
-
Partition tolerance
它们的第一个字母分别是 C、A、P。
Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
二、Availability
Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应。
用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。
三、Consistency
Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。
接下来,用户的读操作就会得到 v1。这就叫一致性。
问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。
为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。
这样的话,用户向 G2 发起读操作,也能得到 v1。
四、Partition tolerance
先看 Partition tolerance,中文叫做"分区容错"。
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
五、Consistency 和 Availability 的矛盾
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
六、可用性高于一致性需求场景
读者问,在什么场合,可用性高于一致性?
举例来说,发布一张网页到 CDN,多个服务器有这张网页的副本。后来发现一个错误,需要更新网页,这时只能每个服务器都更新一遍。
一般来说,网页的更新不是特别强调一致性。短时期内,一些用户拿到老版本,另一些用户拿到新版本,问题不会特别大。当然,所有人最终都会看到新版本。所以,这个场合就是可用性高于一致性。
ACID原则
即 Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)。
ACID 原则描述了对分布式数据库的一致性需求,同时付出了可用性的代价。
-
Atomicity:每次操作是原子的,要么成功,要么不执行;
-
Consistency:数据库的状态是一致的,无中间状态;
-
Isolation:各种操作彼此互相不影响;
数据库事务的隔离级别有4个,由低到高依次为
Read uncommitted(未授权读取、读未提交)、
Read committed(授权读取、读提交)、
Repeatable read(可重复读取)、
Serializable(序列化)、
这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。
-
Durability:状态的改变是持久的,不会失效。
BASE原则
BASE(Basic Availiability,Soft state,Eventually Consistency),牺牲掉对一致性的约束(最终一致性),来换取一定的可用性。
CAP指导分布式软件实施
在一个分布式系统中(指互相连接并共享数据的节点集合)中,当涉及到读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个须被牺牲。
服务注册中心,是选择 CP 还是选择 AP?
服务注册中心解决的问题
在讨论 CAP 之前先明确下服务注册中心主要是解决什么问题:
目前作为注册中心的一些组件大致有:
目前微服务主流是 Dubbo 和 Spring Cloud,使用更多的是 Zookeeper 和 Eureka,我们就来看看应该根据 CAP 理论怎么去选择注册中心。(Spring Cloud 也可以用 ZK,不过不是主流不讨论)
Zookeeper 选择 CP
Zookeeper 保障 CP,即任何时刻对 Zookeeper 的访问请求能得到一致性的数据结果,同时系统对网络分割具备容错性,但是它不能保障每次服务的可用性。
从实际情况来分析,在使用 Zookeeper 获取服务列表时,如果 ZK 正在选举或者 ZK 集群中半数以上的机器不可用,那么将无法获取数据。所以说,ZK 不能保障服务可用性。
Eureka 选择 AP
Eureka 保障 AP,Eureka 在设计时优先保障可用性,每一个节点都是平等的。
一部分节点挂掉不会影响到正常节点的工作,不会出现类似 ZK 的选举 Leader 的过程,客户端发现向某个节点注册或连接失败,会自动切换到其他的节点。
只要有一台 Eureka 存在,就可以保证整个服务处在可用状态,只不过有可能这个服务上的信息并不是新的信息。
ZK 和 Eureka 的数据一致性问题
先要明确一点,Eureka 的创建初心就是为一个注册中心,但是 ZK 更多是作为分布式协调服务的存在。
只不过因为它的特性被 Dubbo 赋予了注册中心,它的职责更多是保障数据(配置数据,状态数据)在管辖下的所有服务之间保持一致。
所以这个就不难理解为何 ZK 被设计成 CP 而不是 AP,ZK 核心的算法 ZAB,就是为了解决分布式系统下数据在多个服务之间一致同步的问题。
更深层的原因,ZK 是按照 CP 原则构建,也就是说它须保持每一个节点的数据都保持一致。
如果 ZK 下节点断开或者集群中出现网络分割(例如交换机的子网间不能互访),那么 ZK 会将它们从自己的管理范围中剔除,外界不能访问这些节点,即使这些节点是健康的可以提供正常的服务,所以导致这些节点请求都会丢失。
而 Eureka 则没有这方面的顾虑,它的节点都是相对独立,不需要考虑数据一致性的问题,这个应该是 Eureka 的诞生就是为了注册中心而设计。
相对 ZK 来说剔除了 Leader 节点选取和事务日志机制,这样更有利于维护和保障 Eureka 在运行的健壮性。
再来看看,数据不一致性在注册服务中会给 Eureka 带来什么问题,无非就是某一个节点被注册的服务多,某个节点注册的服务少,在某一个瞬间可能导致某些 IP 节点被调用数多,某些 IP 节点调用数少的问题。
也有可能存在一些本应该被删除而没被删除的脏数据。
服务注册应该选择 AP 还是 CP
对于服务注册来说,针对同一个服务,即使注册中心的不同节点保存的服务注册信息不相同,也并不会造成灾难性的后果。
对于服务消费者来说,能消费才是最重要的,就算拿到的数据不是新的数据,消费者本身也可以进行尝试失败重试。总比为了追求数据的一致性而获取不到实例信息整个服务不可用要好。
所以,对于服务注册来说,可用性比数据一致性更加的重要,选择 AP。
分布式锁,是选择 CA 还是选择 CP?
这里实现分布式锁的方式选取了三种:
-
基于数据库实现分布式锁
-
基于 Redis 实现分布式锁
-
基于 Zookeeper 实现分布式锁
#
基于数据库实现分布式锁
构建表结构:
利用表的 UNIQUE KEY idx_lock(method_lock)作为主键,当进行上锁时进行 Insert 动作,数据库成功录入则以为上锁成功,当数据库报出 Duplicate entry 则表示无法获取该锁。
不过这种方式对于单主却无法自动切换主从的 MySQL 来说,基本就无法实现 P 分区容错性(MySQL 自动主从切换在目前并没有很好的解决方案)。
可以说这种方式强依赖于数据库的可用性,数据库写操作是一个单点,一旦数据库挂掉,就导致锁的不可用。这种方式基本不在 CAP 的一个讨论范围。
基于 Redis 实现分布式锁
Redis 单线程串行处理天然就是解决串行化问题,用来解决分布式锁是再适合不过。
实现方式:
setnx key value Expire_time获取到锁 返回 1 , 获取失败 返回 0
为了解决数据库锁的无主从切换的问题,可以选择 Redis 集群,或者是 Sentinel 哨兵模式,实现主从故障转移,当 Master 节点出现故障,哨兵会从 Slave 中选取节点,重新变成新的 Master 节点。
哨兵模式故障转移是由 Sentinel 集群进行监控判断,当 Maser 出现异常即复制中止,重新推选新 Slave 成为 Master,Sentinel 在重新进行选举并不在意主从数据是否复制完毕具备一致性。
所以 Redis 的复制模式是属于 AP 的模式。保障可用性,在主从复制中“主”有数据,但是可能“从”还没有数据。
这个时候,一旦主挂掉或者网络抖动等各种原因,可能会切换到“从”节点,这个时候可能会导致两个业务线程同时获取得两把锁。
这个过程如下:
上述的问题其实并不是 Redis 的缺陷,只是 Redis 采用了 AP 模型,它本身无法保障我们对一致性的要求。
Redis 推荐 Redlock 算法来保障,问题是 Redlock 至少需要三个 Redis 主从实例来实现,维护成本比较高。
相当于 Redlock 使用三个 Redis 集群实现了自己的另一套一致性算法,比较繁琐,在业界也使用得比较少。
能不能使用 Redis 作为分布式锁?这个本身就不是 Redis 的问题,还是取决于业务场景。
我们先要自己确认我们的场景是适合 AP 还是 CP , 如果在社交发帖等场景下,我们并没有很强的事务一致性问题,Redis 提供给我们高性能的 AP 模型是很适合的。
但如果是交易类型,对数据一致性很敏感的场景,我们可能要寻找一种更加适合的 CP 模型。
基于 Zookeeper 实现分布式锁
刚刚也分析过,Redis 其实无法保障数据的一致性,先来看 Zookeeper 是否适合作为我们需要的分布式锁。
首先 ZK 的模式是 CP 模型,也就是说,当 ZK 锁提供给我们进行访问的时候,在 ZK 集群中能保障这把锁在 ZK 的每一个节点都存在。
这个实际上是 ZK 的 Leader 通过二阶段提交写请求来保障的,这个也是 ZK 的集群规模大了的一个瓶颈点。
①ZK 锁实现的原理
说 ZK 的锁问题之前先看看 Zookeeper 中几个特性,这几个特性构建了 ZK 的一把分布式锁。
ZK 的特性如下:
-
有序节点: 当在一个父目录下如 /lock 下创建 有序节点,节点会按照严格的先后顺序创建出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保障各个自节点按照排序命名生成。
-
临时节点: 客户端建立了一个临时节点,在客户端的会话结束或会话超时,Zookepper 会自动删除该节点 ID。
-
事件监听: 在读取数据时,我们可以对节点设置监听,当节点的数据发生变化(1 节点创建 2 节点删除 3 节点数据变成 4 自节点变成)时,Zookeeper 会通知客户端。
结合这几个特点,来看下 ZK 是怎么组合分布式锁:
-
业务线程 -1,业务线程 -2 分别向 ZK 的 /lock 目录下,申请创建有序的临时节点。
-
业务线程 -1 抢到 /lock0001 的文件,也就是在整个目录下小序的节点,也就是线程 -1 获取到了锁。
-
业务线程 -2 只能抢到 /lock0002 的文件,并不是小序的节点,线程 2 未能获取锁。
-
业务线程 -1 与 lock0001 建立了连接,并维持了心跳,维持的心跳也就是这把锁的租期
-
当业务线程 -1 完成了业务,将释放掉与 ZK 的连接,也就是释放了这把锁。
#
②ZK 分布式锁的代码实现
ZK 官方提供的客户端并不支持分布式锁的直接实现,我们需要自己写代码去利用 ZK 的这几个特性去进行实现。
究竟该用 CP 还是 AP 的分布式锁
首先得了解清楚我们使用分布式锁的场景,为何使用分布式锁,用它来帮我们解决什么问题,先聊场景后聊分布式锁的技术选型。
无论是 Redis,ZK,例如 Redis 的 AP 模型会限制很多使用场景,但它却拥有了几者中很高的性能。
Zookeeper 的分布式锁要比 Redis 可靠很多,但他繁琐的实现机制导致了它的性能不如 Redis,而且 ZK 会随着集群的扩大而性能更加下降。
简单来说,先了解业务场景,后进行技术选型。
分布式事务,是怎么从 ACID 解脱,投身 CAP/BASE
如果说到事务,ACID 是传统数据库常用的设计理念,追求强一致性模型,关系数据库的 ACID 模型拥有高一致性+可用性,所以很难进行分区。
在微服务中 ACID 已经是无法支持,我们还是回到 CAP 去寻求解决方案,不过根据上面的讨论,CAP 定理中,要么只能 CP,要么只能 AP。
如果我们追求数据的一致性而忽略可用性这个在微服务中肯定是行不通的,如果我们追求可用性而忽略一致性,那么在一些重要的数据(例如支付,金额)肯定出现漏洞百出,这个也是无法接受。所以我们既要一致性,也要可用性。
都要是无法实现的,但我们能不能在一致性上作出一些妥协,不追求强一致性,转而追求最终一致性,所以引入 BASE 理论。
在分布式事务中,BASE 重要是为 CAP 提出了一致性的解决方案,BASE 强调牺牲高一致性,从而获取可用性,数据允许在一段时间内不一致,只要保障一致性就可以了。
实现一致性
弱一致性: 系统不能保障后续访问返回更新的值。需要在一些条件满足之后,更新的值才能返回。
从更新操作开始,到系统保障任何观察者总是看到更新的值的这期间被称为不一致窗口。
一致性: 这是弱一致性的特殊形式;存储系统保障如果没有对某个对象的新更新操作,所有的访问将返回这个对象的更新的值。
BASE 模型
BASE 模型是传统 ACID 模型的反面,不同于 ACID,BASE 强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保障最终一致就可以了。
BASE 模型反 ACID 模型,完全不同 ACID 模型,牺牲高一致性,获得可用性或可靠性:Basically Available 基本可用。
支持分区失败(e.g. sharding碎片划分数据库)Soft state 软状态,状态可以有一段时间不同步,异步。
Eventually consistent 一致,最后数据是一致的就可以了,而不是时时一致。
完!
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)