kafka源码---生产者(1)

《深入理解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

我们在下一篇中介绍,内容比较多。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值