Kafka消息发送解析(一) 用户主线程发送逻辑

867 阅读5分钟

简介

本文主要介绍kafka发送消息的流程,有一点需要在查看源码前先了解的,那就是kafka在producer设计处处透露着批量处理的逻辑 只要掌握了这一点 相信查看源代码会解惑很多

producer架构

这是kafka生产者的架构图 主要的线程有两个 一个是用户自己的主线程,一个是kafka负责发送消息的sender线程

  • 1.用户调用kafka producer的send方法 发送一条消息 首先会经过一系列的Interceptor进行数据处理 这些Intercepotors是用户自己定义的
  • 2.进过拦截器处理之后的数据会被kafka的序列化器进行序列化操作 用户可自己实现自己的序列化和反序列化逻辑,在KafkaProducer对象创建的时候传入相应的序列化类即可
  • 3.kafka的每个topic对应着多个分区 发送的数据经过序列化之后 过为这些数据选择合适的分区 这时候就会用分区器进行选择 用户也可以自己实现自己的分区逻辑 同样也可以指定向某一固定分区发送数据
  • 4.在处理好数据之后 主线程调用 RecordAccumulator对象的append方法 将消息添加进相应分区对应的队列之中 等待sender线程来获取数据进行批量发送
  • 5.sender线程从RecordAccumulator对象处获取到待发送的消息
  • 6.sender线程创建ClientRequest请求
  • 7.将client请求交给NetworkClient
  • 8.NetworkClient将请求交给KafkaChannel 准备执行网络io
  • 9 执行网络io 并且收到server返回的数据
  • 10 收到响应 回调ClientRequest
  • 11 调用BatachRequest的回调函数 这个用户需要自己实现callback接口 实现回调 kafka 的发送流程如上所述 接下来我们将仔细分析每一步kafka是如何做的

发送流程

Producer 用户主线程发送流程解析

1.1 onSend()

用户在调用send方法之后 kafka首先会调用Interceptor的onSend方法执行统一的拦截处理 具体代码如下

    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
       
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        //执行doSend方法
        return doSend(interceptedRecord, callback);
    }

1.3 在执行完拦截器之后 会调用自身的doSend方法 (部分代码会被省略 由于根本次分析无关 影响阅读 抛异常的地方直接用exception代替 自己阅读源码的时候一定要注意 笔者是为了简化无关此次分析代码)

    private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
        try {
            //等待元数据
            ClusterAndWaitTime clusterAndWaitTime;
            try {
            /**1.3.1 等待元数据更新 主要是等待待发送数据topic的分区元数据更新  获取并且更新元数据的逻辑主要在sender线程*/
                clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), nowMs, maxBlockTimeMs);
            } catch (KafkaException e) {
				throw exception()
            }
            
            Cluster cluster = clusterAndWaitTime.cluster;
            byte[] serializedKey;
            try {
            //1.3.2 序列化key
                serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
            } catch (ClassCastException cce) {
                throw exception();
            }
            byte[] serializedValue;
            try {
            //1.3.3 序列化value
                serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
            } catch (ClassCastException cce) {
               throw exception();
            }
            //1.3.4 执行分区器 选择本次发送topic的分区
            int partition = partition(record, serializedKey, serializedValue, cluster);
            //创建topic对象
            tp = new TopicPartition(record.topic(), partition);
			//在record头中设置为只读
            setReadOnly(record.headers());
            Header[] headers = record.headers().toArray();

            int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
                    compressionType, serializedKey, serializedValue, headers);
                    //校验消息的大小是否大于maxRequestSizey和totalMemorySize
            ensureValidRecordSize(serializedSize);
            long timestamp = record.timestamp() == null ? nowMs : record.timestamp();
            if (log.isTraceEnabled()) {
                log.trace("Attempting to append record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
            }
			//组装异步发送的回调接口
            Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);
            //省略事物相关
            //1.3.5 追加发送信息
            RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs);
			//1.3.6 粘粘性分区
            if (result.abortForNewBatch) {
                int prevPartition = partition;
                partitioner.onNewBatch(record.topic(), cluster, prevPartition);
                partition = partition(record, serializedKey, serializedValue, cluster);
                tp = new TopicPartition(record.topic(), partition);
                if (log.isTraceEnabled()) {
                    log.trace("Retrying append due to new batch creation for topic {} partition {}. The old partition was {}", record.topic(), partition, prevPartition);
                }
                // producer callback will make sure to call both 'callback' and interceptor callback
                interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);

                result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs);
            }

            if (transactionManager != null && transactionManager.isTransactional())
                transactionManager.maybeAddPartitionToTransaction(tp);
            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);
                this.sender.wakeup();
            }
            return result.future;
        }
		//省略异常处理逻辑
    }
