try {
val kafkaProducerRecord =new ProducerRecord[String, String]("live_order_id_info_back", sedMsg)
procuder.send(kafkaProducerRecord)
} catch {
case _: Exception => procuder.close()
}
调用的是 KafkaProducer构造函数, 调用send发送(其实都是异步处理) ----> 调用的doSend()
public Future send(ProducerRecord record) {
return this.send(record, (Callback)null);
}
public Future send(ProducerRecord record, Callback callback) {
ProducerRecord interceptedRecord = this.interceptors.onSend(record);
return this.doSend(interceptedRecord, callback);
}
private void throwIfProducerClosed() {
if (this.ioThread == null || !this.ioThread.isAlive()) {
throw new IllegalStateException("Cannot perform operation after producer has been closed");
}
}
private Future KafkaProducer(ProducerConfig config, Serializer keySerializer, Serializer valueSerializer) {
try {
log.trace("Starting the Kafka producer");
Map userProvidedConfigs = config.originals();
this.producerConfig = config;
this.time = new SystemTime();
clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
//配置中解析出clientId,用于跟踪程序运行情况,在有多个KafkProducer时,若没有配置 client.id则clientId 以前 辍”producer-”后加一个从 1 递增的整数
if (clientId.length() <= 0)
clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement();
//注册用于Kafka metrics指标收集的相关对象,用于对 Kafka 集群相关指标的追踪
Map metricTags = new LinkedHashMap();
metricTags.put("client-id", clientId);
MetricConfig metricConfig = new MetricConfig().samples(config.getInt(ProducerConfig.METRICS_NUM_SAMPLES_CONFIG))
.timeWindow(config.getLong(ProducerConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), TimeUnit.MILLISECONDS)
.tags(metricTags);
List reporters = config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG,
MetricsReporter.class);
reporters.add(new JmxReporter(JMX_PREFIX));
this.metrics = new Metrics(metricConfig, reporters, time);
//初始化分区选择器 通過反射獲取
this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
//初始集群元数据、消息缓冲区大小、压缩策略
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG));
this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
//实例化用于存储消息的RecordAccumulator,作用类似一个队列
//指定每个RecordBatch的大小,单位是字节
this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
this.totalMemorySize,
this.compressionType,
config.getLong(ProducerConfig.LINGER_MS_CONFIG),
retryBackoffMs,
metrics,
time);
List addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values());
//NetworkClient对象构造一个用于数据发送的Sender实例sender 线程,最后通过sender创建一个KafkaThread线 程,启动该线程,该线程是一个守护线程,在后台不断轮询,将消息发送给代理
NetworkClient client = new NetworkClient(
new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder),
this.metadata,
clientId,
config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION),
config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
this.requestTimeoutMs, time);
this.sender = new Sender(client,
this.metadata,
this.accumulator,
config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1,
config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
(short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
config.getInt(ProducerConfig.RETRIES_CONFIG),
this.metrics,
new SystemTime(),
clientId,
this.requestTimeoutMs);
String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
this.errors = this.metrics.sensor("errors");
//序列化key
if (keySerializer == null) {
this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
Serializer.class);
this.keySerializer.configure(config.originals(), true);
} else {
config.ignore(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG);
this.keySerializer = keySerializer;
}
//序列化value
if (valueSerializer == null) {
this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
Serializer.class);
this.valueSerializer.configure(config.originals(), false);
} else {
config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG);
this.valueSerializer = valueSerializer;
}
log.debug("Kafka producer started");
} catch (Throwable t) {
....
}
}
dosend()调用
private Future doSend(ProducerRecord record, Callback callback) {
TopicPartition tp = null;
try {
// first make sure the metadata for the topic is available
//步骤一:同步等待拉取元数据。maxBlockTimeMs 最多能等待多久。
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
//clusterAndWaitTime.waitedOnMetadataMs 代表的是拉取元数据用了多少时间。
//maxBlockTimeMs -用了多少时间 = 还剩余多少时间可以使用。
long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
//获取元数据里面的集群相关信息
Cluster cluster = clusterAndWaitTime.cluster;
//对key进行序列化
byte[] serializedKey;
try {
serializedKey = keySerializer.serialize(record.topic(), record.key());
} catch (ClassCastException cce) {
throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
" to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
" specified in key.serializer");
}
//对value进行序列化
byte[] serializedValue;
try {
serializedValue = valueSerializer.serialize(record.topic(), record.value());
} catch (ClassCastException cce) {
throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
" to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
" specified in value.serializer");
}
//根据分区器选择消息应该发送的分区
//根据元数据的信息计算一下,我们应该要把这个数据发送到哪个分区上面。
int partition = partition(record, serializedKey, serializedValue, cluster);
int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue);
//确认一下消息的大小是否超过了最大值, KafkaProdcuer初始化的时候,
//指定了一个参数,代表的是Producer这儿最大能发送的是一条消息能有多大
//默认最大是1M,我们一般都回去修改它
ensureValidRecordSize(serializedSize);
//根据元数据信息,封装分区对象
tp = new TopicPartition(record.topic(), partition);
//给每一条消息都绑定他的回调函数。因为我们使用的是异步的方式发送的消息
long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
// producer callback will make sure to call both 'callback' and interceptor callback
Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp);
//将要发送的消息追加到RecordAccmulator里面
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, interceptCallback, remainingWaitMs);
// 把消息放入accumulator(32M的一个内存)
//然后有accumulator把消息封装成为一个批次一个批次的去发送。
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);
//唤醒sender线程,他才是真正发送数据的线程。
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
} catch (ApiException e) {
// ..................省略
}
}
在分析waitOnMetadata之前,先说一下kafka集群的元数据,我们知道,每个topic有多个分区,每个分区有多个副本,而每个分区的副本里面都需要有一个Leader副本,其他副本只需要同步leader副本的数据即可,而Kafak的元数据就是记录了比如某个分区有哪些副本,leader副本在哪台机器上,follow副本在哪台机器上,哪些副本在ISR(可以理解为follower副本中数据和Leader副本数据相差不大的副本节点)里面 , 在kafka里面主要通过下面的几个类来进行元数据的维护
接下来我们回到waitOnMetadata,来看一下元数据是如何更新的
private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
// add topic to metadata topic list if it is not there already and reset expiry
// 把当前的topic存入到元数据里面
metadata.add(topic);
//我们使用的是场景驱动的方式,然后我们目前代码执行到的producer端初始化完成。
//我们知道这个cluster里面其实没有元数据,只是有我们写代码的时候设置address
Cluster cluster = metadata.fetch();
//根据当前的topic从这个集群的cluster元数据信息里面查看分区的信息。
//因为我们目前是第一次执行这段代码,所以这儿肯定是没有对应的分区的信息的。
Integer partitionsCount = cluster.partitionCountForTopic(topic);
// Return cached metadata if we have it, and if the record's partition is either undefined
// or within the known partition range
//如果在元数据里面获取到了 分区的信息
//我们用场景驱动的方式,我们知道如果是第一次代码进来这儿,代码是不会运行这儿。
if (partitionsCount != null && (partition == null || partition < partitionsCount))
//直接返回cluster元数据信息,拉取元数据花的时间。
return new ClusterAndWaitTime(cluster, 0);
//如果代码执行到这儿,说明,真的需要去服务端拉取元数据。
//记录当前时间
long begin = time.milliseconds();
//剩余多少时间,默认值给的是 最多可以等待的时间。
long remainingWaitMs = maxWaitMs;
//已经花了多少时间。
long elapsed;
// Issue metadata requests until we have metadata for the topic or maxWaitTimeMs is exceeded.
// In case we already have cached metadata for the topic, but the requested partition is greater
// than expected, issue an update request only once. This is necessary in case the metadata
// is stale and the number of partitions for this topic has increased in the meantime.
do {
log.trace("Requesting metadata update for topic {}.", topic);
//1)获取当前元数据的版本
//在Producer管理元数据时候,对于他来说元数据是有版本号的。
//每次成功更新元数据,都会递增这个版本号。
//2)把needUpdate 标识赋值为true
int version = metadata.requestUpdate();
/**
* 我们发现这儿去唤醒sender线程。
* 其实是因为,拉取有拉取元数据这个操作是有sender线程去完成的。
* 这个地方把线程给唤醒了以后
* 我们知道sender线程肯定就开始进行干活了!!
* 很明显,真正去获取元数据是这个线程完成。
*/
sender.wakeup();
try {
//TODO 等待元数据
//同步的等待
//等待这sender线程获取到元数据。
metadata.awaitUpdate(version, remainingWaitMs);
} catch (TimeoutException ex) {
// Rethrow with original maxWaitMs to prevent logging exception with remainingWaitMs
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
}
//尝试获取一下集群的元数据信息。
cluster = metadata.fetch();
//计算一下 拉取元数据已经花了多少时间
elapsed = time.milliseconds() - begin;
//如果花的时间大于 最大等待的时间,那么就报超时。
if (elapsed >= maxWaitMs)
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
//如果已经获取到了元数据,但是发现topic没有授权
if (cluster.unauthorizedTopics().contains(topic))
throw new TopicAuthorizationException(topic);
//计算出来 还可以用的时间。
remainingWaitMs = maxWaitMs - elapsed;
//尝试获取一下,我们要发送消息的这个topic对应分区的信息。
//如果这个值不为null,说明前面sender线程已经获取到了元数据了。
partitionsCount = cluster.partitionCountForTopic(topic);
//如果获取到了元数据以后,这儿代码就会退出。
//
} while (partitionsCount == null);
if (partition != null && partition >= partitionsCount) {
throw new KafkaException(
String.format("Invalid partition given with record: %d is not in the range [0...%d).", partition, partitionsCount));
}
//代码就执行到这儿,返回一个对象
//有两个参数:
//cluster: 集群的元数据
//elapsed: 代表的是拉取元数据花了多少时间。
return new ClusterAndWaitTime(cluster, elapsed);
}
当元数据更新后,下一步选择一个分区用来存放咱们的消息,
int partition = partition(record, serializedKey, serializedValue, cluster);
如果你发过来的消息已经指定了某个分区,那么直接返回即可。
private int partition(ProducerRecord 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);
如果没有指定,调用partitioner.partition进行判断,kafka提供了默认的实现,当然你可以自己定制分发策略
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//根据指定topic获取所有分区信息
List partitions = cluster.partitionsForTopic(topic);
//获取分区个数
int numPartitions = partitions.size();
//如果没有指定消息key
if (keyBytes == null) {
int nextValue = counter.getAndIncrement();
//获取指定topic对应的可利用的分区信息,这些可利用是说副本有leader副本的,有些分区他是没有leader副本的,有可能因为一些原因导致
List availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
// 对可利用的分区数取模获取下标
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
//如果指定了消息key,直接对key进行hash后然后对分区数大小进行取模操作
} else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
总结一下:
1、如果你指定了分区,那么只会将这条消息发送到指定分区
2、如果你同时指定了分区和消息key,也是指发送到这个分区
3、如果没有指定分区,指定了消息key,那么对key进行hash后对当前分区数进行取模后得出消息应该放到哪个分区
4、如果没有分区,也没有指定key,则按照一定的轮询方式(counter和分区数取模,counter每次递增,确保消息不会发送到同一个分区里面)来获取分区数
说完如何获取消息发送的分区后,下一步就是将消息放到暂存区RecordAccumulator,我们下一节RecordAccumulator
具体说明。
doSend在这个方法中一共做了下面这几件事:
1、waitOnMetadata阻塞方式采用RPC方式获取到broker cluster 上broker cluster的信息
2、将key和value序列化 (内置了基于String、Integer、Long、Double、Bytes、ByteBuffer、ByteArray的序列化工具。)
3、为当前消息选择一个合适的分区
4、确保消息的大小合法
MAX_REQUEST_SIZE_CONFIG=”max.request.size”
BUFFER_MEMORY_CONFIG=”buffer.memory”
5、创建要给TopicPartition对象
6、将该record压缩后放到BufferPool中
关于record的压缩方式,kafka producer在支持了几种方式:
·NONE:就是不压缩。
·GZIP:压缩率为50%
·SNAPPY:压缩率为50%
·LZ4:压缩率为50%
这一步是由RecordAccumulator来完成的。RecordAccumulator中为每一个topic维护了一个双端队列Deque
7、唤醒Sender线程、(这个一步的目的就是唤醒NIO Selector)