跟我学Kafka之NIO通信机制

很久没有做技术方面的分享了,今天闲来有空写一篇关于Kafka通信方面的文章与大家共同学习。

一、Kafka通信机制的整体结构


74EACA88-8B9D-45F8-B7BF-202D658205A9.png


这个图采用的就是我们之前提到的SEDA多线程模型,链接如下:
http://www.jianshu.com/p/e184fdc0ade4
1、对于broker来说,客户端连接数量有限,不会频繁新建大量连接。因此一个Acceptor thread线程处理新建连接绰绰有余。
2、Kafka高吐吞量,则要求broker接收和发送数据必须快速,因此用proccssor thread线程池处理,并把读取客户端数据转交给缓冲区,不会导致客户端请求大量堆积。
3、Kafka磁盘操作比较频繁会且有io阻塞或等待,IO Thread线程数量一般设置为proccssor thread num两倍,可以根据运行环境需要进行调节。

二、SocketServer整体设计时序图


Kafka 通信时序图.jpg

说明:

Kafka SocketServer是基于Java NIO来开发的,采用了Reactor的模式,其中包含了1个Acceptor负责接受客户端请求,N个Processor线程负责读写数据,M个Handler来处理业务逻辑。在Acceptor和Processor,Processor和Handler之间都有队列来缓冲请求。

下面我们就针对以上整体设计思路分开讲解各个不同部分的源代码。

2.1 启动初始化工作

def startup() {
    val quotas = new ConnectionQuotas(maxConnectionsPerIp, maxConnectionsPerIpOverrides)
    for(i <- 0 until numProcessorThreads) {
      processors(i) = new Processor(i, 
                                    time, 
                                    maxRequestSize, 
                                    aggregateIdleMeter,
                                    newMeter("IdlePercent", "percent", TimeUnit.NANOSECONDS, Map("networkProcessor" -> i.toString)),
                                    numProcessorThreads, 
                                    requestChannel,
                                    quotas,
                                    connectionsMaxIdleMs)
      Utils.newThread("kafka-network-thread-%d-%d".format(port, i), processors(i), false).start()
    }

    newGauge("ResponsesBeingSent", new Gauge[Int] {
      def value = processors.foldLeft(0) { (total, p) => total + p.countInterestOps(SelectionKey.OP_WRITE) }
    })

    // register the processor threads for notification of responses
    requestChannel.addResponseListener((id:Int) => processors(id).wakeup())

    // start accepting connections
    this.acceptor = new Acceptor(host, port, processors, sendBufferSize, recvBufferSize, quotas)
    Utils.newThread("kafka-socket-acceptor", acceptor, false).start()
    acceptor.awaitStartup
    info("Started")
  }

说明:

ConnectionQuotas对象负责管理连接数/IP, 创建一个Acceptor侦听者线程,初始化N个Processor线程,processors是一个线程数组,可以作为线程池使用,默认是三个,Acceptor线程和N个Processor线程中每个线程都独立创建Selector.open()多路复用器,相关代码在下面:

val numNetworkThreads = props.getIntInRange("num.network.threads", 3, (1, Int.MaxValue));

val serverChannel = openServerSocket(host, port);

范围可以设定从1到Int的最大值。

2.2 Acceptor线程

def run() {
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    startupComplete()
    var currentProcessor = 0
    while(isRunning) {
      val ready = selector.select(500)
      if(ready > 0) {
        val keys = selector.selectedKeys()
        val iter = keys.iterator()
        while(iter.hasNext && isRunning) {
          var key: SelectionKey = null
          try {
            key = iter.next
            iter.remove()
            if(key.isAcceptable)
               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)
          }
        }
      }
    }
    debug("Closing server socket and selector.")
    swallowError(serverChannel.close())
    swallowError(selector.close())
    shutdownComplete()
  }
2.1.1 注册OP_ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
2.1.2 内部逻辑

此处采用的是同步非阻塞逻辑,每隔500MS轮询一次,关于同步非阻塞的知识点在http://www.jianshu.com/p/e9c6690c0737。当有请求到来的时候采用轮询的方式获取一个Processor线程处理请求,代码如下:

currentProcessor = (currentProcessor + 1) % processors.length

之后将代码添加到newConnections队列之后返回,代码如下:

def accept(socketChannel: SocketChannel) {  newConnections.add(socketChannel)  wakeup()}

//newConnections是一个线程安全的队列,存放SocketChannel通道
private val newConnections = new ConcurrentLinkedQueue[SocketChannel]()

2.3 kafka.net.Processor

