#
连接
客户端
- 客户端与服务端是长连接,以心跳维持不断开.如果连接断开,将轮询使用ServerList中下一个
如果将加入权重比,可以考虑使用重复Server地址,但如果权重高Server有问题会增加切换耗时 - 写入任意一个节点,都将转发给leader写入
- 读取时将提交上次的最后一个ZXID,服务端节点如果自身在ZXID之前将拒绝连接
这称为统一视图模型,依次来保证客户端见证数据的一致性 - 不保证所有客户端在某一时刻看到的数据是全部一致的(有延时)
但保证客户端最终会看到后续一致(最终一致)
并且保证对同一个客户端看到的是始终一致的
心跳过程
- 客户端与服务端会互相发送心跳
- 服务端会根据客户端的心跳重置下一次Session_Timeout,以此类推
所以正常情况下服务端会一直持有客户端Session - 如果网络出现问题,客户端将试图在重连,重连时将以Session_Timeout为准
如果ST时间内重连成功,即视为无缝迁移,此时将出现connection_lost并很快恢复
如果ST时间内没有重连成功,之后就算重连也会出现sesion_expired并拒绝连接
事件通知
事件类型
节点级
- NodeCreated
- NodeDeleted
- NodeDataChange
- NodeChildrenchanged
状态级 - KeeperState.Disconnected
- KeeperState.SyncConnected
- KeeperState.AuthFailed
- KeeperState.Expired
Watcher类型
- DataWatcher
- ExistsWatcher
- ChildWatcher
具体触发过程
- exist
- getChildren
- getData
选举
选举状态
- leading 选举完毕时leader状态
leading节点会向所有following节点广播自己的事务确保follow与自己一致 - following 选举完毕时following状态
following节点将定时从leader检测,同步leader事务确保自己与leader一致 - looking 寻址leader(未选举)
- observe 只同步状态,不参与投票
选举过程
- 一开始都是looking状态
looking状态下的节点会向所有节点发起投票,固定选自己 - 节点会响应其它投票结果,发回自己的投票
leading和following会提交自己的选票,此时表示选主已成,选票就是leader
looking节点依然是固定选自己 - 每个节点都会汇总自己的选票并进行合并(时钟向量)
如果ZXID比自己大就会使用新选票,如果相同则比较NodeID大,否则使用自己的选票 - 节点汇总选票如果发现某个节点已占据大多数,即视对方为leader退出looking
最终leader会指向当前持有ZXID最大的节点
如果leader指向自己则进入leader,否则进入following - 新加入节点为looking,也会发起选票,但因为此时有主会收到大量leader指向就可以确认leader
- leader和follower都会保持心跳并维持轮询检测线程
leader如果检测自己的心跳follower不足一半会立即退出leader进入looking
follower如果检测自己的心跳leader超时也会立即退出follower进入looking
恢复过程
- 恢复过程是新leader正式上位的过程,恢复的目标是保持所有节点与新leader保持一致
follower会向新leader发送自己的ZXID,leader会响应并发送leader自己的ZXID
在这个过程会锁住leader(仅仅是计算差异的时候锁住,数据同步不在此列) - leader会根据follower的ZXID确定差异,并向该follower发送同步数据
如果follower自身超过了leader,会删掉回滚超过的部分(严格与leader一致)
同步完毕后follower会向leader报告自己的同步进度 - leader会判断如果超过半数的follower同步完毕,则视为同步完成,正式对外服务
- 新节点的加入也会遵循该过程
所以一个新节点加入,会极大的增大leader的同步工作量 - 数据由leader发向follower
但follower自身会判定自己当前leader是否是该节点,如果不是会拒绝请求
与Raft的差异对比
- 投票
Raft投票是谁先投票就可能先获得投票,但网络不同步可能造成投票分割
Raft的做法是设置随机超时时间,然后超时时间短的更容易获得投票
ZK的做法是投ZXID+NodeID,好处是必然可以统一且自带指向最新节点
坏处是理论讲ZK可能需要不止一轮选举才能最终选出 - 识别leader
Raft中leader会向新节点发出RPC要求加入,新节点可能会加入老leader后才加入新leader
ZK做法是发起投票,由集群大多数告知leader是谁而不会加入老leader - 选主检测
Raft只由follower检测,也就是说leader本身不知道地位丢失,而必须由新轮次新leader告知
ZK由leader和follower各自检测 - 事务ID
Raft的事务ID是连续递增的(轮次不体现在事务中),连续则表示任意节点的相同ID是同一条事务
所以同步时消耗更少,可以批量提交.并且只需要比对最后数字使用相同即可
ZK的事务ID只是趋势递增,中间有断行(递增是每个leader的私有概念,同ID不一定是同事务)
所以计算差异时需要读锁住然后计算差异逐个比对是否需要同步 - 残留数据恢复
Raft的残留数据必须过半,过半则要求全体同步
ZK则是只有leader上有则全体同步
与Paxos差异对比
依靠选主
来简化大多数
的繁复和性能低下
- 由leader来负责操作,而如何保证大多数由leader负责
这样数据先经过大多数确认,就可以在使用时不必再走一次大多数 - 如何选主则依旧由大多数决定
读写一致性
写入
- 写入过程可以由任意节点接受,但最终会转向leader处理
- leader写入类似2PC
leader节点负责生成唯一递增ZXID,然后广播要求follower同步
与2PC不同的是不要求全部follower确认,只需要超过半数确认即视为成功,同时提交事务
读取
- 读取过程可以由任意节点处理
- 任意节点的事务节奏可以不一致,即可能造成数据有延时,为此引入统一视图模型
即客户端会提交自己的最终ZXID,而接受连接的节点如果发现ZXID比自己大则拒绝连接
以此保证客户端所见数据的一致性
通知机制
ZK通知机制可以理解为一个分布式的观察者模型
服务端
事件Event可以被多个Watcher处理(触发多个事件处理),但事件对象只传递一次
- 以PATH为单位接受客户端感兴趣的注册
- 服务端以
PATH->Set<Watcher>
的方式存储
服务端Watcher是客户端通讯接口,也就是说每一个客户端节点多次注册只会产生一个 - 服务端处理Node时,如果发现该PATH有watcher会调用触发
执行触发是先从PATH中remove后process,也就是无论成功与否,只触发一次 - Watcher仅存放于服务端内存中不会持久化,也就是说重启会丢失
客户端
客户端接受到Event,将在客户端进行重组
- Event只有PATH,事件类型等属性,只有一个但可能触发多个,所以在客户端会从注册时生成的Watcher组里取出重组来形成一对多的Event-Watcher组来进行具体触发
- 重组是有序的
Event-Watcher组是按照Event的事务ID进行排序(也就是服务端执行顺序) - 重组是串行的
重组的结果是加入一个队列然后由一个线程轮询处理(所以必须注意Watcher处理中耗时)
注意点
只通知一次
- 重复监听的实现手段是重复注册,但再次注册的来回中间变更将不会触发事件
- 通知事件不等于变更数量
也就是假设数据变更了N次,可能只收到一次通知 - 通知不可靠,即不确认到达
服务端因为是先移除后通知,所以通知失败也不会再次通知
比如网络中断,客户端断线重连等等,都会造成通知丢失 - 断线重连如果成功,客户端会自动再次提交注册所以的Watcher
但对一个未创建节点监听exits例外 - exits监听
如果目标节点不存在,那么在断线重连阶段该节点被创建后删除,将无法感知
因为在断线重连的时候,客户端会重提交所有Watcher和ZXID等,如果服务端判断客户端的ZXID更早且之后有新的数据变动会立即触发Watcher
但Exits不行,因为这个感知判断ZXID是以节点为单位进行的