深入理解Kafka设计:高性能服务器模型(2)

版权声明:本文为博主原创文章,未经博主允许不得转载。

摘要

KafkaServer作为整个Broker的核心,它管理着所有的功能模块(例如:ReplicaManagerLogManagerTopicConfigManager等),其中SocketServer便是NIO服务器模块,负责网络通信。接下来我们会以请求处理的角度来分析SocketServer的相关代码。

请求处理流程

1. 启动

当我们通过脚本kafka-broker-start.sh启动Broker时,其调用流程是

Kafka.main→KafkaServerStartable.startup→KafkaServer.startup

,其中KafkaServer.startup方法如下:

val requestChannel = new RequestChannel(numProcessorThreads, maxQueuedRequests)
.....
def startup() {
   .....
   for(i <- 0 until numProcessorThreads) {
      processors(i) = new Processor(.....)
      Utils.newThread("kafka-network-thread-%d-%d".format(port, i), processors(i), false).start()
    }
    .....
    this.acceptor = new Acceptor(host, port, processors, sendBufferSize, recvBufferSize, quotas)
    Utils.newThread("kafka-socket-acceptor", acceptor, false).start()
    acceptor.awaitStartup
    .....
  }

可以看到,SocketServer不仅创建了RequetsChannel,而且创建并启动了1个Acceptor线程和N个Processor线程。

2. 建立新连接

Acceptor启动后主要职责就是负责监听和建立新连接。

private[kafka] class Acceptor(.....) extends AbstractServerThread(connectionQuotas) {
  val serverChannel = openServerSocket(host, port)
  def run() {
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);//关注ACCEPT事件
    .....
    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) {
            .....
            accept(key, processors(currentProcessor))//指定某一个Processor
            .....
            currentProcessor = (currentProcessor + 1) % processors.length//轮询下一个Processor
          .....
        }
      }
    }
   .....
  }

建立连接以后,以轮询的方式将新连接均衡的分配给每一个Processor

  def accept(key: SelectionKey, processor: Processor) {
    val serverSocketChannel = key.channel().asInstanceOf[ServerSocketChannel]
    val socketChannel = serverSocketChannel.accept()//获取新连接
    try {
      .....
      socketChannel.configureBlocking(false)//设置非阻塞模式
      socketChannel.socket().setTcpNoDelay(true)//打开Nagle's算法,禁止大量小包发送
      socketChannel.socket().setSendBufferSize(sendBufferSize)
      .....
      processor.accept(socketChannel)
    } catch {
      .....
      close(socketChannel)
    }
  }

实际上AcceptorSocketChannel放入Processor的新连接处理队列newConnections中。

  def accept(socketChannel: SocketChannel) {
    newConnections.add(socketChannel)
    wakeup()//唤醒阻塞在select()上的selector
  }

3. 连接处理

Processor线程主循环在不断的处理连接的读写。

override def run() {
    .....
    while(isRunning) {//主循环
      // 从newConnections队列获取并处理新连接
      configureNewConnections()
      // 从相应的response队列获取Response并处理
      processNewResponses()
      .....
      val ready = selector.select(300)//获取就绪事件
      .....
      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 {
            ......
            close(key)
          }
        }
      }
      maybeCloseOldestConnection//检查空闲的连接
    }
    .....
    closeAll()
    .....
  }
  
  private val connectionsMaxIdleNanos = connectionsMaxIdleMs * 1000 * 1000
  private var currentTimeNanos = SystemTime.nanoseconds
  private val lruConnections = new util.LinkedHashMap[SelectionKey, Long]//连接与最近访问的时间戳Map
  private var nextIdleCloseCheckTime = currentTimeNanos + connectionsMaxIdleNanos
  
  private def maybeCloseOldestConnection {
    if(currentTimeNanos > nextIdleCloseCheckTime) {//是否该检查空闲连接了
      if(lruConnections.isEmpty) {
        nextIdleCloseCheckTime = currentTimeNanos + connectionsMaxIdleNanos
      } else {
        val oldestConnectionEntry = lruConnections.entrySet.iterator().next()//获取最早的连接
        val connectionLastActiveTime = oldestConnectionEntry.getValue
        nextIdleCloseCheckTime = connectionLastActiveTime + connectionsMaxIdleNanos
        if(currentTimeNanos > nextIdleCloseCheckTime) {//检查连接是否空闲
          val key: SelectionKey = oldestConnectionEntry.getKey
          .....
          close(key)
        }
      }
    }
  }

Processor不仅要处理新连接,而且也处理旧连接上的数据读写和关闭,并用一段标准的NIO事件处理代码来处理相应事件。同时还有重要的一步,那就是检查和清除空闲连接(超过10分钟没有读操作的连接),以免浪费带宽和内存。

//获取新连接,并在selector注册OP_READ事件
private def configureNewConnections() {
    while(newConnections.size() > 0) {
      val channel = newConnections.poll()
      .....
      channel.register(selector, SelectionKey.OP_READ)
    }
}

//获取Response,根据结果在selector注册不同的事件
private def processNewResponses() {
    var curr = requestChannel.receiveResponse(id)//获取对应的response队列
    while(curr != null) {//如果有大量的response,一直要清空为止
      val key = curr.request.requestKey.asInstanceOf[SelectionKey]
      try {
        curr.responseAction match {
          case RequestChannel.NoOpAction => {
            //不需要返回响应给客户端,等待可读事件,然后读取后续数据
            ..... 
            key.interestOps(SelectionKey.OP_READ)
            key.attach(null)
          }
          case RequestChannel.SendAction => {
            .....
            //等待可写事件,然后将响应写回客户端
            key.interestOps(SelectionKey.OP_WRITE)
            key.attach(curr)
          }
          case RequestChannel.CloseConnectionAction => {
            .....
            //没有后续操作,直接关闭连接
            close(key)
          }
          case responseCode => throw new KafkaException("No mapping found for response code " + responseCode)
        }
      } catch {
        case e: CancelledKeyException => {
          debug("Ignoring response for closed socket.")
          close(key)
        }
      } finally {
        curr = requestChannel.receiveResponse(id)//获取下一个response
      }
    }
  }

