转载地址:https://github.com/CyC2018/CS-Notes/blob/master/notes/%E5%88%86%E5%B8%83%E5%BC%8F.md
注:如Paxos等的多个发起者发起的提案,和接受提案,实际上就是一个分布式事务过程。部署为集群多个节点,半数以上通过则通过,就是为了避免单点故障导致分布式事务无法提交的问题。不接受序列号小于当前接受序列号的算法,可以保证多个事务下只有一个提案被集体通过,且可以通过发起,和接受这整个的分布式事务过程在所有节点可以顺利提交,生效,达到多节点事务同步,也就是一致性。分区的节点,在上线后,也不会接受序列号过期的事务,且会通过算法机制同步到最新的节点状态。
在主从算法模型中,通过主节点向从节点发送心跳和选主机制保证单个主节点,从节点从主节点同步写入信息。如果大多数从节点无法收到主节点心跳,判定主节点挂掉,则会重新选主。选主过程也如Paxos算法,各从节点按序列号分别发起选自己的提案,最后根据分布式事务,只有一个会被接受,成为新主。旧主或断线从节点从主节点同步最新节点信息。
对于Dubbo,Kafka等集群,通过ZooKeeper管理,ZooKeeper集群本身有选主过程,是主从算法模型。ZooKeeper集群的每个节点向访问Dubbo,Kafka的客户端返回注册到各个ZooKeeper节点的全量Dubbo,Kafka节点信息,则是通过每个注册触发的分布式事务保证的:保证注册提案被每个ZooKeeper节点接受,更新列表,完成这个分布式事务,使每个ZooKeeper节点更新Dubbo,Kaka列表信息。在ZooKeeper单节点的情况下,Kafka集群间则通过类似的分布式事务算法,通过ZooKeeper相互访问,存取,达到单个Kafka收发消息,Partition偏移量等信息被更新到所有Kafka节点,保障访问任意Kafka节点的客户端获取同样的数据,没有分布式环境下数据不一致问题。Dubbo集群的情况与此类似,访问Dubbo单点的客户端,是Dubbo集群在ZooKeeper的帮助下,通过分布式事务协议,达到每个Dubbo节点对新注册的或下线的Dubbo节点列表的一致性,保障在任何单点可以获取所有Dubbo节点上线列表、某些单点下线等的全量信息。
作为程序员,要时刻有单独想办法解决问题的想法,思路,结合网上方案和以往经验,进行试验和压测。
分布式事务结合多节点集群负载、超时机制可以用来解决熔断,自动切换,幂等,补偿等问题。比如设置一个超时时间,从事务开始时计时,如果超时,则让协调者通知各节点回滚事务,同时通知用户失败状态,完成超时管理。如果需要严格保证一次性成功,则考虑超时终止,放入超时队列和使用异步线程取超时任务进行反复调用。如果要严格幂等,比如转账事务(实际一般用存储过程),付款+减库存业务(一般可以使用缓存+异步,中间状态,最终一致性),则任何一个节点出现异常,也要全部回滚,则需要设计协调者让全部节点严格返回一个成功状态,如果没有收到所有成功状态,则通知各节点全部回滚,再通知用户,可以结合ZooKeeper集群和注册临时节点Watcher来实现(临时节点监听也可解决某服务单点故障造成的分布式事务回滚问题)。幂等也包括防止用户一次付款多次点击的情况,此时可以使用分布式事务结合全局唯一序列号进行判断,保证只有一次生效。
限流,降级问题:漏桶、令牌桶算法等,计时,让同一时间段(窗口)所有请求都先拿到有限个数的令牌桶中的令牌才可执行,否则(流量超量)导入降级页面。降级页面也出现在大流量冲击导致分布式事务某些服务单点宕机,无法提供服务时。降级页面的服务器需要与正常提供服务器隔离开,且有好的伸缩性,高可用,抗冲击性,结合高可用分布式缓存和多级应用缓存,静态化缓存等,承受大流量冲击。
削峰:使用消息中间件,多级缓存等措施。使用异步线程消费流量高峰消息,结合消息中间件消息持久化和消息确认机制(至少一次保障,重新消费,并防止重复消费)
缓存击穿:大流量恶意访问不存在的值造成瞬间大流量穿过缓存,对数据库的访问。可以将常见热数据做一个列表,提前缓存起来,并设置一个短时间缓存命中列表的百分比阈值。低于这个阈值则对所有未命中缓存热数据的请求返回降级页面。也可以结合对击穿缓存的大量无数据库结果的访问在缓存中设置空值,下次访问直接返回缓存空值。其他手段包括使用异步任务监控恶意ip,设置恶意ip列表,直接在访问时用拦截器拦截。
缓存雪崩:多个缓存同时过期造成。可以设置不同的缓存时间,比如可以设置一定范围的随机数。也可结合定时任务,分布式调度事务分时,定时更新不同的缓存。
一、分布式锁
在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
阻塞锁通常使用互斥量来实现:
- 互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态;
- 互斥量为 1 表示未锁定状态。
1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示。
数据库的唯一索引
获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
存在以下几个问题:
- 锁没有失效时间,解锁失败的话其它进程无法再获得该锁。
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
- 不可重入,已经获得锁的进程也必须重新获取锁。
Redis 的 SETNX 指令
使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。
Redis 的 RedLock 算法
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
- 尝试从 N 个相互独立 Redis 实例获取锁;
- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,那么就认为锁获取成功了;
- 如果锁获取失败,就到每个实例上释放锁。
Zookeeper 的有序节点
1. Zookeeper 抽象模型
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父节点为 /app1。
2. 节点类型
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。
3. 监听器
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
4. 分布式锁实现
- 创建一个锁目录 /lock;
- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点;
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
- 执行业务代码,完成后,删除对应的子节点。
5. 会话超时
如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。
6. 羊群效应
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
二、分布式事务
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
本地消息表
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
- 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
- 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
- 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
2PC
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
1. 运行过程
1.1 准备阶段
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
1.2 提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
2. 存在的问题
2.1 同步阻塞
所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
2.2 单点问题
协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待,无法完成其它操作。
2.3 数据不一致
在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
2.4 太过保守
任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
三、CAP
分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。
一致性
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。
可用性
可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
分区容忍性
网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。
权衡
在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
- 为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
- 为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
四、BASE
BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
软状态
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。
最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。
ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
五、Paxos
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
主要有三类节点:
- 提议者(Proposer):提议一个值;
- 接受者(Acceptor):对每个提议进行投票;
- 告知者(Learner):被告知投票的结果,不参与投票过程。
执行过程
规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。
1. Prepare 阶段
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送 Prepare 请求。
当 Acceptor 接收到一个 Prepare 请求,包含的提议为 [n1, v1],并且之前还未接收过 Prepare 请求,那么发送一个 Prepare 响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。
如下图,Acceptor X 在收到 [n=2, v=8] 的 Prepare 请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的 Prepare 响应,设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。
如果 Acceptor 接收到一个 Prepare 请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送 Prepare 响应,该 Prepare 响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。
如下图,Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的 Prepare 请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2,因此就抛弃该提议请求;Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的 Prepare 请求,因为之前接收到的提议为 [n=2, v=8],并且 2 <= 4,因此就发送 [n=2, v=8] 的 Prepare 响应,设置当前接收到的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。
2. Accept 阶段
当一个 Proposer 接收到超过一半 Acceptor 的 Prepare 响应时,就可以发送 Accept 请求。
Proposer A 接收到两个 Prepare 响应之后,就发送 [n=2, v=8] Accept 请求。该 Accept 请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
Proposer B 过后也收到了两个 Prepare 响应,因此也开始发送 Accept 请求。需要注意的是,Accept 请求的 v 需要取它收到的最大提议编号对应的 v 值,也就是 8。因此它发送 [n=4, v=8] 的 Accept 请求。
3. Learn 阶段
Acceptor 接收到 Accept 请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送 Learn 提议给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
约束条件
1. 正确性
指只有一个提议值会生效。
因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
2. 可终止性
指最后总会有一个提议生效。
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
六、Raft
Raft 也是分布式一致性协议,主要是用来竞选主节点。
单个 Candidate 的竞选
有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。
- 下图展示一个分布式系统的最初阶段,此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
- 此时 Node A 发送投票请求给其它所有节点。
- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
多个 Candidate 竞选
- 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票。例如下图中 Node B 和 Node D 都获得两票,需要重新开始投票。
- 由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
数据同步
- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
- Leader 会把修改复制到所有 Follower。
- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
参考
-
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- Distributed locks with Redis
- 浅谈分布式锁
- 基于 Zookeeper 的分布式锁
- Raft: Understandable Distributed Consensus
- 聊聊分布式事务,再说说解决方案
- 分布式系统的事务处理
- 深入理解分布式事务
- What is CAP theorem in distributed database system?
- NEAT ALGORITHMS - PAXOS
- Paxos By Example