Zookeeper 源码解析——服务端与客户端网络通信

一、概述

  在前一篇博文中我们已经分析了 Zookeeper 中客户端网络通信的源码,在本篇博文中我们将会分析 Zookeeper 中服务端的通信,但 Zookeeper 中服务端的通信又可分为服务端与服务端之间的通信和服务端和客户端之间的通信,为了保证梳理的清晰度,所以本篇博文将仅分析 服务端中服务端和客户端通信 的源码逻辑,且着重于使用 注释源码 的方式进行解析。

  在阅读源码的过程中也遇到一些比较有趣的注释,比如下面这个:

t

  博客内所有文章均为 原创,所有示意图均为 原创,若转载请附原文链接。


二、涉及的核心类

2.1 核心类简介

  1. ZooKeeperServerMain :ZkServer 核心启动类;
  2. ServerCnxnFactory :服务端连接管理器工厂(工厂模式);
  3. NettyServerCnxnFactory :服务端连接管理器工厂 Netty 实现(ServerCnxnFactory 实现类);
  4. NettyServerCnxn :单条连接的服务端连接管理器 Netty 实现;
  5. RequestProcessor :请求处理器接口,实现该接口类可用作 Request Processor Pipeline 中的节点;

三、核心源码解析

3.1 Standalone 模式下建立 Netty 网络连接

// ZooKeeperServerMain.java
public static void main(String[] args) {
	ZooKeeperServerMain main = new ZooKeeperServerMain();
    try {
    	// 根据命令行参数初始化并运行 Zookeeper 服务端
    	main.initializeAndRun(args);
    } 
    System.exit(0);
}

// ZooKeeperServerMain.java
protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {
	try {
    	ManagedUtil.registerLog4jMBeans();
    }

	// 从命令行解析参数至 ServerConfig 实例中
	ServerConfig config = new ServerConfig();
    if (args.length == 1) {
    	config.parse(args[0]);
    } else {
    	config.parse(args);
    }

	// 调用该方法创建并启动 Zookeeper 服务端
    runFromConfig(config);
}

// ZooKeeperServerMain.java
public void runFromConfig(ServerConfig config) throws IOException, AdminServerException {
	FileTxnSnapLog txnLog = null;
	try {
    	// 创建本地文件事务存储
		txnLog = new FileTxnSnapLog(config.dataLogDir, config.dataDir);
		// 创建 Zookeeper 服务端实例
		final ZooKeeperServer zkServer = new ZooKeeperServer(txnLog, config.tickTime, config.minSessionTimeout, config.maxSessionTimeout, null);
		// 将 Zookeeper 服务端实例与本地事务文件存储进行绑定
		txnLog.setServerStats(zkServer.serverStats());

		boolean needStartZKServer = true;
		if (config.getClientPortAddress() != null) {
	
			// 通过静态方法 createFactory 创建 ServerCnxnFactory 实例 
			cnxnFactory = ServerCnxnFactory.createFactory();
			// 根据配置文件中的 ClientPostAddress 配置其客户端端口
			cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);
			// 使用 ServerCnxnFactory 启动 zookeeperServer
			cnxnFactory.startup(zkServer);
			// 因为在此处 zkServer 已经启动,所以我们不需要在 secureCnxnFactory 中再次启动它
			needStartZKServer = false;
		}
		
		// 省略其它包括 secureCnxnFactory 在内的组件初始化和配置代码...
	} finally {
        if (txnLog != null) {
        	txnLog.close();
    	}
	}
}
// ServerCnxnFactory.java
static public ServerCnxnFactory createFactory() throws IOException {
	// 从配置文件中获取将要创建的 ServerCnxnFactory 类型
	String serverCnxnFactoryName = System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);
	
	if (serverCnxnFactoryName == null) {
		// 如果系统配置文件中未设置该属性则默认使用 JDK 的 NIO 实现版本 NIOServerCnxnFactory
		serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
	}
	try {
		// 通过反射调用构造方法实例化 ServerCnxnFactory 对象
		ServerCnxnFactory serverCnxnFactory = (ServerCnxnFactory) Class.forName(serverCnxnFactoryName).getDeclaredConstructor().newInstance();
		return serverCnxnFactory;
	}
}
// ServerCnxnFactory.java
public void startup(ZooKeeperServer zkServer) throws IOException, InterruptedException {
	// 启动 zkServer
	startup(zkServer, true);
}

