kafka的线程模型之一

网上有不少关于kafka架构的博客,但浏览下来大多属于层次比较高,细节比较少的介绍.因此我想要另辟蹊径,讲一讲一台向外提供服务的broker,有哪写线程,每个线程从源码的角度来说在何时哪个类中初始化,分别负责什么,线程之间又是通过什么通信的?


方法很简单,jstack + pid,后可以得到所有线程的stack以及当前状态.由于线程名称是kafka自己决定的,根据线程名称的前缀在源码工程中使用全局搜索就可以很方便地找到初始化的位置,接下来想怎么看就随意了.

很典型的线程,可以看到有这些:

首先是kafka server,使用的是大名鼎鼎的reactor模式,这个模式也是老生常谈了.

kafkaServer.startup()->
socketServer = new SocketServer(config, metrics, kafkaMetricsTime)->

for (i <- processorBeginIndex until processorEndIndex)
     processors(i) = newProcessor(i, connectionQuotas, protocol)

val acceptor = new Acceptor(endpoint, sendBufferSize, recvBufferSize, brokerId,
     processors.slice(processorBeginIndex, processorEndIndex), connectionQuotas)

一路追溯到new SocketServer中,就可以直接看到这两种线程的初始化过程了. 上面的源码可以看到,这里初始化了n个processor线程和1个acceptor线程.acceptor线程负责接收tcp连接,将收到的fd交给processor处理.典型的reactor模式,不了解的同学可以bing或者google一下,这里不多赘述了.

两个线程间通过linkedBlockingQueue进行通信,可以看作是一个单生产者多消费者的结构.

在jstack中可以查到这两种线程,接收连接的线程:

359 "kafka-socket-acceptor-PLAINTEXT-9093" #43 prio=5 os_prio=0 tid=0x00007fadb4cdc000 nid=0x11a74 runnable [0x00007fac1f7f8000]
360    java.lang.Thread.State: RUNNABLE
361     at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
362     at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
363     at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
364     at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
365     - locked <0x00000000c88a70e8> (a sun.nio.ch.Util$2)
366     - locked <0x00000000c88a7090> (a java.util.Collections$UnmodifiableSet)
367     - locked <0x00000000c88a70a0> (a sun.nio.ch.EPollSelectorImpl)
368     at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
369     at kafka.network.Acceptor.run(SocketServer.scala:260)
370     at java.lang.Thread.run(Thread.java:745)

分发请求的线程:
484 "kafka-network-thread-12-PLAINTEXT-0" #35 prio=5 os_prio=0 tid=0x00007fadb4cb5000 nid=0x11a6c runnable [0x00007fac7c2fd000]
485    java.lang.Thread.State: RUNNABLE
486     at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
487     at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
488     at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
489     at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
490     - locked <0x00000000c88a86f8> (a sun.nio.ch.Util$2)
491     - locked <0x00000000c88a86a0> (a java.util.Collections$UnmodifiableSet)
492     - locked <0x00000000c88a86b0> (a sun.nio.ch.EPollSelectorImpl)
493     at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
494     at org.apache.kafka.common.network.Selector.select(Selector.java:470)
495     at org.apache.kafka.common.network.Selector.poll(Selector.java:286)
496     at kafka.network.Processor.poll(SocketServer.scala:476)
497     at kafka.network.Processor.run(SocketServer.scala:416)
498     at java.lang.Thread.run(Thread.java:745)

