kafka producer可以总体上分为两个部分:

  1. producer调用send方法,将消息存放到内存中
  2. sender线程轮询的从内存中将消息通过NIO发送到网络中

1 调用send方法

其实在调用new KafkaProducer初始化一个producer实例的时候,已经初始化了一个sender线程在后台轮询,不过为了方便理解,我们先分析send方法,即producer如何将消息放到内存队列中的。

1.1 producer存储结构

producer的整体存储结构如下图

1.2 整体流程

kafka在发送消息的时候,首先会连接一台broker来获取metadata信息,从metadata中可以知道要发送的topic一共有几个partiton、partiton的leader所在broker等信息。获取到metadata信息后,会通过获取到metadata信息并通过消息的key来计算消息被分配到哪个partiton(注意:消息被分配到哪个partiton是在客户端被计算好的)。然后会将消息按照partiton分组,放到对应的RecordBatch中,如果RecordBatch大于batch.size的大小,则新建一个RecordBatch放在list末尾。

doSend流程

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
// 阻塞并唤醒sender线程,等待sender线程获取到metadata
long waitedOnMetadataMs = waitOnMetadata(record.topic(), this.maxBlockTimeMs);
long remainingWaitMs = Math.max(0, this.maxBlockTimeMs - waitedOnMetadataMs);
byte[] serializedKey; // 序列化key和value
serializedKey = keySerializer.serialize(record.topic(), record.key()); byte[] serializedValue; serializedValue = valueSerializer.serialize(record.topic(), record.value()); // 计算消息应该放在哪个partition分区,如1.3详细介绍
int partition = partition(record, serializedKey, serializedValue, metadata.fetch());
int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue);
ensureValidRecordSize(serializedSize); tp = new TopicPartition(record.topic(), partition);
long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp(); Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp); // 根据计算到的分区,将消息追加到对应分区所在的Deque<RecordBatch>中,如1.4详细介绍
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs); // 如果是新创建batch或者batch满了,那么就唤醒sender线程。
if (result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
}
return result.future;
// handling exceptions and record the errors;
// for API exceptions return them in the future,
// for other exceptions throw directly
//...
}

1.3 选择分区

在doSend方法中,调用partition 来计算消息的分区。如果没有特别指定的话,会使用默认的分区方法:

  1. 如果消息含有key,则计算方式是 key的绝对值 % (partiton个数 - 1)
  2. 如果不包含key的话,则采用round-robin方式发送。

1.4 消息放到内存中

accumulator#append(..)中会首先尝试将消息放到分区所对应的Deque<RecordBatch> 的最后一个batch中,如果添加失败(比如RecordBatch已经满了),则会使用BufferPool从内存中申请一块大小为batch.size的内存出来(如果消息体大于batch.size,则申请消息体大小的内存),将消息放到新的batch中,并将新的batch添加到Deque<RecordBatch>中。

append

public RecordAppendResult append(TopicPartition tp,
long timestamp,
byte[] key,
byte[] value,
Callback callback,
long maxTimeToBlock) throws InterruptedException { appendsInProgress.incrementAndGet();
try {
Deque<RecordBatch> dq = getOrCreateDeque(tp);
// 注意,这里操作是加锁的。加锁的原因是
// 1.producer是可以多线程访问的
// 2.sender线程也会操作Deque<RecordBatch>
synchronized (dq) {
if (closed)
throw new IllegalStateException("Cannot send after the producer is closed.");
RecordBatch last = dq.peekLast();
if (last != null) {
FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
if (future != null)
return new RecordAppendResult(future, dq.size() > 1 || last.records.isFull(), false);
}
} // 申请内存,大小为消息体和batch.size的最大值,另外buffer中其实只缓存batch.size大小的内存,只有batch.size大小的内存申请才会从buffer中获取,大于batch.size会重新开辟空间,
// 所以合理规划batch.size和消息体大小可以有效提供客户端内存使用效率
int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
// 从池子中申请
ByteBuffer buffer = free.allocate(size, maxTimeToBlock); // 注意,这里又用到锁,不将两个锁合并成一个锁原因是减少锁的粒度
synchronized (dq) {
if (closed)
throw new IllegalStateException("Cannot send after the producer is closed.");
RecordBatch last = dq.peekLast();
if (last != null) {
FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
if (future != null) {
free.deallocate(buffer);
return new RecordAppendResult(future, dq.size() > 1 || last.records.isFull(), false);
}
}
MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds())); dq.addLast(batch);
incomplete.add(batch);
return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
}
} finally {
appendsInProgress.decrementAndGet();
}
}