// NettyServerCnxnFactory.java
public void startup(ZooKeeperServer zks, boolean startServer) throws IOException, InterruptedException {
	// 绑定 Netty 监听端口
	start();
	// 完成 zkServer 和 ServerCnxnFactory 的双向绑定
    setZooKeeperServer(zks);
    if (startServer) {
    	// 启动 zkServer
        zks.startdata();
        zks.startup();
    }
}

// NettyServerCnxnFactory.java
public void start() {
	// 绑定监听端口
    parentChannel = bootstrap.bind(localAddress).syncUninterruptibly().channel();
    
    // 如果原始端口为 0 则在调用 bind 方法后该端口会发生改变
    // 因此更新 localAddress 以获得真正的端口
    localAddress = (InetSocketAddress) parentChannel.localAddress();
}

// NettyServerCnxnFactory.java
final public void setZooKeeperServer(ZooKeeperServer zks) {
	// 实现 zkServer 和 ServerCnxnFactory 的双向绑定
	this.zkServer = zks;
    if (zks != null) {
    	if (secure) {
        	zks.setSecureServerCnxnFactory(this);
        } else {
            zks.setServerCnxnFactory(this);
        }
    }
}

3.2 配置 Netty

// NettyServerCnxnFactory.java
NettyServerCnxnFactory() {
	x509Util = new ClientX509Util();

	// 创建与客户端端口数目相同的线程,使得每一个线程监听一个端口
	// 且在创建 bossGroup 时优先选择使用更高性能的 EpollEventLoopGroup
	EventLoopGroup bossGroup = NettyUtils.newNioOrEpollEventLoopGroup(NettyUtils.getClientReachableLocalInetAddressCount());
	// 创建 workerGroup 且优先选择使用更高性能的 EpollEventLoopGroup
	EventLoopGroup workerGroup = NettyUtils.newNioOrEpollEventLoopGroup();
	ServerBootstrap bootstrap = new ServerBootstrap()
			.group(bossGroup, workerGroup)
            .channel(NettyUtils.nioOrEpollServerSocketChannel())
            // 父 Channel 配置
            .option(ChannelOption.SO_REUSEADDR, true)
            // 子 Channel 配置
            .childOption(ChannelOption.TCP_NODELAY, true)
            .childOption(ChannelOption.SO_LINGER, -1)
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                	ChannelPipeline pipeline = ch.pipeline();
                    if (secure) {
                    	initSSL(pipeline);
                    }
                    // 向 pipeline 添加 channelHandler 处理器
                    pipeline.addLast("servercnxnfactory", channelHandler);
                }
            });
    this.bootstrap = configureBootstrapAllocator(bootstrap);
    this.bootstrap.validate();
}
// NettyServerCnxnFactory.CnxnChannelHandler.java
public void channelActive(ChannelHandlerContext ctx) throws Exception {
	// Netty Channel 初始化完成后会调用该方法
	
	// 创建一个 NettyServerCnxn 并绑定当前的 Channel 和 zkServer
	NettyServerCnxn cnxn = new NettyServerCnxn(ctx.channel(), zkServer, NettyServerCnxnFactory.this);
	// 将 NettyServerCnxn 保存至 Channel 属性中(接收请求时会用到)
	ctx.channel().attr(CONNECTION_ATTRIBUTE).set(cnxn);

	if (secure) {
		SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
		Future<Channel> handshakeFuture = sslHandler.handshakeFuture();
		handshakeFuture.addListener(new CertificateVerifier(sslHandler, cnxn));
	} else {
		// 将 Channel 和 NettyServerCnxn 分别添加到集合中保存
		allChannels.add(ctx.channel());
        addCnxn(cnxn);
	}
}
// NettyServerCnxnFactory.CnxnChannelHandler.java
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
	// Netty Channel 结束前会调用该方法

	// 将 Channel 从 allChannels 集合中移除
	allChannels.remove(ctx.channel());
	// 解除 Channel 和 NettyServerCnxn 之间的绑定关系
    NettyServerCnxn cnxn = ctx.channel().attr(CONNECTION_ATTRIBUTE).getAndSet(null);
	if (cnxn != null) {
		// 关闭 NettyServerCnxn
		cnxn.close();
	}
}

