简介
本文主要介绍kafka发送消息的流程,有一点需要在查看源码前先了解的,那就是kafka在producer设计处处透露着批量处理的逻辑 只要掌握了这一点 相信查看源代码会解惑很多
producer架构
这是kafka生产者的架构图 主要的线程有两个 一个是用户自己的主线程,一个是kafka负责发送消息的sender线程
- 1.用户调用kafka producer的send方法 发送一条消息 首先会经过一系列的Interceptor进行数据处理 这些Intercepotors是用户自己定义的
- 2.进过拦截器处理之后的数据会被kafka的序列化器进行序列化操作 用户可自己实现自己的序列化和反序列化逻辑,在KafkaProducer对象创建的时候传入相应的序列化类即可
- 3.kafka的每个topic对应着多个分区 发送的数据经过序列化之后 过为这些数据选择合适的分区 这时候就会用分区器进行选择 用户也可以自己实现自己的分区逻辑 同样也可以指定向某一固定分区发送数据
- 4.在处理好数据之后 主线程调用 RecordAccumulator对象的append方法 将消息添加进相应分区对应的队列之中 等待sender线程来获取数据进行批量发送
- 5.sender线程从RecordAccumulator对象处获取到待发送的消息
- 6.sender线程创建ClientRequest请求
- 7.将client请求交给NetworkClient
- 8.NetworkClient将请求交给KafkaChannel 准备执行网络io
- 9 执行网络io 并且收到server返回的数据
- 10 收到响应 回调ClientRequest
- 11 调用BatachRequest的回调函数 这个用户需要自己实现callback接口 实现回调 kafka 的发送流程如上所述 接下来我们将仔细分析每一步kafka是如何做的
发送流程
Producer 用户主线程发送流程解析
1.1 onSend()
用户在调用send方法之后 kafka首先会调用Interceptor的onSend方法执行统一的拦截处理 具体代码如下
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
//执行doSend方法
return doSend(interceptedRecord, callback);
}
1.3 在执行完拦截器之后 会调用自身的doSend方法 (部分代码会被省略 由于根本次分析无关 影响阅读 抛异常的地方直接用exception代替 自己阅读源码的时候一定要注意 笔者是为了简化无关此次分析代码)
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
try {
//等待元数据
ClusterAndWaitTime clusterAndWaitTime;
try {
/**1.3.1 等待元数据更新 主要是等待待发送数据topic的分区元数据更新 获取并且更新元数据的逻辑主要在sender线程*/
clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), nowMs, maxBlockTimeMs);
} catch (KafkaException e) {
throw exception()
}
Cluster cluster = clusterAndWaitTime.cluster;
byte[] serializedKey;
try {
//1.3.2 序列化key
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
} catch (ClassCastException cce) {
throw exception();
}
byte[] serializedValue;
try {
//1.3.3 序列化value
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
} catch (ClassCastException cce) {
throw exception();
}
//1.3.4 执行分区器 选择本次发送topic的分区
int partition = partition(record, serializedKey, serializedValue, cluster);
//创建topic对象
tp = new TopicPartition(record.topic(), partition);
//在record头中设置为只读
setReadOnly(record.headers());
Header[] headers = record.headers().toArray();
int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
compressionType, serializedKey, serializedValue, headers);
//校验消息的大小是否大于maxRequestSizey和totalMemorySize
ensureValidRecordSize(serializedSize);
long timestamp = record.timestamp() == null ? nowMs : record.timestamp();
if (log.isTraceEnabled()) {
log.trace("Attempting to append record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
}
//组装异步发送的回调接口
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);
//省略事物相关
//1.3.5 追加发送信息
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs);
//1.3.6 粘粘性分区
if (result.abortForNewBatch) {
int prevPartition = partition;
partitioner.onNewBatch(record.topic(), cluster, prevPartition);
partition = partition(record, serializedKey, serializedValue, cluster);
tp = new TopicPartition(record.topic(), partition);
if (log.isTraceEnabled()) {
log.trace("Retrying append due to new batch creation for topic {} partition {}. The old partition was {}", record.topic(), partition, prevPartition);
}
// producer callback will make sure to call both 'callback' and interceptor callback
interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);
result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs);
}
if (transactionManager != null && transactionManager.isTransactional())
transactionManager.maybeAddPartitionToTransaction(tp);
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;
}
//省略异常处理逻辑
}
1.3.1 waitOnMetadata() 等待元数据更新
kafka的元数据主要包括topic server有哪些节点 对应的分区信息 分区的副本在哪些节点上 分区的leader在哪个节点上
下面介绍三个类
-
node类 表示server(broker)实体 id:server(broker)的id标识
idString : broker的id标识
host: server的ip地址
post:server的端口号 -
TopicPartition:topic对应的分区信息实体
partition:分区id topic:主题 -
PartitionInfo:node和TopicPartition相应映射的实体
topic : 主题
partition:分区id
leader:分区副本leader所在节点
replicas:所有副本所在节点
inSyncReplicas: 完全同步副本所在节点(也称为ISR副本)
offlineReplicas: 离线副本所在节点
通过这三个类 我们可以完成的描述kafka需要的元数据信息 这些信息被保存在 Cluster.class 这个类中
我们都知道所有的网络i/o其实都是由sender线程执行的,用户主线程主要是调用requestUpdate()方法 设置needUpdate字段 当sender线程起来时 检查改字段 就知道要去更新元数据集合 更新完成之后增加版本号 主线程检查到版本号变化 退出等待 继续执行
public synchronized int requestUpdateForTopic(String topic) {
if (newTopics.contains(topic)) {
//执行部分更新 设置needPartialUpdate = true
return requestUpdateForNewTopics();
} else {
//执行全量更新 设置needFullUpdate = true
return requestUpdate();
}
}
主线程阻塞等待代码
public synchronized void awaitUpdate(final int lastVersion, final long timeoutMs) throws InterruptedException {
long currentTimeMs = time.milliseconds();
long deadlineMs = currentTimeMs + timeoutMs < 0 ? Long.MAX_VALUE : currentTimeMs + timeoutMs;
time.waitObject(this, () -> {
// Throw fatal exceptions, if there are any. Recoverable topic errors will be handled by the caller.
maybeThrowFatalException();
//版本号作为是否更新完成的依据
return updateVersion() > lastVersion || isClosed();
}, deadlineMs);
if (isClosed())
throw new KafkaException("Requested metadata update after close");
}
@Override
public void waitObject(Object obj, Supplier<Boolean> condition, long deadlineMs) throws InterruptedException {
synchronized (obj) {
while (true) {
if (condition.get())
//当条件为真 就返回
return;
long currentTimeMs = milliseconds();
if (currentTimeMs >= deadlineMs)
throw new TimeoutException("Condition not satisfied before deadline");
//wait等待
obj.wait(deadlineMs - currentTimeMs);
}
}
}
1.3.2 ,1.3.3 序列化key和value
1.3.4 选择分区器
为topic选择一个分区 如果用户指定了分区 那么优先使用用户的分区 看看默认分区的做法(如果key不为空 kafka会将同一个key的消息发送到同一个分区 如果key为空 kafka会随机选择一个分区进行发送)
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster,
int numPartitions) {
if (keyBytes == null) {
//如果key为空 采用粘连性分区(粘连性分区是新版本的一个优化点 专题讲解)
return stickyPartitionCache.partition(topic, cluster);
}
// 对key进行取模运算
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}