Java-第十九部分-Zookeeper-面试题、算法和源码

170 阅读10分钟

Zookeeper全文

面试题

选举机制

  • 半数机制,超过半数的投票通过,即通过
  • 启动时选举,当超过半数的设备上线,会将票投给id大的节点
  • 后续选举,根据EPOCH/事务id/服务器id的优先级,大的为leader

生产集群下安装多少个zk

  • 奇数台
  1. 假设有6台zk,挂了三台,整个集群宕机;5台zk,挂了三台,集群也是宕机;所以5台和6台没区别,5台节约资源
  • 10台服务器-3台zk;20台-5台zk;100台以上-11台zk
  • 服务器台数越多
  1. 可靠性高
  2. 由于半数机制,会产生更长的通信延时

常用命令

  • ls/get/create/delete

算法

  • Zookeeper如何保证数据一致性
  1. 拜占庭将军问题,所有将军必须全体一致,才能攻击某一支敌军,但是将军在地理位置上是分割开的,其中会有叛徒,产生一个不是所有将军都同意的决定,或者无法做出决定

Paxos算法

  • 基于消息传递且具有高度容错性的一致性算法,解决快速正确的在一个分布式系统中对某个数据值达成一致,并保证不论发生任何异常,都不会破坏系统一致性
  1. 机器宕机
  2. 网络异常
  • 所有节点,有三类角色,并且一个节点可以为多个角色
  1. Proposer 提议者,发起请求
  2. Acceptor 接受者,投票者
  3. Learner 学习者,执行命令,不参与投票
  • 死锁情况,有多个Proposer通知提出,由于都不能达到半数Acceptor,那么会进行争抢Acceptor

改进,一次只有一个节点作为leader,能够发起提案

阶段

  • 准备阶段,针对请求进行投票
  1. Proposer向多个Acceptor发出Propose请求Promise。Processer生成全局唯一且递增的Proposal ID,向所有Acceptor发送Propose请求,无需携带内容,只携带ProposalID
  2. Acceptor针对收到的Propose请求进行Promise投票。做两个承诺,一个应答。不再接受ProposalID小于等于当前请求的Propose请求;不再接受ProposalID小于当前请求的Accept请求;不违背以前作出的承诺下,回复已经Accept过的提案中最大的ProposalID的提案的Value和ProposalID,没有则返回null
  • 接受阶段,真正发出请求
  1. Proposer收到多数(半数)Acceptro承诺的投票后,向Acceptro发出Propose请求。从应答中选择ProposalID最大的提案的Value,作为本次要发次的提案,如果所有应答的提案value为null,则自己决定提案,然后携带当前ProposalID,向所有Acceptor发送Propose请求
  2. Acceptor针对收到的Propose请求进行Accept,持久化当前ProposalID和value
  • 学习阶段,Proposer将形成的决议发给其他Learners,Learners进行持久化

ZAB协议

  • Paxos的改进算法,一次只有一个节点为leader,两种模式,消息广播/崩溃恢复
  • Zookeeper采用ZAB,只有一台服务器提交了Proposal,要确保所有服务器都能提交

消息广播

  • 两阶段提交
  1. 广播事务阶段
  2. 广播提交阶段
  • 客户端写操作
  • Leader服务器将对客户端的请求转换成事务Proposal提案,同时为每个Proposal分配一个全局id,zxis
  • Leader服务器为每个Follower服务器分配一个单独的队列,将需要广播的Proposal依次加入队列中,根据FIFO策略进行消息发送
  • Follower接收到Proposal后,会将其以事务日志的方式持久化到磁盘,成功后,给Leader返回一个ACK
  • Leader接受超过半数以上的ACK,就认为消息发送成功,发送commit信息,同时自身完成事务提交
  • Follower接收到commit信息后,进行事务提交

崩溃恢复

  • 崩溃情况
  1. leader发起事务Proposal后就宕机,Follower都没有Proposal
  2. leader收到半数ACK后宕机,导致事务没有提交
  • 两个要求
  1. 确保已经被Leader提交的Proposal,必须被所有Follower服务器提交
  2. 确保已经提出,但是没有被提交的Proposal,能被丢弃
  • leader选举,新leader条件
  1. 新的leader不能包含未提交的Proposal,必须都是已经提交所有的Proposal的Follower节点
  2. 新选举的leader含有最大的事务id
  • 数据恢复
  1. 正式开始工作之前,新leader确认事务日志中的所有Proposal是否已经被集群中过半的服务器commit
  2. 确保所有follower将所有事务同步,并加载进内存后,在会被leader加入可用的follower列表

