ZooKeeper:架构和算法
ZooKeeper主要用来解决分布式应用场景中存在的一些问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置管理等。
它支持Standalone模式和分布式模式,在分布式模式下,能够为分布式应用提供高性能和可靠地协调服务,而且使用ZooKeeper可以大大简化分布式协调服务的实现,为开发分布式应用极大地降低了成本。
总体架构
ZooKeeper分布式协调服务框架的总体架构,如图所示:
ZooKeeper集群由一组Server节点组成,这一组Server节点中存在一个角色为Leader的节点,其他节点都为Follower。
- 写请求:写请求会序列化,每个写请求转发给leader通过ZAB协议达到一致性和顺序性(所有节点更新顺序一致)。
- 读请求:与chubby不同,chubby所有读写请求必须转发给leader,在zookeeper中,所有的server(leader和follower)都可以响应读请求。这样会带来不一致的问题,zab协议还没有把写消息同步到所有节点上。zookeeper可以通过sync调用(怎么实现的?)强制同步。
- zxid编号:每次读写,zookeeper都会返回一个zxid编号,zookeeper保证返回的数据不会比客户端传过来的zxid编号更新
- 模糊快照和日志:zookeeper周期性地将内存中的数据保存在磁盘中形成模糊快照(模糊的含义是不一定是最新的),zookeeper将更新操作都会先写入磁盘。zookeeper保证更新操作都是“幂等的”,所以可以通过模糊快照和日志恢复内存数据。
数据模型
zookeeper通过树形命名空间来进行整个系统的协调,其中的每个节点称为znode,znode有两种持久节点和临时节点。临时节点和会话绑定,会话下线临时节点删除(心跳监控是否宕机)
zookeeper的全部数据都保存在内存中,以此来实现高吞吐,低延迟。
API
//创建节点,flag可以指定递增,临时节点
string create(path, data, acl, flags)
//删除节点
void delete(path, expectedVerstion)
//改
stat setData(path, data, expectedVersion)
//查
(data, stat) getData(path, watch)
//存在
stat exist(path, watch)
//获取孩子节点
String[] getChildren(path, watch)
//同步
void sync(path)
所有有关查的api(3个),可以设置监视器,状态发生变化可以通知
主要功能
领导者选举
在一个特定的目录下,创建一个leader临时节点。热备机周期性地检查这个节点是否存在,如果不存在,就尝试在这个目录下创建leader节点,升级为leader。
(data, stat) = zookeeper.get(handle, "/services/myservice/leader",true);
if(stat == none)
path = zookeeper.create(handle, "/services/myservice/leader", 临时节点)
if(path = none)//失败,获取当前leader
(data, stat) = zookeeper.get(handle, "/services/myservice/leader",true);
else
成功,自己是leader
配置管理(pub-sub服务)
创建特定的节点,用于管理集群的配置信息。所有客户端看见的都是一致的内容,并且可以设置watch在节点内容变更的时候通知客户端。
注意:不要使用zookeeper同步大块数据,进行会急剧下降。会走一致性协议同步,写磁盘,然后在反应在内存中。
成员管理
成员管理的目的就是,集群内某个节点宕机或者新加入节点,zookeeper可以及时感知。实现方式是为每台机器创建临时节点,临时节点和会话绑定。同时,监控客户端可以通过getChildren设置watch,临时节点有变更可以及时通知。
任务分配
- 新任务到来:在tasks目录下创建任务节点
- 任务分配:监控进程感知到tasks目录下的节点变化,触发任务分配。图中task1分配到machine1的目录下,代表task1分配给machine1
- 任务执行:machine1发现自己目录下的任务,执行该任务。结束后删除machine1目录下的task1和task目录下的task1。
锁服务
创建互斥锁
- 在l目录下,创建以lock为前缀的递增临时节点,返回节点编号
- 获取l目录下所有节点信息
- 如果刚刚创建的编号是所有节点编号最小的(最先创建节点的),获得锁
- 没有获得锁,进入第2步
创建读写锁
创建写锁
- 在l目录下,创建以write为前缀的递增临时节点,返回节点编号
- 获取l目录下所有节点信息
- 如果刚刚创建的编号是所有节点编号最小的(最先创建节点的),获得锁
- 没有获得锁,进入第2步
创建读锁
- 在l目录下,创建以read为前缀的递增临时节点,返回节点编号
- 获取l目录下所有节点信息
- 如果刚刚创建的编号比所有write前缀的编号小,获得读锁
- 没有获得锁,进入第2步
释放锁
删除临时节点
双向路障同步
任务统一开始
- 在路障节点下创建当前任务节点,代表该任务到达路障
- 检测路障节点下任务个数是否足够,足够代表可以继续统一执行
任务统一离开
- 任务结束后,任务路障节点下的任务节点
- 如果路障节点下没有任务节点,代表所有任务已经执行结束
ZAB协议(ZooKeeper Atomic Broadcast原子消息广播协议)
zab协议所有事务请求必须由leader协调,首先leader发起proposal消息,大多数server同意后,然后leader发送commit消息。
zxid编号
- 低32位为计数器,客户端每次请求+1
- 高32位为epochID,每次选举新leader+1
状态和阶段
- Looking:系统刚启动时或者Leader崩溃后正处于选举状态
- Following:Follower节点所处的状态,Follower与Leader处于数据同步阶段
- Leading:Leader所处状态,当前集群中有一个Leader为主进程
zookeeper主要分为5个阶段,选举,发现,同步,广播
- 选举:Looking状态中选举出Leader节点,Leader的lastZXID总是最新的
- 发现:Follower节点向准Leader推送FOllOWERINFO,该信息中包含了上一周期的epoch,接受准Leader的NEWLEADER指令,检查newEpoch有效性,准Leader要确保Follower的epoch与ZXID小于或等于自身的
- 同步:将Follower与Leader的数据进行同步,由Leader发起同步指令,最总保持集群数据的一致性
- 广播:Leader广播Proposal与Commit,Follower接受Proposal与Commit;
选举
zab必须确保选举出来的leader具有最大的zxid(这和raft很像啊)。这里要注意一下,和raft一样,选举可能会出现两种结果:
- 上一轮的多数派(此时leader zxid可能不是最大的,需要同步的时候调用trunc)
- 上一轮的少数派,leader zxid真的是最大的(但是这个最大的zxid没有提交,在同步阶段提交?)
过程详解:
- 每个Follower都向其他节点发送选自身为Leader的Vote投票请求,等待回复;
- Follower接受到的Vote如果比自己的ZXID更新时则投票,并更新自身的Vote,否则拒绝投票;
- 每个Follower中维护着一个投票记录表,当某个节点收到过半的投票时,结束投票并把该Follower选为Leader,投票结束;
恢复(发现和同步)
发现
- leader生成新的zxid和epoch,接受follower发送来的FOllOWERINFO(含有当前节点的LastZXID)
- leader向follower发送NEWLEADER;Leader根据follower发送过来的LastZXID根据数据更新策略向Follower发送更新指令;
同步
- SNAP:如果follower数据太老,epoch还在上上一轮,leader将发送快照snap指令给follower同步
- DIFF:正常同步阶段
- TRUNC:如果Follower是上一轮的少数派(通过对比ztid),发送TRUNC指令让follower丢弃这段数据
广播
进入正常leader提交阶段,产生递增的zxid,接受半数投票后,再提交。
与raft的区别
我觉得zab和raft没有本质的区别,它们唯一的不同点就是如何处理上一个leader的残留日志。
- raft中,需要本轮iterm有日志提交后,才提交以前的
- zab中,在同步阶段全部解决