什么?你还不了解ZooKeeper?
1 前言
谈到分布式应用,就离不开ZooKeeper,那么ZooKeeper是怎么来的,它又是做什么的?通过这篇文章,希望大家最ZooKeeper有个基本的了解。
本篇文章主要介绍ZooKeeper的基本概念及其在并发情况下的读写流程分析,希望能对大家有所帮助!
2 ZooKeeper简介
2.1 背景
众所周知,分布式应用程序都需要一个协调服务,那么为什么分布式应用程序需要协调服务呢?因为分布式应用程序是分布在多台主机上面的,分布在多台主机上面的应用要想共同地去很好地完成任务,当然得需要一个协调者了,ZooKeeper就是这样一个协调者。其实分布式应用程序就跟团队一样,团队要想高效的完成任务,当然需要一个协调者去协调各项工作了,这个协调者就是团队Leader啦。
我们都知道协调者不是这么好当的,对于分布式应用程序来说也是如此,协调服务很容易出现竞态条件、死锁等问题。为了减少分布式应用程序开发协调服务的成本,所以就诞生了ZooKeeper——开源的分布式协调服务。
2.2 基本概念
ZooKeeper 是一个开源的分布式协调服务,由雅虎创建,是 Google Chubby 的开源实现。分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、命名服务、分布式协调/通知、集群管理、配置维护、分布式锁等功能。
2.3 特点
1、简单
分布式应用程序可以通过一个共享的层级命名空间(类似标准的文件系统)来进行协调,该层级命名空间由数据节点构成——在ZooKeeper中我们称它为znode,znode类似标准文件系统中的文件或是目录。不同于标准文件系统,ZooKeeper中的数据是存在在内存中的,所以ZooKeeper可以实现高吞吐量和低延迟。
2、基于复制
ZooKeeper集群中的server是互相复制的,所以集群中的每个server数据都是一样的,正因如此,我们可以通过任意server来读取数据。
ZooKeeper集群中的主机是互相复制的
集群中的server必须能够互相感知,它们拥有相同的状态。客户端可以连接任意server,然后与server保持着TCP连接,通过此连接可以发送请求、获得响应,当此连接断掉的时候,客户端会重新连接一个不同的server。
集群中每一个server都可以服务客户端,读请求通过每个server的本地数据副本来提供,写请求则由ZooKeeper协议来处理。作为协议的一部分,所有的写请求都会被转发到集群中一个叫做Leader的节点来处理,集群中的其余节点则称为Follower,Follower用来接收并处理来自Leader的提案。Leader宕机重新选举与Follower与Leader的同步也都是此协议的内容,感兴趣可以搜索ZooKeeper zab协议进一步了解。
3、有序性
ZooKeeper通过为每一个更新操作编号来保证事务有序性。
4、快速
ZooKeeper在读多写少的场景中,拥有极快的速度。
2.4 数据模型
ZooKeeper的数据模型是层次型,类似文件系统。
ZooKeeper的数据模型
每个数据节点,在ZooKeeper中叫做znode,并且其有一个唯一的路径标识,znode既可以包含数据也可以包含子节点,好比文件系统中允许文件也作为目录一样。znode中数据的读写都是原子的。
ZooKeeper中也有临时节点的概念,只要创建临时节点的会话存在,该节点就存在,会话一结束,那么该节点就会被删除。
2.5 ZooKeeper的一些保证
作为构建复杂分布式服务的基础,ZooKeeper提供了一些保证,利用这些保证,我们可以保证应用的正确性:
- 顺序一致性:来自同一客户端的更新将按顺序进行。
- 原子性:更新操作要么成功要么失败,不存在中间状态。
- 单系统映像:不论连接到哪个服务器,客户端都会看到相同的视图,因为ZooKeeper是基于复制的嘛。
- 可靠性:更新操作成功后,znode状态会一直持续到下次更新操作,在此期间,状态不变。
- 及时性:客户端会及时获取ZooKeeper最新状态。
3 ZooKeeper的读写流程分析
在了解了ZooKeeper的基本概念后,我们来详细看下ZooKeeper的读写流程,以及ZooKeeper在并发情况下的读写控制。以求对ZooKeeper有进一步的了解。
3.1 读流程分析
读流程如下图所示:
ZooKeeper读流程
因为ZooKeeper集群中所有的server节点都拥有相同的数据,所以读的时候可以在任意一台server节点上,客户端连接到集群中某一节点,读请求,然后直接返回。当然因为ZooKeeper协议的原因(一半以上的server节点都成功写入了数据,这次写请求便算是成功),读数据的时候可能会读到数据不是最新的server节点,所以比较推荐使用watch机制,在数据改变时,及时感应到。
3.2 写流程分析
写流程如下图所示:
ZooKeeper写流程
当一个客户端进行写数据请求时,会指定ZooKeeper集群中的一个server节点,如果该节点为Follower,则该节点会把写请求转发给Leader,Leader通过内部的协议进行原子广播,直到一半以上的server节点都成功写入了数据,这次写请求便算是成功,然后Leader便会通知相应Follower节点写请求成功,该节点向client返回写入成功响应。
3.3 ZooKeeper并发读写情况分析
我们已经知道ZooKeeper的数据模型是层次型,类似文件系统,不过ZooKeeper的设计目标定位是简单、高可靠、高吞吐、低延迟的内存型存储系统,因此它的value不像文件系统那样适合保存大的值,官方建议保存的value大小要小于1M,key为路径。
ZooKeeper的数据模型每个数据节点在ZooKeeper中叫做znode,并且其有一个唯一的路径标识,znode节点可以包含数据和子节点。
ZooKeeper的层次模型是通过ConcurrentHashMap实现的,key为path,value为DataNode,DataNode保存了znode中的value、children、 stat等信息。
ZooKeeper的层次模型是通过ConcurrentHashMap实现的,而ConcurrentHashMap是线程安全的Hash Table,它采用了锁分段技术来减少锁竞争,提高性能的同时又保证了并发安全,其结构如下图所示:
ConcurrentHashMap结构ConcurrentHashMap由两部分组成,Segment和HashEntry,锁的粒度是Segment,每个Segment 对象包含整个散列映射表的若干个桶,散列冲突时通过链表来解决。
因为插入键 / 值对操作只是在 Segment 包含的某个桶中完成,所以这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,不需要锁定整个ConcurrentHashMap,所以对于ConcurrentHashMap,可以进行并发的写操作,只要写入的Segment不同。而所有的读线程几乎不会因为写操作的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值。这便是锁分段技术,保证并发安全的情况下又提高了性能。
对于ZooKeeper来讲,ZooKeeper的写请求由Leader处理,Leader能够保证并发写入的有序性,即同一时刻,只有一个写操作被批准,然后对该写操作进行全局编号,最后进行原子广播写入,所以ZooKeeper的并发写请求是顺序处理的,而底层又是用了ConcurrentHashMap,理所当然写请求是线程安全的。而对于并发读请求,同理,因为用了ConcurrentHashMap,当然也是线程安全的了。总结来说,ZooKeeper的并发读写是线程安全的。
但是对于ZooKeeper的客户端来讲,如果使用了watch机制,在进行了读请求但是watch znode前这段时间中,如果znode的数据变化了,客户端是无法感知到的,这段时间客户端的数据就有一定的滞后性了,只有当下次数据变化后,客户端才能感知到,所以对于客户端来说,数据是最终一致性。
4 总结
通过上述阅读,相信大家对ZooKeeper的基本概念与读写流程都有了一定的了解。这篇文章内容不算多,但却花了我不少时间,主要时间花在了对知识、概念正确性的考察上面,尽力避免因为我的理解错误而误导大家。最后,文章有什么不正确的地方,感谢大家的指出,如果文章对你有帮助,也欢迎点赞。
5 参考资料
[1] ZooKeeper官方文档资料. http://zookeeper.apache.org/doc/current/zookeeperOver.html.
[2] zookeeper的原理和应用场景. https://www.jianshu.com/p/b48d50e1fcb1.
[3] 探索 ConcurrentHashMap 高并发性的实现机制. https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/.