Zookeeper服务端集群模式启动流程原理。
一,Zookeeper服务端的三种启动模式
1,standalone,单机模式。
2,伪分布式模式。
3,分布式模式(集群模式)。
不管哪种模式,启动类都是QuorumPeerMain.class。今天我们讨论第三种:集群启动流程。
二,集群模式启动流程
第一步:启动类QuorumPeerMain。
无论是单机模式还是集群模式,在zkServer.cmd和zkServer.sh两个脚本文件中,都会配置QuorumPeerMain作为启动入口类。如果判断当前是单机模式,会把接下来的处理转给ZookeeperServerMain。关键源码如下:
进入QuorumPeerMain.main()方法,
QuorumPeerMain main = new QuorumPeerMain();
main.initializeAndRun(args);
第二步:解析配置文件zoo.cfg。
main.initializeAndRun(args)方法首先会根据指定路径解析配置文件。解析以后封装到QuorumPeerConfig。
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);
}
进入config.parse(path)方法,主要是读取配置文件,然后加载到Hashtable容器中,然后根据键值对解析,解析之后,这些启动参数就封装到QuorumPeerConfig了。这个QuorumPeerConfig封装了所有配置文件中配置的参数。
Zookeeper启动时,会读取配置文件,默认就是/ZK_HOME/conf/目录下的zoo.cfg。解析以后会生成一个java.util.Properties对象。通过这个解析,就完成了启动参数的设置。
比如参数tickTime、dataDir、clientPort等都是在这里解析完成的。
第三步:创建并启动历史文件清理器DatadirCleanupManager。
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config.getDataDir(), config.getDataLogDir(), config.getSnapRetainCount)), config.getPurgeInterval());
purgeMgr.start();
大致流程是:根据配置的dataDir和DataLogDir,去对应的路径下读取文件列表,然后根据配置的自动清理周期,启动定时任务执行清理。注意,snapRetainCount的默认值为3,且该值的配置不能小于3。
日志清理规则的细节这里不再赘述,感兴趣的朋友可以自行研究源码。核心类有以下几个:
DatadirCleanupManager
PurgeTxnLog
FileTxnSnapLog
FileTxnLog
FileSnap
Zookeeper是内存数据库,同时也提供了持久化功能。通过快照和事务日志来实现持久化功能。
第四步:判断当前是集群模式还是单机模式。
HashMap
if (args.length == 1 && config.servers.size > 0) {
// 集群模式
this.runFromConfig(config);
} else {
LOG.warn("Either no config or quorum defined in config, running in standalone mode");
// 单机模式
ZookeeperServerMain. main();
}
Either no config or quorum defined in config, running in standalone mode
Zookeeper根据服务器列表地址来判断当前是集群模式还是单机模式。也就是说如果servers集合中的元素个数大于0,则是集群模式,否则是单机模式。如果是单机模式,则委托给ZookeeperServerMain进行处理。
如果是集群模式,则执行下面的方法进入集群启动流程。
this.runFromConfig(config);
第五步:创建并初始化ServerCnxnFactory。
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
创建ServerCnxnFactory实例时,首先会从配置文件中读取zookeeper.serverCnxnFactory,也就是说,ServerCnxnFactory是可配置的,如果项目没有配置,则默认使用NIOServerCnxnFactory。
然后使用反射机制生成ServerCnxnFactory实例。
第六步:配置ServerCnxnFactory。
cnxnFactory.configure(config.getClientPortAddress, config.getMaxClientCnxns());
这里主要配置了SASL登录,创建守护线程处理请求,创建socket通信。这个socket通信,使用的是Java NIO或者Netty,是个不错的NIO实例,感兴趣的朋友可以研究一下。
ServerCnxnFactory是Zookeeper中的重要组件,负责处理客户端与服务器的通信。主要有两个实现,一个是NIOServerCnxnFactory,使用Java原生NIO处理网络IO事件。另一个是NettyServerCnxnFactory,使用Netty处理网络IO事件。
注意:虽然这里的客户端服务端口已经对外开放,客户端能够访问到Zookeeper的客户端服务端口2181,但是此时Zookeeper服务器还是无法处理客户端请求的。
第七步:创建QuorumPeer。
QuorumPeer是Zookeeper集群的核心类,Zookeeper集群的关键功能都是在这里实现的。
this.quorumPeer = new QuorumPeer();
创建QuorumPeer时,首先会启动一个名字叫做QuorumPeer的线程。
接着初始化了以下参数:
this.learnerType = QuorumPeer.learnerType.PARTICIPANT;
this.running = true;
this.minSessionTimeout = -1;
this.maxSessionTimeout = -1;
this.syncEnabled = true;
this.quorumListenOnAllIPs = false;
this.state = QuorumPeer.ServerState.LOOKING;
this.logFactory = null;
this.acceptedEpoch = -1L;
this.currentEpoch = -1L;
this.quorumStats = new QuorumStats(this);
第八步:设置服务端IP地址和端口。
this.quorumPeer.setClientPortAddress(config.getClientPortAddress());
这个地址是从配置文件中读取的。clientPortAddress和clientPort是配合使用的,这个地址是供客户端连接的地址。
第九步:创建Zookeeper数据管理器FileTxnSnapLog。
this.quorumPeer.setTxnFactory(new FileTxnSnapLog(new File(config.getDataLogDir(), new File(config.getDataDir()))));
初始化数据管理器的过程就是初始化FileTxnSnapLog的过程。FileTxnSnapLog类中定义了FileTxnLog和FileSnap。FileTxnLog负责处理事务日志,FileSnap负责处理快照。FileTxnSnapLog类是Zookeeper上层服务器与底层数据存储的中间层,提供一系列操作数据文件的方法。具体细节可以参考FileTxnSnapLog类的源码。
第十步:设置服务端参与者participant,或者换一种说法叫设置法定选举人。
this.quorumPeer.setQuorumPeers(config.getServers());
注意这里的servers,是配置文件中配置的ip+port的列表,也就是说,Zookeeper拿到我们配置的server信息后,会把这些都设置为法定选举人,为接下来选举leader做准备。
QuorumServers是QuorumPeer的内部类,是对法定选举人的抽象。QuorumServers包含4个属性:
addr:当前法定选举人的IP+端口,相当于身份证,是个唯一标识。
electionAddr:选举地址。相当于人民大会堂,大家都来到这里进行选举。
id:机器号sid。
QuorumPeer.LearnerType type:是否参与选举。这里定义了2种类型,PARTICIPANT和OBSERVER,默认值是PARTICIPANT,参与选举。我们可以在zoo.cfg中指定type,示例如下:
server.1=127.0.0.1:2888:3888:observer
这里的重点是config.servers的解析逻辑。
servers这个属性对应zoo.cfg中的server.x
配置项。关于这个配置项,通常是这样来配置的。
service.A = B:C:D
A表示这是第几号服务器。
B表示该服务器的IP地址。
C表示服务器与集群中的Leader服务器交换信息的端口。
D表示如果Leader挂了,在新端口D上进行选举。
2181:Zookeeper服务端对外提供的端口。
2888:内部同步端口。
3888:Leader挂了,选举新的Leader的端口。
对于一个有3台机器组成的集群,该配置项可能是下面这样:
server.1=127.0.0.1:2888:3888
server.2=127.0.0.2:2888:3888
server.3=127.0.0.3:2888:3888
解析以后,config.servers就会有3条记录。
这3条数据的结构是这样的:
{
"1":{"addr":"127.0.0.1:2888","electionAddr":"127.0.0.1:3888","id":"1","type":"PARTICIPANT"},
"2":{"addr":"127.0.0.2:2888","electionAddr":"127.0.0.2:3888","id":"2","type":"PARTICIPANT"},
"3":{"addr":"127.0.0.3:2888","electionAddr":"127.0.0.3:3888","id":"3","type":"PARTICIPANT"}
}
OK,法定选举人的信息登记基本结束了,接下来进入下一步。
第11步:设置选举算法。
this.quorumPeer.setElectionType(config.getElectionAlg());
electionAlg可以在zoo.cfg中配置,如果没有指定,默认值是3。3指的是什么意思呢?
目前Zookeeper支持3种选举算法:
1代表LeaderElection算法
2代表AuthFastLeaderElection算法
3代表FastLeaderElection算法。
这三种算法的细节,由于篇幅原因,这里不再赘述。
第12步:设置myid。
this.quorumPeer.setMyid(config.getServerId());
这里的myid就是当前机器的机器号,每台服务器都有一个自己的机器号,这个机器号会保存到myid文件中。这个文件位于项目配置的dataDir目录下。通常myid文件只有一行记录,比如server.1对应的myid文件中的记录就是1。myid在集群中是唯一的,并且取值范围是1到255。myid和zoo.cfg中的配置server.x中的“x”是一一对应的。
第13步:设置tickTime、minSessionTimeout、maxSessionTimeout。
this.quorumPeer.setTickTime(config.getTickTime());
tickTime可配置,默认值是3000毫秒。
这个时间是Zookeeper服务器和客户端的心跳间隔。
this.quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
this.quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
minSessionTimeout和maxSessionTimeout如果zoo.cfg中没有配置,则根据tickTime计算。计算的逻辑是这样的:
this.minSessionTimeout == -1? this.tickTime * 2 : this.minSessionTimeout;
this.maxSessionTimeout == -1? this.tickTime * 20 : this.maxSessionTimeout;
如果我们的tickTime取默认值3秒,那么:
this.minSessionTimeout = 6秒
this.maxSessionTimeout = 60秒
minSessionTimeout和maxSessionTimeout有什么作用呢?作用就是控制客户端连接的超时时间。因为客户端在连接服务端时,会指定一个超时时间timeout,但是服务端可能并不会直接使用这个超时时间,服务端强制要求客户端的超时时间大于minSessionTimeout且小于maxSessionTimeout。如果timeout介于minSessionTimeout和maxSessionTimeout之间,则把客户端的timeout作为超时时间,否则,使用minSessionTimeout或者maxSessionTimeout作为超时时间。
第14步:设置选举时间initLimit和syncLimit。
this.quorumPeer.setInitLimit(config.getInitLimit());
this.quorumPeer.setSyncLimit(config.getSyncLimit());
initLimit和syncLimit都是设置选举相关时间。initLimit的默认值是10,也就是10个tickTime的时间。syncLimit的默认值是5,也就是5个tickTime的时间。
initLimit可以看做是follower与leader进行数据同步的超时时间。选举出leader以后,leader与follower之间需要进行数据同步。initLimit就是这个同步的超时时间。
syncLimit是单个follower与leader的交互的超时时间,如果在这个时间段,follower还是无法连上leader,那么这个follower将被废弃。
第15步:设置统计选票接口。
this.quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
QuorumVerifier是统计选票接口,主要定义了2个方法:
// 获取权重
long getWeight(long sid);
// 判断是否当选
boolean containsQuorum(HashSet
QuorumVerifier的具体实现类是QuorumHierarchical,统计选票的具体细节都在这个类中。
第16步:初始化内存数据库。
this.quorumPeer.setZKDatabase(new ZKDatabase(this.quorumPeer.getTxnFactory));
ZKDatabase是Zookeeper的内存数据库。ZKDatabase的主要功能如下:
1,定时向磁盘dump数据,也就是快照。
2,启动阶段,会从快照和事务日志中load数据。
这些功能的实现细节都在ZKDatabase类中。
第17步,设置learnType。
this.quorumPeer.setLearnType(config.getPeerType());
是否参与选举。这里定义了2种类型,PARTICIPANT和OBSERVER,默认值是PARTICIPANT,参与选举。
第18步,设置观察者是否记录事务日志和快照。
this.quorumPeer.setSyncEnabled(config.getSyncEnabled());
syncEnabled可配置,默认值是true,也就是说Observer也记录事务日志和快照。如果设置为false,则Observer不记录事务日志和快照。
第19步,设置quorumListenOnAllIPs。
this.quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
quorumListenOnAllIPs可配置,默认值是false。这个属性的含义是:是否在所有可用的IP上监听来自其对等节点的连接请求。
值为false意味着只在zoo.cfg中配置的服务器ip地址上监听连接请求。
第20步,启动Zookeeper集群。
this.quorumPeer.start();
this.quorumPeer.join();
1,加载内存数据库。
2,启动ServerCnxnFactory。
3,启动leader选举。
4,Zookeeper集群主线程进入WAITING状态,开始对外提供服务。
OK,到这里,Zookeeper集群启动流程结束。这篇文章只是概括性的总结,限于篇幅,中间很多细节并没有深入讲解,接下来会另起文章单独讲解下面2个核心流程:
1,Zookeeper的选举机制。
2,Zookeeper的内存数据库的工作原理。