ZooKeeper:分布式环境下的一些问题及一致性协议
弱鸡的学习总结,zk篇第一回。 本篇文章总结了分布式环境下产生的一些问题,及分布式一致性协议。顺便学习了ZooKeeper的ZAB协议,学完对事务的提交、Leader的选举有了更深刻的认识。
什么是ZooKeeper
zk是一个分布式协调组件,为了解决分布式一致性问题,实现分布式锁。
分布式环境中存在哪些问题
- 通信异常:消息丢失和消息延迟
- 网络分区:分布式系统部分节点和其他节点丢失通信,一部分节点之间可以互相通信,另外一些节点不能,俗称“脑裂”
- 三态:分布式系统的每一次请求与响应,只有3种状态:成功、失败和超时
- 节点故障:服务器节点宕机或僵死
什么是分布式一致性问题?
一致性是指,数据在多个副本之间要保持一致的特性。 在一个分布式系统中,存在多个节点,每个节点都会提出一个请求,这些请求必须要被所有节点确认,达成一致,确定最后只有一个请求被通过。
分布式事务
狭义上的事务是指数据库事务,广义上它可以看作程序对系统中的数据进行的一系列访问与更新操作组成的一个程序执行逻辑单元。
事务具有四个特性,即 ACID特性,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
一个分布式事务可以看作是多个分布式的操作序列组成(子事务),因此分布式事务可以堪称一种嵌套型的事务。
CAP理论和BASE理论
CAP理论 :一个分布式系统不可能同时满足一致性(C: Consistency)、可用性(A: Availability)和分区容错性(P: Partition tolerance)这三个基本需求,最多只能同时满足其中两项。
但是,对于一个分布式系统而言,分区容错性(P)是一个最基本的要求,不能放弃。因此,必须根据业务特点在一致性(C)和可用性(A)之间寻求平衡。
前辈们在权衡 C\A 的实践过程中总结出了BASE理论:
BASE理论: Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)。
- 基本可用:在系统出现不可预知的故障时,允许损失部分可用性,如响应时间的损失、功能降级。
- 软状态:允许系统在不同节点的数据副本间进行数据同步过程中的延时。
- 最终一致性:系统中的所有数据副本,在经过一段时间的同步后,最终能达到一个一致的状态。
一致性协议
当一个事务跨越多个节点时,为了保持事务的ACID特性,就需要引入一个“协调者 Coordinator”来统一调度所有节点的执行逻辑,被调度的节点称为“参与者 Participant”。 协调者负责调度参与者的行为,并最终决定是否把事务真正进行提交。
-
2PC (Two-Phase-Commit) , 二阶段提交
-
阶段一: 提交事务请求
- 事务询问:协调者向所有参与者发送事务内容,询问是否可以执行事务提交操作。
- 执行事务:各参与者执行事务操作,将Undo和Redo信息写入事务日志。(注意,执行 但未提交)
- 各参与者向协调者反馈事务询问的响应: 如果参与者成功执行事务操作,则反馈给协调者Yes响应,否则反馈No。
-
阶段二:执行事务提交 或 回滚
如果协调者从所有参与者收到的反馈都是Yes,则执行事务提交。
- 协调者向所有参与者节点发送Commit请求
- 参与者收到Commit请求后,正式执行事务提交操作
- 参与者在事务提交完成后,向协调者发送Ack消息
- 协调者收到所有Ack后,完成事务。
相反,任何一个参与者向协调者反馈了No,或者协调者在规定等待时间未收到所有响应,则中断事务。
- 协调者向所有参与者节点发送Rollback请求。
- 参与者接收到Rollback请求后,记录Undo信息执行回滚操作,回滚完成后释放资源
- 参与者反馈回滚Ack
- 协调者收到所有参与者反馈的Ack消息后,完成事务中断
缺点: 协调者存在单点问题;过于保守,所有参与者的事务操作都在等待其他参与者而处于阻塞状态;可能存在数据不一致的情况(当协调者发送Commit请求后,只有部分参与者收到,这部分参与者将提交事务,而其他没有收到Commit请求的参与者则无法提交事务)
优点:原理简单
-
-
3PC
-
阶段一:CanCommit
- 协调者向所有参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作
- 参与者收到canCommit请求后,如果自身认为可以顺利执行则返回Yes,并进入预备状态,否则返回No
-
阶段二:PreCommit or abort
如果协调者从参与者收到的所有返回都是Yes,则执行事务的预提交
- 协调者向所有参与者节点发送preCommit请求,并进入prepared状态
- 参与者收到preCommit请求后,执行事务操作,并记录Undo和Redo信息到日志中
- 如果参与者执行成功,则返回给协调者Ack响应,并等待最终指令:commit或abort
如果任意一个参与者向协调者返回No响应,或超时,则协调者准备中断事务
- 协调者向所有参与者发送abort请求
- 参与者无论是收到abort请求还是等待协调者请求超时,都中断事务
-
阶段三:doCommit
- 协调者收到所有参与者的Ack响应(若有一个No或超时,则协调者将进入abort状态),则状态由“预提交”转换到“提交”状态,并向所有参与者发送doCommit请求
- 参与者执行事务提交
- 参与者完成事务提交后,向协调者发送Ack消息
- 协调者收到所有Ack消息后,完成事务;
优点: 降低了阻塞范围
缺点: 在进入阶段三,无论协调者出现问题还是协调者与参与者之间出现网络故障,参与者都会执行事务提交。依然存在数据不一致问题
-
-
ZAB协议 (zookeeper使用的 改进了的2pc)
ZAB(ZooKeeper Atomic Broadcast,原子消息广播协议)协议的核心是定义了 对于会改变ZooKeeper服务器数据状态的事务请求的处理方式:
所有事务请求必须由一个全局唯一的服务器来协调处理,即Leader。余下的其他服务器则成为Follower服务器。 Leader服务器负责将一个客户端事务请求转换成一个Proposal(提议),并将该Proposal分发给集群中的所有Follower服务器。之后Leader等待所有Follower的反馈,一旦超过半数的Follower服务器返回了正确的响应,则Leader再次向所有Follower分发Commit消息,要求其将前一个Proposal进行提交。
ZAB协议包括两种基本模式:崩溃恢复 和 消息广播。
崩溃恢复:当集群中的Leader服务器出现崩溃、掉线或重启等异常情况时,ZAB协议就进入恢复模式并选举产生新的Leader服务器。当选举产生了新的Leader,同时集群中已经有过半的服务器与新的Leader完成状态同步后,ZAB就退出恢复模式。
消息广播: 类似于一个二阶段提交过程。对于客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并发给集群中所有Follower服务器,然后收集选票进行事务提交。
在广播之前,Leader首先为Proposal分配一个全局递增且唯一的事务ID,即ZXID(64位)。每一个事务Proposal按照ZXID的先后顺序进行排序处理,具体的,Leader为每一个Follower都分配一个队列,并将Proposal按ZXID依次放入队列中,根据FIFO的策略发送给Follower。
Follower接收到Proposal后,先将其以事务日志的形式写入到本地磁盘中,写入成功后返回给Leader一个Ack响应。当Leader收到超过半数Follower的Ack后,就广播一个Commit消息给所有Follower服务器通知其进行事务提交,同时Leader本身也执行这次事务提交。
当Leader失去连接时,需要保证:
a. 已经被Leader提交的事务不丢失
b. 没有被Leader提交的事务要跳过
leader掉线后.png如图,新的集群保留P1 P2后,开始了新Leader统治的新篇章。即:
新选出来的Leader服务器要拥有急群众所有机器最大的ZXID, 这样就能保证新Leader一定具备所有已经提交的Proposal,而且可以省去新的Leader服务器检验Proposal的提交和丢弃这一操作。
ZXID代表Proposal的事务ID,它的前32位用来标识Leader的朝代 epoch,后32位作为计数器记录事务ID.当新的Leader产生后,ZXID的前32位加1,后32位清0。
所以Leader选举时,先比较epoch,epoch大的当选;epoch相同,再比较事务id zxid;都相同比较myid,myid大的优先。
zk的分布式锁
借用curator帮忙实现。 首先需要依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
然后是小demo:
public static void main(String[] args) {
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().
connectString("127.0.0.1:2181").sessionTimeoutMs(5000).
retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();
curatorFramework.start();
//分布式可重入排它锁
InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/lock-demo");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 尝试竞争锁");
lock.acquire();
System.out.println(Thread.currentThread().getName() + " 成功获得了锁");
} catch (Exception e) {
}
try {
//睡3秒,可以登陆zk-cli去服务器上看看生成的节点是什么样的
Thread.sleep(3000);
} catch (Exception e) {
} finally {
try{
lock.release();
System.out.println(Thread.currentThread().getName() + " 释放了锁");
} catch (Exception e) {
}
}
}).start();
}
}
最后总结下以前被面试过的: zk 分布式锁为什么比 redis分布式锁更好?
如果非要说zk分布式锁比redis好,大概主要是利用了zk的两个特性:
-
zk支持有序节点,假设最小的节点为获得锁,那么只要判断当前节点是否为所有子节点中最小的即可。如果当前节点不是所有子节点最小的,那么就意味着没有过的锁。当比自己小的节点删除以后,客户端收到wathcer事件,此时再判断自己的节点是不是最小的,重复这个过程直到获得锁。可以避免所有客户端同时竞争锁(惊群现象)。
-
zk临时节点的特性:当客户端session结束后,临时节点自动删除,即释放锁。不像redis需要手动删除,或者设置超时时间。
参考书籍
《从Paxos到ZooKeeper分布式一致性原理与实践》,倪超