源码项目zookeeper-3.6.3:核心工作流程
ZooKeeper集群启动脚本分析
哪些源码流程需要我们关注?
1、集群启动
2、崩溃恢复(leader选举)+状态同步
3、读写请求
从哪个地方入手看源码?入口
启动ZooKeeper的时候:
zkServer.sh start
注意:zookeeper中每个节点是单独启动的,启动需要半数成功才行
内部细节:
# TODO 注释: 核心的启动命令4
# TODO 注释: $JAVA = $JAVA_HOME/bin/java
# TODO 注释: $ZOOMAIN = 核心java类 = QuorumPeerMain
# TODO 注释: "$ZOOCFG" = zoo.cfg 的路径 = $ZOOKEEPER_HOME/conf/zoo.cfg
nohup "$JAVA" $ZOO_DATADIR_AUTOCREATE "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" \
"-Dzookeeper.log.file=${ZOO_LOG_FILE}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
-XX:+HeapDumpOnOutOfMemoryError -XX:OnOutOfMemoryError='kill -9 %p' \
-cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
简化之后:
nohup "$JAVA" $ZOO_DATADIR_AUTOCREATE -Dkey1=value1 -XX:HeapDumpOnOutOfMemoryError $ZOOMAIN "$ZOOCFG"
$JAVA:java命令
$ZOO_DATADIR_AUTOCREATE:参数
-Dkey1=value1:JVM的参数选项
-XX:JVM虚拟机参数
$ZOOMAIN:QuorumPeerMain类
$ZOOCFG:zookeeper配置文件zoo.cfg
通过java命令启动进程(启动QuorumPeerMain类),该类的参数是zoo.cfg
再简化:
java QuorumPeerMain zoo.cfg.path
底层会转调用QuorumPeerMain.main(),这个main方法的第一个参数,就是zoo.cfg的本地路径
ZOOMAIN=“org.apache.zookeeper.server.quorum.QuorumPeerMain”
ZK JVM进程的名称:QuorumPeerMain
QuorumPeer+Main启动QuorumPeer
QuorumPeer代表了一台服务器的全部功能实现
在QuorumPeerMain进程中创建QuorumPeer实例,并启动QuorumPeer实例
ZooKeeper配置文件zoo.cfg
# The number of milliseconds of each tick
tickTime=2000 #间隔时间
# The number of ticks that the initial synchronization phase can take
initLimit=10 #初始化时间,严格意义上initTime=initLimit*tickTime
# The number of ticks that can pass between sending a request and getting an acknowledgement
syncLimit=5 #同步时间
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/home/bigdata/data/zkdata #数据存储目录 根据实际情况修改 快照文件
dataLogDir=/home/bigdata/data/zklog/ #日志存储目录 根据实际情况修改
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60 #最大连接数
# electionAlg=3 = FastLeaderElection
electionAlg=3 #默认选举算法 FastLeaderElection
maxClientCnxns=60
# 服务器的类型: observer/participant(具有选举权的节点:leader follower)
peerType=observer/participant #节点类型 participant具有选举和被选举权
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
autopurge.snapRetainCount=3 #保留快照文件有几个
# Purge task interval in hours
# Set to "0" to disable auto purge feature
autopurge.purgeInterval=1 #旧快照清理时间间隔
server.2=bigdata02:2888
server.3=bigdata03:2888:3888
server.4=bigdata04:2888:3888:participant
server.5=bigdata05:2888:3888:observer #指明节点类型,省去分配类型
类别 | 角色1 | 角色2 |
---|---|---|
是否可以选举 | Participant(有选举权,包括Leader和Follower) | Observer(没有选举权) |
按照主从划分 | Leader | Leaner(包括Follower和Observer) |
ZooKeeper QuorumPeerMain启动
最重要的类QuorumPeer:代表一台服务器
1、一个组件ZKDatabase(即数据库那个模型)
ZKDatabase是ZK数据库,每个QuorumPeer代表一台ZK物理机,每个QuorumPeer内部都有一个ZKDatabase对象,意味着,每个ZK节点都保存了该集群的所有数据
2、JettyAdminServer:WebUI启动的就是AdminServer,默认绑定的端口8080
ZooKeeper从3.5.x版本增加了AdminServer
3、QuorumCnxManager:其中封装的一切组件都是为选举工作的,线程,监听器,队列,这里会启动一个BIO的服务器端,绑定的端口3888,由Listener启动
QuorumCnxManager是一个选举过程中的链接管理器,内部启动了一个Listener,Listener内部启动了一个BIO服务端,它监听3888端口,等待其他客户端发送选举链接请求来建立连接举行选票交换。
4、ServerCnxnFactory:为客户端提供服务的,绑定端口2181
ServerCnxnFactory是一个Runnable实现,默认实现:NIOServerCnxnFactory,内部启动了一个线程,启动了一个NIO服务端,监听2181端口,等待客户端发送链接请求,然后创建一个ServerCnxn,负责完成该客户端的读写请求
专门用来做同步的服务端什么时候启动,当选举结束;当前这个QP成为了leader,会启动一个新的服务端,这个服务端就会绑定2888,进行同步
5、QuorumVerifier:少数服从多数算法,提供一个方法和变量,变量存的是集群总节点个数(总的participant个数),server.2/server.3/server.4是,server.5不是
6、Election:FastLeaderElection:选举算法,FastLeaderElection负责选举的逻辑执行
7、runing,myid,vote:vote存放谁是leader的信息
QuorumPeer内部保存了一些和选举有关的信息,比如myid,vote,Election选举算法实现,从zoo.cfg中解析得到其他的Server的信息
8、Leader/Follower/Observer三个成员变量:每个节点上只有其中一个有值,即每一个节点只能由一个状态
9、Map<Long,QuorumServer> quorumPeers:集群中所有服务器
10、四个内部类SyncMode同步模式(有四种NONE,DIFF,SNAP,TRUNC),ServerState(当前QuorumPeer的状态,也有四种LOOKING,FOLLOWING,LEADING,OBSERVING),LearnerType(PARTICIPANT,OBSERVER),ZabState(ELECTION,DISCOVERY,SYNCHRONIZATION,BROADCAST)
QuorumPeerMain.java
注意QuorumPeer变量被一分为三,存储在 QuorumMaj 的内部(allMembers, votingMembers, observingMembers)
// 入口方法
QuorumPeerMain.main(args){
// 核心实现,分三步走
QuorumPeerMain.initializeAndRun(args) {
// 第一步;解析配置
config = new QuorumPeerConfig();
config.parse(args[0]) {
Properties cfg = new Properties();
cfg.load(in);
parseProperties(cfg);
}
// 第二步:启动一个线程(定时任务)来执行关于 old snapshot 的 cleanup
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(....);
purgeMgr.start() {
timer = new Timer("PurgeTask", true);
TimerTask task = new PurgeTask(dataLogDir, snapDir, snapRetainCount) {
public void run() {
PurgeTxnLog.purge(logsDir, snapsDir, snapRetainCount) {
FileTxnSnapLog txnLog = new FileTxnSnapLog(dataDir, snapDir);
List<File> snaps = txnLog.findNValidSnapshots(num);
int numSnaps = snaps.size();
if (numSnaps > 0) {
purgeOlderSnapshots(txnLog, snaps.get(numSnaps - 1));
}
}
}
}
timer.scheduleAtFixedRate(task, 0, TimeUnit.HOURS.toMillis(purgeInterval));
}
// 第三步:启动(有两种模式:standalone,集群模式)重点关注集群启动,分两步走
if (args.length == 1 && config.isDistributed()) {
runFromConfig(config) {
// 第一件事:服务端的通信组件 的初始化,但是并未启动(factory = ServerCnxnFactory)
cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), ...,false){
// 启动会话过期线程
initMaxCnxns();
// 管理 NIOServerCnxn 的超时, 链接超时管理
sessionlessCnxnTimeout = Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);
cnxnExpiryQueue = new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
expirerThread = new ConnectionExpirerThread();
// 启动相应数量的 SelectorThread
int numCores = Runtime.getRuntime().availableProcessors();
int n = ZOOKEEPER_NIO_NUM_SELECTOR_THREADS;
numSelectorThreads = Integer.getInteger(n, Math.max((int) Math.sqrt((float) numCores / 2), 1));
numWorkerThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);
for (int i = 0; i < numSelectorThreads; ++i) {
selectorThreads.add(new SelectorThread(i));
}
// 启动一个 ServerSocketChannel
this.ss = ServerSocketChannel.open();
// 初始化一个 AcceptThread
acceptThread = new AcceptThread(ss, addr, selectorThreads);
}
// 第二件事:抽象一个zookeeper节点,然后把解析出来的各种参数给配置上,然后启动
// QuorumPeer 就是所谓的一台服务器的抽象
quorumPeer = getQuorumPeer() {
return new QuorumPeer();
}
quorumPeer.setTxnFactory(new FileTxnSnapLog(config.getDataLogDir(), config.getDataDir()));
quorumPeer.setXXX(...);
quorumPeer.start() {
// 第一件事:把磁盘数据恢复到内存
loadDataBase() {
zkDb.loadDataBase() {
// 冷启动的时候,从磁盘恢复数据到内存
snapLog.restore(...., ....){
// 从快照恢复
snapLog.deserialize(dt, sessions);
// 从操作日志恢复
fastForwardFromEdits(dt, sessions, listener) {
// 恢复执行一条事务
rocessTransaction(hdr, dt, sessions, itr.getTxn());
}
}
}
}
// 第二件事:服务端的通信组件工厂的真正启动(cnxnFactory = ServerCnxnFactory)
startServerCnxnFactory();
// 第三件事:启动 AdminServer
adminServer.start();
// 第四件事:准备选举的一些必要操作(初始化一些队列和一些线程)
startLeaderElection();
// 第五件事:启动 JVM 监视器
startJvmPauseMonitor();
// 第六件事:调用 start() 跳转到 run() 方法。因为 QuorumPeer被封装成 Thread 了
super.start() {
// 执行选举
QuorumPeer.run() {
// 选举入口
FastLeaderElection.lookForLeader()
}
}
}
}
}
}
}
总结
QuorumPeerMain类启动流程总结:
- 入口:QuorumPeerMain的main方法
- 解析配置
- 读取zoo.cfg得到Properties对象,解析这个对象,获取各种配置,设置到QuorumPeerConfig
- 解析server.开头的各项配置,获取allMembers,votingMembers,observerMembers集合,然后构建QuorumMaj实例
- 解析myid
- 启动删除旧快照文件定时任务
- 启动QuorumPeer
- 创建NIO服务端相关组件和线程 ServerCnxnFactory.createFactory()
- 创建QuorumPeer实例,然后把QuorumPeerConfig中的各种配置,设置到QuorumPeer 很多
- 调用start方法启动 quorumPeer.start()
- 冷启动数据恢复 loadDataBase()
- 启动NIO服务端 startServerCnxnFactory()
- 启动AdminServer即WebUI adminServer.start()
- startLeaderElection为选举做准备 startLeaderElection()
- 启动JVM监视器 startJvmPauseMonitor()
- 启动QuorumPeer线程进入ZAB工作模式:QuorumPeer.run()
while(running) { switch(getPeerState()) { case LOOKING: lookForLeader(); case LEADING: lead(); case FOLLOWING: followerLeader(); case OBSERVING: observerLeader(); } }
- 解析配置