再来看看read和write事件的处理过程:

def read(key: SelectionKey) {
    lruConnections.put(key, currentTimeNanos)//更新连接最近访问时间戳
    val socketChannel = channelFor(key)
    var receive = key.attachment.asInstanceOf[Receive]
    .....
    val read = receive.readFrom(socketChannel)//根据Kafka消息协议,从中解析出请求数据
    .....
    if(read < 0) {//读取不到数据,可能客户端已经断掉连接
      close(key)
    } else if(receive.complete) {//如果解析完成
      val req = RequestChannel.Request(.....)//根据requestId封装成不同类型的Request对象
      requestChannel.sendRequest(req)//投入RequestQueue中
      key.attach(null)
      //已经收到一个完整的请求了,先处理这个请求,因此不再关心可读,也没必要立即wakeup selector
      key.interestOps(key.interestOps & (~SelectionKey.OP_READ))
    } else {//解析尚未完成,继续等待数据可读
      key.interestOps(SelectionKey.OP_READ)
      wakeup()
    }
  }
  
  def write(key: SelectionKey) {
    val socketChannel = channelFor(key)
    val response = key.attachment().asInstanceOf[RequestChannel.Response]
    val responseSend = response.responseSend
    .....
    val written = responseSend.writeTo(socketChannel)//将响应数据写回
    .....
    if(responseSend.complete) {//响应数据写回完成
      key.attach(null)
      key.interestOps(SelectionKey.OP_READ)//关注下一次数据请求
    } else {
      key.interestOps(SelectionKey.OP_WRITE)//否则继续等待连接可写
      wakeup()//立即唤醒selector
    }
  }

由于Request和Response作为Network层与API层的交互对象,所以read的一个重要工作就是将请求数据解析出来并封装成Request对象,而write就是将业务返回的Response对象写回。

以上便是网络层处理数据读写的过程。接下来介绍API层处理Request和返回Response的过程。

4. 业务逻辑处理

为了不影响网络层的吞吐量,Kafka将繁重的逻辑处理独立出来作为API层,交给一组KafkaRequestHandler线程来完成的。这种设计与Netty是一致的。

class KafkaRequestHandlerPool(val brokerId: Int,
                              val requestChannel: RequestChannel,
                              val apis: KafkaApis,
                              numThreads: Int) extends Logging with KafkaMetricsGroup {
  ..... 
  val threads = new Array[Thread](numThreads)
  val runnables = new Array[KafkaRequestHandler](numThreads)
  for(i <- 0 until numThreads) {
    runnables(i) = new KafkaRequestHandler(i, brokerId, aggregateIdleMeter, numThreads, requestChannel, apis)
    threads(i) = Utils.daemonThread("kafka-request-handler-" + i, runnables(i))
    threads(i).start()//启动所有handler线程
  }
  
  def shutdown() {
    info("shutting down")
    for(handler <- runnables)
      handler.shutdown//发送一个AllDone的Request,通知handler线程结束
    for(thread <- threads)
      thread.join//等待所有的handler线程都执行完毕才结束
    info("shut down completely")
  }
}

KafkaRequestHandlerPool管理所有handler线程的创建和关闭,同时也保证了正常情况下业务逻辑执行完成后才结束handler线程,不会导致不完整的业务。
所有的KafkaRequestHandler线程都从唯一的RequetsChannel.RequestQueue中争抢Request,并交给KafakApis中相应的逻辑来处理。

def run() {
    while(true) {//handler主循环
      try {
        var req : RequestChannel.Request = null
        while (req == null) {
          .....
          req = requestChannel.receiveRequest(300)//从RquestQueue中获取request
          .....
        }
        if(req eq RequestChannel.AllDone) {//KafkaRequestHandlerPool的关闭通知
            ......
          return
        }
        .....
        apis.handle(req)//交给KafkaApis对应的逻辑去处理
      } catch {
        .....
      }
    }
 }

可以看到KafkaRequestHandler只有当收到关闭通知后,才会结束线程,否则一直执行下去。

KafkaApis.handle作为所有业务逻辑的入口,会根据requestId将Request分发给相应的逻辑代码来处理。

def handle(request: RequestChannel.Request) {
    try{
      .....
      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)
      .....
    } finally
      .....
  }

每个业务逻辑处理完成以后,会根据具体情况判断是否需要返回Response并放入ResponseQueue中,由相应的Processor继续处理。

总结

至此,我们通过请求处理的角度分析了Kafka SocketServer的相关代码,并了解到其网络层与API层的设计和工作原理,从中也学习到如何利用Java Nio实现服务器的方法和一些细节。

参考文档

  1. http://ju.outofmemory.cn/entry/124628
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容

  • 摘要 Kafka作为一个高性能的消息中间件,其高效的原因可以归纳为以下这几个方面: 高性能服务器模型 PageCa...
    小吴酱呵呵阅读 634评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • kafka的定义:是一个分布式消息系统,由LinkedIn使用Scala编写,用作LinkedIn的活动流(Act...
    时待吾阅读 5,234评论 1 15
  • 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下: 以时间复杂度为O...
    高广超阅读 12,712评论 8 167
  • 有一次和朋友一起吃饭,问他:“钱是靠赚的还是攒的?” 他毫不犹豫地回答我:“当然是靠攒,赚钱很难,攒钱很容易,所以...
    财商大叔阅读 430评论 0 7