override def run() {
    startupComplete()
    while(isRunning) {
      // setup any new connections that have been queued up
      configureNewConnections()
      // register any new responses for writing
      processNewResponses()
      val startSelectTime = SystemTime.nanoseconds
      val ready = selector.select(300)
      currentTimeNanos = SystemTime.nanoseconds
      val idleTime = currentTimeNanos - startSelectTime
      idleMeter.mark(idleTime)
      // We use a single meter for aggregate idle percentage for the thread pool.
      // Since meter is calculated as total_recorded_value / time_window and
      // time_window is independent of the number of threads, each recorded idle
      // time should be discounted by # threads.
      aggregateIdleMeter.mark(idleTime / totalProcessorThreads)

      trace("Processor id " + id + " selection time = " + idleTime + " ns")
      if(ready > 0) {
        val keys = selector.selectedKeys()
        val iter = keys.iterator()
        while(iter.hasNext && isRunning) {
          var key: SelectionKey = null
          try {
            key = iter.next
            iter.remove()
            if(key.isReadable)
              read(key)
            else if(key.isWritable)
              write(key)
            else if(!key.isValid)
              close(key)
            else
              throw new IllegalStateException("Unrecognized key state for processor thread.")
          } catch {
            case e: EOFException => {
              info("Closing socket connection to %s.".format(channelFor(key).socket.getInetAddress))
              close(key)
            } case e: InvalidRequestException => {
              info("Closing socket connection to %s due to invalid request: %s".format(channelFor(key).socket.getInetAddress, e.getMessage))
              close(key)
            } case e: Throwable => {
              error("Closing socket for " + channelFor(key).socket.getInetAddress + " because of error", e)
              close(key)
            }
          }
        }
      }
      maybeCloseOldestConnection
    }
    debug("Closing selector.")
    closeAll()
    swallowError(selector.close())
    shutdownComplete()
  }

先来重点看一下configureNewConnections这个方法:

private def configureNewConnections() {
    while(newConnections.size() > 0) {
      val channel = newConnections.poll()
      debug("Processor " + id + " listening to new connection from " + channel.socket.getRemoteSocketAddress)
      channel.register(selector, SelectionKey.OP_READ)
    }
  }

循环判断NewConnections的大小,如果有值则弹出,并且注册为OP_READ读事件。
再回到主逻辑看一下read方法。

def read(key: SelectionKey) {
    lruConnections.put(key, currentTimeNanos)
    val socketChannel = channelFor(key)
    var receive = key.attachment.asInstanceOf[Receive]
    if(key.attachment == null) {
      receive = new BoundedByteBufferReceive(maxRequestSize)
      key.attach(receive)
    }
    val read = receive.readFrom(socketChannel)
    val address = socketChannel.socket.getRemoteSocketAddress();
    trace(read + " bytes read from " + address)
    if(read < 0) {
      close(key)
    } else if(receive.complete) {
      val req = RequestChannel.Request(processor = id, requestKey = key, buffer = receive.buffer, startTimeMs = time.milliseconds, remoteAddress = address)
      requestChannel.sendRequest(req)
      key.attach(null)
      // explicitly reset interest ops to not READ, no need to wake up the selector just yet
      key.interestOps(key.interestOps & (~SelectionKey.OP_READ))
    } else {
      // more reading to be done
      trace("Did not finish reading, registering for read again on connection " + socketChannel.socket.getRemoteSocketAddress())
      key.interestOps(SelectionKey.OP_READ)
      wakeup()
    }
  }

说明

1、把当前SelectionKey和事件循环时间放入LRU映射表中,将来检查时回收连接资源。
2、建立BoundedByteBufferReceive对象,具体读取操作由这个对象的readFrom方法负责进行,返回读取的字节大小。

  • 如果读取完成,则修改状态为receive.complete,并通过requestChannel.sendRequest(req)将封装好的Request对象放到RequestQueue队列中。
  • 如果没有读取完成,则让selector继续侦听OP_READ事件。

2.4 kafka.server.KafkaRequestHandler

def run() {
    while(true) {
      try {
        var req : RequestChannel.Request = null
        while (req == null) {
          // We use a single meter for aggregate idle percentage for the thread pool.
          // Since meter is calculated as total_recorded_value / time_window and
          // time_window is independent of the number of threads, each recorded idle
          // time should be discounted by # threads.
          val startSelectTime = SystemTime.nanoseconds
          req = requestChannel.receiveRequest(300)
          val idleTime = SystemTime.nanoseconds - startSelectTime
          aggregateIdleMeter.mark(idleTime / totalHandlerThreads)
        }

        if(req eq RequestChannel.AllDone) {
          debug("Kafka request handler %d on broker %d received shut down command".format(
            id, brokerId))
          return
        }
        req.requestDequeueTimeMs = SystemTime.milliseconds
        trace("Kafka request handler %d on broker %d handling request %s".format(id, brokerId, req))
        apis.handle(req)
      } catch {
        case e: Throwable => error("Exception when handling request", e)
      }
    }
  }