可以看到的是这些连接都阻塞在epollwait上,是很典型的IO线程.重点讨论一下network-thread,这是一个典型的eventloop线程,通过一个while循环地处理IO请求,while循环中有6项任务.

   while (isRunning) {
      try {
        // setup any new connections that have been queued up
        configureNewConnections()
        // register any new responses for writing
        processNewResponses()
        poll()
        processCompletedReceives()
        processCompletedSends()
        processDisconnected()
      }

1.查看与acceptor进行通信的队列,上面挂的是新收到的连接,这里network-thread将他们注册进自己的selector上.

2.查看有没有什么消息是要发送给其他broker或者client的,有的话那就发送.并将其放在一个responseOnFlight的队列中.

3.这个poll()函数里边就是简单地selector.poll(300),这些先前被注册的channel有没有消息发送过来,如果有就立刻返回,如果没有等300毫秒,

4.把收到的信息封装成一个kafkaRequest,挂到一个叫requestQueue的ArrayBlockingQueue上去,这个requestQueue是kafka-request-Handler线程与network-thread进行线程间通信的队列

5.查看哪些reponse是发完的了,如果发现这些resonse发完,就将其从responseOnfFlight队列中删除,这里解释一下,由于采用的是nio模式,上层传递会一个buffer给selector去发送,这个发送的过程是异步的.如果和对端的网络通信不好,那么这个buffer的消息就会一直积压着发不出去,所以要用一个队列来标识哪些请求时发出去了,哪些请求还卡在buffer中.

6.处理于其他broker或者是client连接断开的情况.

这就是while循环中的6项任务.这个network-thread就是一个专门负责IO的线程,这里不像netty或者muduo一样允许把一部分业务逻辑放在IO线程中处理.kafka的业务逻辑大多涉及到与磁盘的交互,处理时间不确定.所以不好放在网络IO线程中.所以kafka又开辟了一个业务处理的线程池.也就是第4点提到的kafka-Request-Handler.

处理请求的线程:

133 "kafka-request-handler-7" #62 daemon prio=5 os_prio=0 tid=0x00007fadb4d69800 nid=0x11a87 waiting on condition [0x00007fac1e2e5000]
134    java.lang.Thread.State: TIMED_WAITING (parking)
135     at sun.misc.Unsafe.park(Native Method)
136     - parking to wait for  <0x00000000c88a6a78> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
137     at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
138     at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
139     at java.util.concurrent.ArrayBlockingQueue.poll(ArrayBlockingQueue.java:418)
140     at kafka.network.RequestChannel.receiveRequest(RequestChannel.scala:238)
141     at kafka.server.KafkaRequestHandler.run(KafkaRequestHandler.scala:48)
142     at java.lang.Thread.run(Thread.java:745)

这是一个线程池,处理逻辑的核心就是把requestQueue中的请求拿出来给kafkaApis去处理.在我之前的文章中提到过,kafkaApis是kafka业务处理的核心,打开源码来到这个类下,可以看到大段的match...case...语句,根据request的种类调用不同的处理逻辑.

以上提到的三种线程,可以认为是kafka的骨架,撑起了kafka的运转.接下来介绍的线程好像kafka的血肉,这些线程使kafka功能更加完善,迅速,容错性更高.

首当其冲的就是scheduler线程.

512 "kafka-scheduler-2" #34 daemon prio=5 os_prio=0 tid=0x00007fadb4bce000 nid=0x11a6a waiting on condition [0x00007fac94131000]
513    java.lang.Thread.State: WAITING (parking)
514     at sun.misc.Unsafe.park(Native Method)
515     - parking to wait for  <0x00000000c85ce6c8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
516     at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
517     at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
518     at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1088)
519     at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
520     at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
521     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
522     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
523     at java.lang.Thread.run(Thread.java:745)
从调用栈可以直接看出,这一次kafka没有再自己写线程池了,而是直接使用juc的ScheduledThreadPool作为线程池.由此可知,这个线程池就是用来执行定时任务的.那么执行哪些定时任务呢?

在ReplicaManager中,有两段向scheduler注册任务的代码

  def startHighWaterMarksCheckPointThread() = {
    //定期地将watermark的信息flush进入磁盘
    if(highWatermarkCheckPointThreadStarted.compareAndSet(false, true))
      scheduler.schedule("highwatermark-checkpoint", checkpointHighWatermarks, period = config.replicaHighWatermarkCheckpointIntervalMs, unit = TimeUnit.MILLISECONDS)
  }


  def startup() {
    // start ISR expiration thread
    scheduler.schedule("isr-expiration", maybeShrinkIsr, period = config.replicaLagTimeMaxMs, unit = TimeUnit.MILLISECONDS)
    scheduler.schedule("isr-change-propagation", maybePropagateIsrChanges, period = 2500L, unit = TimeUnit.MILLISECONDS)
  }

依次来解释这些任务负责写什么,首先是replicaManager,看过源码的话,你会发现会频繁出现highWaterMark,LEO这些词.这其实是kafka用来保证ISR提出的两个概念,强烈推荐这篇文章,很好的介绍了这两个概念