1.3.1 waitOnMetadata() 等待元数据更新
kafka的元数据主要包括topic server有哪些节点 对应的分区信息 分区的副本在哪些节点上 分区的leader在哪个节点上

下面介绍三个类

  • node类 表示server(broker)实体 id:server(broker)的id标识
    idString : broker的id标识
    host: server的ip地址
    post:server的端口号

  • TopicPartition:topic对应的分区信息实体
    partition:分区id topic:主题

  • PartitionInfo:node和TopicPartition相应映射的实体
    topic : 主题
    partition:分区id
    leader:分区副本leader所在节点
    replicas:所有副本所在节点
    inSyncReplicas: 完全同步副本所在节点(也称为ISR副本)
    offlineReplicas: 离线副本所在节点

通过这三个类 我们可以完成的描述kafka需要的元数据信息 这些信息被保存在 Cluster.class 这个类中

我们都知道所有的网络i/o其实都是由sender线程执行的,用户主线程主要是调用requestUpdate()方法 设置needUpdate字段 当sender线程起来时 检查改字段 就知道要去更新元数据集合 更新完成之后增加版本号 主线程检查到版本号变化 退出等待 继续执行


    public synchronized int requestUpdateForTopic(String topic) {
        if (newTopics.contains(topic)) {
        //执行部分更新 设置needPartialUpdate = true
            return requestUpdateForNewTopics();
        } else { 
        	//执行全量更新 设置needFullUpdate = true
            return requestUpdate();
        }
    }
    

主线程阻塞等待代码

    public synchronized void awaitUpdate(final int lastVersion, final long timeoutMs) throws InterruptedException {
        long currentTimeMs = time.milliseconds();
        long deadlineMs = currentTimeMs + timeoutMs < 0 ? Long.MAX_VALUE : currentTimeMs + timeoutMs;
        time.waitObject(this, () -> {
            // Throw fatal exceptions, if there are any. Recoverable topic errors will be handled by the caller.
            maybeThrowFatalException();
            //版本号作为是否更新完成的依据
            return updateVersion() > lastVersion || isClosed();
        }, deadlineMs);

        if (isClosed())
            throw new KafkaException("Requested metadata update after close");
    }
    
    
        @Override
    public void waitObject(Object obj, Supplier<Boolean> condition, long deadlineMs) throws InterruptedException {
        synchronized (obj) {
            while (true) {
                if (condition.get())
                //当条件为真 就返回
                    return;

                long currentTimeMs = milliseconds();
                if (currentTimeMs >= deadlineMs)
                    throw new TimeoutException("Condition not satisfied before deadline");
                //wait等待
                obj.wait(deadlineMs - currentTimeMs);
            }
        }
    }
1.3.2 ,1.3.3 序列化key和value
1.3.4 选择分区器

为topic选择一个分区 如果用户指定了分区 那么优先使用用户的分区 看看默认分区的做法(如果key不为空 kafka会将同一个key的消息发送到同一个分区 如果key为空 kafka会随机选择一个分区进行发送)

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster,
                         int numPartitions) {
        if (keyBytes == null) {
        	//如果key为空 采用粘连性分区(粘连性分区是新版本的一个优化点 专题讲解)
            return stickyPartitionCache.partition(topic, cluster);
        }
        // 对key进行取模运算
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }
1.3.5 通过accumulator的append方法追加消息到accumulator中缓存起来 等待sender来发送 将在下一节详细讲解append方法
1.3.6 如果需要产生新的batchrecord会重新选择一个分区 如果消息有key那么依旧会发往同一个分区 如果消息没有key 那么会触发粘连性分区 这样会避免某些分区产生饥饿效应 粘粘性分区会在专题讲解 敬请期待