3.3 接收并处理请求

// NettyServerCnxnFactory.CnxnChannelHandler.java
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
	try {
		try {
			// 在初始化时的 channelActive 方法中我们将 NettyServerCnxn 注册至 Channel 属性中
			NettyServerCnxn cnxn = ctx.channel().attr(CONNECTION_ATTRIBUTE).get();
			if (cnxn == null) {
				LOG.error("channelRead() on a closed or closing NettyServerCnxn");
			} else {
				// 如果 NettyServerCnxn 未被关闭或未被正在关闭则调用 processMessage 处理请求
				cnxn.processMessage((ByteBuf) msg);
			}
		}
	} finally {
		// 释放 Buffer
		ReferenceCountUtil.release(msg);
	}
}
// NettyServerCnxn.java
void processMessage(ByteBuf buf) {
	checkIsInEventLoop("processMessage");
	if (throttled.get()) {
    	// 如果当前为限流状态则直接进行排队
    	if (queuedBuffer == null) {
    		queuedBuffer = channel.alloc().compositeBuffer();
    	}
    	// 添加至 queuedBuffer 中排队
    	appendToQueuedBuffer(buf.retainedDuplicate());
    } else {
    
		if (queuedBuffer != null) {
			// 如果存在 queueBuffer 则仍然让响应排队
			appendToQueuedBuffer(buf.retainedDuplicate());
			// 该方法中包含对于 Channel 正在关闭时的处理逻辑,但对于响应的处理实质还是调用 receiveMessage 方法
			processQueuedBuffer();
        } else {
        	// 调用 receiveMessage 处理响应
            receiveMessage(buf);
            // 必须再次检查通道是否正在关闭,因为在 receiveMessage 方法中可能出现错误而导致 close() 被调用          
            if (!closingChannel && buf.isReadable()) {
            	if (queuedBuffer == null) {
                	queuedBuffer = channel.alloc().compositeBuffer();
                }
				appendToQueuedBuffer(buf.retainedSlice(buf.readerIndex(), buf.readableBytes()));
        	}
    	}
    	
	}
}

  关于 Netty.CompositeByteBuf

  • CompositeByteBuf 在聚合时使用,多个 buffer 合并时,不需要 copy,通过 CompositeByteBuf 可以把需要合并的 bytebuf 组合起来;
  • 对外提供统一的 readindex 和 writerindex ,CompositeByteBuf 里面有个ComponentList,继承自 ArrayList,聚合的 bytebuf 都放在 ComponentList 里面,其最小容量为16 ;
