配置维护
在分布式系统中,很多服务都是部署在集群中,即多台服务器中部署着完全相同的应用,起着完全相同的作用。当然,集群中的这些服务器的配置文件耶斯完全相同的。
若集群中服务器的配置文件需要进行修改,那么我们就需要逐台修改这些服务器中的配置文件。如果我们集群服务器比较少,那么这些修改还不是太麻烦,但是如果集群服务器特别多,比如大型互联网公司的Hadoop集群有数千台服务器,那么纯手工的更改这些配置文件几乎就是一件不可能完成的任务,即使使用大量人力进行修改可行,但过多的人参与,出错的概率大大提升,对于集群所造成的危险是很大的。
zk可以通过“发布/订阅模型”实现对集群配置文件的替换与维护。“发布/订阅模型”分为推模式(Push)与拉模式(Pull),zk的“发布/订阅”模型采用的是推拉相结合的模式。
首先每一个集群客户端需要向zk注册一个某数据节点的watcher监听,当发布者将更新过额配置数据发布到zk之后,就会触发watcher监听,即zk会向每一个订阅者推送watcher事件,订阅者接收到watcher事件之后,就从zk中拉取最新更新过的配置数据,
命名服务
命名服务是指可以为一个范围内的元素命名一个唯一的标识,以与其他元素进行区分,在分布式系统中被命名的实体可以是集群中的主机、服务地址等。
通过利用
zk中顺序节点自动生成唯一编号的特点来实现命名服务。
首先创建一组业务相关的节点,然后再在这些节点下再创建顺序节点,此时的顺序节点的路径加名称即为生成的唯一标识。
分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式,我们可以通过上层业务关系型数据库中添加额外的行锁、表锁或者事务处理等来实现分布式锁,但是目前绝大多数分布式系统的性能瓶颈都集中在数据库操作引发的数据库性能上,若再给数据库添加更多的额外内容,会更加加重数据库的负担,降低整个系统的性能。
Zookeeper作为第三方,可以实现分布式锁功能,根据用户操作类型的不同,可以分为排他锁与共享锁。
在zk上对于分布式锁的实现,使用的是类似于/xs_lock/[hostname]-请求类型-序号 的临时顺序节点。
分布式实现原理
具体实现过程如下:
- 1.首先客户端会对
xs_lock节点注册子节点列表变更事件的watcher监听。 - 2.然后,若客户端需要获取分布式锁时,会到
xs_lock节点下创建一个临时顺序节点。若当前是读请求,则会创建一个hostname-R-序号的子节点;若当前是写请求,则会创建一个hostname-W-序号的子节点,读写操作的顺序性就是通过这些子节点的顺序体现的。 - 3.在创建完子节点后,当前子节点会对比其他子节点序号的大小关系,并根据读写操作的不同,执行不同的逻辑。
- 读请求:若没有比自己序号小的子节点,或者所有比自己序号小的子节点都是读请求,则表明自己可以获取到共享锁,可以开hi数据读取了‘若比自己序号小的子节点中有写请求,则当前客户端无法获取到共享锁,不能对数据进行读操作,而是进入等待状态,等待前面的写操作的完成。
- 写请求:若自己是序号最小的子节点,则表名当前客户端可以获取到排他锁,可以开始数据更新了;若发现还有比自己序号更小的子节点,则当前客户端无法获取到排他锁,不能对数据进行写操作,而是进入等待状态,等待前面的操作完成。
羊群效应
在前面的执行流程中存在这样的一个问题:若host1客户端的读操作执行完毕,则该会话将会关闭,此时这个host1-R-00000000001临时节点将被删除。由于每一个客户短均在zk中注册了/xs_lock节点的子节点列表数量监听的watcher,所以该节点的删除将会引发后续所有节点重新加载/xs_lock子节点列表。当然,由于host2已经开始了共享数据的读取操作,所以其不会再次加载子节点列表,但是host3、host4、host5均会被加载,但是host3发现自己当前还不是最小序号子节点,所以该写操作仍处于等待状态,等待所有共享锁释放。同样host4、host5在加载了新的子节点列表后,发现前面还有写操作请求,所以它们也要等待,等待前面的写操作执行完毕。
以上过程中请求执行完毕后就要删除该请求对应节点,而节点的删除则会触发大量的客户端watcher,并使得大量客户端重新加载/xs_lock子节点列表,那么,这大量的watcher的触发与子节点列表的加载对于每个客户端是否都有用呢?对于host3没用,因为其要获取到排他锁,必须等待host2操作完毕’对于host4没用,因为其要想获取到共享锁,必须等到host3的排他锁执行完毕;同样对于host5,因为其要获取到排他锁,必须等待host4的排他锁释放。
由于一个操作而引发大量无用操作的执行,这种情况成为羊群效应。当集群的规模很大时(一般指超过10台主机),羊群效应会明显降低系统性能。
经济学里经常用羊群效应来描述经济个体的从众跟风心里。羊群是一种很散乱的组织,平常在一起也是盲目地左冲右撞,一旦有一只羊动起来,其他杨也会不假思索地一哄而上,全然不顾前面可能有狼或者不远处有更好地草。
分布式锁改进
为了解决“羊群效应”所带来地性能下降,可以对前面描述地分布式锁地实现进行改进。当客户端请求发出后,在zk中创建相应的临时节点后马上获取/xs_Lock的所有子节点列表,但任何客户端都不向xs_lcok注册watcher,watcher的注册需要根据请求类型的不同向不同的子节点进行注册。
- 读请求:若能直接获取到共享锁,则直接开始读操作,若不能获取到共享锁,则说明前面有写操作,其只需向序号小于自己的最后一个写请求节点注册
watcher然后等待。 - 写操作:若查看到自己的序号是当前最小的子节点,则直接获取到排他锁,然后开始写操作,若其不是序号最小的节点,则其只需向序号小于自己的最后一个子节点注册
watcher,然后等待。
分布式队列
说到分布式队列,我们马上可以想到RabbitMQ、kafka等消息中间件产品,其实zk也可以实现简单的消息队列。
FIFO队列
zk实现FIFO队列的思路是:利用顺序节点的有序性,为每个数据在zk中创建一个相应的节点,具体过程如下:
- 为每一个数据按照其达到的顺序为其创建顺序子节点,且将数据作为子节点的数据内容。
- 每个子节点在创建时,其若不是序号最小的节点,则需要再向小于其序号的最后一个节点中注册一个
watcher监听。 - 当应用程序对一个节点的数据内容消费过后,马上修改该节点的数据内容,当后面的节点监听到其前面的节点的数据内容发生了改变,则表示当前节点的数据可以被消费了。
分布式屏障Barrier队列
Barrier,屏障、障碍物。Barrier队列是分布式系统中的一种同步协调器,规定了一个队列中的元素必须全部聚集齐后才能继续执行后面的任务,否则一直等待,最常见于大规模分布式并行计算的应用场景,最终的合并计算需要基于很多并行计算的子结果来进行。zk实现Barrier队列的原理如下:
- 首先要创建一个
/barrier节点,其数据内容设置为屏障打开的阈值,即当其下面的子节点数量达到该阈值后,app才会进行最终的计算,否则一直等待。 - 然后应用程序再在
/barrier中注册一个watcher监听,监听其下子结点数量变化。 - 然后就可以开始每一个并行计算了。对于每个并行计算,梅计算出一个子结果,就会在
/barrier下创建一个子结点,而每增加一个子节点,/barrier就会向app发送一个子节点数量变化的watcher事件,通知应用程序获取/barrier子节点列表,以便获取子节点个数。 - 应用程序在获取到子节点数量后,会马上对比其与阈值的大小关系,当子节点个数达到阈值时会开启最终的合并计算,即打开了屏障。