引言

Kafka是一款很棒的消息系统,今天我们就来深入了解一下它的实现细节,首先关注Producer这一方。

要使用kafka首先要实例化一个KafkaProducer,需要有brokerIP、序列化器等必要Properties以及acks(0、1、n)、compression、retries、batch.size等非必要Properties,通过这个简单的接口可以控制Producer大部分行为,实例化后就可以调用send方法发送消息了。

核心实现是这个方法:

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);//①
return doSend(interceptedRecord, callback);//②
}

通过不同的模式可以实现发送即忘(忽略返回结果)、同步发送(获取返回的future对象,回调函数置为null)、异步发送(设置回调函数)三种消息模式。

我们来看看消息类ProducerRecord有哪些属性:

private final String topic;//主题
private final Integer partition;//分区
private final Headers headers;//头
private final K key;//键
private final V value;//值
private final Long timestamp;//时间戳

它有多个构造函数,可以适应不同的消息类型:比如有无分区、有无key等。

①中ProducerInterceptors(有0 ~ 无穷多个,形成一个拦截链)对ProducerRecord进行拦截处理(比如打上时间戳,进行审计与统计等操作)

public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
ProducerRecord<K, V> interceptRecord = record;
for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
try {
interceptRecord = interceptor.onSend(interceptRecord);
} catch (Exception e) {
// 不抛出异常,继续执行下一个拦截器
if (record != null)
log.warn("Error executing interceptor onSend callback for topic: {}, partition: {}", record.topic(), record.partition(), e);
else
log.warn("Error executing interceptor onSend callback", e);
}
}
return interceptRecord;
}

如果用户有定义就进行处理并返回处理后的ProducerRecord,否则直接返回本身。

然后②中doSend真正发送消息,并且是异步的(源码太长只保留关键):

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
try {
// 序列化 key 和 value
byte[] serializedKey;
try {
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
} catch (ClassCastException cce) {
}
byte[] serializedValue;
try {
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
} catch (ClassCastException cce) {
}
// 计算分区获得主题与分区
int partition = partition(record, serializedKey, serializedValue, cluster);
tp = new TopicPartition(record.topic(), partition);
// 回调与事务处理省略。
Header[] headers = record.headers().toArray();
// 消息追加到RecordAccumulator中
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs);
// 该批次满了或者创建了新的批次就要唤醒IO线程发送该批次了,也就是sender的wakeup方法
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;
} catch (Exception e) {
// 拦截异常并抛出
this.interceptors.onSendError(record, tp, e);
throw e;
}
}

下面是计算分区的方法:

private int partition(ProducerRecord<K, V> record,
byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
Integer partition = record.partition();
// 消息有分区就直接使用,否则就使用分区器计算
return partition != null ?
partition :
partitioner.partition(
record.topic(), record.key(), serializedKey,
record.value(), serializedValue, cluster);
}

默认的分区器DefaultPartitioner实现方式是如果partition存在就直接使用,否则根据key计算partition,如果key也不存在就使用round robin算法分配partition。

/**
* The default partitioning strategy:
* <ul>
* <li>If a partition is specified in the record, use it
* <li>If no partition is specified but a key is present choose a partition based on a hash of the key
* <li>If no partition or key is present choose a partition in a round-robin fashion
*/
public class DefaultPartitioner implements Partitioner { private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>(); public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {//key为空
int nextValue = nextValue(topic);
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);//可用的分区
if (availablePartitions.size() > 0) {//有分区,取模就行
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {// 无分区,
return Utils.toPositive(nextValue) % numPartitions;
}
} else {// key 不为空,计算key的hash并取模获得分区
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
} private int nextValue(String topic) {
AtomicInteger counter = topicCounterMap.get(topic);
if (null == counter) {
counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
if (currentCounter != null) {
counter = currentCounter;
}
}
return counter.getAndIncrement();//返回并加一,在取模的配合下就是round robin
}
}

以上就是发送消息的逻辑处理,接下来我们再看看消息发送的物理处理。

Sender(是一个Runnable,被包含在一个IO线程ioThread中,该线程不断从RecordAccumulator队列中的读取消息并通过Selector将数据发送给Broker)的wakeup方法,实际上是KafkaClient接口的wakeup方法,由NetworkClient类实现,采用了NIO,也就是java.nio.channels.Selector.wakeup()方法实现。

Sender的run中主要逻辑是不停执行准备消息和等待消息:

long pollTimeout = sendProducerData(now);//③
client.poll(pollTimeout, now);//④

③完成消息设置并保存到信道中,然后监听感兴趣的key,由KafkaChannel实现。

