本次阅读源码采用的方法是场景驱动的方式,从Demo入手,先对代码的整体运行流程进行大概了解,对于不重要的代码"不求甚解,观其大略",再对重要的源码进行深度剖析。
(一)生产者
Kafka有producer,broker,consumer三部分组成,producer和consumer属于客户端,broker属于服务端,producer将消息写入broker中,consumer从broker中读取消息。
1.Producer属性配置
首先我们来看看生产者属性配置的代码
public class ProducerTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//给Kafka配置对象添加配置信息
Properties properties = new Properties();
//服务信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.214.128:9092");
//配置key的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//配置value的序列化
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//设置事务id
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_01");
// 每批次的消息大小,默认16K
properties.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "16384");
// 内存池的总大小,默认32M
properties.setProperty(ProducerConfig.BUFFER_MEMORY_CONFIG, "33554432");
// socket发送的缓冲区默认大小128KB
properties.setProperty(ProducerConfig.SEND_BUFFER_CONFIG, "32768");
// socket接收的缓冲区默认大小
properties.setProperty(ProducerConfig.RECEIVE_BUFFER_CONFIG, "32768");
//创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//初始化事务
producer.initTransactions();
//开启事务
producer.beginTransaction();
try{
//发送消息
for (int i = 0; i < 5; i++) {
producer.send(new ProducerRecord<String, String>("first", "wang wu" + i), new Callback() {
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e == null){
System.out.println("主题"+recordMetadata.topic()+" 分区"+recordMetadata.partition());
}
}
});
Thread.sleep(20);
}
//提交事务
producer.commitTransaction();
}catch (Exception e){
//回滚事务
producer.abortTransaction();
}finally {
//关闭生产者资源
producer.close();
}
}
}
首先初始化一个Properties对象,往该对象加入一些配置信息,生产者初始化的时候就会读取这些配置信息。
2. Producer初始化
先来看看KafkaProducer实例化的代码,这里只展示重要代码,对于像日志的一些代码就不贴出来了。
@SuppressWarnings({"unchecked", "deprecation"})
private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
try {
// 配置用户之前自定义的一些参数值
Map<String, Object> userProvidedConfigs = config.originals();
this.producerConfig = config;
this.time = new SystemTime();
//配置客户端ID
clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
//设置分区器
this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
//当生产者发送消息失败的时候会进行重试,重试的时间间隔retry.backoff.ms默认100ms
long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
//设置key和value的序列化器
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;
}
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;
}
//设置拦截器
List<ProducerInterceptor<K, V>> interceptorList = (List) (new ProducerConfig(userProvidedConfigs)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
ProducerInterceptor.class);
this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);
//创建元数据实例对象
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
//发送一条消息的最大容量默认是1M,在生产环境中,经验值是10M。
this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
//内存池的容量大小默认值是32M
this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
//设置消息的压缩格式,从而提高系统的吞吐量
this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
//创建消息记录累加器
this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
this.totalMemorySize,
this.compressionType,
config.getLong(ProducerConfig.LINGER_MS_CONFIG),
retryBackoffMs,
metrics,
time);
//解析并验证broker地址的有效性
List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
//生产者默认每隔5分钟去更新元数据(初始化的时候没有去拉取)
this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
//初始化网络客户端对象
NetworkClient client = new NetworkClient(
//生产者连上broker超过最大空闲时间时就关闭这个网络连接。最大空闲时间默认值是9分钟
new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder),
this.metadata,
clientId,
//生产者发送给broker能够缓存还没有接收到响应的最大请求个数,默认是5个
config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION),
//设置重试时间间隔,默认100ms
config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
//设置发送缓冲区的最大大小,默认128KB
config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
//设置接收缓冲区的默认大小,默认32KB
config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
this.requestTimeoutMs, time);
//初始化sender线程
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);
//创建了一个Kafka线程,传入sender对象。
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
//启动Kafka线程。
this.ioThread.start();
}
2.1消息分区策略
Producer在初始化的时候,会使用默认的分区器DefaultPartitioner,this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
Partitioner是一个接口,据此可以实现该接口实现自定义的分区策略。
自定义分区的例子
public class MyPartition implements Partitioner {
public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
String msg = o1.toString();
//若消息包含hi则发送到1号分区
if(msg.contains("hi")) return 1;
//否则发送到0号分区
else return 0;
}
public void close() {
}
public void configure(Map<String, ?> map) {
}
}
再来看看Kafka默认的分区器实现
public interface Partitioner extends Configurable {}
public class DefaultPartitioner implements Partitioner {
//初始化的时候,定义一个原子类型的计数器
private final AtomicInteger counter = new AtomicInteger(new Random().nextInt());
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//获取消息要发往的topic的分区的信息
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
//计算出来分区的总的个数
int numPartitions = partitions.size();
//情况一,如果发送消息的时候,没有指定key
if (keyBytes == null) {
//计数器每次执行都会自增1,对分区进行轮询,从而实现负载均衡的效果
int nextValue = counter.getAndIncrement();
//获取可用的分区数
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
//当可用分区数大于1,将计数器对可用分区数取余得到目标分区号
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
//根据这个值分配分区好。
return availablePartitions.get(part).partition();
} else {
//当没有可用分区数时,将计数器对总分区数取余
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
//情况2,在指定key的情况下,将key的hash值对总分区数取余,通过指定同一个key可以将消息发往同一个分区.
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
}
可以看出,当需要将消息发往同一个分区时,可以指定key,同一个key的hash值是相同的,将hash值对总分区数取余得到的分区号肯定是一样的;或者自定义分区器实现Partitioner接口将消息发送到指定分区。
2.2重要组件的初始化
生产者将消息批次发送到RecordAccumulator的双端队列中,当满足一定条件时(后面会详细介绍)就会唤醒sender线程从队列中取出消息,通过NetworkClient将消息发送至broker。
首先是RecordAccumulator的初始化代码。
this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
this.totalMemorySize,
this.compressionType,
config.getLong(ProducerConfig.LINGER_MS_CONFIG),
retryBackoffMs,
metrics,
time);
先介绍下RecordAccumulator是怎么存储消息的,在这个类里面有一个用ConcurrentMap实现的数据结构,key是主题分区,value是用双端队列来存储消息批次RecordBatch。
public final class RecordAccumulator {
//TopicPartition -> Deque<RecordBatch> 双端队列
private final ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches;
}
可以知道每个主题分区对应一个双端队列,该队列用来存储发送该分区的消息批次。
再来看看sender对象
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);
//创建了一个Kafka线程,传入sender对象。
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
//启动Kafka线程。
this.ioThread.start();
sender对象实现了Runnable接口,本质上就是一个线程
public class Sender implements Runnable {}
而KafkaThread继承了Thread,本身就是一个线程。
public class KafkaThread extends Thread {}
创建完sender线程后将其Kafka线程中,然后启动,此时sender线程就已经开始运行了,消息的发送,响应的接收以及处理都是由sender线程处理的。
3.sender线程
首先来看看sender线程的run方法
public class Sender implements Runnable {
public void run() {
//这里是一个while死循环,sneder线程是一经启动就一直在运行
while (running) {
try {
// 核心代码
run(time.milliseconds());
} catch (Exception e) {
log.error("Uncaught error in kafka producer I/O thread: ", e);
}
}
}
//上面的run方法调用下面的run方法
void run(long now) {
//步骤一:获取元数据
Cluster cluster = metadata.fetch();
//步骤二:获取要发送partiton的leader副本对应的broker
RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
//对没有拉取到元数据的topic进行标记,以便下一次拉取该主题的元数据
if (!result.unknownLeaderTopics.isEmpty()) {
for (String topic : result.unknownLeaderTopics)
this.metadata.add(topic);
this.metadata.requestUpdate();
}
Iterator<Node> iter = result.readyNodes.iterator();
long notReadyTimeout = Long.MAX_VALUE;
while (iter.hasNext()) {
Node node = iter.next();
//步骤四:检查生产者要发送数据的broker的网络是否已经建立好。
if (!this.client.ready(node, now)) {
//如果与目标broker的网络没有建立好,则从集合中移除该broker
//第一次进来的时候所有主机的网络都没有建立好,所以这里会将所有的broker移除集合
iter.remove();
notReadyTimeout = Math.min(notReadyTimeout, this.client.connectionDelay(node, now));
}
}
// 步骤五:将消息批次按照broker分组,发往同一个broker的分区归为一组,这样可以减少网络请求的传输次数
Map<Integer, List<RecordBatch>> batches = this.accumulator.drain(cluster,
result.readyNodes,
this.maxRequestSize,
now);
//步骤六:对超时的消息批次进行处理
List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);
//步骤七:创建发送消息的请求
//第一次运行到这里的时候与broker的网络连接没有建立好 batches是为空,所以这段代码不会执行
List<ClientRequest> requests = createProduceRequests(batches, now);
//发送请求的操作
for (ClientRequest request : requests)
//绑定 op_write事件
client.send(request, now);
//步骤八:第一次运行的时候首先通过下面的方法先拉取元数据,然后建立网络连接,发送请求,以及响应的接收和处理都是由这个方法完成的
this.client.poll(pollTimeout, now);
}
(二)总结
本文主要讲解了生产者初始化时的一些配置属性,以及消息的3种分配策略,分别是自定义分区,随机指定分区,通过key的hash指定分区三种,还介绍了RecordAccumulator是如何存储消息的,最后分析了sender线程的初始化以及代码的运行流程。下一篇将详细介绍sender线程是如何建立网络连接,发送消息以及处理响应。