说明

KafkaRequestHandler也是一个事件处理线程,不断的循环读取requestQueue队列中的Request请求数据,其中超时时间设置为300MS,并将请求发送到apis.handle方法中处理,并将请求响应结果放到responseQueue队列中去。
代码如下:

try{
      trace("Handling request: " + request.requestObj + " from client: " + request.remoteAddress)
      request.requestId match {
        case RequestKeys.ProduceKey => handleProducerOrOffsetCommitRequest(request)
        case RequestKeys.FetchKey => handleFetchRequest(request)
        case RequestKeys.OffsetsKey => handleOffsetRequest(request)
        case RequestKeys.MetadataKey => handleTopicMetadataRequest(request)
        case RequestKeys.LeaderAndIsrKey => handleLeaderAndIsrRequest(request)
        case RequestKeys.StopReplicaKey => handleStopReplicaRequest(request)
        case RequestKeys.UpdateMetadataKey => handleUpdateMetadataRequest(request)
        case RequestKeys.ControlledShutdownKey => handleControlledShutdownRequest(request)
        case RequestKeys.OffsetCommitKey => handleOffsetCommitRequest(request)
        case RequestKeys.OffsetFetchKey => handleOffsetFetchRequest(request)
        case RequestKeys.ConsumerMetadataKey => handleConsumerMetadataRequest(request)
        case requestId => throw new KafkaException("Unknown api code " + requestId)
      }
    } catch {
      case e: Throwable =>
        request.requestObj.handleError(e, requestChannel, request)
        error("error when handling request %s".format(request.requestObj), e)
    } finally
      request.apiLocalCompleteTimeMs = SystemTime.milliseconds
  }

说明如下:

参数说明对应方法
RequestKeys.ProduceKeyproducer请求ProducerRequest
RequestKeys.FetchKeyconsumer请求FetchRequest
RequestKeys.OffsetsKeytopic的offset请求OffsetRequest
RequestKeys.MetadataKeytopic元数据请求TopicMetadataRequest
RequestKeys.LeaderAndIsrKeyleader和isr信息更新请求LeaderAndIsrRequest
RequestKeys.StopReplicaKey停止replica请求StopReplicaRequest
RequestKeys.UpdateMetadataKey更新元数据请求UpdateMetadataRequest
RequestKeys.ControlledShutdownKeycontrolledShutdown请求ControlledShutdownRequest
RequestKeys.OffsetCommitKeycommitOffset请求OffsetCommitRequest
RequestKeys.OffsetFetchKeyconsumer的offset请求OffsetFetchRequest

2.5 Processor响应数据处理

private def processNewResponses() {  
  var curr = requestChannel.receiveResponse(id)  
  while(curr != null) {  
    val key = curr.request.requestKey.asInstanceOf[SelectionKey]  
    curr.responseAction match {  
      case RequestChannel.SendAction => {  
        key.interestOps(SelectionKey.OP_WRITE)  
        key.attach(curr)  
      }  
    }  
  curr = requestChannel.receiveResponse(id)  
  }  
}

我们回到Processor线程类中,processNewRequest()方法是发送请求,那么会调用processNewResponses()来处理Handler提供给客户端的Response,把requestChannel中responseQueue的Response取出来,注册OP_WRITE事件,将数据返回给客户端。

