摘要:不少人喜欢把RocketMQ与Kafka作对比,其实这两款消息队列的网络通讯层仍是比较类似的,本文就为你们简要地介绍下Kafka的NIO网络通讯模型算法
前面写的两篇RocketMQ源码研究笔记系列:spring
(1) 消息中间件—RocketMQ的RPC通讯(一)后端
(2) 消息中间件—RocketMQ的RPC通讯(二)缓存
基本上已经较为详细地将RocketMQ这款分布式消息队列的RPC通讯部分的协议格式、消息编解码、通讯方式(同步/异步/单向)、消息收发流程和Netty的Reactor多线程分离处理架构讲了一遍。同时,联想业界大名鼎鼎的另外一款开源分布式消息队列—Kafka,具有高吞吐量和高并发的特性,其网络通讯层是如何作到消息的高效传输的呢?为了解开本身心中的疑虑,就查阅了Kafka的Network通讯模块的源码,伺机会写本篇文章。安全
本文主要经过对Kafka源码的分析来简述其Reactor的多线程网络通讯模型和整体框架结构,同时简要介绍Kafka网络通讯层的设计与具体实现。性能优化
1、Kafka网络通讯模型的总体框架概述网络
Kafka的网络通讯模型是基于NIO的Reactor多线程模型来设计的。这里先引用Kafka源码中注释的一段话:数据结构
An NIO socket server. The threading model is 1 Acceptor thread that handles new connections. Acceptor has N Processor threads that each have their own selector and read requests from sockets. M Handler threads that handle requests and produce responses back to the processor threads for writing.
相信你们看了上面的这段引文注释后,大体能够了解到Kafka的网络通讯层模型,主要采用了 1(1个Acceptor线程)+N(N个Processor线程)+M(M个业务处理线程) 。下面的表格简要的列举了下(这里先简单的看下后面还会详细说明):多线程
线程数线程名线程具体说明1kafka-socket-acceptor_%xAcceptor线程,负责监听Client端发起的请求Nkafka-network-thread_%dProcessor线程,负责对Socket进行读写Mkafka-request-handler-_%dWorker线程,处理具体的业务逻辑并生成Response返回架构
Kafka网络通讯层的完整框架图以下图所示:
Kafka消息队列的通讯层模型—1+N+M模型.png
刚开始看到上面的这个框架图可能会有一些不太理解,并没关系,这里能够先对Kafka的网络通讯层框架结构有一个大体了解。本文后面会结合Kafka的部分重要源码来详细阐述上面的过程。这里能够简单总结一下其网络通讯模型中的几个重要概念:
(1), Acceptor :1个接收线程,负责监听新的链接请求,同时注册OP_ACCEPT 事件,将新的链接按照 "round robin" 方式交给对应的 Processor 线程处理;
(2), Processor :N个处理器线程,其中每一个 Processor 都有本身的 selector,它会向 Acceptor 分配的 SocketChannel 注册相应的 OP_READ 事件,N 的大小由 “num.networker.threads” 决定;
(3), KafkaRequestHandler :M个请求处理线程,包含在线程池—KafkaRequestHandlerPool内部,从RequestChannel的全局请求队列—requestQueue中获取请求数据并交给KafkaApis处理,M的大小由 “num.io.threads” 决定;
(4), RequestChannel :其为Kafka服务端的请求通道,该数据结构中包含了一个全局的请求队列 requestQueue和多个与Processor处理器相对应的响应队列responseQueue,提供给Processor与请求处理线程KafkaRequestHandler和KafkaApis交换数据的地方。
(5), NetworkClient :其底层是对 Java NIO 进行相应的封装,位于Kafka的网络接口层。Kafka消息生产者对象—KafkaProducer的send方法主要调用NetworkClient完成消息发送;
(6), SocketServer :其是一个NIO的服务,它同时启动一个Acceptor接收线程和多个Processor处理器线程。提供了一种典型的Reactor多线程模式,将接收客户端请求和处理请求相分离;
(7), KafkaServer :表明了一个Kafka Broker的实例;其startup方法为实例启动的入口;
(8), KafkaApis :Kafka的业务逻辑处理Api,负责处理不一样类型的请求;好比 “发送消息”、 “获取消息偏移量—offset” 和 “处理心跳请求” 等;
2、Kafka网络通讯层的设计与具体实现
这一节将结合Kafka网络通讯层的源码来分析其设计与实现,这里主要详细介绍网络通讯层的几个重要元素—SocketServer、Acceptor、Processor、RequestChannel和KafkaRequestHandler。本文分析的源码部分均基于Kafka的0.11.0版本。
一、SocketServer
SocketServer是接收客户端Socket请求链接、处理请求并返回处理结果的核心类,Acceptor及Processor的初始化、处理逻辑都是在这里实现的。在KafkaServer实例启动时会调用其startup的初始化方法,会初始化1个 Acceptor和N个Processor线程(每一个EndPoint都会初始化,通常来讲一个Server只会设置一个端口),其实现以下:
def startup() { this.synchronized { connectionQuotas = new ConnectionQuotas(maxConnectionsPerIp, maxConnectionsPerIpOverrides) val sendBufferSize = config.socketSendBufferBytes val recvBufferSize = config.socketReceiveBufferBytes val brokerId = config.brokerId var processorBeginIndex = 0 // 一个broker通常只设置一个端口 config.listeners.foreach { endpoint => val listenerName = endpoint.listenerName val securityProtocol = endpoint.securityProtocol val processorEndIndex = processorBeginIndex + numProcessorThreads //N 个 processor for (i <- processorBeginIndex until processorEndIndex) processors(i) = newProcessor(i, connectionQuotas, listenerName, securityProtocol, memoryPool) //1个 Acceptor val acceptor = new Acceptor(endpoint, sendBufferSize, recvBufferSize, brokerId, processors.slice(processorBeginIndex, processorEndIndex), connectionQuotas) acceptors.put(endpoint, acceptor) KafkaThread.nonDaemon(s"kafka-socket-acceptor-$listenerName-$securityProtocol-${endpoint.port}", acceptor).start() acceptor.awaitStartup() processorBeginIndex = processorEndIndex } }
二、Acceptor
Acceptor是一个继承自抽象类AbstractServerThread的线程类。Acceptor的主要任务是监听而且接收客户端的请求,同时创建数据传输通道—SocketChannel,而后以轮询的方式交给一个后端的Processor线程处理(具体的方式是添加socketChannel至并发队列并唤醒Processor线程处理)。
在该线程类中主要能够关注如下两个重要的变量:
(1), nioSelector :经过NSelector.open()方法建立的变量,封装了JAVA NIO Selector的相关操做;
(2), serverChannel :用于监听端口的服务端Socket套接字对象;
下面来看下Acceptor主要的run方法的源码:
def run() { //首先注册OP_ACCEPT事件 serverChannel.register(nioSelector, SelectionKey.OP_ACCEPT) startupComplete() try { var currentProcessor = 0 //以轮询方式查询并等待关注的事件发生 while (isRunning) { try { val ready = nioSelector.select(500) if (ready > 0) { val keys = nioSelector.selectedKeys() val iter = keys.iterator() while (iter.hasNext && isRunning) { try { val key = iter.next iter.remove() if (key.isAcceptable) //若是事件发生则调用accept方法对OP_ACCEPT事件处理 accept(key, processors(currentProcessor)) else throw new IllegalStateException("Unrecognized key state for acceptor thread.") //轮询算法 // round robin to the next processor thread currentProcessor = (currentProcessor + 1) % processors.length } catch { case e: Throwable => error("Error while accepting connection", e) } } } } //代码省略 } def accept(key: SelectionKey, processor: Processor) { val serverSocketChannel = key.channel().asInstanceOf[ServerSocketChannel] val socketChannel = serverSocketChannel.accept() try { connectionQuotas.inc(socketChannel.socket().getInetAddress) socketChannel.configureBlocking(false) socketChannel.socket().setTcpNoDelay(true) socketChannel.socket().setKeepAlive(true) if (sendBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE) socketChannel.socket().setSendBufferSize(sendBufferSize) processor.accept(socketChannel) } catch { //省略部分代码 } } def accept(socketChannel: SocketChannel) { newConnections.add(socketChannel) wakeup() }
在上面源码中能够看到,Acceptor线程启动后,首先会向用于监听端口的服务端套接字对象—ServerSocketChannel上注册OP_ACCEPT 事件。而后以轮询的方式等待所关注的事件发生。若是该事件发生,则调用accept()方法对OP_ACCEPT事件进行处理。这里,Processor是经过 round robin 方法选择的,这样能够保证后面多个Processor线程的负载基本均匀。
Acceptor的accept()方法的做用主要以下:
(1)经过SelectionKey取得与之对应的serverSocketChannel实例,并调用它的accept()方法与客户端创建链接;
(2)调用connectionQuotas.inc()方法增长链接统计计数;并同时设置第(1)步中建立返回的socketChannel属性(如sendBufferSize、KeepAlive、TcpNoDelay、configureBlocking等)
(3)将socketChannel交给processor.accept()方法进行处理。这里主要是将socketChannel加入Processor处理器的并发队列newConnections队列中,而后唤醒Processor线程从队列中获取socketChannel并处理。其中,newConnections会被Acceptor线程和Processor线程并发访问操做,因此newConnections是ConcurrentLinkedQueue队列(一个基于连接节点的无界线程安全队列)
三、Processor
Processor同Acceptor同样,也是一个线程类,继承了抽象类AbstractServerThread。其主要是从客户端的请求中读取数据和将KafkaRequestHandler处理完响应结果返回给客户端。在该线程类中主要关注如下几个重要的变量:
(1), newConnections :在上面的 Acceptor 一节中已经提到过,它是一种ConcurrentLinkedQueue[SocketChannel]类型的队列,用于保存新链接交由Processor处理的socketChannel;
(2), inflightResponses :是一个Map[String, RequestChannel.Response]类型的集合,用于记录还没有发送的响应;
(3), selector :是一个类型为KSelector变量,用于管理网络链接;
下面先给出Processor处理器线程run方法执行的流程图:
Kafk_Processor线程的处理流程图.png
从上面的流程图中可以能够看出Processor处理器线程在其主流程中主要完成了这样子几步操做:
(1), 处理newConnections队列中的socketChannel 。遍历取出队列中的每一个socketChannel并将其在selector上注册OP_READ事件;
(2), 处理RequestChannel中与当前Processor对应响应队列中的Response 。在这一步中会根据responseAction的类型(NoOpAction/SendAction/CloseConnectionAction)进行判断,若为“NoOpAction”,表示该链接对应的请求无需响应;若为“SendAction”,表示该Response须要发送给客户端,则会经过“selector.send”注册OP_WRITE事件,而且将该Response从responseQueue响应队列中移至inflightResponses集合中;“CloseConnectionAction”,表示该链接是要关闭的;
(3), 调用selector.poll()方法进行处理 。该方法底层即为调用nioSelector.select()方法进行处理。
(4), 处理已接受完成的数据包队列—completedReceives 。在processCompletedReceives方法中调用“requestChannel.sendRequest”方法将请求Request添加至requestChannel的全局请求队列—requestQueue中,等待KafkaRequestHandler来处理。同时,调用“selector.mute”方法取消与该请求对应的链接通道上的OP_READ事件;
(5), 处理已发送完的队列—completedSends 。当已经完成将response发送给客户端,则将其从inflightResponses移除,同时经过调用“selector.unmute”方法为对应的链接通道从新注册OP_READ事件;
(6), 处理断开链接的队列 。将该response从inflightResponses集合中移除,同时将connectionQuotas统计计数减1;
四、RequestChannel
在Kafka的网络通讯层中,RequestChannel为Processor处理器线程与KafkaRequestHandler线程之间的数据交换提供了一个数据缓冲区,是通讯过程当中Request和Response缓存的地方。所以,其做用就是在通讯中起到了一个数据缓冲队列的做用。Processor线程将读取到的请求添加至RequestChannel的全局请求队列—requestQueue中;KafkaRequestHandler线程从请求队列中获取并处理,处理完之后将Response添加至RequestChannel的响应队列—responseQueue中,并经过responseListeners唤醒对应的Processor线程,最后Processor线程从响应队列中取出后发送至客户端。
五、KafkaRequestHandler
KafkaRequestHandler也是一种线程类,在KafkaServer实例启动时候会实例化一个线程池—KafkaRequestHandlerPool对象(包含了若干个KafkaRequestHandler线程),这些线程以守护线程的方式在后台运行。在KafkaRequestHandler的run方法中会循环地从RequestChannel中阻塞式读取request,读取后再交由KafkaApis来具体处理。
六、KafkaApis
KafkaApis是用于处理对通讯网络传输过来的业务消息请求的中心转发组件。该组件反映出Kafka Broker Server能够提供哪些服务。
3、总结
仔细阅读Kafka的NIO网络通讯层的源码过程当中仍是能够收获很多关于NIO网络通讯模块的关键技术。Apache的任何一款开源中间件都有其设计独到之处,值得借鉴和学习。对于任何一位使用Kafka这款分布式消息队列的同窗来讲,若是可以在必定实践的基础上,再经过阅读其源码能起到更为深刻理解的效果,对于大规模Kafka集群的性能调优和问题定位都大有裨益。
对于刚接触Kafka的同窗来讲,想要本身掌握其NIO网络通讯层模型的关键设计,还须要不断地使用本地环境进行debug调试和阅读源码反复思考。
欢迎工做一到五年的Java工程师朋友们加入Java高级交流:698581634。群内提供免费的Java架构学习资料(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化等...)这些成为架构师必备的知识体系。合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!