《从Paxos到Zookeeper 分布式一致性原理与实践》读书笔记
本文:总结脑图地址:脑图
前言
所有的典型应用场景,都是利用了ZK的如下特性:
- 强一致性:在高并发情况下,能够保证节点的创建一定是全局唯一的。
- Watcher机制和异步通知:可以对指定节点加上监听,当节点变更时,会收到ZK的通知。
数据发布订阅(配置中心)
常见的应用就是配置中心,发布者将数据发布到ZooKeeper的一个或多个节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布订阅系统一般有两种设计模式:分别是推(Push)模式和拉(Pull)模式。在推模式下,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。
ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发送变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需主动到服务端获取最新的数据。
通常,放到配置中心上的数据都具有如下特点:
- 数据量通常比较小。
- 数据内容在运行时会发生动态变化。
- 集群中各机器共享,配置一致。
命名服务
是指通过指定的名字来获取资源或者服务的地址,提供者的信息。利用Zookeeper很容易创建一个全局的路径,而这个路径就可以作为一个名字,它可以指向集群中的集群,提供的服务的地址,远程对象等。简单来说使用Zookeeper做命名服务就是用路径作为名字,路径上的数据就是其名字指向的实体。
阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表。在Dubbo实现中:
服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers
目录下写入自己的URL地址,这个操作就完成了服务的发布。
服务消费者启动的时候,订阅/dubbo/{serviceName}/providers
目录下的提供者URL地址, 并向/dubbo/{serviceName} /consumers
目录下写入自己的URL地址。
注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/{serviceName}
目录下所有提供者和消费者的信息。
分布式协调/通知
- 分布式定时任务:
- 公平的方式:同时向指定目录下创建临时非顺序节点,创建成功的节点即获得执行任务的资格
- 非公平的方式:同时向指定目录下创建临时顺序节点,节点顺序最小的节点就获得执行权限。
-
心跳检测:
基于ZooKeeper的临时节点特性,可以让不同的机器都在ZooKeeper的一个指定节点下创建临时子节点,不同的机器之间可以根据这个临时节点来判断对应的客户端机器是否存活。通过这种方式,检测系统和被检测系统之间并不需要直接相关联,而是通过ZooKeeper上的某个节点进行关联,大大减少了系统耦合。 -
HA:同心跳检测。
分布式锁
使用ZK构建分布式锁都是利用了ZK能够保证高并发情况下,子节点创建的唯一性。
非公平独占锁
在特定目录下,创建临时子节点,创建成功,则表示获得锁。创建失败,直接返回加锁失败或者监听指定节点,然后等待加锁成功的节点释放锁后自己再尝试获取锁。
公平独占锁
以加锁key名称创建永久节点,然后争抢锁的节点都在该永久节点下创建临时有序节点,序号最小的节点即获得锁。序号非最小的节点,监听前一个节点,而非全部节点,收到通知后,再验证自己是否是序号最小的节点,如果是,则表示获得锁,否则重新监听前一个节点。
共享锁(S锁)
所有尝试加锁的节点都在指定目录下创建临时顺序节点,形如/shared_lock/[hostname]-请求类型-序号
的临时顺序节点,例如/shared_lock/192.168.0.1-R-00000001
,就代表了一个共享锁;/shared_lock/192.168.0.1-W-00000001
,就代表是一个写锁。
根据共享锁的定义,不同的事务都可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何事务进行读写操作的情况下进行。基于这个原则,我们来看看如何通过ZooKeeper的节点来确定分布式读写顺序:
- 创建完节点后,获取
/shared_lock
节点下的所有子节点,并确定自己的节点序号在所有子节点中的顺序。 - 对于读请求
- 如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读请求,那么表名自己已经成功获取到了共享锁,同时开始执行读取逻辑。
- 如果比自己序号小的子节点有写请求,那么就需要进入等待。
- 对于写请求:如果自己不是序号最小的子节点,那么就需要进入等待。
- 接收到Watcher通知后,重复步骤1。
Master选举
利用ZooKeeper的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZooKeeper将会保证客户端无法重复创建一个已经存在的数据节点。当多台机器同时争抢Master角色时,大家都往指定目录下创建临时节点,在这个过程中,只有一个客户端能够成功创建这个节点,那么这个客户端所在的机器就称为了Master。同时,没有创建成功的客户端都在该节点上注册一个监听事件,用于监控当前的Master机器是否存活,一旦发现当前的Master挂了,那么其余的客户端将会重新进行Master选举。
分布式队列
FIFO先入先出队列
序号最小的先出队列:
- 所有入队列的节点都往指定目录下创建顺序有序节点。
- 通过调用
getChildren()
接口来获取该节点下的所有子节点,即获取队列中所有的元素。 - 确定自己的节点序号在所有子节点中的顺序
- 如果自己不是序号最小的子节点,那就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
Barrier :分布式屏障
分布式Barrier规定了一个队列的元素必须都集聚后才能统一进行安排,否则一直等待。这往往出现在那些大规模分布式并行计算的应用场景上:最终的合并计算需要基于很多并行计算的子结果来进行。 大致的设计思路如下:
-
开始时,
/queue_barrier
节点是一个已经存在的默认节点,并且将其节点的数据内容赋值为一个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier
节点下的子节点个数达到10后,才会打开Barrier。 -
所有客户端都会到
/queue_barrier
节点下创建一个临时节点。 -
创建成功后,通过调用
getData()
接口获取/queue_barrier
节点的数据内容:10。 -
通过调用
getChildren()
接口获取/queue_barrier
节点下的所有子节点,即获取队列中的所有元素,同时注册对子节点列表变更的Watcher监听。 -
统计子节点的个数。
-
如果子节点个数还不足10个,那就需要进入等待。
-
接收到Watcher通知后,重复步骤2。
注意
对于涉及到顺序有序节点,排序等待的场景。不需要监听父节点或者其他全部兄弟节点,只要监听序号比自己小的第一个节点即可,这样可以防止接收到大量无用的Watcher
通知。