CAP

  • CAP,分布式系统应该满足
  1. Consitency,一致性,多个副本之间能够保持数据一致
  2. Available,可用性,系统服务一直处于可用状态,zk需要半数才能正常服务
  3. Partition Tolarance,分区容错性,遇到任何分区故障,都需要对外提供满足一致性和可用性的服务
  • zk保证CP
  1. 不能保证每次服务请求的可用性,极端条件,可能会丢弃一些请求,客户端需要重新提交请求
  2. 进行leader选举时,集群不可用

源码

持久化

  • Leader和Follower的数据会在内存和磁盘中各保存一份
  1. 内存数据先将操作更新到磁盘日志中
  2. 日志操作达到一定数量后(参数10W,过半随机机制,大于5W的某个值,避免集群中的zk同一时间生成快照),会将内存数据序列化一次,生成快照文件
  3. 每次重启后都会加载日志和快照 image.png
  • SnapShot接口定义了磁盘快照的功能
  • TxnLog接口定义了日志的功能

序列化源码

  • 不同节点的数据交互涉及序列化和反序列化 image.png
  • Record接口定义了序列化和反序列化的行为
  • OutputArchiveInputArchive定义了输入输出的格式

服务端初始化

  • 启动流程 image.png

脚本

  • zkServer.sh启动脚本,涉及启动类org.apache.zookeeper.server.quorum.QuorumPeerMain image.png

参数解析

  • 创建对象QuorumPeerMain main = new QuorumPeerMain();
  • 初始化main.initializeAndRun(args);,其中config.parse(args[0]);解析命令行参数,其实也就是解析zoo.cfg
  • 通过IO流获取参数,解析配置parseProperties(cfg);
  • for循环遍历kv值,进行赋值 image.png
  • 进一步解析,setupQuorumPeerConfig(zkProp, true);,其中设置serveridsetupMyId();,通过myid文件读取 image.png

过期快照删除

  • 创建清理管理
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(
        config.getDataDir(),  //数据路径
        config.getDataLogDir(),  //日志路径
        config.getSnapRetainCount(),  //最少保留快照数,默认3
        config.getPurgeInterval()); //是否开启自动过期清理,默认0关闭
  • 启动清理线程,purgeMgr.start();,开启了定时任务,其中的任务线程的run方法调用PurgeTxnLog.purge(logsDir, snapsDir, snapRetainCount);
  1. 根据日志和现有的快照进行清理过期快照,purgeOlderSnapshots(txnLog, snaps.get(numSnaps - 1));
  • 配置小于等于0,不进行清理 image.png

通信初始化并启动

  • 准备启动runFromConfig(config);
  • 创建通信工厂cnxnFactory = ServerCnxnFactory.createFactory();
  1. 默认创建NIOServerCnxnFactoryserverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
  2. 如果是TLS通信,那么就是NettyServerCnxnFactory
  • 启动NIO服务,cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);
  • 绑定端口ss.socket().bind(addr);

this.ss = ServerSocketChannel.open();,socket也就是ServerSocketChannel

  • QuorumPeer赋值,并启动

QuorumPeer继承了线程,启动的是一个线程的start方法

quorumPeer.start();
// 阻塞其他线程
quorumPeer.join();

加载数据

数据结构

  • 内存中为文件式的树结构,集群中的数据保持同步
  • 磁盘数据包括日志和快照,写操作时,先将操作持久化到日志,关闭时再将日志持久化到快照 image.png

流程

image.png

  • 加载数据loadDataBase();,调用long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
  1. 恢复快照,将快照恢复到内存,snapLog.deserialize(dt, sessions);
  2. 加载编辑日志long highestZxid = fastForwardFromEdits(dt, sessions, listener);
  • 恢复快照,通过迭代恢复节点 image.png
  • 加载编辑日志,根据事务id开始迭代遍历日志,根据日志操作进行不同调用 image.png

选举

  • 单个节点的投票结构
  1. 需要两个类,一个类负责选举算法,一个类负责对外投票和接受票 image.png

选举准备

image.png

  • 开始选举startLeaderElection();
  1. 如果状态为getPeerState() == ServerState.LOOKING,准备选票currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());,包装三个值
  • 创建投票通信组件QuorumCnxManager qcm = createCnxnManager();
  1. 创建接受队列this.recvQueue
  2. 创建发送队列this.queueSendMap,是一个map,对每一个节点都有对应的发送队列
  3. 创建通信组件this.senderWorkerMap
  • 启动监听器,QuorumCnxManager.Listener listener = qcm.listener;

继承了线程,启动后客户端一直接收消息,client = ss.accept();

  • 创建投票管理器,并启动FastLeaderElection fle = new FastLeaderElection(this, qcm);
  1. 创建该节点的投票发送队列sendqueue
  2. 创建该节点的投票接受队列recvqueue

