1.ZOOKEEPER是啥
其实定义很模糊,根本看不懂。一种分布式协调服务。从其作用来看,其实更好理解。它基本上主要做下面几个事情。
1.统一配置管理
2.统一命名服务
3.分布式锁
4.集群管理
感觉有些名词也不好懂,等下一个一个去解释即可。
在了解所有功能之前,先要了解下ZK的数据结构,这个如果你装了ZK且用过应该都知道,其实是一种树形结构。这个网上很多,就不解释了。
1.1 统一配置管理
比如我要配置一个文件存储路径,应用上所有上传的文件都放在这个路径下。如果是基于springboot,大家一般想到就放在application文件里面。那如果我想不重启应用就能改路径,并且让它生效的话。是不是有点难?你要做几件事情
1.代码要改,保证改后要立即生效。(这一步是逃不掉的)
2.如果放在application文件里面,如果是集群部署,那我要每台机器都要改。有人说我发布就可以了,记住前提是我不想重启,更别提发布了。
其实还有个办法就是把数据放在ZK里面,一开始配置好。应用启动的时候从ZK里面把数据get出来就可以了。如果改了呢,ZK有个WATCHER机制,也就是每个客户端可以监听这个值的变化,并且可以触发响应动作。所以这种配置方式是不是比application更好些。
1.2统一命名服务
这个其实和统一配置没啥区别吧。
1.3 分布式锁
分布式锁实现方式很多。比如数据库乐观锁,再比如redis。这里其实要说到ZK一种特殊的临时节点。这种节点有种特性,在客户端和服务端会话断开的时候,会被服务器自动删除掉。分布式锁的实现思路如下:
1.每个客户端都想向AK请求在同一树路径下创建有序节点。
2.ZK会保障在多个客户端创建有序节点的时候,不会创建出重复的节点。创建完之后,每个节点就会监测自己在这个目录下创建的节点是不是{BANNED}最佳小的节点。
3.如果是{BANNED}最佳小节点,则说明你抢到了锁,你就可以开始做自己对应的业务了。等你的业务做完记得删掉自己创建的节点
4.如果你创建的节点不是{BANNED}最佳小值,说明你没抢到锁,此时,你应该阻塞住自己。注意要去监听这个路径下节点的变化。这时是不是用到了ZK的监听机制。
5.一旦监听到变化,从新检查自己是不是{BANNED}最佳小节点(说白了就是从新来检查自己是不是拿到了锁),如果事,则回到步骤3,如果没有拿到锁,则回到步骤4.
这5个步骤是不是满足了分布式锁的需求。
1.4集群状态和选举MASTER
集群状态:笔者以前碰到过这个问题,有一堆待放款的单据(这些单据是异步去执行),我们启动了后台定时器任务去跑这些数据。当然我们的机器会比较多,应为单据量很大。这个时候就要监测跑这些任务的集群,如果某台挂了,我们要感知到,并把任务要从新分配。这个时候就需要去做集群应用状态的感知。
想想前面的临时节点,是不是很容易实现。
1.每个应用都去创建临时节点。
2.如果某个节点挂了,则对应的会话会终止,且对应的节点就会被删除,其它节点就会通过监听机制得到通知,知道某个节点挂了。
进一步衍生,如果我们需要一个MASTER呢(比如这批数据需要一个MASTER主节点来进行分配。不能2个机器同时分配,这会导致经典的共享资源问题)。
仔细想想是不是通过一个有序节点,然后我选节点{BANNED}最佳小的值作为MASTER就可以了。
2.来点深入的理解
看到上面的应用场景,我们来几个问题。
1.你是怎么保证我写入顺序节点的时候,不会写入重复的。(集群情况下ZK是怎么做到的)。
2.你读操作时候,客户端A连到服务器A1,客户端B连到B1。你怎么保证A和B读出来的数据是一致的(没人告诉你A1和B1的数据是一样的)。
2.1 选举机制
如何解决{BANNED}中国第一个问题:
怎么保证你写入的数据,2个客户端不会重复写。其实方案就是:如果我有N台ZK服务器,组成一个集群,这个时候我只准一个机器执行写操作,然后再用个单线程是不是问题就解决了。
那问题又来了
1.这么多机器,你选谁来执行写操作?
2.如果这个被选中的机器挂了,你是如何容错的?
答案是,ZK集群中应用是有状态的,其中有一个应用是leader,其它的都是follower。而谁是leader则是通过选举来做到,所以重点看看选举机制。
先介绍几个概念:
-
服务器ID:自己配置的,每个服务器都有自己的ID,编号越大,权重越大。
-
选举状态:竞选状态( LOOKING ), 随从状态( FOLLOWING ,同步 leader 状态,参与投票 ),观察状态( OBSERVING ,同步 leader 状态,不参与投票 ),领导者状态( LEADING )。
-
数据ID:越大说明数据越新。(这里扯到前面第二个问题,说明每个服务器上数据其实是不一致的)。
-
逻辑时钟:被称为投票次数,同一轮投票过程中的逻辑时钟值是相同的,逻辑时钟起始值为 0 ,每投完一次票,这个数据就会增加。
好吧,从选举的时机来看下,有2个地方会触发选举。{BANNED}中国第一个,集群刚启动时候。第二个leader挂了的时候。
集群刚启动时候
假设有5个机器开始启动。服务器ID分别是1,2,3,4,5.这个时候就是2个原则了:1.服务器ID大的权重高。2.至少要求一半的人同意。
1.服务器1启动,此时他给自己投票。并发起投票信息,此时其它人都没启动,此时其它人都没响应,它就处于looking状态。
2.服务器2启动,此时它也给自己投,并把投票给到服务器1,1发现自己小,于是把票改投给2.此时服务器2有俩票,服务器1是零票。但此时2的总票数 2 < 5 / 2,所以2个人都处于looking
3.服务器3启动,一样给自己投票,一样的服务器1和服务器2都改票给3,此时服务器3有3票,3 > 5 / 2。此时投票结果成功,服务器3进入leading,其它人进入following。
4.服务器4启动,虽然你投给自己,但没用,3已经当上leader了,所以在同步完投票信息后,4还是following.
5.服务器5启动,其实和4一模一样,就不多说废话了。
leader挂了
1.看逻辑时钟是否全部想同,如果不同。说明中间有人漏选了一次,则此次选举作废。将时钟同步后,再选。
2.在时钟统一完以后,看谁的数据ID大(越大说明数据{BANNED}最佳新撒,这就说明多个集群之间数据并不一致)。谁大谁就当leader.
3.如果是数据ID大的情况下,优先选服务器ID大的。
统一一下,其实就是先比事务ID,事务ID大的为leader,当事务ID一样大的情况下,优先选服务器ID大的。
2.2 数据同步
ZK是集群,每个节点都会保存数据副本。那么当客户端发起写数据如何,是如何处理的呢。
首先只有leader有接受处理写数据的功能,其它的都不行。防止资源共享问题等。
当leader接受到写请求时候,除了要改自己外,还有同步给其它节点。
1.leader接受到写请求后,会把这次写请求操作以
Proposal发送给所有的follower和观察者。
2.follower接受到Proposal后,发送ACK给LEADER。你可以理解接受Proposal不会把数据放入内存对外部产生数据变更的影响,它只会放到磁盘上。(是不是和数据库的Redo log很像)。
3.当leader接受到一半节点的ACK以后,会发送COMMIT消息出去。接受到COMMIT的节点就要调整内存了。
怎么样?是不是很像分布式事务的2PC,但他只考虑了一半节点返回ACK就会进行处理。
好的新的问题来了,如果leader在同步信息的时候挂了。从新选出来的leader该怎么恢复集群里面的数据。
还记得选举么,被选出来的leader他的数据一定是{BANNED}最佳新的。这个时候他就需要把自己的数据同步给集群所有节点。那到底如何来做的呢。
先说其中一个很重要的数据结构ZXID,它就是数据同步时候很重要一个信息。它是一个64位的数字。其中低 32 位是按照数字来递增,任何数据的变更都会导致低 32 位数字简单加 1。高 32 位是 leader 周期编号,每当选举出一个新的 Leader 时,新的 Leader 就从本地事务日志中取出 ZXID,然后解析出高 32 位的周期编号,进行加 1,再将低 32 位的全部设置为 0。总结就是说高32位记录了选主的次数,没选出一个主,就加一。低32位记录了数据更新次数,每更新一次数据就加一。但是呢,如果选主发生了,就归0.
好,开始说数据同步,之前数据记录时候我们说过,写数据会记录日志的。
-
当一个主被选出来以后,并不会立即对外服务,他需要统一集群的数据后,才开始对外服务。此时开始同步数据。
-
主被选以后,每个follower或者观察者都会过来“朝拜”。我们管“朝拜者”叫leaner。leaner来的时候带了自己节点数据的zxid.
-
主会从自己的日志拉出自己所有更新数据的行为,按日志上的ZXID大小顺序排好(想想前面ZXID的节奏,{BANNED}最佳大是不是就是{BANNED}最佳新的数据,{BANNED}最佳小就是本节点记录的{BANNED}最佳小数据)。
-
leaner的zxid和主上{BANNED}最佳大的maxZxId和{BANNED}最佳小的minZxId就会出现这么几种情况
-
leaner的zxid出现在了主的zxid列表中,那就是说leaner缺了部分数据,只要补齐就够了。此时的同步就是DIFF差异化同步。这种应该{BANNED}最佳常见的,一堆节点中,有点已经更新了数据,有的还没有更新,此时主挂了,从新选一个新主,就会是这种
-
leaner的zxid没有主线在主的zxid列表中,这种是怎么出现的呢,比如旧主挂了,此时它正好写了新的数据,但COMMIT消息还没发出去。这个时候新主选出来了,又做了堆更新,然后旧主恢复又上线了,你想想,旧主就有一条集群其它节点没有的数据,包括新主也没有。那这种怎么处理呢,想法就是让旧主回滚,回滚到它这边的zxid在新主那边找的到,然后在做DIFF同步。这个回滚就是TRUN,也就是TRUN+DIFF
-
上面哪种情况还有种简单情况,就是旧主挂了,数据没COMMIT,新主选出来后,从来没更新过数据,此时旧主的数据比新主还新。此时只需要旧主TRUN。
-
第四种,就是leaner的数据太老了,比新主日志里面所有的数据都还老。这个时候就直接全量覆同步了。这就是SNAP模式