kafka架構 - send message (一) *

生產者流程圖

本文分析以下流程的步驟1、2、3、4。
在這裏插入圖片描述

主要是經過攔截器處理,然後更新並獲取集羣元數據,接着經由序列化器、分區器處理,最後將消息追加到RecordAccumulator。

源碼分析

本文基於Spring for Kafka 2.4.4.版本。

Kafka客戶端發送消息,可以使用KafkaProducer的如下方法:

public Future<RecordMetadata> send(ProducerRecord<K, V> record) {...}

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {...}

第一種方法,實際上是第二個方法callback爲null的方式。兩種方式,都是異步發送的方式。

對於實際的同步發送消息,實際上就是通過future.get(…),等待獲取結果。對於實際的異步發送消息,一般採用第二個有回調的方法。

接下來展開源碼分析,來看看它內部是怎麼發送的,發送到哪裏。

    @Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
        return send(record, null);
    }
    
    @Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        /* ProducerInterceptors<K, V> */
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        return doSend(interceptedRecord, callback);
    }

this.interceptors 是 KafkaProducer 的 ProducerInterceptors 屬性,而ProducerInterceptors 封裝了ProducerInterceptor列表。

攔截器對消息進行處理

onSend(…)方法,實際上遍歷所有的ProducerInterceptor,分別調用其onSend(…)方法。

    public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
        ProducerRecord<K, V> interceptRecord = record;
        for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
            try {
            	/* 交給自定義的ProducerInterceptor實現該方法 */
                interceptRecord = interceptor.onSend(interceptRecord);
            } catch (Exception e) {
               ......
            }
        }
        return interceptRecord;
    }

現在剩下doSend(interceptedRecord, callback)方法,也是本文分析的核心方法。接下來,會拆分這個方法,展開詳細分析。

內部的第一個方法如下:

等待集羣元數據的更新完成
  1. 檢查Sender是否是running狀態
throwIfProducerClosed();
private void throwIfProducerClosed() {
	/* volatile boolean running */
    if (sender == null || !sender.isRunning())
        throw new IllegalStateException("Cannot perform operation after producer has been closed");
}

Sender,Kafka的後臺線程,用來發送獲取集羣元數據請求以及發送消息到節點。

接下來的方法如下:

  1. 等待集羣元數據的更新完成
try {
    clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
} catch (KafkaException e) {
    if (metadata.isClosed())
        throw new KafkaException("Producer closed while send in progress", e);
    throw e;
}

這個方法是用來等待集羣元數據可用。

這裏傳入三個參數topic、partition以及maxBlockTimeMs(等待的超時時間)。

waitOnMetadata(…):等待集羣元數據可用(包括給定主題的分區)。

首先是調用如下方法:

2.1

Cluster cluster = metadata.fetch();

這裏的 metadata 是 ProducerMetadata,fetch()用來獲取Cluster實例。

Cluster屬性如下:
在這裏插入圖片描述

獲取Cluster實例之後,又調用如下方法:

2.2

if (cluster.invalidTopics().contains(topic))
    throw new InvalidTopicException(topic);

判斷給定topic是否是無效的topic,也就是它是否在無效的topic集合中。

2.3

metadata.add(topic);
public synchronized void add(String topic) {
    Objects.requireNonNull(topic, "topic cannot be null");
    /* HashMap<String, Long> topics */
    /* put(topic, -1L) */
    if (topics.put(topic, TOPIC_EXPIRY_NEEDS_UPDATE) == null) {
        requestUpdateForNewTopics();
    }
}

topics:用來維護topic的名字與topic的失效時間。

然後看下requestUpdateForNewTopics()方法。

public synchronized void requestUpdateForNewTopics() {
    // Override the timestamp of last refresh to let immediate update.
    this.lastRefreshMs = 0;
    this.requestVersion++;
    this.needUpdate = true;
}

lastRefreshMs:記錄上一次更新元數據的時間戳。
requestVersion:元數據的版本號。
needUpdate:是否強制更新元數據。

2.4

Integer partitionsCount = cluster.partitionCountForTopic(topic);

用於獲取給定topic的分區數。

public Integer partitionCountForTopic(String topic) {
    List<PartitionInfo> partitions = this.partitionsByTopic.get(topic);
    return partitions == null ? null : partitions.size();
}

2.5

if (partitionsCount != null && (partition == null || partition < partitionsCount))
    return new ClusterAndWaitTime(cluster, 0);

