源码项目zookeeper-3.6.3:核心工作流程
ZooKeeper选举和状态同步结束之后的服务启动
在Leader的lead()方法的最后,即Leader完成了和集群过半Follower的同步之后,就会调用startZkServer()来启动必要的服务,主要包括:
SessionTracker
RequestProcessor
更新Leader ZooKeeperServer的状态
Leader.lead(){
startZkServer(){
zk.startup(){
//ZooKeeperServer启动
super.startup(){
startupWithServerState(State.RUNNING);
}
//ZK Container ZNode定时清除任务
if(containerManager != null){
containerManager.start();
}
}
}
}
同理,Follower也是一样的,在完成了和Leader的状态同步之后,也就是接收到Leader发送过来的NEWLEADER消息的时候,先拍摄快照,然后调用zk.startupWithoutServing()来启动Follower必要的一些基础服务,包括:
SessionTracker
RequestProcessor
更新Leader ZooKeeperServer的状态
Leader.syncWithLeader(long newLeaderZxid){
//创建SessionTacker
zk.createSessionTracker();
//启动一些服务
zk.startupWithoutServing(){
startupWithoutServerState(State.INITIAL);
}
}
不管Leader还是Follower,最后都执行startupWithServerState(State state),具体实现:
ZooKeeperServer.startupWithServerState(State state){
//创建和启动SessionTracker 重要
if(sessionTracker == null){
createSessionTracker();
}
startSessionTracker();
//初始化RequestProcessors 重要
setupRequestProcessors();
//其他各项基础服务
startRequestThrottler();
registerJMX();
startJvmPauseMonitor();
registerMetrics();
//更新状态为RUNNING
setState(state);
//解除其他线程的阻塞
notifyAll();
}
ZooKeeper SessionTracker启动和工作机制
需求背景:现在每个客户端链接到ZK服务器的时候,在服务器内部,都会创建一个Session来管理这个链接
如果链接断开,删除这个session
如果链接超时,删除这个session(在规定的超时时间前,没有任何动作)
会遇到什么问题?大量的session管理,ZK提供的方案:桶管理机制(Session管理,Connection管理)
在Leader启动的时候,Leader会创建LeaderSessionTracker,在Follower启动的时候,内部会创建一个LearnerSessionTracker。SessionTracker的内部都有globalSessionTracker和localSessionTracker之分,都通过SessionTrackerImpl和ExpiryQueue来完成Session管理。
SessionTrackerImpl的定义:时间轮
public class SessionTrackerImpl extends ZooKeeperCriticalThread implements SessionTracker {
// 存储 session 信息,key 是 sessionID, value 是 Session 对象
protected final ConcurrentHashMap<Long,SessionImpl> sessionById = new ConcurrentHashMap<Long,SessionImpl>();
// SessionTrackerImpl 本身是一个线程
// 注意这个数据结构,这是理解 Session 和 Connection 超时管理的关键,是一个类似于 时间轮 的数据结构设计
// 将所有要进行管理的 Session 或者 Connection 加入到不同时刻对应的某个队列中,ZK 只会每隔一段时间,针对一个队列,进行全部过期操作处理
private final ExpiryQueue<SessionImpl> sessionExpiryQueue;
// 存储 session 的超时时间信息,key 是 sessionID, value 是 超时时间
private final ConcurrentMap<Long,Integer> sessionWithTimeout;
// SessionTrackerImpl 本身是一个线程
public void run() {
try {
while(running) {
// TODO 注释: 获取下一个队列的超时时间,等待
long waitTime = sessionExpiryQueue.getWaitTime();
if(waitTime > 0) {
Thread.sleep(waitTime);
continue;
}
// TODO 注释: 执行 expire
for(SessionImpl s : sessionExpiryQueue.poll()) {
ServerMetrics.getMetrics().STALE_SESSIONS_EXPIRED.add(1);
setSessionClosing(s.sessionId);
expirer.expires(s);
}
}
} catch(InterruptedException e) {
handleException(this.getName(), e);
}
LOG.info("SessionTrackerImpl exited loop!");
}
public synchronized boolean touchSession(long sessionId,int timeout) {
SessionImpl s = sessionsById.get(sessionId);
if(s==null){
logTraceTouchInvalidSession(sessionId,timeout);
return false;
}
if(s.isClosing()) {
logTraceTouchClosingSession(sessionId,timeout);
return false;
}
// 更新该 Session 的超时信息
// 该方法的内部就是调用 ExpiryQueue 的 update 方法!
updateSessionExpiry(s,timeou);
return true;
}
}
ExpiryQueue的定义:
public class ExpiryQueue<E> {
// E 是管理对象,比如 Session, value Long 是超时时间
private final ConcurrentHashMap<E,Long> elemMap = new ConcurrentHashMap<E,Long>();
// 最核心的数据结构 key = 超时时间, value = 需要进行超时处理的一个集合, 在进行 Session 管理的时候:E = SessionImpl
private final ConcurrentHashMap<Long,Set<E>> expiryMap = new ConcurrentHashMap<Long,Set<E>>();
// 桶间隔,默认 10 s,意味着,每隔 10s 执行一个 Set 的过期
private final int expirationInterval;
private long roundToNextInterval(long time) {
return (time / expirationInterval + 1) * expirationInterval;
}
// 执行 Session 的 timeout 的更新和换桶操作
public Long update(E elem, int timeout) {
// 获取当前 Session elem 的超时时间
Long prevExpiryTime = elemMap.get(elem);
//计算 它对应的 session bucket
long now = Time.currentElapsedTime();
Long newExpiryTime = roundToNextInterval(now + timeout);
//如果前后两次 expireTime 的更新依然处于同一个桶,则不做任何操作
if(newExpiryTime.equals(prevExpiryTime)) {
return null;
}
//根据桶对应的 ExpiryTime 找到 存储会话的 set 集合, 这个 set 集合就是一个所谓的 桶
//所以其实 Session 管理,就是把所有 Session 分散成多个桶。每隔一段时间,对一个桶的所有 Session 执行过期处理
Set<E> set = expiryMap.get(newExpriyTime);
// 如果桶为空,则创建桶,加入到 expiryMap 中
if(set == null) {
//新的桶
set = Collections.newSetFromMap(new ConcurrentHashMap<E,Boolean>());
//旧的桶
Set<E> existingSet = expiryMap.putIfAbsent(newExpiryTime,set);
if(existingSet != null) {
set = existingSet;
}
}
//将该 Session 加入到新的桶中
set.add(elem);
// 更新 elemMap 中的桶信息 即换桶
prevExpiryTime = elemMap.put(elem,newExpiryTime);
if(prevExpiryTime != null && !newExpiryTime.equals(prevExpiryTime)) {
Set<E> prevSet = expiryMap.get(prevExpiryTime);
if(prevSet != null) {
//从原来的桶中,移除掉该 session
prevSet.remove(elem);
}
}
return newExpiryTime;
}
}
SessionTrackerImpl 线程的意义,在于不停的按照顺序取这个 expiryMap 中的 Set,然后针对这个 Set 执行过期处理。
updateSessionExpiry(s, timeout) 方法是理解 Session 管理的核心。它的作用是,如果某个 Session 活跃了,则会调用 touchSession 方法来更新 Session 的超时时间。
Leader的RequestProcessor初始化
Leader的setupRequestProcessors()方法的核心逻辑:
LeaderZooKeeperServer.java
protected void setupRequestProcessors() {
//第六个: FinalRequestProcessor
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
//第五个: ToBeAppliedRequestProcessor
RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(finalProcessor,getLeader());
//第四个: CommitProcessor
commitProcessor = new CommitProcessor(toBeAppliedProcessor,Long.toString(getServerId()),false,getZooKeeperServerListener());
commitProcessor.start();
//第三个: ProposalRequestProcessor
//内部初始化 SyncRequestProcessor 和 AckRequestProcessor
ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,commitProcessor);
proposalProcessor.initialize();
//第二个: PrepRequestProcessor
prepRequestProcessor = new PrepRequestProcessor(this,proposalProcessor);
prepRequestProcessor.start();
//第一个: LeaderRequestProcessor
firstProcessor = new LeaderRequestProcessor(this,prepRequestProcessor);
//启动删除 Container 节点的一个定时任务
setupContainerManager();
}
LeaderRequestProcessor:Leader调用链开始,这个处理器主要是处理本地session相关的
PrepRequestProcessor:请求预处理器,能够识别出当前客户端请求是否是事务请求,PrepRequestProcessor处理器会对其进行一系列预处理,如创建请求事务头、事务体、会话检查、ACL检查和版本检查等。
ProposalRequestProcessor:事务投票处理。Leader服务器事务处理流程的发起者。接收到非事务请求不做什么处理,会直接将请求转发到CommitProcessor,接收到事务请求,除了将请求转发到CommitProcessor外,还会根据请求类型创建对应的Proposal提议并广播给所有Follower进行投票。另外,它还会将事务请求交付给SyncRequestProcessor进行事务日志的记录。
CommitProcessor:事务提交处理器。对于非事务请求,该处理器会直接将其交付给下一级处理器处理;对于事务请求,其会等待集群内针对Proposal的投票直到该Proposal可被提交,利用CommitProcessor,每个服务器都可以很好的控制对事务请求的顺序处理。
ToBeAppliedRequestProcessor:该处理器有一个toBeApplied队列,用来存储那些已经被CommitProcessor处理过的可被提交的Proposal。其会将这些请求交付给FinalRequestProcessor处理器处理,待其处理完后,再将其从toBeApplied队列中移除。
FinalRequestProcessor:FinalRequestProcessor用来进行客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理还会负责将事务应用到内存数据库中去。
SyncRequestProcessor:事务日志记录处理器。负责将事务持久化到磁盘上。实际上就是将事务数据按顺序追加到事务日志中,同时会触发ZooKeeper进行数据快照。
AckRequestProcessor:负责再SyncRequestProcessor完成事务日志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务日志记录。
Follower的RequestProcessor初始化
Follower的setupRequestProcessor()方法的核心逻辑:
FollowerZooKeeperServer.java
protected void setupRequestProcessors() {
//第三个: FinalRequestProcessor
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
//第二个: CommitProcessor
commitProcessor = new CommitProcessor(finalProcessor,Long.toString(getServerId()),true,getZooKeeperServerListener());
commitProcessor.start();
//第一个: FollowerRequestProcessor
firstProcessor = new FollowerRequestProcessor(this,commitProcessor);
((FollowerRequestProcessor)firstProcessor).start();
//注释: SyncRequestProcessor
syncProcessor = new SyncRequestProcessor(this,new SendAckRequestProcessor(getFollower()));
syncProcessor.start();
}
Follower RequestProcessor详解:
FollowerRequestProcessor:识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器是在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进行处理。
CommitProcessor:同Leader的CommitProcessor
FinalRequestProcessor:同Leader的FinalRequestProcessor
SyncRequestProcessor:同Leader的SyncRequestProcessor
SendAckRequestProcessor:其承担了事务日志记录反馈的角色,在完成事务日志记录后,会向Leader服务器发送ACK消息以表明自身完成了事务日志的记录工作。当Leader服务器接收到足够确认消息来提交这个提议时,Leader就会发送提交事务消息给追随者(同时也会发送INFORM消息给观察者服务器)。当接收到提交事务消息时,追随者就通过CommitRequestProcessor处理器进行处理。
ZooKeeper客户端初始化
new ZooKeeper 的时候内部初始化了 ClientCnxn 的客户端通信对象
// 初始化了 ClientCnxn 的客户端通信对象
ZooKeeper zk = new ZooKeeper("bigdata01:2181,bigdata02:2181", 5000, watcher);
// 创建 znode 节点
zk.create();
// 查询 znode 节点数据
zk.getData();
ZooKeeper.java
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher) throws IOException {
/************************************************
* 注释:
* 1、链接地址
* 2、会话超时时间
* 3、默认监听器
*/
this(connectString, sessionTimeout, watcher, false);
}
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, ZKClientConfig conf) throws IOException {
this(connectString, sessionTimeout, watcher, false, conf);
}
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly,
HostProvider aHostProvider) throws IOException {
this(connectString, sessionTimeout, watcher, canBeReadOnly, aHostProvider, null);
}
// 进入ZooKeeper 构造方法
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly,
HostProvider aHostProvider, ZKClientConfig clientConfig) throws IOException {
LOG.info("Initiating client connection, connectString={} sessionTimeout={} watcher={}", connectString, sessionTimeout, watcher);
if (clientConfig == null) {
clientConfig = new ZKClientConfig();
}
this.clientConfig = clientConfig;
watcherManager = defaultWatchManager();
watcherManager.defaultWatcher = watcher;
ConnectStringParser connectStringParser = new ConnectStringParser(connectString);
hostProvider = aHostProvider;
/*************************************************
* 注释: 创建和 ZK Server 的链接
* 1、这个方法创建一个链接对象
* 2、具体其实是创建: ClientCnxnSocketNIO,内部启动了一个 NIO 客户端
* cnxn = ClientCnxn 包装了 ClientCnxnSocketNIO
*/
cnxn = createConnection(connectStringParser.getChrootPath(), hostProvider, sessionTimeout, this, watchManager, getClientCnxnSocket(), canBeReadOnly);
cnxn.start();
}
//进入createConnection方法
public ClientCnxn createConnection(String chrootPath, HostProvider hostProvider, int sessionTimeout,
ZooKeeper zooKeeper, ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket,
boolean canBeReadOnly) throws IOException {
//真正意义上的客户端
return new ClientCnxn(chrootPath,hostProvider,sessionTimeout,this,watcherManager,clientCnxnSocket,canBeReadOnly);
}
ClientCnxn.java
//进入ClientCnxn方法
public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper,
ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket, boolean canBeReadOnly) throws IOException {
this(chrootPath, hostProvider, sessionTimeout, zooKeeper, watcher, clientCnxnSocket, 0, new byte[16], canBeReadOnly);
}
public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper,
ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket, long sessionId, byte[] sessionPasswd,
boolean canBeReadOnly) throws IOException {
this.zooKeeper = zooKeeper;
this.watcher = watcher;
this.sessionId = sessionId;
this.sessionPasswd = sessionPasswd;
this.sessionTimeout = sessionTimeout;
this.hostProvider = hostProvider;
this.chrootPath = chrootPath;
connectTimeout = sessionTimeout / hostProvider.size();
readTimeout = sessionTimeout * 2 / 3;
readOnly = canBeReadOnly;
//发起请求的线程(链接请求,写数据请求,读数据请求)
sendThread = new SendThread(clientCnxnSocket);
//接收响应,执行处理的(watcher,callback,response)
eventThread = new EventThread();
this.clientConfig = zooKeeper.getClientConfig();
initRequestTimeout();
}
public void start() {
//先发起链接请求,建立链接之后,再发起正常的读写数据请求
//SendThread 内部保存了一个 ClientCnxnSocketNIO,相当于一个 NIO 的客户端,
//负责和 ZooKeeper 的 ServerCnxnFactory 中启动的服务端建立连接,然后负责消费 outgoingQueue 中的消息,执行请求发送。
sendThread.start();
// 接收到请求的响应,执行处理的线程
//EventThread 线程消费 waitingEvents 队列,调用 processEvent(event) 负责处理服务端返回回来的消息,事件,异步回调等。
eventThread.start();
}
ZooKeeper服务端初始化
ZooKeeper集群启动服务的入口类QuorumPeerMain.java
QuorumPeerMain.java
-main
-main.initializeAndRun(args);
-initializeAndRun
-runFromConfig(config);
-runFromConfig
//第一件事:创建。通过反射创建 NIOServerCnxnFactory 实例
-ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
//第二件事:初始化。启动内部组件,即各种线程
-cnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), true);
//进入NIOServerCnxnFactory.java
//第三件事:启动工作线程。启动 QuorumPeer
-quorumPeer.start();
//进入QuorumPeer.java
QuorumPeer.java
-start
-startServerCnxnFactory();
-secureCnxnFactory.start();
//进入NIOServerCnxnFactory.java
NIOServerCnxnFactory.java
-configure
//
initMaxCnxns();
//管理 NIOServerCnxn 的超时
sessionlessCnxnTimeout = Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT,10000);
//启动一个链接管理线程
cnxnExpiryQueue = new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
expirerThread = new ConnectionExpirerThread();
//获取cpu个数
int numCores = Runtime.getRuntime().availableProcessors();
//获取selectorThread的个数
numSelectorThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_SELECTOR_THREADS, Math.max((int)Math.sqrt((float)numCores/2),1));
//获取WorkThread个数
numWorkerThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2*numCores);
//初始化SelectorThread
for(int i = 0;i<numSelectorThreads;++i) {
selectorThreads.add(new SelectorThread(i));
}
//启动 NIO 服务端 ServerSocketChannel
this.ss = ServerSocketChannel.open();
ss.socket().setReuseAddress(true);
ss.socket().bind(addr);
//初始化一个 AcceptThread
acceptThread = new AcceptThrea(ss,addr,selectorThreads);
-start
//启动WorkThread
workPool = new WorkerService("NIOWorker", numWorkerThreads, false);
//进入WorkService.java
//启动SelectorThread
-for(SelectorThread thread:selectorThreads) {
if(thread.getState() == Thread.State.NEW) {
thread.start();
}
}
//启动AcceptThread
if(acceptThread.getState() == Thread.State.NEW) {
acceptThread.start();
}
//启动ConnectionExpirerThread
if(expirerThread.getState() == Thread.State.New) {
expirerThread.start();
}
WorkService.java
-WorkerService
-start();
-start
-workers.add(Executors.newFixedThreadPool(numWorkerThreads,new DaemonThreadFactory(threadNamePrefix)));
NIOServerCnxnFactory内部首先启动(一个AcceptThread,多个SelectorThread,一个线程池WorkThread)每次接收到一个客户端链接请求:在服务端会生成一个ServerCnxn的组件,这个对象内部就封装了一个SocketChannel=专门对某个client执行服务的。
在ZooKeeper的客户端ClientCnxn初始化的时候,是由内部的SendThread发起链接请求给服务端建立连接,然后服务端会给当前的客户端生成一个ServerCnxn,一个客户端就会有一个对应的ServerCnxn,相当于ServerCnxn是一一对应的关系。而且当服务端生成好了ServerCnxn之后,还会给当前这个链接创建一个Session,通过Session来管理这个链接。
第一步:创建服务端NIOServerCnxnFactory
第二步:对NIOServerCnxnFactory进行初始化
第三步:启动NIOServerCnxnFactory内部的各种工作线程
AcceptThread负责接收链接请求,建立连接
SelectorThread负责IO读写
WorkService负责请求处理
ConnectionExpirerThread线程负责连接超时管理