<think>好的,用户之前已经询问过Java分布式学习路线图,现在又进一步询问全套学习资料、学习要点以及对应的B站视频。我需要先回顾之前的回答,看看已经覆盖了哪些内容,避免重复,同时补充更详细的资料和视频链接。 用户可能的需求是希望有一个更具体的学习资源清单,尤其是B站视频,因为视频学习可能更直观。需要确认用户是否有Java基础,但根据之前的提问,用户可能已经有一定基础,所以资料不需要太基础。 要整理各个学习要点的资料,包括书籍、文档、视频和实战项目。视频部分需要找B站的高质量资源,可能需要搜索热门教程,比如尚硅谷、黑马程序员的课程,这些通常在B站上有较高播放量和好评。 注意分阶段,基础、进阶、高阶,和之前的路线图对应,确保结构一致。同时,学习要点需要简明扼要,突出重点技术,比如分布式理论、RPC、微服务、消息队列等。 另外,用户可能希望推荐的资料是中文的,所以书籍和文档链接需要选择中文资源。注意检查链接是否有效,如果有的话最好提供具体的URL,但可能受限于知识截止日期,需要确保推荐的资源仍然存在。 可能还需要补充一些学习建议,比如如何结合理论和实践,项目实战的重要性,以及学习顺序的建议。避免用户陷入过于理论或过于细节的困境,保持学习的效率。 最后,要确保回答结构清晰,分点明确,方便用户逐步学习。可能需要使用列表、表格等方式整理信息,但用户要求用中文,所以保持自然的口语化表达。</think>以下是针对 **Java分布式开发的全套学习资料+要点+B站视频** 整理(附精选资源链接),建议按顺序学习: --- ### 一、核心学习要点与资料对应表 | **技术领域** | **必学内容** | **书籍推荐** | **官方文档** | |----------------------|--------------------------------------|------------------------------------------|----------------------------------------| | **分布式理论** | CAP/BASE、Paxos/Raft算法 | 《数据密集型应用系统设计》 | [Raft算法论文](https://raft.github.io) | | **网络通信** | Netty核心组件、NIO模型 | 《Netty权威指南》 | [Netty官方文档](https://netty.io) | | **RPC框架** | Dubbo服务暴露/调用流程、SPI机制 | 《深度剖析Apache Dubbo核心技术》 | [Dubbo官方文档](https://dubbo.apache.org) | | **微服务架构** | Spring Cloud Alibaba全家桶 | 《Spring Cloud微服务架构进阶》 | [Spring Cloud文档](https://spring.io/projects/spring-cloud) | | **消息队列** | Kafka架构设计、分区与副本机制 | 《Kafka权威指南》 | [Kafka官方文档](https://kafka.apache.org/documentation/) | | **分布式事务** | Seata AT/TCC模式、Saga模式 | 《分布式事务原理与实战》 | [Seata文档](https://seata.io) | --- ### 二、B站精选视频教程(按学习顺序) 1. **分布式基础理论** - [【尚硅谷】分布式系统架构全面指南(7小时)](https://www.bilibili.com/video/BV1R14y1J7mY) - 重点:CAP定理推导、一致性算法动画演示 2. **Netty网络编程** - [【黑马程序员】Netty全套教程(28小时)](https://www.bilibili.com/video/BV1py4y1E7oA) - 含实战:手写WebSocket即时通讯系统 3. **Dubbo源码级教学** - [【图灵学院】Dubbo3.0源码解析(35小时)](https://www.bilibili.com/video/BV1XQ4y1M7h3) - 重点:服务注册发现流程、集群容错机制 4. **Spring Cloud Alibaba** - [【程序员青戈】SpringCloudAlibaba实战(22小时)](https://www.bilibili.com/video/BV1Vq4y1T7Kq) - 项目:电商系统秒杀+熔断限流实现 5. **Kafka深度解析** - [【尚硅谷】Kafka3.x从入门到精通(15小时)](https://www.bilibili.com/video/BV1vr4y1677k) - 含:生产者分区策略、消费者Rebalance机制 6. **Seata分布式事务** - [【享学课堂】Seata源码实战(12小时)](https://www.bilibili.com/video/BV1h44y1z7qM) - 重点:全局锁实现原理、事务回滚日志存储 --- ### 三、配套实战项目推荐 1. **高并发秒杀系统** - 技术栈:Redis+Lua脚本、Sentinel限流、RocketMQ削峰 - [B站跟练视频](https://www.bilibili.com/video/BV1SL4y1V7Zg) 2. **IM即时通讯平台** - 技术栈:Netty自定义协议、Zookeeper服务注册、Protobuf序列化 - [Github开源项目](https://github.com/letcheng/im-system) 3. **物流调度系统** - 技术栈:Seata AT模式、ShardingSphere分库分表、SkyWalking全链路追踪 - [实战课程](https://www.bilibili.com/video/BV1U34y1Y7tS) --- ### 四、避坑指南 1. **学习优先级建议**: ```text 网络编程基础 > 分布式理论 > 微服务开发 > 中间件源码 (先能跑通分布式系统,再研究底层原理) ``` 2. **环境搭建技巧**: ```bash # Windows用户必装 Docker Desktop + WSL2(解决Linux环境问题) # 开发工具 IDEA插件:Alibaba Java Coding Guidelines、Arthas Idea ``` 3. **常见误区**: - 过早研究ZooKeeper源码(先掌握使用场景) - 盲目追求新技术(优先掌握Kafka/RabbitMQ等主流中间件) --- 需要任何方向的深入学习路线或资料补充可以随时告诉我! 🚀
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值