ClusterAndWaitTime 這個類,顯而易見,封裝Cluster和等待元數據更新花費的時間。

2.6

long begin = time.milliseconds();
long remainingWaitMs = maxWaitMs;

begin:作爲當前的開始時間。
remainingWaitMs:剩餘的等待時間。

2.7
接下來的方法,是一個do while循環。用來更新元數據。

判斷的條件是partitionsCount == null || (partition != null && partition >= partitionsCount

Cluster中保存的給定topic的分區數爲空,說明此時Cluster還沒有保存相關的數據,我們需要更新元數據。

爲什麼會有大於我們在Cluster保存的分區數的情況呢?

我們可以動態添加(比如腳本的方式)分區,對於這種情況,說明我們現在的Cluster保存的不是最新的數據,因此需要更新。

對於循環內部的方法,比較長,我們這裏也拆分來分析。

2.7.1

if (partition != null) {
    log.trace("Requesting metadata update for partition {} of topic {}.", partition, topic);
} else {
    log.trace("Requesting metadata update for topic {}.", topic);
}

簡單的記錄。

2.7.2

metadata.add(topic);
int version = metadata.requestUpdate();
sender.wakeup();

第一個方法我們前面分析過,這裏不再敘述。

再來看metadata.requestUpdate()。方法內部如下:

this.needUpdate = true;
return this.updateVersion;

設置更新元數據的標識。返回此時的版本號。

接下來的sender.wakeup()比較重要。我們前面有簡單介紹sender的作用。這裏是用來喚醒Sender,也就是Sender的NetworkClient屬性,調用它的wakeup()方法。最後,是進行元數據的更新操作。

2.7.3

try {
    metadata.awaitUpdate(version, remainingWaitMs);
} catch (TimeoutException ex) {
    throw new TimeoutException(...)
}

awaitUpdate(…)等待元數據更新完成。判斷完成的依據是(正常完成時)updateVersion大於lastVersion,或者在更新的過程中元數據實例關閉。

方法如下:

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");
}

2.7.4

cluster = metadata.fetch();
elapsed = time.milliseconds() - begin;
if (elapsed >= maxWaitMs) {
    throw new TimeoutException("......");
}

計算執行時間,如果超出了閾值,就會拋出異常。

2.7.5

 metadata.maybeThrowExceptionForTopic(topic);

對於不可恢復的異常,直接拋出對應的異常。停止後續的操作。

public synchronized void maybeThrowExceptionForTopic(String topic) {
    clearErrorsAndMaybeThrowException(() -> recoverableExceptionForTopic(topic));
}
private void clearErrorsAndMaybeThrowException(Supplier<KafkaException> recoverableExceptionSupplier) {
    KafkaException metadataException = Optional.ofNullable(fatalException).orElseGet(recoverableExceptionSupplier);
    fatalException = null;
    clearRecoverableErrors();
    if (metadataException != null)
        throw metadataException;
}
private KafkaException recoverableExceptionForTopic(String topic) {
    if (unauthorizedTopics.contains(topic))
        return new TopicAuthorizationException(Collections.singleton(topic));
    else if (invalidTopics.contains(topic))
        return new InvalidTopicException(Collections.singleton(topic));
    else
        return null;
}
private void clearRecoverableErrors() {
    invalidTopics = Collections.emptySet();
    unauthorizedTopics = Collections.emptySet();
}

2.7.6

remainingWaitMs = maxWaitMs - elapsed;
partitionsCount = cluster.partitionCountForTopic(topic);

計算剩餘的等待時間。獲取此時給定topic的分區數。

2.7.7

return new ClusterAndWaitTime(cluster, elapsed);

elapsed:是整個的更新元數據花費的時間。

序列化消息
long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
Cluster cluster = clusterAndWaitTime.cluster;

計算剩餘的等待時間。獲取此時的Cluster實例。

  1. 序列化
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());

使用指定的序列化器,來序列化消息。

計算並分配分區
  1. 計算並分配分區
int partition = partition(record, serializedKey, serializedValue, cluster);
tp = new TopicPartition(record.topic(), partition);
private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
    Integer partition = record.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) {
	/* 獲取給定topic的所有分區 */
    List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
    /* 分區大小 */
    int numPartitions = partitions.size();
    if (keyBytes == null) {
        int nextValue = nextValue(topic);
        /* 獲取給定topic的所有可用分區 */
        List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
        if (availablePartitions.size() > 0) {
            int part = Utils.toPositive(nextValue) % availablePartitions.size();
            return availablePartitions.get(part).partition();
        } else {
            return Utils.toPositive(nextValue) % numPartitions;
        }
    } else {
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }
}