public void setSend(Send send) {
if (this.send != null)
throw new IllegalStateException("Attempt to begin a send operation with prior send operation still in progress, connection id is " + id);
this.send = send;
this.transportLayer.addInterestOps(SelectionKey.OP_WRITE);
} // transportLayer的一种实现中的相关方法
public void addInterestOps(int ops) {
key.interestOps(key.interestOps() | ops);
}

④主要是Selector的poll,其select被wakeup唤醒:

public void poll(long timeout) throws IOException {
/* check ready keys */
long startSelect = time.nanoseconds();
int numReadyKeys = select(timeout);//wakeup使其停止阻塞
long endSelect = time.nanoseconds();
this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds()); if (numReadyKeys > 0 || !immediatelyConnectedKeys.isEmpty() || dataInBuffers) {
Set<SelectionKey> readyKeys = this.nioSelector.selectedKeys(); // Poll from channels that have buffered data (but nothing more from the underlying socket)
if (dataInBuffers) {
keysWithBufferedRead.removeAll(readyKeys); //so no channel gets polled twice
Set<SelectionKey> toPoll = keysWithBufferedRead;
keysWithBufferedRead = new HashSet<>(); //poll() calls will repopulate if needed
pollSelectionKeys(toPoll, false, endSelect);
} // Poll from channels where the underlying socket has more data
pollSelectionKeys(readyKeys, false, endSelect);
// Clear all selected keys so that they are included in the ready count for the next select
readyKeys.clear(); pollSelectionKeys(immediatelyConnectedKeys, true, endSelect);
immediatelyConnectedKeys.clear();
} else {
madeReadProgressLastPoll = true; //no work is also "progress"
} long endIo = time.nanoseconds();
this.sensors.ioTime.record(endIo - endSelect, time.milliseconds());
}

其中pollSelectionKeys方法会调用如下方法完成消息发送:

public Send write() throws IOException {
Send result = null;
if (send != null && send(send)) {
result = send;
send = null;
}
return result;
}
private boolean send(Send send) throws IOException {
send.writeTo(transportLayer);
if (send.completed())
transportLayer.removeInterestOps(SelectionKey.OP_WRITE);
return send.completed();
}

Send是一次数据发包,一般由ByteBufferSend或者MultiRecordsSend实现,其writeTo调用transportLayer的write方法,一般由PlaintextTransportLayer或者SslTransportLayer实现,区分是否使用ssl:

public long writeTo(GatheringByteChannel channel) throws IOException {
long written = channel.write(buffers);
if (written < 0)
throw new EOFException("Wrote negative bytes to channel. This shouldn't happen.");
remaining -= written;
pending = TransportLayers.hasPendingWrites(channel);
return written;
} public int write(ByteBuffer src) throws IOException {
return socketChannel.write(src);
}

到此就把Producer的业务相关逻辑处理和非业务相关的网络 2方面的主要流程梳理清楚了。其他额外的功能是通过一些配置保证的。

比如顺序保证就是max.in.flight.requests.per.connection,InFlightRequests的doSend会进行判断(由NetworkClient的canSendRequest调用),只要该参数设为1即可保证当前包未确认就不能发送下一个包从而实现有序性

public boolean canSendMore(String node) {
Deque<NetworkClient.InFlightRequest> queue = requests.get(node);
return queue == null || queue.isEmpty() ||
(queue.peekFirst().send.completed() && queue.size() < this.maxInFlightRequestsPerConnection);
}

再比如可靠性,通过设置acks,Sender中sendProduceRequest的clientRequest加入了回调函数:

  RequestCompletionHandler callback = new RequestCompletionHandler() {
public void onComplete(ClientResponse response) {
handleProduceResponse(response, recordsByPartition, time.milliseconds());//调用completeBatch
}
}; /**
* 完成或者重试投递,这里如果acks不对就会重试
*
* @param batch The record batch
* @param response The produce response
* @param correlationId The correlation id for the request
* @param now The current POSIX timestamp in milliseconds
*/
private void completeBatch(ProducerBatch batch, ProduceResponse.PartitionResponse response, long correlationId,
long now, long throttleUntilTimeMs) {
} public class ProduceResponse extends AbstractResponse {
/**
* Possible error code:
* INVALID_REQUIRED_ACKS (21)
*/
}

kafka源码一层一层包装很多,错综复杂,如有错误请大家不吝赐教。

