前面说了ZooKeeper一些基础性的东西,包括客户端编程框架。这里我们来探索如何更好的运用ZooKeeper。
开始之前,我想先借用Linus Torvalds(Linux创始人)的一句话。
Bad programmers worry about the code. Good programmers worry about data structures and their relationships
差的程序员关注代码,好的程序员关注数据结构和数据之间的关系。
与文无关
不管如何,我们能感受到数据结构的重要性,而平时我们开发所涉及到的各种东西,本质还是:
程序=算法+数据结构。数据结构也是编程的基础,我们这里说如何通过ZooKeeper构建分布式系统,和它涉及的数据结构。
涉及到的数据结构:
Barrir (障碍)
Quene (队列)
Lock (锁)
Leader Selection
Group MemberShip
Service Discovery
这是一篇介绍技术的文章,但是里面一张图也没有,捂脸...
Barrier
Barrier中文译为栅栏,障碍。意思是暂时阻塞部分东西,直到某个条件满足。比如:“百米赛跑的起跑线,把起跑线当做障碍,大家还不能跑,只有人到齐了才能跑”。意思就是满足某个条件之后才能继续接下来的操作。
可以参考多线程的:CountDownLatch和CyclicBarrir。
使用ZooKeeper算法实现:(使用伪代码)
首先,创建一个障碍节点 /zk_barrier
如果障碍节点存在的话,也就是系统中是存在某个障碍,需要满足某个条件的。
客户端在/zk_barrier节点调用exists()方法,注册watch事件。
如果exists()方法返回为false,也就是条件满足,客户端可以进行接下来的操作了。
如果exists()方法返回为true,客户端就等待watch的事件。
当障碍节点需要的条件满足的时候,负责/zk_barrier节点的客户端会删除这个节点。
删除节点触发watch事件,客户端再次调用exists()方法再障碍节点上。
如果exists()返回为true的话,客户端继续原本的操作,等待条件满足。
上面的这个结构相对比较简单,还有种结构用来 锁住分布式计算的开始和结束,也叫做Double Barrier. 双重障碍的实现,当节点的数量满足条件的时候,开始任务,当节点的数量为0的时候,结束任务。
算法实现:
第一部分:
首先创建一个/barrier节点,其余客户端在障碍节点上注册事件,并且在/barrier节点下面创建临时节点,也就是以/barrier节点为父节点。 新创建的节点一般是客户端自己的主机名。
客户端同时注册一个事件,检查/barrier/ready节点是否存在。
系统中已经预定义了 N 数字,这个数目为任务开始的最小满足节点数。节点的数目必须要等于或多与N的时候,才开始任务。
当有新的节点加入的时候,新的节点都获取一下当前/barrier节点下子节点的数目。注意getChildren没有设置监听事件。
M = getChildren(/barrier, watch=false)如果M小于N,继续等待ready节点的出现。
如果M等于N,然后这个客户端在/barrier节点下创建/barrier/ready节点。
由于之前有节点注册了监听事件,每个客户端都会开始任务。
第二部分:客户端完成它需要完成的任务之后,删除它在/barrier节点下创建的子节点。
删除完之后,客户端再调用
M = getChildren(/barrier, watch=true)。
如果M大于0,客户端仍然等待通知。
如果M等于0,客户端退出。
上面可能有个风险是网络流量过于庞大,一旦ready节点注册,它像所有的节点发送通知。有个办法是每个客户端注册ready的下一个最小的序列临时节点。这样每次只有一个节点收到通知。不会一下子所有的节点都被唤醒。
Queue
分布式队列也是分布式系统中一种非常常见的数据结构,一种比较特殊的队列叫做。生产者-消费者 队列。有一个集合,生产者生产的东西存放在这个集合里面,消费者从集合中拿出东西来消费。它遵守FIFO(先进先出原则)。
直接看伪代码:
创建一个/QUEUE节点,用作实现队列的集合。
作为生产者的客户端,调用create()方法创建名为"queue-"的节点,这个节点是临时节点,同时是序列节点,并且是/QUEUE的子节点。一般节点名称像
queue-N
。作为消费者的客户端,在/QUEUE节点上调用getChildren()方法,同时设置监听事件。
M = getChildren(/_QUEUE_, true)
对子节点排序,拿出数字最小的节点进行消费。然后删除最小的那个节点。
有可能在删除节点的时候,因为有其它的节点在对这个节点进行访问,导致删除失败,这时候我恩需要再次重试删除消费者的客户端不断的从子节点列表中取出节点进行消费,一旦列表中的节点都被消费了。消费者重新调用getChildren方法。
直到getChildren()返回为空的列表的时候,这意味着/QUEUE节点下没有更多的节点了。
有更多精力的话可以实现优先级队列。
注意:建议看一下ZooKeeper官方对这两种数据结构的实现地址为http://zookeeper.apache.org/doc/r3.4.6/zookeeperTutorial.html
分布式锁
分布式锁也就是意味着在任何时间,系统中最多只能有一个客户端持有这把锁。一般在下面场景的时候有用:
写入共享数据库或文件
处理I/O请求
简单描述一下共享锁:假设在lock-node下同时创建了三个子节点,l1,l2,l3。那么创建l1的子节点拥有锁。当客户端想释放锁的时候,只需要删除l1节点,然后创建l2的客户端就拥有了锁,依次类推。
伪代码实现:
第一部分:获取锁
在/locknode下创建子节点,这个节点是 临时有序的,
create("/_locknode_/lock-",CreateMode=EPHEMERAL_SEQUENTIAL)
调用getChilren(/locknode/lock-,false)方法,不设置监听器。
检查获取到的节点列表,如果自己创建的节点拥有最小的序号,那么当前客户端拥有锁。退出算法。
调用
exists("/_locknode_/<最小的节点>, True)
方法如果exists返回为false,那么重新进行第2步。
如果exists为true,那么等待存在的这个节点的 watch事件。然后进行第4步。
第二部分:释放锁
持有锁的客户端删除节点,触发下一个节点获取锁。
比当前节点序号大的程度最小节点将会获取到锁。
Leader选取
Leader选取算法有两个要求:
大多数时间内,只有一个Leader
在任何时间内,要么没有Leader要么只有一个Leader
暂时我们先把Leader选取算法理解为锁算法,谁拥有了锁,谁就是Leader。关于Leader选取算法还需要深入分析。
Group Membership 成员关系
组员关系也是分布式系统的核心组件,目的是为了维护服务的可靠性和一致性。它可以让组内的任何一个实体知道当前组的状况,谁加入了进来,谁离开了。
实现起来较为简单,伪代码如下:
创建/Membership持久节点,代表成员关系树
加入组的节点在/Membership节点下创建临时节点。
所有的组内成员都有注册/Membership的监听事件,因此可以知悉/Membership节点下的变化
当有新的节点加入进来,其它的节点会收到通知。当有节点离开,或故障了,ZooKeeper也会自动的删除这个节点,所有的组员也会知道。
当有节点想知道其它节点的状态。使用getChildren()获取/Membership节点下的子节点即可。
服务发现
如果有认真看上面的几种实现,我想大部分人都该了解ZooKeeper的套路了,服务发现,也就是我们要知道提供某个服务的服务器有哪些。我们去找谁来提供服务。
可以理解为“物以类聚,人以群分”,这样一群一群的,就变成了上面的成员关系管理。数据库服务的一组,web服务的一组。文件服务器的一组,想要知道这个组内的状态,获取这个组的节点就好了。比如web服务器,可以创建/web_service节点,然后web服务都去这个节点找。
其余的就不多说了。和成员关系基本一样
实现
光有理论不去实现怎么行呢,代码贴出来一方面不太好讲解,编程靠的是思想,你把东西想明白了,开发起来才容易,自己都想不明白的事情,又如何指望计算机来帮你处理呢。
关于本次所有涉及的理论部分,实现的地方有两处:
ZooKeeper的github项目实现 https://github.com/apache/zookeeper/tree/master/src/recipes ,它实现了Quene,Lock,Leader Selection
Apache Curaror直接给我们封装好了常用的分布式数据结构,追求快速的话,可以直接使用Apache Curator. maven包为org.apache.curator.curator-recipes,貌似curator也实现了服务发现。感兴趣的都可以自己看。
最后
这次主要还是理论偏多,但是大型项目所依赖的也不过是这些基础的部分。关于ZooKeeper的应用场景,如何使用到这些数据结构的,下面我们再谈。
参考
《Apache ZooKeeper Essential》
共同学习,写下你的评论
评论加载中...
作者其他优质文章