Utils.murmur2(byte[] arr):一種生成32位的murmur2 hash算法。這裏不展開分析,有興趣的同學,可以去源碼中看看。
Utils.toPositive(…):對整數取絕對值,這裏採用位運算即 number & 7fffffff

事實上,我們可以自己定義分區器,實現Partitioner接口的partition(…)方法,然後在application.yml文件中添加如下:

spring:
	kafka:
		producer:
			properties:
				partitoner-class: xxx.xxx.CustomPartitioner

之後,使用這個經過分區器計算的分區partition,作爲TopicPartition實例的一個參數。TopicPartition,封裝了topic和partition。

setReadOnly(record.headers());
Header[] headers = record.headers().toArray();

record.headers():獲取消息的Headers對象,也就是消息頭。

private void setReadOnly(Headers headers) {
    if (headers instanceof RecordHeaders) {
        ((RecordHeaders) headers).setReadOnly();
    }
}

實際上對isReadOnly屬性設置爲true,做了一個只讀的標識。然後將這個Headers轉換成Header數組。

  1. 確保消息的相關屬性,不超過閾值
int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
        compressionType, serializedKey, serializedValue, headers);
ensureValidRecordSize(serializedSize);

estimateSizeInBytesUpperBound(…)方法是對消息的相關屬性,使用給定的壓縮算法壓縮後,計算相關屬性大小的估計值,將其作爲這個消息的大小。

private void ensureValidRecordSize(int size) {
	/* max.request.size */
    if (size > this.maxRequestSize)
        throw new RecordTooLargeException("......");
     /* buffer.memory */
    if (size > this.totalMemorySize)
        throw new RecordTooLargeException("......");
}

確保消息的大小不超過設置的單次請求最大值、buffer緩衝區大小。

long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);

這個callback,封裝了發送消息時設置的回調、攔截器數組、TopicPartition實例。

  1. 對指定的TopicPartition以及相關信息,進行記錄
/* transactionalId != null */
if (transactionManager != null && transactionManager.isTransactional())
    transactionManager.maybeAddPartitionToTransaction(tp);

maybeAddPartitionToTransaction(…)方法用於對指定的TopicPartition以及相關信息,進行記錄。

	public synchronized void maybeAddPartitionToTransaction(TopicPartition topicPartition) {
		/* 校驗currentState和producerId(開啓事務時)是否符合要求 */
        failIfNotReadyForSend();

		/* partitionsInTransaction#Set<TopicPartition>.contains(partition) */
		/* newPartitionsInTransaction.contains(partition) || pendingPartitionsInTransaction.contains(partition) */
        if (isPartitionAdded(topicPartition) || isPartitionPendingAdd(topicPartition))
            return;

        topicPartitionBookkeeper.addPartition(topicPartition);
        /* Set<TopicPartition> */
        newPartitionsInTransaction.add(topicPartition);
    }
    
	/* TransactionManager$TopicPartitionBookkeeper */
	public void addPartition(TopicPartition topic) {
        if (!topicPartitionBookkeeping.containsKey(topic))
            topicPartitionBookkeeping.put(topic, new TopicPartitionEntry());
    }

對currentState以及producerId(開啓事務時)屬性的校驗。

	synchronized void failIfNotReadyForSend() {
		/* currentState == State.ABORTABLE_ERROR || currentState == State.FATAL_ERROR */
        if (hasError())
            throw new KafkaException("......");
		/* transactionalId != null */
		/* 也就是設置了spring.kafka.producer.properties.transaction.id */
        if (isTransactional()) {
        	/* 如果producerId <= -1L */
            if (!hasProducerId())
                throw new IllegalStateException(".......");
            if (currentState != State.IN_TRANSACTION)
                throw new IllegalStateException(".......");
        }
    }
追加消息到Accumulator
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
        serializedValue, headers, interceptCallback, remainingWaitMs);
/* 如果批次滿了 或者 創建新的批次 */
if (result.batchIsFull || result.newBatchCreated) {
    /* 喚醒Sender */
    this.sender.wakeup();
}
return result.future;

本地旅途的目的地,將消息放置到RecordAccumulator中。具體的方法分析,見下一篇文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章