// NettyServerCnxn.java
private void receiveMessage(ByteBuf message) {
	checkIsInEventLoop("receiveMessage");
    try {
		while(message.isReadable() && !throttled.get()) {
			if (bb != null) {
				if (bb.remaining() > message.readableBytes()) {
					int newLimit = bb.position() + message.readableBytes();
					bb.limit(newLimit);
				}
				// 从数据包中读取长度为 bb 的 ByteBuffer
				message.readBytes(bb);
				bb.limit(bb.capacity());
            
            	if (bb.remaining() == 0) {
            		packetReceived();
                	bb.flip();

                	ZooKeeperServer zks = this.zkServer;
					if (zks == null || !zks.isRunning()) {
						throw new IOException("ZK down");
					}
 					if (initialized) {
                        // 源码注:如果将 zks.processPacket() 改为使用 ByteBuffer[] 则可以实现零拷贝队列
                        // 调用 processPacket 方法处理数据包中的实际数据
						zks.processPacket(this, bb);

						if (zks.shouldThrottle(outstandingCount.incrementAndGet())) {
							disableRecvNoWait();
						}
					} else {
						zks.processConnectRequest(this, bb);
                        initialized = true;
                    }
                    bb = null;
                }
            } else {
				if (message.readableBytes() < bbLen.remaining()) {
					bbLen.limit(bbLen.position() + message.readableBytes());
				}
				// 4 byte 的 ByteBuffer 用于读取数据包中前 4 byte 所记录的数据包中实际数据长度 
                message.readBytes(bbLen);
                bbLen.limit(bbLen.capacity());
                if (bbLen.remaining() == 0) {
                    bbLen.flip();
                    // 读取前 4 byte 所代表的的 Int 数值
                    int len = bbLen.getInt();
                    bbLen.clear();
                    if (!initialized) {
                    	if (checkFourLetterWord(channel, message, len)) {
                        	return;
                        }
                    }
                    if (len < 0 || len > BinaryInputArchive.maxBuffer) {
                    	throw new IOException("Len error " + len);
                    }
                    // 将 bb 赋值为数据包中前 4 byte Int 值长度的 ByteBuffer
                    bb = ByteBuffer.allocate(len);
                }
            }
        }
    } 
}

  上面这个 receiveMessage 方法中的逻辑比较复杂一点,但是处理的流程是跟客户端类似的,在这里简单总结一下代码主流程:

  1. 当数据包首次进入该方法时 bb 为空,所以直接进入第二个语句块;
  2. 在第二个语句块中会从数据包中读入长度为 4 byte 的 ByteBuffer( bblen = ByteBuffer.allocate(4) ),然后将其转换为一个 Int 整型值 len ;
  3. 根据整型值 len 申请长度为 len 的 ByteBuffer 赋值给 bb ,然后结束此轮循环;
  4. 进入第二轮循环时 bb 已经是长度为 len 的 ByteBuffer( len 为数据包中有效数据的长度 ),所以进入第一个语句块;
  5. 在第一个语句块中会直接从传入的数据包中读长度为 len 的数据并写入到 bb 中(一次性完整的将全部有效数据读入);
  6. 最后将获取到的有效数据传入 processPacket 方法中进行处理;
// ZooKeeperServer.java
public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
	// We have the request, now process and setup for next
    InputStream bais = new ByteBufferInputStream(incomingBuffer);
    BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
    // 解析请求头至临时变量 h
    RequestHeader h = new RequestHeader();
    h.deserialize(bia, "header");
    // 从原缓冲区的当前位置开始创建一个新的字节缓冲区
	incomingBuffer = incomingBuffer.slice();
	if (h.getType() == OpCode.auth) {
		AuthPacket authPacket = new AuthPacket();
        ByteBufferInputStream.byteBuffer2Record(incomingBuffer, authPacket);
            
        // 省略认证等代码...
        else {
        	// 将数据包中的有效数据组装为 Request 请求
            Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(), h.getType(), incomingBuffer, cnxn.getAuthInfo());
            si.setOwner(ServerCnxn.me);
            // 始终将来自客户端的请求视为可能的本地请求
            setLocalSessionFlag(si);
            // 将组装好的 Request 请求通过 submitRequest 方法发送给上层逻辑处理
            submitRequest(si);
        }
    }
    cnxn.incrOutstandingRequests(h);
}

  关于 ByteBuffer.slice

  • 创建一个新的字节缓冲区,其内容是原缓冲区内容的共享子序列,新缓冲区的内容将从原缓冲区的当前位置开始;
  • 原缓冲区内容的更改将在新缓冲区中可见,反之亦然,但两个缓冲区的 position 、limit 和 mark 是相互独立的;
  • 新缓冲区的 position 为零,它的 capacity 和 limit 为原缓冲区中剩余(remaining = limit - position)的字节数,且它的 mark 将是未定义的。当且仅当原缓冲区是 direct 和 read-only 时,新的缓冲区才将是 direct 的;
