1.引入
zk单机服务端虽然在实际生产使用中不可能碰到,但是用于接下来分析zk集群服务端有很大用处,我们秉承循序渐进的方式,先介绍一下单机服务端的启动和处理流程
2.单机服务器启动
在单机启动过程中,会创建服务端连接对象,用于处理客户端的请求。其处理过程,可以从以下代码开始介绍。
ZooKeeperServerMain.java
-------------------------
public void runFromConfig(ServerConfig config)
throws IOException, AdminServerException {
.....
if (config.getClientPortAddress() != null) {
cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);
cnxnFactory.startup(zkServer);
needStartZKServer = false;
}
......
上面代码中的cnxnFactory.startup就是触发单机服务端启动的入口,它的实现有NIO和Netty两种方式,这里我们直接介绍NIO也就是默认的实现方式,它主要包含一下几个步骤:
- 启动SelectorThread(处理客户端请求线程),accepThread(处理接收连接进行事件),ExpirerThread(处理过期连接)
- 加载内存数据库
- 定时清理session,设置请求处理器(当前的处理器链为: PrepRequestProcessor -> SyncRequestProcessor -> FinalRequestProcessor,在处理客户端请求时会依次介绍这几个处理器)
NIOServerCnxnFactory.java
-------------------------
public void startup(ZooKeeperServer zks, boolean startServer)
throws IOException, InterruptedException {
// 启动相关线程
start();
setZooKeeperServer(zks);
// zk server启动()
if (startServer) {
// 加载数据到zkDataBase,清除无效的session,将zk数据和session序列化后保存到文件中等
zks.startdata();
// 启动定时清除session的管理器,注册jmx,添加请求处理器
zks.startup();
}
}
SelectorThread是专门用于处理客户端请求的线程,它的主要就是监听NIO读写事件并将请求封装成IOWorkRequest对象并存储到调度池中。
NIOServerCnxnFactory.java
-------------------------
public void run() {
try {
while (!stopped) {
try {
// 处理读写请求,select方法中最主要的是handleIO方法(即处理读写)
select();
// 接收连接(忽略) processAcceptedConnections();
// 设置NIO监听事件(忽略) processInterestOpsUpdateRequests();
} catch (RuntimeException e) {
LOG.warn("Ignoring unexpected runtime exception", e);
} catch (Exception e) {
LOG.warn("Ignoring unexpected exception", e);
}
}
// 处理读写
private void handleIO(SelectionKey key) {
IOWorkRequest workRequest = new IOWorkRequest(this, key);
NIOServerCnxn cnxn = (NIOServerCnxn) key.attachment();
cnxn.disableSelectable();
key.interestOps(0);
// 防止连接过期
touchCnxn(cnxn);
// 将请求放入调度池
workerPool.schedule(workRequest);
}
调度池的实现方式很简单就是使用多个线程池并行处理多个任务,最终会调用workRequest的doWork方法,所以看看这个方法的实现:
NOIServerCnxnFactory.IOWorkRequest
----------------------------
public void doWork() throws InterruptedException {
if (!key.isValid()) {
selectorThread.cleanupSelectionKey(key);
return;
}
if (key.isReadable() || key.isWritable()) {
// 最终调用连接对象(NOIServerCnxn)的doIO方法
cnxn.doIO(key);
if (stopped) {
cnxn.close();
return;
}
if (!key.isValid()) {
selectorThread.cleanupSelectionKey(key);
return;
}
touchCnxn(cnxn);
}
处理过程其实就是调用NIOServerCnxn的doIO方法(这里只介绍核心逻辑),当需要处理客户端的请求是会调用readPayload方法,如果响应客户端会调用handleWrite方法。
NIOServerCnxn.java
---------------------
void doIO(SelectionKey k) throws InterruptedException {
if (k.isReadable()) {
......
// 处理客户端的请求
readPayload();
......
}
if (k.isWritable()) {
// 处理发送数据到客户端(响应)
handleWrite(k);
.....
}
- 服务端响应流程
由于handleWrite方法比较简单,我们直接从这个方法说起,从下面的代码片段可以看出,这个方法其实就是将outgoingBuffers队列中的数据发送到客户端,即如果需要响应客户端,只需要将响应数据添加到outgoingBuffers队列中即可。
NIOServerCnxn.java
------------------
void handleWrite(SelectionKey k) throws IOException, CloseRequestException {
if (outgoingBuffers.isEmpty()) {
return;
}
ByteBuffer directBuffer = NIOServerCnxnFactory.getDirectBuffer();
if (directBuffer == null) {
ByteBuffer[] bufferList = new ByteBuffer[outgoingBuffers.size()];
sock.write(outgoingBuffers.toArray(bufferList));
......
}
} else {
......
for (ByteBuffer b : outgoingBuffers) {
if (directBuffer.remaining() < b.remaining()) {
b = (ByteBuffer) b.slice().limit(
directBuffer.remaining());
}
int p = b.position();
directBuffer.put(b);
b.position(p);
if (directBuffer.remaining() == 0) {
break;
}
}
......
}
}
- 服务端读流程(处理请求流程)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eHka0Bh-1577608488262)(C58DB0F3D39047B1A85159384E6EDE79)]
通过上面的时序图,可以清楚得看到zk处理客户端请求的主要流程,在submitRequest之前只是将请求数据进行二次封装和校验,最终还是得通过 PrepRequestProcessor , SyncRequestProcessor , FinalRequestProcessor依次处理,所以这里直接从PrepRequestProcessor开始介绍。
- PrepRequestProcessor
它的processRequest方法很简单,就是将请求对象添加到submittedRequests,而Processor是一个线程,在启动之后会执行run方法,这个run方法就是取submittedRequests队列中的请求进行处理。
PreRequestProcessor.java
-------------------------
public void run() {
try {
while (true) {
Request request = submittedRequests.take();
......
// 是否是结束请求
if (Request.requestOfDeath == request) {
break;
}
// 处理请求
pRequest(request);
}
......
其pRequest方法虽然比较长,但是细看这个方法会发现就是将请求对象分成多种请求类型(比如:create,setData,getData等),对于客户端写处理转成对应事务对象并设置到request中,如果是读请求只是会进行校验,比如:path,session等校验,然后调用方法
nextProcessor.processRequest(request)
交给SyncRequestProcessor处理。
2.SyncRequestProcessor
和PrepRequestProcessor一样,processRequest只是用于添加队列元素,在run方法中才是处理逻辑。
SyncRequestProcessor.java
--------------------------
public void run() {
try {
int logCount = 0;
int randRoll = r.nextInt(snapCount/2);
while (true) {
Request si = null;
// 从请求处理队列中,拿出一个请求
if (toFlush.isEmpty()) {
si = queuedRequests.take();
} else {
si = queuedRequests.poll();
// 如果没有请求,就写入磁盘,并且交给下一个处理器处理
if (si == null) {
flush(toFlush);
continue;
}
}
if (si == requestOfDeath) {
break;
}
if (si != null) {
// track the number of records written to the log
if (zks.getZKDatabase().append(si)) {
logCount++;
// 控制大于某个随机数才写入事务数据和快照,是为了防止多个节点同时同步数据
if (logCount > (snapCount / 2 + randRoll)) {
randRoll = r.nextInt(snapCount/2);
// 准备创建一个快照文件,所以需要将事务日志刷新,并将日志流置为null,保证再次使用的时候会重新创建一个新的事务日志文件
zks.getZKDatabase().rollLog();
if (snapInProcess != null && snapInProcess.isAlive()) {
LOG.warn("Too busy to snap, skipping");
} else {
snapInProcess = new ZooKeeperThread("Snapshot Thread") {
public void run() {
try {
zks.takeSnapshot();
} catch(Exception e) {
LOG.warn("Unexpected exception", e);
}
}
};
snapInProcess.start();
}
logCount = 0;
}
} else if (toFlush.isEmpty()) {
if (nextProcessor != null) {
nextProcessor.processRequest(si);
if (nextProcessor instanceof Flushable) {
((Flushable)nextProcessor).flush();
}
}
continue;
}
toFlush.add(si);
// 如果请求数达到1000保存事务日志。
if (toFlush.size() > 1000) {
flush(toFlush);
}
}
}
} catch (Throwable t) {
handleException(this.getName(), t);
} finally{
running = false;
}
LOG.info("SyncRequestProcessor exited!");
}
......
代码看起来比较复杂,所以在这里总结一下执行流程:
- 循环取queuedRequests队列中的请求,如果请求不存在就直接刷新事务日志并交给下一个处理器(FinalRequestProcessor)处理。
- 为了保证所有的服务器尽可能不在同一时刻创建快照,增加了一个随机数控制当前的日志数是不是符合条件,如果符合条件,就将原来的事务日志文件刷新等待创建新的事务日志文件,并通过异步的方式保存当前的快照。
- 如果处理的日志请求数已经达到1000条,不管queuedRequests队列中是不是还有请求,直接刷新到磁盘并且交给一下处理器处理。
- FinalRequestProcessor
在SyncRequestProcessor达到条件并且刷新到磁盘中,就会调用FinalRequestProcessor的processRequest方法处理(注意:这个处理器和前面两个不一样,它不是线程,所以处理逻辑就是在processRequest方法中实现了)
这个类主要是做两件事:
- 修改zk的内存数据库ZkDataBase
- 根据客户端请求,创建对应的响应对象,并发送到客户端。
修改内存数据库
FinalRequestProcessor.java
-------------------------
public void processRequest(Request request) {
ProcessTxnResult rc = null;
synchronized (zks.outstandingChanges) {
// Need to process local session requests
// 处理事务和会话
rc = zks.processTxn(request);
......
// 事务性请求存放到committedLog
if (request.isQuorum()) {
zks.getZKDatabase().addCommittedProposal(request);
}
}
创建对应的响应对象
FinalRequestProcessor.java
-------------------------
ServerCnxn cnxn = request.cnxn;
String lastOp = "NA";
zks.decInProcess();
Code err = Code.OK;
Record rsp = null;
// 处理不同的请求类型
switch (request.type) {
case OpCode.ping: {
// 更新延迟
zks.serverStats().updateLatency(request.createTime);
lastOp = "PING";
cnxn.updateStatsForResponse(request.cxid, request.zxid, lastOp,
request.createTime, Time.currentElapsedTime());
cnxn.sendResponse(new ReplyHeader(-2,
zks.getZKDatabase().getDataTreeLastProcessedZxid(), 0), null, "response");
return;
}
......
case OpCode.create: {
lastOp = "CREA";
rsp = new CreateResponse(rc.path);
err = Code.get(rc.err);
break;
}
来源:CSDN
作者:missv5
链接:https://blog.csdn.net/missv5/article/details/103754946