【原创】Kafka producer原理 (Scala版同步producer)
本文分析的Kafka代码为kafka-0.8.2.1。另外,由于Kafka目前提供了两套Producer代码,一套是Scala版的旧版本;一套是Java版的新版本。虽然Kafka社区极力推荐大家使用Java版本的producer,但目前很多已有的程序还是调用了Scala版的API。今天我们就分析一下旧版producer的代码。

val requestRequiredAcksOpt = parser.accepts("request-required-acks", "The required acks of the producer requests")
.withRequiredArg
.describedAs("request required acks")
.ofType(classOf[java.lang.Integer])
.defaultsTo(0) // 此处默认设置为0
do {
message = reader.readMessage() // 从LineMessageReader类中读取消息。该类接收键盘输入的一行文本作为消息
if(message != null)
producer.send(message.topic, message.key, message.message) // key默认是空,如果想要指定,需传入参数parse.key=true,默认key和消息文本之间的分隔符是'\t'
} while(message != null) // 循环接收消息,除非Ctrl+C或其他其他引发IOException操作跳出循环
下面代码是Producer.scala中的发送方法:
def send(messages: KeyedMessage[K,V]*) {
lock synchronized {
if (hasShutdown.get) //如果producer已经关闭了抛出异常退出
throw new ProducerClosedException
recordStats(messages //更新producer统计信息
sync match {
case true => eventHandler.handle(messages) //如果是同步发送,直接使用DefaultEventHandler的handle方法发送
case false => asyncSend(messages) // 否则,使用ayncSend方法异步发送消息——本文不考虑这种情况
}
}
}
由上面的分析可以看出,真正的发送逻辑其实是由DefaultEventHandler类的handle方法来完成的。下面我们重点分析一下这个类的代码结构。
五、DefaultEventHandler与消息发送
这个类的handler方法可以同时支持同步和异步的消息发送。我们这里只考虑同步的代码路径。下面是消息发送的完整流程图:
以下代码是发送消息的核心逻辑:
while (remainingRetries > 0 && outstandingProduceRequests.size > 0) { // 属性message.send.max.retries指定了消息发送的重试次数,而outstandingProducerRequests就是序列化之后待发送的消息集合
topicMetadataToRefresh ++= outstandingProduceRequests.map(_.topic) //将待发送消息所属topic加入到待刷新元数据的topic集合
if (topicMetadataRefreshInterval >= 0 &&
SystemTime.milliseconds - lastTopicMetadataRefreshTime > topicMetadataRefreshInterval) { //查看是否已过刷新元数据时间间隔
Utils.swallowError(brokerPartitionInfo.updateInfo(topicMetadataToRefresh.toSet, correlationId.getAndIncrement)) // 更新topic元数据信息
sendPartitionPerTopicCache.clear() //如果消息key是空,代码随机选择一个分区并记住该分区,以后该topic的消息都会往这个分区里面发送。sendPartitionPerTopicCache就是这个缓存
topicMetadataToRefresh.clear //清空待刷新topic集合
lastTopicMetadataRefreshTime = SystemTime.milliseconds
}
outstandingProduceRequests = dispatchSerializedData(outstandingProduceRequests) // 真正的消息发送方法
if (outstandingProduceRequests.size > 0) { // 如果还有未发送成功的消息
info("Back off for %d ms before retrying send. Remaining retries = %d".format(config.retryBackoffMs, remainingRetries-1))
// back off and update the topic metadata cache before attempting another send operation
Thread.sleep(config.retryBackoffMs) // 等待一段时间并重试
// get topics of the outstanding produce requests and refresh metadata for those
Utils.swallowError(brokerPartitionInfo.updateInfo(outstandingProduceRequests.map(_.topic).toSet, correlationId.getAndIncrement))
sendPartitionPerTopicCache.clear()
remainingRetries -= 1 // 更新剩余重试次数
producerStats.resendRate.mark()
}
}
下面具体说说各个子模块的代码逻辑:
serializedMessages +=
new KeyedMessage[K,Message](
topic = e.topic,
key = e.key,
partKey = e.partKey,
message = new Message(bytes = encoder.toBytes(e.message))) // new Message时没有指定key
构建完KeyedMessage之后返回对应的消息集合即可。
def updateInfo(topics: Set[String], correlationId: Int) {
var topicsMetadata: Seq[TopicMetadata] = Nil // TopicMetadata = topic信息+ 一组PartitionMetadata (partitionId + leader + AR + ISR)
val topicMetadataResponse = ClientUtils.fetchTopicMetadata(topics, brokers, producerConfig, correlationId) //构造TopicMetadataRequest并随机排列所有broker,然后从第一个broker开始尝试发送请求。一旦成功就终止后面的请求发送尝试。
topicsMetadata = topicMetadataResponse.topicsMetadata //从response中取出zookeeper中保存的对应topic元数据信息
// throw partition specific exception
topicsMetadata.foreach(tmd =>{
trace("Metadata for topic %s is %s".format(tmd.topic, tmd))
if(tmd.errorCode == ErrorMapping.NoError) {
topicPartitionInfo.put(tmd.topic, tmd) //更新到broker的topic元数据缓存中
} else
warn("Error while fetching metadata [%s] for topic [%s]: %s ".format(tmd, tmd.topic, ErrorMapping.exceptionFor(tmd.errorCode).getClass))
tmd.partitionsMetadata.foreach(pmd =>{
if (pmd.errorCode != ErrorMapping.NoError && pmd.errorCode == ErrorMapping.LeaderNotAvailableCode) {
warn("Error while fetching metadata %s for topic partition [%s,%d]: [%s]".format(pmd, tmd.topic, pmd.partitionId,
ErrorMapping.exceptionFor(pmd.errorCode).getClass))
} // any other error code (e.g. ReplicaNotAvailable) can be ignored since the producer does not need to access the replica and isr metadata
})
})
producerPool.updateProducer(topicsMetadata)
}
关于上面代码中的最后一行, 我们需要着重说一下。每个producer应用程序都会保存一个producer池对象来缓存每个broker上对应的同步producer实例。具体格式为brokerId -> SyncProducer。SyncProducer表示一个同步producer,其主要的方法是send,支持两种请求的发送:ProducerRequest和TopicMetadataRequest。前者是发送消息的请求,后者是更新topic元数据信息的请求。为什么需要这份缓存呢?我们知道,每个topic分区都应该有一个leader副本在某个broker上,而只有leader副本才能接收客户端发来的读写消息请求。对producer而言,即只有这个leader副本所在的broker才能接收ProducerRequest请求。在发送消息时候,我们会首先找出这个消息要发给哪个topic,然后发送更新topic元数据请求给任意broker去获取最新的元数据信息——这部分信息中比较重要的就是要获取topic各个分区的leader副本都在哪些broker上,这样我们稍后会创建连接那些broker的阻塞通道(blocking channel)去实现真正的消息发送。Kafka目前的做法就是重建所有topic分区的leader副本所属broker上对应的SyncProducer实例——虽然我觉得这样实现有线没有必要,只更新消息所属分区的缓存信息应该就够了(当然,这只是我的观点,如果有不同意见欢迎拍砖)。以下是更新producer缓存的一些关键代码:
val newBrokers = new collection.mutable.HashSet[Broker]
topicMetadata.foreach(tmd => {
tmd.partitionsMetadata.foreach(pmd => {
if(pmd.leader.isDefined) //遍历topic元数据信息中的每个分区元数据实例,如果存在leader副本的,添加到newBrokers中以备后面更新缓存使用
newBrokers+=(pmd.leader.get)
})
})
lock synchronized {
newBrokers.foreach(b => { //遍历newBrokers中的每个broker实例,如果在缓存中已经存在,直接关闭掉然后创建一个新的加入到缓存中;否则直接创建一个加入
if(syncProducers.contains(b.id)){
syncProducers(b.id).close()
syncProducers.put(b.id, ProducerPool.createSyncProducer(config, b))
} else
syncProducers.put(b.id, ProducerPool.createSyncProducer(config, b))
})
}
前面说了,如果只发送一条消息的话,其实真正需要更新的分区leader副本所述broker对应的SyncProducer实例只有一个,但目前的代码中会更新所有分区,不知道Java版本的producer是否也是这样实现,这需要后面继续调研!
Topic | 分区 | Leader副本所在的broker ID |
test-topic | P0 | 0 |
test-topic | P1 | 1 |
test-topic | P2 | 3 |
如果基于这样的配置,假定我们使用producer API一次性发送4条消息,分别是M1,M2, M3和M4。现在就可以开始分析代码了,首先从消息分组及整理开始:
消息 | 要被发送到的分区ID | 该分区leader副本所在broker ID |
M1 | P0 | 0 |
M2 | P0 | 0 |
M3 | P1 | 1 |
M4 | P2 | 3 |
val index = Utils.abs(Random.nextInt) % availablePartitions.size // 随机确定broker id
val partitionId = availablePartitions(index).partitionId
sendPartitionPerTopicCache.put(topic, partitionId) // 加入缓存中以便后续使用
def startup() {
...
// 创建一个请求处理的线程池,在构造时就会开启多个线程准备接收请求
requestHandlerPool = new KafkaRequestHandlerPool(config.brokerId, socketServer.requestChannel, apis, config.numIoThreads)
...
} class KafkaRequestHandlerPool {
...
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() // 启动每个请求处理线程
}
...
}
KafkaRequestHandler实际上是一个Runnable,它的run核心方法中以while (true)的方式调用api.handle(request)不断地接收请求处理,如下面的代码所示:
class KafkaRequestHandler... extends Runnable {
...
def run() {
...
while (true) {
...
apis.handle(request) // 调用apis.handle等待请求处理
}
...
}
...
}
在KafkaApis中handle的主要作用就是接收各种类型的请求。本文只关注ProducerRequest请求:
def handle(request: RequestChannel.Request) {
...
request.requestId match {
case RequestKeys.ProduceKey => handleProducerOrOffsetCommitRequest(request) // 如果接收到ProducerRequest交由handleProducerOrOffsetCommitRequest处理
case ...
}
...
}
如此看来,核心的方法就是handleProducerOrOffsetCommitRequest了。这个方法之所以叫这个名字,是因为它同时可以处理ProducerRequest和OffsetCommitRequest两种请求,后者其实也是一种特殊的ProducerRequest。从Kafka 0.8.2之后kafka使用一个特殊的topic来保存提交位移(commit offset)。这个topic名字是__consumer_offsets。本文中我们关注的是真正的ProducerRequest。下面来看看这个方法的逻辑,如下图所示:
整体逻辑看上去非常简单,如下面的代码所示:
def handleProducerOrOffsetCommitRequest(request: RequestChannel.Request) {
...
val localProduceResults = appendToLocalLog(produceRequest, offsetCommitRequestOpt.nonEmpty) // 将消息追加写入本地提交日志
val numPartitionsInError = localProduceResults.count(_.error.isDefined) // 计算是否存在发送失败的分区
if(produceRequest.requiredAcks == 0) { // request.required.acks = 0时的代码路径
if (numPartitionsInError != 0) {
info(("Send the close connection response due to error handling produce request " +
"[clientId = %s, correlationId = %s, topicAndPartition = %s] with Ack=0")
.format(produceRequest.clientId, produceRequest.correlationId, produceRequest.topicPartitionMessageSizeMap.keySet.mkString(",")))
requestChannel.closeConnection(request.processor, request) // 关闭底层Socket以告知客户端程序有发送失败的情况
} else {
...
}
} else if (produceRequest.requiredAcks == 1 || // request.required.acks = 0时的代码路径,当然还有其他两个条件
produceRequest.numPartitions <= 0 ||
numPartitionsInError == produceRequest.numPartitions) {
val response = offsetCommitRequestOpt.map(_.responseFor(firstErrorCode, config.offsetMetadataMaxSize))
.getOrElse(ProducerResponse(produceRequest.correlationId, statuses))
requestChannel.sendResponse(new RequestChannel.Response(request, new BoundedByteBufferSend(response))) // 发送response给客户端
} else { // request.required.acks = -1时的代码路径
// create a list of (topic, partition) pairs to use as keys for this delayed request
val producerRequestKeys = produceRequest.data.keys.toSeq
val statuses = localProduceResults.map(r =>
r.key -> DelayedProduceResponseStatus(r.end + 1, ProducerResponseStatus(r.errorCode, r.start))).toMap
val delayedRequest = new DelayedProduce(...) // 此时需要构造延时请求进行处理,此段逻辑比较复杂,需要理解Purgatory的概念,本文暂不考虑
...
}
由上面代码可见,无论request.required.acks是何值,都需要首先将待发送的消息集合追加写入本地的提交日志中。此时如何按照默认值是是0的情况,那么这写入日志后需要判断下所有消息是否都已经发送成功了。如果出现了发送错误,那么就将关闭连入broker的Socket Server以通知客户端程序错误的发生。现在的关键是追加写是如何完成的?即方法appendToLocalLog如何实现的?该方法整体逻辑流程图如下图所示:
由于逻辑很直观,不对代码做详细分析,不过值得关注的是这个方法会捕获很多异常:
异常名称 | 具体含义 | 异常处理 |
KafakStorageException | 这可能是不可恢复的IO错误 | 既然无法恢复,则终止该broker上JVM进程 |
InvalidTopicException | 显式给__consumer_offsets topic发送消息就会有这个异常抛出,不要这么做,因为这是内部topic | 将InvalidTopicException封装进ProduceResult返回 |
UnknownTopicOrPartitionException | topic或分区不在该broker上时抛出该异常 | 将UnknownTopicOrPartitionException封装进ProduceResult返回 |
NotLeaderForPartitionException | 目标分区的leader副本不在该broker上 | 将NotLeaderForPartitionException封装进ProduceResult返回 |
NotEnoughReplicasException | 只会出现在request.required.acks=-1且ISR中的副本数不满足min.insync.replicas指定的最少副本数时会抛出该异常 | 将NotEnoughReplicasException封装进ProduceResult返回 |
其他 | 处理ProducerRequest时发生的其他异常 | 将对应异常封装进ProduceResult返回 |
okay,貌似现在我们就剩下最后一个主要的方法没说了。分析完这个方法之后整个producer发送消息的流程应该就算是完整地走完了。最后的这个方法就是Partition的appendMessagesToLeader,其主要代码如下:
def appendMessagesToLeader(messages: ByteBufferMessageSet, requiredAcks: Int=0) = {
inReadLock(leaderIsrUpdateLock) {
val leaderReplicaOpt = leaderReplicaIfLocal() // 判断目标分区的leader副本是否在该broker上
leaderReplicaOpt match {
case Some(leaderReplica) => // 如果leader副本在该broker上
val log = leaderReplica.log.get // 获取本地提交日志文件句柄
val minIsr = log.config.minInSyncReplicas
val inSyncSize = inSyncReplicas.size // Avoid writing to leader if there are not enough insync replicas to make it safe
if (inSyncSize < minIsr && requiredAcks == -1) { //只有request.required.acks等于-1时才会判断ISR数是否不足
throw new NotEnoughReplicasException("Number of insync replicas for partition [%s,%d] is [%d], below required minimum [%d]"
.format(topic,partitionId,minIsr,inSyncSize))
}
val info = log.append(messages, assignOffsets = true) // 真正的写日志操作,由于涉及Kafka底层写日志的,以后有机会写篇文章专门探讨这部分功能
// probably unblock some follower fetch requests since log end offset has been updated
replicaManager.unblockDelayedFetchRequests(new TopicAndPartition(this.topic, this.partitionId))
// we may need to increment high watermark since ISR could be down to 1
maybeIncrementLeaderHW(leaderReplica)
info
case None => // 如果不在,直接抛出异常表明leader不在该broker上
throw new NotLeaderForPartitionException("Leader not local for partition [%s,%d] on broker %d"
.format(topic, partitionId, localBrokerId))
}
}
至此,一个最简单的scala版同步producer的代码走读就算正式完成了,可以发现Kafka设计的思路就是在每个broker上启动一个server不断地处理从客户端发来的各种请求,完成对应的功能并按需返回对应的response。希望本文能对希望了解Kafka producer机制的人有所帮助。
【原创】Kafka producer原理 (Scala版同步producer)的更多相关文章
- 【转】Kafka producer原理 (Scala版同步producer)
转载自:http://www.cnblogs.com/huxi2b/p/4583249.html 供参考 本文分析的Kafka代码为kafka-0.8.2.1.另外,由于Kafka目前提供了两 ...
- Kafka原理与java simple producer示例
brokers和消费者使用zk来获取状态信息和追踪消息坐标. 每一个partition是一个有序的,不可变的消息序列. 只有当partition里面的file置换到磁盘文件以后,才开放给消费者来消费. ...
- Kafka深度解析(如何在producer中指定partition)(转)
原文链接:Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统.主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能 ...
- kafka 0.10.2 消息生产者(producer)
package cn.xiaojf.kafka.producer; import org.apache.kafka.clients.producer.*; import org.apache.kafk ...
- Kafka 0.11.0.0 实现 producer的Exactly-once 语义(中文)
很高兴地告诉大家,具备新的里程碑意义的功能的Kafka 0.11.x版本(对应 Confluent Platform 3.3)已经release,该版本引入了exactly-once语义,本文阐述的内 ...
- Kafka 详解(三)------Producer生产者
在第一篇博客我们了解到一个kafka系统,通常是生产者Producer 将消息发送到 Broker,然后消费者 Consumer 去 Broker 获取,那么本篇博客我们来介绍什么是生产者Produc ...
- Kafka 0.11.0.0 实现 producer的Exactly-once 语义(英文)
Exactly-once Semantics are Possible: Here’s How Kafka Does it I’m thrilled that we have hit an excit ...
- Apache Kafka(六)- High Throughput Producer
High Throughput Producer 在有大量消息需要发送的情况下,默认的Kafka Producer配置可能无法达到一个可观的的吞吐.在这种情况下,我们可以考虑调整两个方面,以提高Pro ...
- kafka学习指南(总结版)
版本介绍 从使用上来看,以0.9为分界线,0.9开始不再区分高级/低级消费者API. 从兼容性上来看,以0.8.x为分界线,0.8.x不兼容以前的版本. 总体拓扑架构 从上可知: 1.生产者不需要访问 ...
随机推荐
- ABP理论学习之功能管理
返回总目录 本篇目录 介绍 功能类型 定义功能 检查功能 功能管理者 版本说明 介绍 大多数的Saas(多租户)应用都有不同 功能的 版本(包).因此,他们可以给租户(客户)提供不同的 价格和功能选项 ...
- 一道关于Promise应用的面试题
题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次:如何让三个灯不断交替重复亮灯?(用Promse实现) 三个亮灯函数已经存在: function red(){ console.log('red') ...
- 探索C#之微型MapReduce
MapReduce近几年比较热的分布式计算编程模型,以C#为例简单介绍下MapReduce分布式计算. 阅读目录 背景 Map实现 Reduce实现 支持分布式 总结 背景 某平行世界程序猿小张接到B ...
- 探索c#之storm的TimeCacheMap
阅读目录: 概述 算法介绍 清理线程 获取.插入.删除 总结 概述 最近在看storm,发现其中的TimeCacheMap算法设计颇为高效,就简单分享介绍下. 思考一下如果需要一个带过期淘汰的缓存容器 ...
- 《Entity Framework 6 Recipes》中文翻译系列 (46) ------ 第八章 POCO之领域对象测试和仓储测试
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 8-8 测试领域对象 问题 你想为领域对象创建单元测试. 这主要用于,测试特定的数 ...
- UITest 单元测试常用的断言
XCTFail(format…) 生成一个失败的测试: XCTFail(@"Fail"); XCTAssertNil(a1, format...) 为空判断, a1 为空时通过,反 ...
- 《Node web开发》笔记
还是因为学习kibana,才开始了解node. Node是一种基于事件驱动的异步系统,基于Chrome的引擎V8. Node中由于大量的使用模块,因此出现了很多开源模块,有点像java社区的样子. 笔 ...
- 修改windows自带的Ctrl+Space输入法切换快捷键
使用场景: 多为我等码农使用一些编辑器时,编辑器的默认代码提示热键为 ctrl+space ,但这个热键被系统的输入法开关占用.如果遇到可以设置快捷键的编辑器还好,要是不能设置的话(比如火狐浏览器的代 ...
- Java学习笔记(04)
Java学习笔记(04) 如有不对或不足的地方,请给出建议,谢谢! 一.对象 面向对象的核心:找合适的对象做合适的事情 面向对象的编程思想:尽可能的用计算机语言来描述现实生活中的事物 面向对象:侧重于 ...
- 【Win10应用开发】协议-下篇:自定义多个协议
前面介绍了如何为应用程序自定义协议,于是有朋友会问,我希望为我的应用注册多个协议,不同的协议处理不同的事情,能吗?答案是能的. 方法主要在配置清单文件上,这里我给出一个例子,示例应用将注册两个协议,分 ...