★
《深入理解kafka》学习笔记
源码阅读环境搭建参考:
特别要注意scala和kafka对应的版本号!!!
kafka主要是面向大数据的,所以它的一些应用场景,不是传统web能体现的。
数据来源:日志消息,度量指标,用户活动记录,响应消息等等。
一,启动
最开始kafka的客户端是由scala语言开发的,后来0.9之后升级成为了java开发的客户端,但是kafka是支持多语言的中间件,所以还存在其他开发的客户端。Kafka源码中自带examples。直接打开获取源码。
启动:
1.kafka的启动,首先要启动服务器,kafka.scale文件,该文件要指定server.properties配置文件。
2.启动生产者+消费者,该组合在下面的Demo中一起启动的。
入口: 我们从生产者开始分析
public class KafkaConsumerProducerDemo {
public static void main(String[] args) {
boolean isAsync = args.length == 0 || !args[0].trim().equalsIgnoreCase("sync");
Producer producerThread = new Producer(KafkaProperties.TOPIC, isAsync);
producerThread.start();
Consumer consumerThread = new Consumer(KafkaProperties.TOPIC);
consumerThread.start();
}
}
生产者的操作大概分为四步:
1.创建配置信息;
2.创建生产者(使用1中的配置信息)
3.创造信息块(包含需要发送的信息和发送的地点);
4.发送-关闭;
第一个调用Producer类的构造方法,构造方法中重要的是创建了KafkaProducer对象,它是发送是实现者!
第二个调用start()启动调用run()方法,这里在main()中设置的异步方式,所以直接走第一个if()
public class Producer extends Thread {
private final KafkaProducer<Integer, String> producer;
private final String topic; //主题
private final Boolean isAsync; //异步标识
public Producer(String topic, Boolean isAsync) {
Properties props = new Properties(); //这个是jdk中的类
//kafka服务器的 ip+端口号
props.put("bootstrap.servers", KafkaProperties.KAFKA_SERVER_URL + ":" + KafkaProperties.KAFKA_SERVER_PORT);
//客户端id,可有可无,如果没有会自动生成
props.put("client.id", "DemoProducer");
//消息key-value 的序列化方式,这里一定要写,而且必须是全限名。
props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//使用基本信息构造producer
producer = new KafkaProducer<>(props); //【★】
this.topic = topic;
this.isAsync = isAsync;
}
public void run() {
int messageNo = 1;
while (true) {
String messageStr = "Message_" + messageNo; //待发送的消息
long startTime = System.currentTimeMillis();
//消息的发送方式,同步or异步 send()为入口
if (isAsync) { // Send asynchronously 第二个参数是消息的key,它将用来分配 分区,如果没有设置,也会自动补充。一般就是一个数字
//一个消息对应一个 ProducerRecord
producer.send(new ProducerRecord<>(topic,
messageNo,
messageStr), new DemoCallBack(startTime, messageNo, messageStr)); //异步方式,增加异步回调
} else { // Send synchronously
try {
producer.send(new ProducerRecord<>(topic,
messageNo,
messageStr)).get(); //同步,阻塞等待服务端的ack响应
System.out.println("Sent message: (" + messageNo + ", " + messageStr + ")");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
++messageNo;
}
}
}
注意:kafkaProducer是线程安全的,可以使用多个线程进行共享使用,也可以池化使用。
消息的发送:消息的构建中,topic和value是必填的。发送消息有三种模式:发后即忘,同步,异步。
- 发后既忘:使用普通的send(),发送之后,不管消息是否到达broker,效率最高,可靠性最差
- 同步:发后阻塞,等待回应,send()其实有返回对象:Future<RecordMetadata>的,调用它的get()方法即可,get()获得的是就是元数据,里面包含了消息的主题,分区号,分区偏移量等等。send()本身是异步的,由于加了get()导致阻塞,而可以根据这个阻塞时间,来判断网络的质量,进而可以使用重发决策。
- 异步:在send()方法中,添加第二个参数callback回调函数,这个和zk优点相似,在回调函数里面书写处理流程,
在这个demo中出现了几个类:
1.Properties类:它是jdk自带的,存放参数信息的,后面会被kafka封装为ProducerConfig类,简称config
2.KafkaProducer类,生产者的核心,它对消息进行构建,是主线程中核心流程掌控者,在第二小节中分析
3.ProducerRecord类:相当于一个pojo,包装了我们传入的信息参数,
public class ProducerRecord<K, V> {
private final String topic; //主题
private final Integer partition; //分区号
private final Headers headers; //消息头部
private final K key;
private final V value;
private final Long timestamp; //消息的时间戳
...
}
4.DemoCallBack类:回调函数,实现了Callback接口
class DemoCallBack implements Callback { //回调对象
private final long startTime;
private final int key; //消息的key
private final String message; //消息的value
... construction
public void onCompletion(RecordMetadata metadata, Exception exception) {
long elapsedTime = System.currentTimeMillis() - startTime;
if (metadata != null) {
//RecoredMetadata 中包含了分区信息,offset信息等。
System.out.println(
"message(" + key + ", " + message + ") sent to partition(" + metadata.partition() +
"), " +
"offset(" + metadata.offset() + ") in " + elapsedTime + " ms");
} else {
exception.printStackTrace();
}
}
}
二,kafkaProducer构造
整体流程:图中三个红框,我们也将消息的发送,分为3个步骤,我将使用3篇进行介绍。
当我们的消息准备好了,经过了分区器,然后会缓冲到消息累加器RecordAccumulator中(也就是图中的第二个红框),这是主线程的工作,还有一个发送线程会把消息从消息累加器中取出来,发送到broker中。
KafkaProducer开始分析,在启动demo中,使用了它的构造方法,最后调用了send()方法(这里还要区分异步还是同步),kafkaProducer结构简单,只实现了Producer接口。
2.1 字段
private final String clientId;
final Metrics metrics;
private final Partitioner partitioner; //分区器
private final int maxRequestSize; //消息最大长度
private final long totalMemorySize; //发送单个消息的缓存区大小
private final Metadata metadata; //整个kafka集群 元数据
private final RecordAccumulator accumulator; //用于收集并缓存消息,等待sender线程发送
private final Sender sender; //发送消息的Sender任务,实现了Runnable接口
private final Thread ioThread; //执行sender任务的线程
private final CompressionType compressionType; //压缩算法
private final Sensor errors;
private final Time time;
private final ExtendedSerializer<K> keySerializer; //key的序列化器
private final ExtendedSerializer<V> valueSerializer;
private final ProducerConfig producerConfig; //生产者配置信息
private final long maxBlockTimeMs;
private final int requestTimeoutMs; //消息的超时时间,等待返回ack最长时间
private final ProducerInterceptors<K, V> interceptors; //拦截器
1 Metadata元数据
public final class Metadata {
private static final Logger log = LoggerFactory.getLogger(Metadata.class);
public static final long TOPIC_EXPIRY_MS = 5 * 60 * 1000;
private static final long TOPIC_EXPIRY_NEEDS_UPDATE = -1L;
private final long refreshBackoffMs; //避免更新频繁,设置更新最小时间差
private final long metadataExpireMs; //update cycle 更新周期
private int version; //metadata update -> version++
private long lastRefreshMs; //最后一次 refresh 时间戳
private long lastSuccessfulRefreshMs; //上一次,成功refresh 时间戳
private AuthenticationException authenticationException;
private Cluster cluster; //集群 元数据 【★】
private boolean needUpdate; //是否强制更新 cluster
/* Topics with expiry time */
private final Map<String, Long> topics; //all topic
private final List<Listener> listeners;
private final ClusterResourceListeners clusterResourceListeners;
private boolean needMetadataForAllTopics; //是否需要refresh all topic ,一般 refresh 用到的 topic
private final boolean allowAutoTopicCreation;
private final boolean topicExpiryEnabled;
...
}
里面有 cluster集群信息,保存了最后更新时间,版本号,还要topics,listeners等等,this class is shared by the client thread and sender thread 所以它必须线程安全。
client thread 一般更新 metadata ,而sender thread 一般更新 里面封装的cluster。
2 Cluster 集群元数据
node 表示集群中的一个节点
TopicPartition 表示某Topic的一个分区
PartitionInfo 表示一个分区的详细信息
public final class Cluster {
private final boolean isBootstrapConfigured;
private final List<Node> nodes; //标识集群中一个节点,记录这个节点的host,ip,port等
private final Set<String> unauthorizedTopics;
private final Set<String> internalTopics;
private final Node controller;
//TopicPartition 表示某topic的一个分区,info表示它的具体信息(内容其实也不多) 两个pojo
private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition;
private final Map<String, List<PartitionInfo>> partitionsByTopic; //根据topic存放的分区信息
private final Map<String, List<PartitionInfo>> availablePartitionsByTopic; //有效分区
private final Map<Integer, List<PartitionInfo>> partitionsByNode; //根据节点存放的分区
private final Map<Integer, Node> nodesById; //根据id存放的node
//集群信息,其实只有一个cluster Id
private final ClusterResource clusterResource;
...
}
demo中的集群信息,由于我只是一个server,所以只有一个Node
由于只使用了一个partition分区,所以在cluster中的信息,都是显示的1个。
2.2 构造方法
public KafkaProducer(Properties properties) {
this(new ProducerConfig(properties), null, null);
}
这里就把jdk的properties封装成为了ProducerConfig,而且这个类里面存放了很多静态字符串。其实properties就理解为一个map。
主构造方法:
private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
try {
Map<String, Object> userProvidedConfigs = config.originals();
this.producerConfig = config;
this.time = Time.SYSTEM;
//set clientId
String clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
if (clientId.length() <= 0)
clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement();
this.clientId = clientId;
// sset transactionalId
String transactionalId = userProvidedConfigs.containsKey(ProducerConfig.TRANSACTIONAL_ID_CONFIG) ?
(String) userProvidedConfigs.get(ProducerConfig.TRANSACTIONAL_ID_CONFIG) : null;
// 日志设置
LogContext logContext;
...
//metrcTags 不重要
Map<String, String> metricTags = Collections.singletonMap("client-id", clientId);
.....
//通过反射,实例化配置类partitioner,keySerializer,valueSerializer
this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
if (keySerializer == null) {
this.keySerializer = ensureExtended(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 = ensureExtended(keySerializer);
}
... value 同上
// load interceptors and make sure they get clientId
userProvidedConfigs.put(ProducerConfig.CLIENT_ID_CONFIG, clientId); //放入id,第一个参数就是一个静态的 字符串 “clientId"
//拦截器链 初始化
List<ProducerInterceptor<K, V>> interceptorList = (List) (new ProducerConfig(userProvidedConfigs, false)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
ProducerInterceptor.class);
this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);
//集群 源信息 监听链
ClusterResourceListeners clusterResourceListeners = configureClusterResourceListeners(keySerializer, valueSerializer, interceptorList, reporters);
// //创建并更新 kafka集群的元数据
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG),
true, true, clusterResourceListeners);
....set 成员变量值
//重试次数
int retries = configureRetries(config, transactionManager != null, log);
int maxInflightRequests = configureInflightRequests(config, transactionManager != null);
short acks = configureAcks(config, transactionManager != null, log);
this.apiVersions = new ApiVersions();
//创建 RecordAccumulator (消息累计器)
this.accumulator = new RecordAccumulator(...);
List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
//元数据 update
this.metadata.update(Cluster.bootstrap(addresses), Collections.<String>emptySet(), time.milliseconds());
//NIO 三大元素:通道,选择器,缓冲区
ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config);
Sensor throttleTimeSensor = Sender.throttleTimeSensor(metricsRegistry.senderMetrics);
//创建NetworkClient 它是kafkaProducer网络i/o的核心
NetworkClient client = new NetworkClient(
new Selector(...);
String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
//启动Sender对应的线程
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
...
}
它的任务,就是给kafkaProducer 第一点中的字段,赋值。
注意:这里产生了一个kafkaProducer,也就是一个客户端, 初始化了一个Sender线程。
三,send()
同步,异步方式,都会走这个send() 区别就是第二个参数,是否有值
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// 过滤 record,可以跳过
ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
return doSend(interceptedRecord, callback); //第二个 【入】
}
拦截器,没有拦截器,直接返回record,此处demo中没有设置拦截器,所以返回的就是record信息,如果设置了拦截器就会调用onSend()方法,对record进行一定得修改,如同aop操作一般。
doSend()核心
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
try {
// first make sure the metadata for the topic is available
ClusterAndWaitTime clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
Cluster cluster = clusterAndWaitTime.cluster;
byte[] serializedKey;
//序列化 key
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
byte[] serializedValue;
//序列化 value
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
//分区
int partition = partition(record, serializedKey, serializedValue, cluster);
//对应到具体的 主题+分区
tp = new TopicPartition(record.topic(), partition);
setReadOnly(record.headers());
//记录头
Header[] headers = record.headers().toArray();
int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
compressionType, serializedKey, serializedValue, headers);
ensureValidRecordSize(serializedSize);
long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
Callback interceptCallback = this.interceptors == null ? callback : new InterceptorCallback<>(callback, this.interceptors, tp);
// 将record add in RecordAccumulator中 tp:TopcPartition 返回的是:是否计入队列成功
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs);
if (result.batchIsFull || result.newBatchCreated) { //dequeue full or create queue
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup(); //new sender thread and Main thread ;now, main thread is over
}
return result.future;
}
这里的流程,就和图中的第一个红色框一致,
1.update metadata |
2.key-value序列化, |
3.获得分区号partition, |
4.放入RecordAccumulator中 |
当recordAccumulator中的队列,满了,创建新的时候,就会唤醒一个sender。
3.1 partition
在doSend()方法中,根据record,提出里面的分区,如果没有设置,则需要进行计算。
private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
Integer partition = record.partition(); //get 开始创建时候,放入的partition
//if null ,computes a partition
return partition != null ?
partition :
partitioner.partition(
record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
}
如果没有设置,则需要进入DefaultPartitioner 产生一个(也就是分区器)
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); //get partition list by topic from cluster'set
int numPartitions = partitions.size();
if (keyBytes == null) { //key-value 中的key by serialized
int nextValue = nextValue(topic); //产生一个随机数,并且保存(重发会使用)
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic); //get partition list
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size(); //随机产生的可能越界,处理一下
return availablePartitions.get(part).partition(); //return partition
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
3.2.TopicPartition
使用topic+partition将partition和topic封装到TopicPartition中
public final class TopicPartition implements Serializable {
private int hash = 0;
private final int partition;
private final String topic;
}
3.3 append to RecordAccumulator