Zookeeper全文
面试题
选举机制
- 半数机制,超过半数的投票通过,即通过
- 启动时选举,当超过半数的设备上线,会将票投给id大的节点
- 后续选举,根据EPOCH/事务id/服务器id的优先级,大的为leader
生产集群下安装多少个zk
- 奇数台
- 假设有6台zk,挂了三台,整个集群宕机;5台zk,挂了三台,集群也是宕机;所以5台和6台没区别,5台节约资源
- 10台服务器-3台zk;20台-5台zk;100台以上-11台zk
- 服务器台数越多
- 可靠性高
- 由于半数机制,会产生更长的通信延时
常用命令
- ls/get/create/delete
算法
- Zookeeper如何保证数据一致性
- 拜占庭将军问题,所有将军必须全体一致,才能攻击某一支敌军,但是将军在地理位置上是分割开的,其中会有叛徒,产生一个不是所有将军都同意的决定,或者无法做出决定
Paxos算法
- 基于消息传递且具有高度容错性的一致性算法,解决快速正确的在一个分布式系统中对某个数据值达成一致,并保证不论发生任何异常,都不会破坏系统一致性
- 机器宕机
- 网络异常
- 所有节点,有三类角色,并且一个节点可以为多个角色
- Proposer 提议者,发起请求
- Acceptor 接受者,投票者
- Learner 学习者,执行命令,不参与投票
- 死锁情况,有多个Proposer通知提出,由于都不能达到半数Acceptor,那么会进行争抢Acceptor
改进,一次只有一个节点作为leader,能够发起提案
阶段
- 准备阶段,针对请求进行投票
- Proposer向多个Acceptor发出Propose请求Promise。Processer生成全局唯一且递增的Proposal ID,向所有Acceptor发送Propose请求,无需携带内容,只携带ProposalID
- Acceptor针对收到的Propose请求进行Promise投票。做两个承诺,一个应答。不再接受ProposalID小于等于当前请求的Propose请求;不再接受ProposalID小于当前请求的Accept请求;不违背以前作出的承诺下,回复已经Accept过的提案中最大的ProposalID的提案的Value和ProposalID,没有则返回null
- 接受阶段,真正发出请求
- Proposer收到多数(半数)Acceptro承诺的投票后,向Acceptro发出Propose请求。从应答中选择ProposalID最大的提案的Value,作为本次要发次的提案,如果所有应答的提案value为null,则自己决定提案,然后携带当前ProposalID,向所有Acceptor发送Propose请求
- Acceptor针对收到的Propose请求进行Accept,持久化当前ProposalID和value
- 学习阶段,Proposer将形成的决议发给其他Learners,Learners进行持久化
ZAB协议
- Paxos的改进算法,一次只有一个节点为leader,两种模式,消息广播/崩溃恢复
- Zookeeper采用ZAB,只有一台服务器提交了Proposal,要确保所有服务器都能提交
消息广播
- 两阶段提交
- 广播事务阶段
- 广播提交阶段
- 客户端写操作
- Leader服务器将对客户端的请求转换成事务Proposal提案,同时为每个Proposal分配一个全局id,zxis
- Leader服务器为每个Follower服务器分配一个单独的队列,将需要广播的Proposal依次加入队列中,根据FIFO策略进行消息发送
- Follower接收到Proposal后,会将其以事务日志的方式持久化到磁盘,成功后,给Leader返回一个ACK
- Leader接受超过半数以上的ACK,就认为消息发送成功,发送commit信息,同时自身完成事务提交
- Follower接收到commit信息后,进行事务提交
崩溃恢复
- 崩溃情况
- leader发起事务Proposal后就宕机,Follower都没有Proposal
- leader收到半数ACK后宕机,导致事务没有提交
- 两个要求
- 确保已经被Leader提交的Proposal,必须被所有Follower服务器提交
- 确保已经提出,但是没有被提交的Proposal,能被丢弃
- leader选举,新leader条件
- 新的leader不能包含未提交的Proposal,必须都是已经提交所有的Proposal的Follower节点
- 新选举的leader含有最大的事务id
- 数据恢复
- 正式开始工作之前,新leader确认事务日志中的所有Proposal是否已经被集群中过半的服务器commit
- 确保所有follower将所有事务同步,并加载进内存后,在会被leader加入可用的follower列表
CAP
- CAP,分布式系统应该满足
- Consitency,一致性,多个副本之间能够保持数据一致
- Available,可用性,系统服务一直处于可用状态,zk需要半数才能正常服务
- Partition Tolarance,分区容错性,遇到任何分区故障,都需要对外提供满足一致性和可用性的服务
- zk保证CP
- 不能保证每次服务请求的可用性,极端条件,可能会丢弃一些请求,客户端需要重新提交请求
- 进行leader选举时,集群不可用
源码
持久化
- Leader和Follower的数据会在内存和磁盘中各保存一份
- 内存数据先将操作更新到磁盘日志中
- 日志操作达到一定数量后(参数10W,过半随机机制,大于5W的某个值,避免集群中的zk同一时间生成快照),会将内存数据序列化一次,生成快照文件
- 每次重启后都会加载日志和快照
SnapShot
接口定义了磁盘快照的功能TxnLog
接口定义了日志的功能
序列化源码
- 不同节点的数据交互涉及序列化和反序列化
Record
接口定义了序列化和反序列化的行为OutputArchive
和InputArchive
定义了输入输出的格式
服务端初始化
- 启动流程
脚本
zkServer.sh
启动脚本,涉及启动类org.apache.zookeeper.server.quorum.QuorumPeerMain
参数解析
- 创建对象
QuorumPeerMain main = new QuorumPeerMain();
- 初始化
main.initializeAndRun(args);
,其中config.parse(args[0]);
解析命令行参数,其实也就是解析zoo.cfg
- 通过IO流获取参数,解析配置
parseProperties(cfg);
- for循环遍历kv值,进行赋值
- 进一步解析,
setupQuorumPeerConfig(zkProp, true);
,其中设置serveridsetupMyId();
,通过myid
文件读取
过期快照删除
- 创建清理管理
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(
config.getDataDir(), //数据路径
config.getDataLogDir(), //日志路径
config.getSnapRetainCount(), //最少保留快照数,默认3
config.getPurgeInterval()); //是否开启自动过期清理,默认0关闭
- 启动清理线程,
purgeMgr.start();
,开启了定时任务,其中的任务线程的run
方法调用PurgeTxnLog.purge(logsDir, snapsDir, snapRetainCount);
- 根据日志和现有的快照进行清理过期快照,
purgeOlderSnapshots(txnLog, snaps.get(numSnaps - 1));
- 配置小于等于0,不进行清理
通信初始化并启动
- 准备启动
runFromConfig(config);
- 创建通信工厂
cnxnFactory = ServerCnxnFactory.createFactory();
- 默认创建
NIOServerCnxnFactory
,serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
- 如果是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();
加载数据
数据结构
- 内存中为文件式的树结构,集群中的数据保持同步
- 磁盘数据包括日志和快照,写操作时,先将操作持久化到日志,关闭时再将日志持久化到快照
流程
- 加载数据
loadDataBase();
,调用long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
- 恢复快照,将快照恢复到内存,
snapLog.deserialize(dt, sessions);
- 加载编辑日志
long highestZxid = fastForwardFromEdits(dt, sessions, listener);
- 恢复快照,通过迭代恢复节点
- 加载编辑日志,根据事务id开始迭代遍历日志,根据日志操作进行不同调用
选举
- 单个节点的投票结构
- 需要两个类,一个类负责选举算法,一个类负责对外投票和接受票
选举准备
- 开始选举
startLeaderElection();
- 如果状态为
getPeerState() == ServerState.LOOKING
,准备选票currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
,包装三个值
- 创建投票通信组件
QuorumCnxManager qcm = createCnxnManager();
- 创建接受队列
this.recvQueue
- 创建发送队列
this.queueSendMap
,是一个map,对每一个节点都有对应的发送队列- 创建通信组件
this.senderWorkerMap
- 启动监听器,
QuorumCnxManager.Listener listener = qcm.listener;
继承了线程,启动后客户端一直接收消息,
client = ss.accept();
- 创建投票管理器,并启动
FastLeaderElection fle = new FastLeaderElection(this, qcm);
- 创建该节点的投票发送队列
sendqueue
- 创建该节点的投票接受队列
recvqueue
开始选举
- 调用
super.start()
,其实是调用了QuorumPeer
的run()
- 如果当前节点状态为looking,更新当前选票
setCurrentVote(makeLEStrategy().lookForLeader());
- 开始选举
makeLEStrategy().lookForLeader()
- 更新选票,
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
- 进行发送通知,提示其他节点要进行投票,
sendNotifications();
,创建ToSend
,加入发送队列sendqueue.offer(notmsg);
- 由
WorkerSender
往外发,最终调用manager.toSend(m.sid, requestBuffer);
- 如果要发送的myid是自己,直接添加到接受队列,
addToRecvQueue(new Message(b.duplicate(), sid));
- 如果不是,发给其他节点,添加到发送队列,连接目标节点
connectOne(sid);
,初始化nio连接,并创建输入输出流,启动sendWork/recvWork
,通过nio读写缓存- 此时如果目标sid大于自己的id,就不发这个通知了,也不需要建立连接;大于目标节点发送后,对方节点后进行投票
- 先收到其他节点的投票通知,再进行投票
- 通过
WorkerReceiver
进行接受信息
- 如果是个其他节点正在需要投票的信息,就发送投票
- 如果是是票,那就放入自己的
recvqueue
状态同步
- 保证数据一致
- DIFF一样,不需要同步
- TRUNC follower的zxid比leader的大,Follower回滚
- COMMIT leader的zxid比follower的zxid大,发送Proposal给follower提交执行
- follower没有任何数据,使用SNAP的方式进行数据同步
leader
- 调用
leader.lead();
,创建一个cnxAcceptor = new LearnerCnxAcceptor();
,进行数据接受,并启动,阻塞接受followers = ss.accept();
- 接收到通道的信息,为每一个节点创建处理器
LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
- 接收消息
ia.readRecord(qp, "packet");
- 创建新的Epoch,包装成
newEpochPacket
并返回给follower,并等待应答- 收到ack后,进行数据同步
- 获取数据同步模式,其中会根据事务id进行操作
boolean needSnap = syncFollower(peerLastZxid, leader.zk.getZKDatabase(), leader);
,如果需要快照同步,进行快照同步操作
- 如果follower事务比leader小,提交事务提案
queueCommittedProposals
,最后进行提交操作queueOpPacket(Leader.COMMIT, packetZxid);
,
follower
- 调用
follower.followLeader();
,通过保存的投票信息,找到leaderQuorumServer leaderServer = findLeader();
socket
连接leader节点connectToLeader(leaderServer.addr, leaderServer.hostname);
- 注册进leader
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
- 往通道里写,并等待leader返回新的Epoch
- 读到数据后,进行应答
QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
- 注册好之后,一直读数据和处理数据
processPacket(qp);
- 当前收到
commit
时,进行事务提交
服务端leader启动
- 数据同步完成后,leader启动
代码逻辑
QuorumPeer
根据节点角色调用leader.lead();
,同步完数据后,startZkServer();
- 设置请求处理器,
setupRequestProcessors();
,创建并启动,((PrepRequestProcessor)firstProcessor).start();
,循环接受请求并根据请求类型处理pRequest(request);
服务端follower启动
代码逻辑
QuorumPeer
根据节点角色调用follower.followLeader();
,找到leader,连接后并注册,循环读取数据- 处理数据
processPacket(qp);
,根据信息类别进行处理
客户端初始化
- 脚本文件,启动
org.apache.zookeeper.ZooKeeperMain
创建
ZooKeeperMain main = new ZooKeeperMain(args);
- 连接服务端
connectToZK(cl.getOption("server"));
- 创建
zk = new ZooKeeperAdmin(host, Integer.parseInt(cl.getOption("timeout")), new MyWatcher(), readOnly);
- 设置监听器
watchManager.defaultWatcher = watcher;
- 解析服务器地址
ConnectStringParser connectStringParser = new ConnectStringParser(connectString);
,也说明连接中不能有空格,用逗号分隔- 创建连接
createConnection
,创建sendThread/eventThread
,并启动
sendThread
,通过NIO连接服务端,处理应答信息clientCnxnSocket.doTransport
,遍历selectKey
事件,doIO(pendingQueue, cnxn);
进行应答数据处理eventThread
,处理watch
事件,和其他事件回调
启动
main.run();
,循环执行executeLine(line);
,也就是处理控制台输入的命令