Kafka 0.11版的副本备份机制

需要注意的是waterMark机制是有缺陷的,在0.11版本之后kafka就放弃了这种机制.转而使用了(epoch,offset)的机制.为什么会采取epoch这种机制?这就涉及到一个新的知识点--Raft协议的log备份.有兴趣的同学可以去学习一下Raft协议.但在这里由于我分析的0.10.0版本所以暂且不讨论Raft协议,并假设同学们已经看过上面那篇文章对waterMark有了基本的了解.

如果你打开一个broker的根目录,你会发除了topic目录外还有现有三个文件,cleaner-offset-checkpoint,recovery-point-offset-checkpoint,replication-offset-checkpoint.保存了各种checkPoint.这里的highwatermark-checkpoint操作的其实就是replication-offset-checkpoint,将hw的值写入这个文件中.

而下边的isr-expiration就是检查isr中是否有broker不能保持同步,判断的依据有两条:

1.broker在某一时间内没有更新过自己的LEO,用于broker crash,down或者stuck

2.broker在某一时间内没有跟上leader的LEO,用于broker slow

isr-change-propagation 就是在检查是否有isr发生改变,如果有就通知所有的broker.

在logManager中,有两段向scheluer注册任务的代码

def startup() {
    /* Schedule the cleanup task to delete old logs */
    if(scheduler != null) {
      info("Starting log cleanup with a period of %d ms.".format(retentionCheckMs))
      scheduler.schedule("kafka-log-retention",
                         cleanupLogs,
                         delay = InitialTaskDelayMs,
                         period = retentionCheckMs,
                         TimeUnit.MILLISECONDS)
      info("Starting log flusher with a default period of %d ms.".format(flushCheckMs))
      scheduler.schedule("kafka-log-flusher", 
                         flushDirtyLogs, 
                         delay = InitialTaskDelayMs, 
                         period = flushCheckMs, 
                         TimeUnit.MILLISECONDS)
      scheduler.schedule("kafka-recovery-point-checkpoint",
                         checkpointRecoveryPointOffsets,
                         delay = InitialTaskDelayMs,
                         period = flushCheckpointMs,
                         TimeUnit.MILLISECONDS)
    }


def createLog(topicAndPartition: TopicAndPartition, config: LogConfig): Log = {
      ......
      log = new Log(dir, 
                    config,
                    recoveryPoint = 0L,
                    scheduler,
                    time)
      ......
}

这里处理的两个文件,其实就是之前提到的cleaner-offset-checkpoint,recovery-point-offset-checkpoint.

1.cleanuplogs任务

"cleanuplogs"并不是清理过期的日志,而是合并可以合并的日志.比如两个相同的key,他们有不同的value只需保存时间靠后的value即可.

合并后的segment的段名称使用要合并的第一个段的文件段名称,合并后段的lastModified使用要合并的最后一个段的lastModified.每一次合并后,都会用新合并的文件替换未合并的文件,名称会做如下变化.可以看作是用一个临时文件,替换了一个原来的文件.

{offset}.log.cleaned ->{offset}.log.swap->{offset}.log
每次LogCleaner完成一次清理后,就把清理后截至的offset写入到对应的cleaner-offset-checkpoint文件中

2.flushDirtyLogs任务
这个任务会定时执行,只要在执行时有数据就会调用channel.force()写入脏数据,这里的脏数据是什么?根据我阅读的源码,其实就是用户发来的producer数据以及index数据.由于kafka用的io.channel以及mmap来保存log,index,timeidex数据,理论上来说这些api不需要显式地调用flush.我理解为这是为了保险起见,或是有什么我所不知道的意外情况.导致每次写数据可能有不及时更新offset的情况.这个问题欢迎留言讨论.

3.checkpointRecoveryPointOffsets任务
在根目录下记录该目录下每个TopicAndPartition和log.recoveryPoint的值,recoveryPoint表示需要恢复的偏移值.checkpointRecoveryOffset这个任务就是 定时把本根目录下TopicAndPartition和log.recoveryPoint的键值对写入到recovery-point-offset-checkpoint文件中





  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值