开始选举

image.png

  • 调用super.start(),其实是调用了QuorumPeerrun()
  1. 如果当前节点状态为looking,更新当前选票setCurrentVote(makeLEStrategy().lookForLeader());
  • 开始选举makeLEStrategy().lookForLeader()
  1. 更新选票,updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
  2. 进行发送通知,提示其他节点要进行投票,sendNotifications();,创建ToSend,加入发送队列sendqueue.offer(notmsg);
  • WorkerSender往外发,最终调用manager.toSend(m.sid, requestBuffer);
  1. 如果要发送的myid是自己,直接添加到接受队列,addToRecvQueue(new Message(b.duplicate(), sid));
  2. 如果不是,发给其他节点,添加到发送队列,连接目标节点connectOne(sid);,初始化nio连接,并创建输入输出流,启动sendWork/recvWork,通过nio读写缓存
  3. 此时如果目标sid大于自己的id,就不发这个通知了,也不需要建立连接;大于目标节点发送后,对方节点后进行投票
  4. 先收到其他节点的投票通知,再进行投票
  • 通过WorkerReceiver进行接受信息
  1. 如果是个其他节点正在需要投票的信息,就发送投票
  2. 如果是是票,那就放入自己的recvqueue

状态同步

  • 保证数据一致
  1. DIFF一样,不需要同步
  2. TRUNC follower的zxid比leader的大,Follower回滚
  3. COMMIT leader的zxid比follower的zxid大,发送Proposal给follower提交执行
  4. follower没有任何数据,使用SNAP的方式进行数据同步 image.png

leader

  • 调用leader.lead();,创建一个cnxAcceptor = new LearnerCnxAcceptor();,进行数据接受,并启动,阻塞接受followers = ss.accept();
  • 接收到通道的信息,为每一个节点创建处理器LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
  1. 接收消息ia.readRecord(qp, "packet");
  2. 创建新的Epoch,包装成newEpochPacket并返回给follower,并等待应答
  3. 收到ack后,进行数据同步
  • 获取数据同步模式,其中会根据事务id进行操作boolean needSnap = syncFollower(peerLastZxid, leader.zk.getZKDatabase(), leader);,如果需要快照同步,进行快照同步操作
  1. 如果follower事务比leader小,提交事务提案queueCommittedProposals,最后进行提交操作queueOpPacket(Leader.COMMIT, packetZxid);

follower

  • 调用follower.followLeader();,通过保存的投票信息,找到leaderQuorumServer leaderServer = findLeader();
  • socket连接leader节点connectToLeader(leaderServer.addr, leaderServer.hostname);
  • 注册进leaderlong newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
  1. 往通道里写,并等待leader返回新的Epoch
  2. 读到数据后,进行应答QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
  • 注册好之后,一直读数据和处理数据processPacket(qp);
  1. 当前收到commit时,进行事务提交

服务端leader启动

  • 数据同步完成后,leader启动 image.png

代码逻辑

  • QuorumPeer根据节点角色调用leader.lead();,同步完数据后,startZkServer();
  • 设置请求处理器,setupRequestProcessors();,创建并启动,((PrepRequestProcessor)firstProcessor).start();,循环接受请求并根据请求类型处理pRequest(request); image.png

服务端follower启动

image.png

代码逻辑

  • QuorumPeer根据节点角色调用follower.followLeader();,找到leader,连接后并注册,循环读取数据
  • 处理数据processPacket(qp);,根据信息类别进行处理 image.png

客户端初始化

image.png

  • 脚本文件,启动org.apache.zookeeper.ZooKeeperMain image.png

创建

  • ZooKeeperMain main = new ZooKeeperMain(args);
  • 连接服务端connectToZK(cl.getOption("server"));
  • 创建zk = new ZooKeeperAdmin(host, Integer.parseInt(cl.getOption("timeout")), new MyWatcher(), readOnly);
  1. 设置监听器watchManager.defaultWatcher = watcher;
  2. 解析服务器地址ConnectStringParser connectStringParser = new ConnectStringParser(connectString);,也说明连接中不能有空格,用逗号分隔
  3. 创建连接createConnection,创建sendThread/eventThread,并启动
  • sendThread,通过NIO连接服务端,处理应答信息clientCnxnSocket.doTransport,遍历selectKey事件,doIO(pendingQueue, cnxn);进行应答数据处理
  • eventThread,处理watch事件,和其他事件回调

启动

  • main.run();,循环执行executeLine(line);,也就是处理控制台输入的命令 image.png