高吞吐量的分布式发布订阅消息系统Kafka之Producer源码分析的更多相关文章

  1. 高吞吐量的分布式发布订阅消息系统Kafka--安装及测试

    一.Kafka概述 Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据. 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因 ...

  2. kafka 基础知识梳理-kafka是一种高吞吐量的分布式发布订阅消息系统

    一.kafka 简介 今社会各种应用系统诸如商业.社交.搜索.浏览等像信息工厂一样不断的生产出各种信息,在大数据时代,我们面临如下几个挑战: 如何收集这些巨大的信息 如何分析它 如何及时做到如上两点 ...

  3. Kafka — 高吞吐量的分布式发布订阅消息系统【转】

    1.Kafka独特设计在什么地方?2.Kafka如何搭建及创建topic.发送消息.消费消息?3.如何书写Kafka程序?4.数据传输的事务定义有哪三种?5.Kafka判断一个节点是否活着有哪两个条件 ...

  4. 一脸懵逼学习KafKa集群的安装搭建--(一种高吞吐量的分布式发布订阅消息系统)

    kafka的前言知识: :Kafka是什么? 在流式计算中,Kafka一般用来缓存数据,Storm通过消费Kafka的数据进行计算.kafka是一个生产-消费模型. Producer:生产者,只负责数 ...

  5. 高吞吐量的分布式发布订阅消息系统Kafka--spring-integration-kafka的应用

    一.概述 Spring Integration Kafka 是基于 Apache Kafka 和Spring Integration来集成Kafka,对开发配置提供了方便. 二.配置 1.spring ...

  6. 高吞吐量的分布式发布订阅消息系统Kafka-- 管理工具 Kafka Manager

    一.概述 Kafka在雅虎内部被很多团队使用,媒体团队用它做实时分析流水线,可以处理高达20Gbps(压缩数据)的峰值带宽. 为了简化开发者和服务工程师维护Kafka集群的工作,构建了一个叫做Kafk ...

  7. 分布式发布订阅消息系统Kafka

    高吞吐量的分布式发布订阅消息系统Kafka--安装及测试   一.Kafka概述 Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据. 这种动作(网页浏览, ...

  8. Kafka logo分布式发布订阅消息系统 Kafka

    分布式发布订阅消息系统 Kafka kafka是一种高吞吐量的分布式发布订阅消息系统,她有如下特性: 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳 ...

  9. 分布式发布订阅消息系统 Kafka 架构设计[转]

    分布式发布订阅消息系统 Kafka 架构设计 转自:http://www.oschina.net/translate/kafka-design 我们为什么要搭建该系统 Kafka是一个消息系统,原本开 ...

随机推荐

  1. 【GISER&&规划】我这二三年

    从从参加工作到现在,已经接近三年了.在这不长不短的时间里,我的职业规划犹如正余弦函数一样变化,一直游离在前端和后端之间. 第一年入职,被安排维护和拓展一套基于JAVA实现的地图瓦片生产工艺程序,不算复 ...

  2. 【Scala】代码实现Actor多种需求

    文章目录 简单实现Actor并发编程 使用Actor实现发送没有返回值的异步消息 使用Actor实现不间断消息发送 用react方法替代receive方法接收消息 结合case class,通过匹配不 ...

  3. Coda docs

    a doc,反过来就是coda,有点冷. 对我而言,在线文档的好处在于在线数据库.存放代码等. Quip能高亮代码,但有时墙内不香,害得我用APP查看记录.Slite能高亮代码,但表格功能弱.号称al ...

  4. JS的数据类型判断函数、数组对象结构处理、日期转换函数,浏览器类型判断函数合集

    工具地址:https://github.com/BothEyes1993/bes-jstools bes-jstools 100多个基础常用JS函数和各种数据转换处理集合大全,此工具包是在 outil ...

  5. java方法调用顺序

    public class JavalearningApplicationTests { static { System.out.println("Test的静态代码块"); } p ...

  6. CentOS 7搭建Zookeeper和Kafka集群

    环境 CentOS 7.4 Zookeeper-3.6.1 Kafka_2.13-2.4.1 Kafka-manager-2.0.0.2 本次安装的软件全部在 /home/javateam 目录下. ...

  7. ORACLE 统计文件后缀SQL

    ORACLE 统计文件后缀SQL查询语句 select lower(substr(file_name,instr(file_name, '.', -1),length(file_name))),cou ...

  8. 走进WebApiClientCore的设计

    WebApiClient WebApiClient是NCC开源社区的一个项目,是目前微服务里http接口调用的一把锋利尖刀,项目早期设计与开发的时候,是基于.netframework的,然后慢慢加入n ...

  9. spark机器学习从0到1逻辑斯蒂回归之(四)

      逻辑斯蒂回归 一.概念 逻辑斯蒂回归(logistic regression)是统计学习中的经典分类方法,属于对数线性模型.logistic回归的因变量可以是二分类的,也可以是多分类的.logis ...

  10. 5.8 Go 单元测试

    5.8 Go 单元测试 如果你不想后半生的美好时光都在寻找BUG中度过,那么必须写些程序用来检测产品代码的结果和预期的一样. Go语言的测试依赖于go test测试命令和一组按约定方式编写的测试函数, ...