可以参考:https://zhuanlan.zhihu.com/p/62526102
ZooKeeper 是一个分布式的,开放源码的分布式应用程序协同服务。ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
适用于存储和协同相关的关键数据,不适合大数据量存储。 因为zookeeper需要把所有数据加载到内存中,所存储的数据量受到内存的限制; zookeeper存储关键数据,保证数据的高可用性和性能才是它的设计目的。
可以看一下Java开发者的相关文章,Kafka用到zookeeper,所以说想要使用Kafka,两个都得装上。 kafka使用zookeeper来管理自己的元数据配置; 在实现分布式锁的情境下,也会使用到zookeeper。
内容参考MIT6.824:https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/lecture-08-zookeeper/8.4-zookeeper
特点1: Zookeeper是一个通用的协调服务
如果想要使用Raft,那么设计自己的应用程序与Raft交互。 Zookeeper就是把分布式服务给封装了,然后提供API, 它使用的一致性协议是ZAB
特点2: Zookeeper 一写多读, 性能提升但是不保证强一致性
如果要求系统的强一致性,增加server数量并不能提升系统性能
性能瓶颈在于leader, leader需要处理每一个请求,将请求的拷贝发送给每一个其他服务器,非leader节点的增加并不能实际完成任何工作。
针对该问题的改进:一写多读,只有写请求需要经过leader,读请求不需要经过leader。 在现实世界中,读请求比写请求多得多,比如web页面,都是通过读请求来生成web页面。
但是,如果把客户端请求发送给副本,能否得到预期结果?副本中的数据,并不一定是最新的,因为append entries只要求大多数节点收到并响应,我们需要读的节点也许为少数节点,此时数据是落后的。
对于Zookeeper来说,并不要求返回最新的写入数据。 Zookeeper放弃线性一致性,不提供线性一致的读。它有自己有关一致性的定义, 两个主要的保证:
尽管客户端可以并发的发送写请求,然后Zookeeper表现的就像以某种顺序,一次只执行一个写请求,并且也符合写请求的实际时间。所以如果一个写请求在另一个写请求开始前就结束了,那么Zookeeper实际上也会先执行第一个写请求,再执行第二个写请求。
对于写请求,最终会以客户端确定的顺序执行。这一点可以理解为,请求的执行顺序按照客户端发出指令的时间为准(?),客户端可以发送异步的写请求,服务端的执行顺序就按照客户端发送请求的时间(序号)。
对于读请求,如果第一个读请求在Log中的一个位置执行,那么第二个读请求只允许在第一个读请求对应的位置或者更后的位置执行。 (也就是读读一致性的要求)这个位置也就是Log对应的条目号zxid,每次读请求的响应中会带上zxid给client, 当client再次发出请求到一个相同或者不同的副本时,会在请求中带上最高的zxid,这样副本就会知道,至少要在Log中的这个点或之后执行读请求。 如果这个副本的最新日志小于zxid,那么就无法响应客户端请求(这里可以是发出信息拒绝client,或是一直阻塞),直到log更新,才能响应client的读请求。
如果客户端先发送写请求,紧接着发读请求,此时需要写请求执行以后,才能接着处理读请求。这是为了满足写读一致性。 为了处理这种情况,客户端的读请求还要带上 上一次写请求对应的zxid, 副本必须看到对应zxid的写请求才能再执行读请求。
zookeeper有一个弥补非严格线性一致的方法 —— sync
假设现在需要刚刚写入的最新的数据,可以发送sync请求(本质上是写请求),让sync请求写入到所有副本的sync 。 我认为sync可以理解为一个标志点。
然后向副本发送读请求并携带信息告知(发送了sync这事), 副本需要在看到sync后才能回复这个读请求。 因为收到了sync,也就意味着收到了sync之前的所有日志。副本的响应至少是发送sync请求时对应的状态。
sync是个代价很高的操作,本来读请求是不经过leader的,发送sync后意味着客户端需要等待日志同步到sync的位置才能响应。
znode是zookeeper的数据点, file = znode, 和linux一致,“一切皆文件”
考虑zookeeper管理配置文件场景:Master节点更新一个存有集群信息的配置,而大量客户端需要读取配置,此时Master节点能做到原子更新吗? 客户端并不希望读到更新到一半的配置。
我希望zookeeper能够对读做一个限制,不读到中间状态。
zookeeper应对这种情况的方式:Ready file
如果Ready file存在,则client可以读取配置
如果Ready file不存在,client不能读取。
Master更新配置的流程:
所有步骤均为写请求,zookeeper可以确保这些请求以线性顺序执行。 对于副本,也需要按这一流程执行,在更新前删除Ready file, 更新后创建Ready file。
clinet若在此时发送读请求,副本发现ready file不存在,那么会过一会再重试。
结合一致保证章节中所提到的zookeeper写读一致性的实现, 如果客户端可以看见创建Ready file的写请求, 读请求必须在该写请求之后 => 因此,zookeeper可以保证读请求看到对配置的全部更新。
问题又来了: 客户端想要看见Ready file需要通过调用exist,客户端调用exist的时候Ready file存在,但是读完后master开始更新配置,这样带来的后果就是:配置虽然更新了,但是客户端还以为是老配置。
zookeeper的解决方法是: 客户端不仅会发送exists来查询Ready file是否存在,还会建立一个针对Ready file的watch通道。一旦Ready file改变,副本节点会向客户端发送通知。client处理完了发来的通知,再重新执行读配置的操作。
zookeeper可以用来解决的问题:
zookeeper的API看起来像是文件系统, 请求zookeeper数据的时候需要指定路径(路径就像Linux文件路径一样),文件和目录都叫做znode,类别如下:
- CREATE(PATH,DATA,FLAG)。入参分别是文件的全路径名PATH,数据DATA,和表明znode类型的FLAG。这里有意思的是,CREATE的语义是排他的。也就是说,如果我向Zookeeper请求创建一个文件,如果我得到了yes的返回,那么说明这个文件之前不存在,我是第一个创建这个文件的客户端;如果我得到了no或者一个错误的返回,那么说明这个文件之前已经存在了。所以,客户端知道文件的创建是排他的。在后面有关锁的例子中,我们会看到,如果有多个客户端同时创建同一个文件,实际成功创建文件(获得了锁)的那个客户端是可以通过CREATE的返回知道的。
- DELETE(PATH,VERSION)。入参分别是文件的全路径名PATH,和版本号VERSION。有一件事情我之前没有提到,每一个znode都有一个表示当前版本号的version,当znode有更新时,version也会随之增加。对于delete和一些其他的update操作,你可以增加一个version参数,表明当且仅当znode的当前版本号与传入的version相同,才执行操作。当存在多个客户端同时要做相同的操作时,这里的参数version会非常有帮助(并发操作不会被覆盖)。所以,对于delete,你可以传入一个version表明,只有当znode版本匹配时才删除。
- EXIST(PATH,WATCH)。入参分别是文件的全路径名PATH,和一个有趣的额外参数WATCH。通过指定watch,你可以监听对应文件的变化。不论文件是否存在,你都可以设置watch为true,这样Zookeeper可以确保如果文件有任何变更,例如创建,删除,修改,都会通知到客户端。此外,判断文件是否存在和watch文件的变化,在Zookeeper内是原子操作。所以,当调用exist并传入watch为true时,不可能在Zookeeper实际判断文件是否存在,和建立watch通道之间,插入任何的创建文件的操作,这对于正确性来说非常重要。
- GETDATA(PATH,WATCH)。入参分别是文件的全路径名PATH,和WATCH标志位。这里的watch监听的是文件的内容的变化。
- SETDATA(PATH,DATA,VERSION)。入参分别是文件的全路径名PATH,数据DATA,和版本号VERSION。如果你传入了version,那么Zookeeper当且仅当文件的版本号与传入的version一致时,才会更新文件。
- LIST(PATH)。入参是目录的路径名,返回的是路径下的所有文件。
错误做法:
count = GET(key)
// get 之后, count还有可能改变
PUT(k, count + 1)
因为read-update-write 不是原子的
正确做法:
WHILE TRUE:
X, V = GETDATA("F") //read操作,任意副本执行
IF SETDATA("f", X + 1, V): // write操作, leader执行。
//如果get后数据被修改,版本号就不是V,无法执行SET操作,会进行下一个循环
BREAK
Test-and-set服务也可以这样实现。旧的数据为0, 想要将它设置为1,那么在set的时候需要带上旧的版本号,版本号必须与读时的版本号相同。