2 sender线程

sender线程在new出一个Kafka Producer实例后就已经开始运行了.

消息放到内存中是按照partiton来进行分组的,但是sender线程发送的时候是按照broker的node节点来发送,这点需要注意。

sender线程的整体逻辑如下:

void run(long now) {
Cluster cluster = metadata.fetch(); // ready用来获取已经ready的节点,注意是节点,所谓ready节点是指partiton满足以下条件之一后,partition的leader所在的节点为ready
// 1. Deque<RecordBatch> size大于1,说明已经有一个Batch满了,可以发送
// 2. 内存池已经耗尽,这时候需要发送写消息,来释放内存
// 3. linger.ms 时间到了,表示可以发送了
// 4. 调用了close方法,需要将内存中消息发送出去
// 如果partiton满足以上条件之一,那么parttion所在的leader节点就算准备好了 RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now); if (result.unknownLeadersExist)
this.metadata.requestUpdate(); Iterator<Node> iter = result.readyNodes.iterator();
long notReadyTimeout = Long.MAX_VALUE;
while (iter.hasNext()) {
Node node = iter.next();
if (!this.client.ready(node, now)) {
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
} // 由于RecordBatch是按照partiton来组织的,而sender线程是按照节点来发送的,所以drain的作用就是将RecordBatch转换为按照节点来组织的方式。drain只会获取每个分区的第一个BatchRecord,而不是将一个分区的所有BatchRecord都发送,主要是避免饥饿
Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
result.readyNodes,
this.maxRequestSize,
now);
if (guaranteeMessageOrder) {
for (List<RecordBatch> batchList : batches.values()) {
for (RecordBatch batch : batchList)
this.accumulator.mutePartition(batch.topicPartition);
}
} List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now); for (RecordBatch expiredBatch : expiredBatches)
this.sensors.recordErrors(expiredBatch.topicPartition.topic(), expiredBatch.recordCount); sensors.updateProduceRequestMetrics(batches); // 一个节点只会产生一个request
List<ClientRequest> requests = createProduceRequests(batches, now); long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
if (result.readyNodes.size() > 0) {
log.trace("Nodes with data ready to send: {}", result.readyNodes);
log.trace("Created {} produce requests: {}", requests.size(), requests);
pollTimeout = 0;
}
for (ClientRequest request : requests)
client.send(request, now); // 真正的发送
this.client.poll(pollTimeout, now);
}

3 一些细节总结

  1. batch.size和linger.ms满足其中之一,sender线程便会被激活进行发送消息
  2. sender每次只拿出一个partiton的一个RecordBatch进行发送,即便该partiton已经有多个RecordBatch满了,这样做主要为了避免其他parttion饥饿, 详见RecordAccumulator#drain(..)
  3. RecordAccumulator#drain(..)后,被drain的RecordBatch会被close,不可写;同时从相应的Deque<RecordBatch>中移除。