// ZooKeeperServer.java
public void submitRequest(Request si) {
	if (firstProcessor == null) {
		synchronized (this) {
			try {
			// 因为所有的请求都被传递给请求处理器,所以应该等待请求处理器链建立完成
			// 且当请求处理器链建立完成后,状态将更新为 RUNNING
			while (state == State.INITIAL) {
				wait(1000);
			}
			if (firstProcessor == null || state != State.RUNNING) {
				throw new RuntimeException("Not started");
			}
		}
	}
	try {
		// 验证 sessionId
		touch(si.cnxn);
		// 验证 Request 是否有效
		boolean validpacket = Request.isValid(si.type);
		if (validpacket) {
			// 如果 Request 有效则将其传递给请求处理链(Request Processor Pipeline)的第一个请求处理器
			firstProcessor.processRequest(si);
			if (si.cnxn != null) {
				incInProcess();
			}
		} else {
			// 该请求来自未知类型的客户端
			new UnimplementedRequestProcessor().processRequest(si);
		}
    }
}
// RequestProcessor.java
public interface RequestProcessor {
    @SuppressWarnings("serial")
    public static class RequestProcessorException extends Exception {
        public RequestProcessorException(String msg, Throwable t) {
            super(msg, t);
        }
    }

    void processRequest(Request request) throws RequestProcessorException;

    void shutdown();
}

3.4 发送响应

  因为 Zookeeper 中对于请求的处理是采用 Request Processor Pipeline 来完成的,所以对于处理请求后组装并发送响应的工作就是由最后一个 FinalRequestProcessor 来完成的,因此我们下面的源码分析就从 FinalRequestProcessor 的 processRequest 方法开始(这里的分析不会过多的涉及对于具体业务流程中响应的生成逻辑,更多的是偏向响应发送的整体流程逻辑),该方法的入参为上一个 Request Processor 处理后的 Request 请求。

// FinalRequestProcessor.java
public void processRequest(Request request) {
	// 因为重点分析发送响应流程,所以省略居多分类别处理请求并生成 hdr 响应头 和 rsp 响应体代码...
	try {
		// 在上面处理过 Request 请求后将生成的响应头和响应体作为入参调用 sendResponse 方法发送响应
		cnxn.sendResponse(hdr, rsp, "response");
		// 如果 Request 的类型为 closeSession 则进入关闭逻辑
		if (request.type == OpCode.closeSession) {
			cnxn.sendCloseSession();
		}
	}
}
// ServerCnxn.java
public void sendResponse(ReplyHeader h, Record r, String tag) throws IOException {
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	BinaryOutputArchive bos = BinaryOutputArchive.getArchive(baos);
	try {
		// 预留首部 4 byte 记录数据包中有效数据的长度
		baos.write(fourBytes);
		// 写入响应头
 		bos.writeRecord(h, "header");
		if (r != null) {
			// 写入响应体
			bos.writeRecord(r, tag);
        }
        // 关闭输出流
        baos.close();
    } 
    
    // 将输出流转换为字节数组
	byte b[] = baos.toByteArray();
	// 重定位指针便于确定有效数据的长度
    serverStats().updateClientResponseSize(b.length - 4);
    // 将字节数组包装到 ByteBuffer 缓冲区中
    ByteBuffer bb = ByteBuffer.wrap(b);
	// 向首部 4 byte 写入数据包中有效数据长度的 Int 整型值
    bb.putInt(b.length - 4).rewind();
    // 发送组装好的 ByteBuffer
    sendBuffer(bb);
}
// NettyServerCnxn.java
public void sendBuffer(ByteBuffer sendBuffer) {
	// 如果 ByteBuffer 为 closeConn 则调用 close() 进入关闭逻辑
	if (sendBuffer == ServerCnxnFactory.closeConn) {
		close();
		return;
	}
	// 否则将 ByteBuffer 中的数据写入 Channel 并通过 flush 将其发送    
    channel.writeAndFlush(Unpooled.wrappedBuffer(sendBuffer)).addListener(onSendBufferDoneListener);
}

四、源码总结

4.1 接收请求

  1. 服务端从 Netty Channel 的 channelRead 方法接收请求,并通过 NettyServerCnxn 的 processMessage 方法将其转发给当前 Channel 所绑定的 NettyServerCnxn ;
  2. 在 NettyServerCnxn 的 processMessage 方法中会进行限流(throttle)处理将请求 Buffer 拷贝到 queuedBuffer 中,然后调用 receiveMessage 方法对请求做进一步处理;
  3. receiveMessage 方法的主要工作就是从传入的 ByteBuf 中读取有效的数据(数据包前 4 byte 记录有效数据的长度),并将其转化为 ByteBuffer 传给 ZooKeeperServer 的 processPacket 进行处理;
  4. 在 processPacket 方法中会从 ByteBuffer 中解析出请求头和请求体,并将其封装为 Request 后传给上层的 submitRequest 方法进行处理;
  5. submitRequest 会等待第一个 Request Processor 初始化完成后进行请求的验证工作,然后将验证成功的请求传递给 Request Processor Pipeline 中的第一个 Request Processor 进行处理;

4.2 发送响应

  1. 响应的发送工作是由 Request Processor Pipeline 的最后一个 Request Processor 来完成的,在 FinalRequestProcessor 的 processRequest 方法中会根据请求的类型对传入的请求进行处理,并生成响应头和响应体传给 ServerCnxn 的 sendResponse 方法;
  2. 在 sendResponse 方法中会将入参的响应头和响应体组装为一个完整的响应,并将其转换为 ByteBuffer 通过 NettyServerCnxn 的 sendBuffer 方法传给 NettyServerCnxn ;
  3. 在 NettyServerCnxn 的 sendBuffer 方法中会进行 ByteBuffer 类型的判断,如果类型为 closeConn 则进入关闭逻辑,否则通过 Channel 的 writeAndFlush 方法将响应发送;

五、内容总结

5.1 ByteBuffer.slice()

  • 创建一个新的字节缓冲区,其内容是原缓冲区内容的共享子序列,新缓冲区的内容将从原缓冲区的当前位置开始;
  • 原缓冲区内容的更改将在新缓冲区中可见,反之亦然,但两个缓冲区的 position 、limit 和 mark 是相互独立的;
  • 新缓冲区的 position 为零,它的 capacity 和 limit 为原缓冲区中剩余(remaining = limit - position)的字节数,且它的 mark 将是未定义的。当且仅当原缓冲区是 direct 和 read-only 时,新的缓冲区才将是 direct 的;

5.2 Netty.CompositeByteBuf

  • CompositeByteBuf 在聚合时使用,多个 buffer 合并时,不需要 copy,通过 CompositeByteBuf 可以把需要合并的 bytebuf 组合起来,对外提供统一的 readindex 和 writerindex ;
  • CompositeByteBuf 里面有个ComponentList,继承自 ArrayList,聚合的 bytebuf 都放在 ComponentList 里面,其最小容量为16 ;

5.3 零拷贝队列

  Zookeeper 网络通信的源码中很多地方使用到了零拷贝队列,并且有写地方也直接注释了优化建议即使用零拷贝队列,但是因为这里涉及的技术点比较多,所以打算在分析 Netty 源码的时候在单独写文章进行整体,这里仅做知识点备忘。

// NettyServerCnxn.receiveMessage()

// TODO: if zks.processPacket() is changed to take a ByteBuffer[],
// we could implement zero-copy queueing.
zks.processPacket(this, bb);

六、思考

5.1 为什么 Zookeeper 选择使用 ByteBuffer 而不是 ByteBuf

  通过对源码的阅读我们可以发现 Zookeeper 对于 JDK NIO 的 ByteBuffer 和 Netty 的 ByteBuf 基本是穿插使用的,我在阅读的过程中就在疑问为什么会使用这样的方式。但其实我们可以发现 Zookeeper 服务端对于 Netty 实现的部分其实底层为了效率使用的仍然是 ByteBuf ,但进入到上层 ZooKeeperServer 的 processPacket 方法后就转换为了 ByteBuffer (转换是在 receiveMessage 中完成的),我觉得这样转换的意义可能更多的是为了兼容原本的 JDK NIO 实现吧。


发布了244 篇原创文章 · 获赞 32 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_40697071/article/details/103192375