kafka的线程模型之一
来源:互联网 发布:威海矩阵直销软件价格 编辑:程序博客网 时间:2024/05/22 03:38
网上有不少关于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: RUNNABLE361 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: RUNNABLE486 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.每一次合并后,都会用新合并的文件替换未合并的文件,名称会做如下变化.可以看作是用一个临时文件,替换了一个原来的文件.
- kafka的线程模型之一
- kafka的线程模型之二
- COM线程模型之一[译]
- kafka线程模型之三 QuotaManager
- Kafka的消费编程模型
- kafka源码解析之一kafka诞生的背景
- kafka之一个简单的producer
- Kafka的Repilica分配策略之一
- 线程的基本概念之一
- Kafka学习(五):Kafka的消费编程模型
- Kafka学习(六):Kafka的生产者编程模型
- COM的线程模型
- tomcat的线程模型
- Win32的线程模型
- WPF的线程模型
- os的线程模型
- WPF的线程模型
- Solaris的线程模型
- 5.5
- Oracle 11g,myeclipse2013 2014 PS cc2017 安装包下载
- HDU 1711-Number Sequence
- 顺序表
- Object Pascal 实例代码
- kafka的线程模型之一
- 贝尔数列_集合的划分
- 基础教学 | API 是如何工作的
- 对百度自动推送代码的一点看法
- DNS & CDN & HTTPDNS 原理简析
- 【2017宁波联考】生成树
- 实现一个自己的react-redux
- 浅谈Arrays.asList()方法的使用
- Dubbo分布式日志追踪,多线程不能获取窜ID和IP问题