kafka producer 源码总结的更多相关文章

  1. kafka producer源码

    producer接口: /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor l ...

  2. Kafka Producer源码解析一:整体架构

    一.Producer整体架构 Kafka Producer端的架构整体也是一个生产者-消费者模式 Producer线程调用send时,只是将数据序列化后放入对应TopicPartition的Deque ...

  3. Kafka Producer源码简述

    接着上文kafka的简述,这一章我们一探kafka生产者是如何发送消息到消息服务器的. 代码的入口还是从 kafkaTemplate.send开始 最终我们就会到 org.springframewor ...

  4. kafka C客户端librdkafka producer源码分析

    from:http://www.cnblogs.com/xhcqwl/p/3905412.html kafka C客户端librdkafka producer源码分析 简介 kafka网站上提供了C语 ...

  5. Kakfa揭秘 Day7 Producer源码解密

    Kakfa揭秘 Day7 Producer源码解密 今天我们来研究下Producer.Producer的主要作用就是向Kafka的brokers发送数据.从思考角度,为了简化思考过程,可以简化为一个单 ...

  6. Kafka Eagle 源码解读

    1.概述 在<Kafka 消息监控 - Kafka Eagle>一文中,简单的介绍了 Kafka Eagle这款监控工具的作用,截图预览,以及使用详情.今天笔者通过其源码来解读实现细节.目 ...

  7. kafka 0.8.1 新producer 源码简单分析

    1 背景 最近由于项目需要,需要使用kafka的producer.但是对于c++,kafka官方并没有很好的支持. 在kafka官网上可以找到0.8.x的客户端.可以使用的客户端有C版本客户端,此客户 ...

  8. 高吞吐量的分布式发布订阅消息系统Kafka之Producer源码分析

    引言 Kafka是一款很棒的消息系统,今天我们就来深入了解一下它的实现细节,首先关注Producer这一方. 要使用kafka首先要实例化一个KafkaProducer,需要有brokerIP.序列化 ...

  9. 读Kafka Consumer源码

    最近一直在关注阿里的一个开源项目:OpenMessaging OpenMessaging, which includes the establishment of industry guideline ...

随机推荐

  1. 商业智能BI不仅仅是报表工具,它的真正价值是:决策支持

    作为一个独立系统,商业智能BI解决两个问题.一是信息的发布,它可以在正确的时间向正确的人提供正确的信息.几十年来,我们看到的商业智能项目,至少有90%的项目成果变成了提供信息的报告.这种情况下,改变的 ...

  2. 在服务器的docker里 装anacond3深度学习环境的全流程超基础

    ​ 背景: 实验室给我分配了一个服务器 已经装好了docker 和nvidi docker . 现在我的目标是创建我自己的docker 然后在我自己的docker里装上anaconda环境. 我以前从 ...

  3. C#依赖注入-初步概念了解

    维基百科说:"依赖注入是一种软件设计模式,在这种模式下,一个或更多的依赖(或服务)被注入(或者通过引用传递)到一个独立的对象(或客户端)中,然后成为了该客户端状态的一部分.该模式分离了客户端 ...

  4. ssh静态代理模式讲解与使用

    --作者:飞翔的小胖猪 --创建时间:2021年5月18日 --修改时间:2021年5月23日 一.说明 SSH 为建立在应用层基础上的安全协议.SSH 是较可靠专为远程登录会话和其他网络服务提供安全 ...

  5. 四、Java基础

    Java基础 在开始学习Java基础之前,我们先来学习一下IDEA 打开IDEA,新建一个项目(New Project),选择空项目(Empty Project),填写项目名(Project name ...

  6. WPF空格换行

    换行 (写在Text中才起作用) 空格  https://www.cnblogs.com/dc10101/archive/2011/11/14/2248432.html

  7. 学习linux(centos7)记录的笔记

    此随笔用于记录学习<linux鸟哥的私房菜>过程中1.遇到的问题及解决的过程 2.有必要记录的重要内容 3.对应书上操作的记录 开始于2021年6月18号 一个磁盘的分区通过格式化后成为一 ...

  8. SQL从零到迅速精通【实用函数(1)】

    语法是一个编程语言的基础,真的想玩的6得飞起还是要靠自己定义的函数和变量. 1.使用DECLARE语句创建int数据类型的名为@mycounter的局部变量,输入语句如下: DECLARE @myco ...

  9. ARP攻击的发现、攻击原理、攻击方式、防护,竟然这么简单?!

    ARP协议概述 ARP协议(address resolution protocol)地址解析协议. 一台主机和另一台主机通信,要知道目标的IP地址,但是在局域网中传输数据的网卡却不能直接识别IP地址, ...

  10. spring源码干货分享-对象创建详细解析(set注入和初始化)

    记录并分享一下本人学习spring源码的过程,有什么问题或者补充会持续更新.欢迎大家指正! 环境: spring5.X + idea 建议:学习过程中要开着源码一步一步过 Spring根据BeanDe ...