Kafka的源码解读(一)-- 生产者
该文档及之后的的kafka源码解读均以kafka2.4.0版本进行解读。kafka是用NIO作为通信基础的,这里不做赘述,如有需要连接NIO基础的课参考以下链接:
https://editor.youkuaiyun.com/md/?articleId=113486103
生产者发送数据流程解读
生产者发送消息的流程简图如下:
发送消息的简要流程如下:
- 生产者接受到消息
- 将消息封装为ProducerRecord
- 将消息序列化
- 获取消息的分区信息:如果没有元数据,需要去获取元数据
- 将消息放入缓存区
- sender读取缓存区的消息,并进行batch封装
- 满足batch发送的消息,将消息发送给Broker
发送消息核心流程剖析图如下:
Kafpa源码注释翻译如下:
用于将消息发布到Kafka集群的Kafka生产者客户端。
生产者是线程安全的,因此可以多个线程共享一个生产者示例,且通常情况下多线程共享同一个实例要比每个线程一个实例性能要高。
下面是一个包含键/值对序列化器的使用案例:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++)
producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));
producer.close();
生产者由一个缓冲空间池(RecordAccumulator)和一个I/O后台线程(Sender)组成,缓存空间保存尚未传输到服务器的消息,Sender线程负责将这些消息转换为请求(batch)并将它们发送到集群。 当生产者close失败的时候,这些消息会丢失。
send()方法是异步的。 调用时,它将消息添加到暂挂记录发送和立即返回的缓冲区中。 这样生产者可以将消息通过批处理来提高效率。
acks参数控制用于确定请求完成的条件。 我们指定的“all”设置是指将消息同步到所有分区,这是最慢但最安全的设置。
如果请求失败,生产者可以自动重试,虽然我们将retries指定为0,但仍然会进行重试。 启用重试也将打开重复的可能性(有关详细信息,请参阅有关邮件传递语义 http://kafka.apache.org/documentation.html#semantics的文档)。
生产者为每个分区维护未发送消息的缓冲区(一个分区一个Dequeue)。 这些缓冲区的大小由batch.size配置指定。 增大它可以导致更多的批处理,但是需要更多的内存(因为我们通常会为每个活动分区使用一个缓冲区)。
默认情况下,即使缓冲区中还有其他未使用的空间,缓冲区也可以立即发送。 如果想要减少请求数,可以将linger.ms设置为大于0的值,该参数生产者在发送请求之前等待该毫秒数,以希望会有更多记录来填充。这类似于Nagle在TCP中的算法。 例如,在上面的代码段中,由于我们将延迟时间设置为1毫秒,因此很可能所有100条记录都将在一个请求中发送。 但是,如果我们没有填充缓冲区,此设置将为我们的请求增加1毫秒的延迟,以等待更多记录到达。 请注意,时间接近的记录通常即使linger.ms=0也作为一个批次处理,因此在高负载下将进行批处理,与linger配置无关。 但是,如果不将其设置为大于0的值,则在不处于最大负载的情况下,可能会导致更少的请求和更有效的请求,但以少量的等待时间为代价。
buffer.memory控制生产者缓冲区的内存大小。如果消息的发送速度超过了将消息发送到服务器的速度,则该缓冲区空间将被耗尽。当缓冲区空间用尽时,其他发送调用将阻塞。阻塞时间的阈值由max.block.ms确定,此阈值之后将引发TimeoutException。
key.serializer和value.serializer指示如何将用户通过其ProducerRecord提供的键和值对象转换为字节。 您可以将包含的org.apache.kafka.common.serialization.ByteArraySerializer或org.apache.kafka.common.serialization.StringSerializer用于简单的字符串或字节类型。
从Kafka 0.11开始,KafkaProducer支持两种附加模式:幂等生产者和事务生产者。 幂等的生成器将Kafka的交付语义从至少一次交付增强到恰好一次交付。 特别是,生产者重试将不再引入重复项。 事务产生器允许应用程序原子地将消息发送到多个分区(partition)和主题(topic)
要启用幂等,必须将enable.idempotence参数设置为true。如果启用幂等,则retries配置将默认为Integer.MAX_VALUE,而acks配置将默认为all。幂等生产者没有API更改,因此无需修改现有应用程序即可利用此功能。
为了利用幂等生成器,必须避免重新发送应用程序级别,因为这些应用程序无法重复删除。 因此,如果应用程序启用幂等性,建议将retries配置保留为未设置状态,因为它将默认为Integer.MAX_VALUE 。 此外,如果send(ProducerRecord)即使无限次重试也返回错误(例如,如果消息在发送之前已在缓冲区中过期),则建议关闭生产者并检查最后产生的消息的内容,以确保不能重复。 最后,生产者只能保证在单个会话中发送的消息具有幂等性。
要使用事务性生产者和附带的API,必须设置transactional.id参数属性。如果设置了transactional.id ,则会自动启用幂等性以及幂等性所依赖的生产者配置。此外交易中包含的主题应配置为具有持久性。特别是, replication.factor应该至少为3 ,并且这些主题的min.insync.replicas应该设置为2。最后,为了从端到端实现事务保证,必须使使用者配置为仅读取已提交的消息。transactional.id的目的是启用单个生产者实例的多个会话之间的事务恢复。 它通常从分区的有状态应用程序中的分片标识符派生。 这样,它对于分区应用程序中运行的每个生产者实例都应该是唯一的。所有新的事务性API都将被阻止,并且将在失败时引发异常。 下面的示例说明了如何使用新的API。 除了所有100条消息都是单个事务的一部分外,它与上面的示例相似。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "my-transactional-id");
Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());
producer.initTransactions();
try {
producer.beginTransaction();
for (int i = 0; i < 100; i++)
producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), Integer.toString(i)));
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
// We can't recover from these exceptions, so our only option is to close the producer and exit.
producer.close();
} catch (KafkaException e) {
// For all other exceptions, just abort the transaction and try again.
producer.abortTransaction();
}
producer.close();
如示例所示,每个生产者只能有一个未完成的事务。 在beginTransaction()和commitTransaction()调用之间发送的所有消息都将成为单个事务的一部分。 指定transactional.id ,生产者发送的所有消息都必须是事务的一部分。
事务性生产者使用异常来传达错误状态。 特别是,不需要为producer.send()指定回调或在返回的Future上调用.get() :如果在producer.send()任何producer.send()或事务调用遇到不可恢复的错误,都将抛出KafkaException 。交易。 有关从事务性发送中检测错误的更多详细信息,请参见send(ProducerRecord)文档。通过在接收到KafkaException调用producer.abortTransaction() ,我们可以确保将任何成功的写入标记为已中止,从而保留事务保证。
生产者初始化源码解析
kafka对外提供了如下四个构造器:
-
参数封装为Map传入
-
public KafkaProducer(final Map<String, Object> configs) { this(configs, null, null, null, null, null, Time.SYSTEM); }
-
其他参数封装为map,序列化器单独传入
-
public KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer) { this(configs, keySerializer, valueSerializer, null, null, null, Time.SYSTEM); }
-
参数封装为property传入
-
public KafkaProducer(Properties properties) { this(propsToMap(properties), null, null, null, null, null, Time.SYSTEM); }
-
序列化器单独传入,其他参数封装为property
-
public KafkaProducer(Properties properties, Serializer<K> keySerializer, Serializer<V> valueSerializer) { this(propsToMap(properties), keySerializer, valueSerializer, null, null, null, Time.SYSTEM); }
所有上述构造器,都是通过调用如下构造器完成生产者初始化:
KafkaProducer(Map<String, Object> configs,
Serializer<K> keySerializer,
Serializer<V> valueSerializer,
ProducerMetadata metadata,
KafkaClient kafkaClient,
ProducerInterceptors interceptors,
Time time)
下面分析该方法内容:
KafkaProducer(Map<String, Object> configs,
Serializer<K> keySerializer,
Serializer<V> valueSerializer,
ProducerMetadata metadata,
KafkaClient kafkaClient,
ProducerInterceptors interceptors,
Time time) {
/**
* 如果参数中传入了键值的序列化器,这里进行序列化器配置
*/
ProducerConfig config = new ProducerConfig(ProducerConfig.addSerializerToConfig(configs, keySerializer,
valueSerializer));
try {
/**
* 获取配置参数
*/
Map<String, Object> userProvidedConfigs = config.originals();
this.producerConfig = config;
this.time = time;
//获取事务ID
String transactionalId = userProvidedConfigs.containsKey(ProducerConfig.TRANSACTIONAL_ID_CONFIG) ?
(String) userProvidedConfigs.get(ProducerConfig.TRANSACTIONAL_ID_CONFIG) : null;
//获取clientId
this.clientId = buildClientId(config.getString(ProducerConfig.CLIENT_ID_CONFIG), transactionalId);
//日志配置
LogContext logContext;
if (transactionalId == null)
logContext = new LogContext(String.format("[Producer clientId=%s] ", clientId));
else
logContext = new LogContext(String.format("[Producer clientId=%s, transactionalId=%s] ", clientId, transactionalId));
log = logContext.logger(KafkaProducer.class);
log.trace("Starting the Kafka producer");
/*
测量工具
*/
Map<String, String> metricTags = Collections.singletonMap("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)
.recordLevel(Sensor.RecordingLevel.forName(config.getString(ProducerConfig.METRICS_RECORDING_LEVEL_CONFIG)))
.tags(metricTags);
List<MetricsReporter> reporters = config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG,
MetricsReporter.class,
Collections.singletonMap(ProducerConfig.CLIENT_ID_CONFIG, clientId));
reporters.add(new JmxReporter(JMX_PREFIX));
this.metrics = new Metrics(metricConfig, reporters, time);
//分区器,若没有配置,使用DefaultPartitioner作为分区器
this.partitioner = config.getConfiguredInstance(ProducerC