一文帶你看懂-生產者-KafkaProducer

1.發送消息流程

生產者與服務端完成一次網絡通信步驟如下:

  1. 生產者客戶端應用程序產生消息

  2. 客戶端連接對象將消息包裝到請求中,發送到服務端

  3. 服務短連接對象負責接收請求,並將消息以文件形式存儲

  4. 服務端返回響應結果給生產者客戶端
    在這裏插入圖片描述
    以上流程對應到代碼中如下:

  5. 創建ProducerRecord對象,此對象包含目標主題和要發送的內容,還可以指定鍵或者分區

  6. 生產者對ProducerRecord對象進行序列化

  7. 分區器接收到消息後,先來選擇一個分區

  8. 這條記錄被添加到一個記錄批次裏,這個批次會被髮送到相同的主題和分區上

  9. 獨立線程負責把這些記錄批次,發送到相應的broker上

  10. broker就是服務器,在接收到消息時,會返回一個響應,如果成功,返回一個RecordMetaData對象,它包含了主題和分區信息,以及記錄在分區裏的偏移量。如果失敗,就會返回一個錯誤

  11. 生產者如果接收到錯誤之後,會嘗試重新發送,幾次嘗試後,還是失敗,就返回錯誤信息

生產者發送的代碼在clients包下,下面我們詳細講解下發送流程,附有源碼和流程圖,方便大家理解。

2.實例

要往Kafka裏面寫入消息,首先要創建一個生產者對象,並設置一些屬性。
配置屬性:

  1. bootstrap.servers
    該屬性指定broker的地址清單,地址的格式爲host:port
  2. key.serializer
    指定了序列化的類,生產者會使用這個類把鍵對象序列化成字節數組
  3. value.serializer
    指定了序列化的類,生產者會使用這個類把值序列化
  4. client.id
    客戶端的id
    發送消息的方式:
  5. 發送並忘記
    我們把消息發送給服務器,但並不關心它是否正常到達
  6. 同步發送
    我們使用send()方法發送消息,它會返回一個Future對象,調用get()方法進行等待,就可以知道消息是否發送成功
  7. 異步發送
    我們調用send()方法,並指定一個回調 函數,服務器在返回響應時調用該函數

我們來看下源碼中的例子

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();
        //Kafka服務端的主機名和端口號
        props.put("bootstrap.servers", "localhost:9092");
        //客戶端的id
        props.put("client.id", "DemoProducer");
        //key的序列化方法
        props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
        //value的序列化方法
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        producer = new KafkaProducer<>(props);
        this.topic = topic;
        this.isAsync = isAsync;
    }

    public void run() {
        //消息的key
        int messageNo = 1;
        while (true) {
            //消息的value
            String messageStr = "Message_" + messageNo;
            long startTime = System.currentTimeMillis();
            if (isAsync) { // Send asynchronously 異步發送
                //第一個參數是ProducerRecord對象,封裝了目標Topic、消息的key,消息的value
                //第二個參數是一個Callback對象,當生產者接收到Kafka發來的ACK確認消息的時候,
                //會調用此CallBack對象的onCompletion()方法,實現回調
                producer.send(new ProducerRecord<>(topic,
                    messageNo,
                    messageStr), new DemoCallBack(startTime, messageNo, messageStr));
            } else { // Send synchronously 同步發送
                try {
                    //send()返回的是一個Future<RecordMetadata>這裏通過Future.get()方法,阻塞當前線程
                    //等待Kafka服務端的ACK響應
                    producer.send(new ProducerRecord<>(topic,
                        messageNo,
                        messageStr)).get();
                    System.out.println("Sent message: (" + messageNo + ", " + messageStr + ")");
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
            //遞增消息的Key
            ++messageNo;
        }
    }
}

//回調對象
class DemoCallBack implements Callback {
    //開始發送消息的時間戳
    private final long startTime;
    //消息的Key
    private final int key;
    //消息的value
    private final String message;

    public DemoCallBack(long startTime, int key, String message) {
        this.startTime = startTime;
        this.key = key;
        this.message = message;
    }

    /**
     * 生產者成功發送消息,收到Kafka服務端發來的ACK確認消息後,會調用此回調函數
     *
     * @param metadata  生產者發送的消息的元數據,如果發送過程中出現異常,此參數爲null
     * @param exception 發送過程中出現的異常,如果發送成功,則此參數爲null
     */
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        if (metadata != null) {
            //RecordMetadata中包含了分區信息、offset信息等
            System.out.println(
                "message(" + key + ", " + message + ") sent to partition(" + metadata.partition() +
                    "), " +
                    "offset(" + metadata.offset() + ") in " + elapsedTime + " ms");
        } else {
            exception.printStackTrace();
        }
    }
}

KafkaProducer只用了一個send方法,就可以完成同步和異步兩種模式的消息發送。這是因爲send方法返回的是一個Future。基於Future,我們可以實現同步或異步的消息發送語義。

  • 同步。調用send返回Future時,需要立即調用get,因爲Future.get在沒有返回結果時會一直阻塞。
  • 異步。提供一個回調,調用send後,可以繼續發送消息而不用等待。當有結果返回時,會自動執行回調函數。

源碼中的註釋寫的很清楚了,就不再贅述了,大家有興趣也可以把源碼從github上下載下來。

3.kafkaProducer分析

我們來看下發送消息時的類圖
在這裏插入圖片描述消息發送的流程如上,主要有3個模塊,主線程,負責生產消息並序列化,選擇分區。然後將消息存放到RecordAccumulator中,相當於記錄的緩衝區,當緩衝區滿了之後,sender線程會從緩衝區中讀取數據,併發送到Kafka的服務器上。

  1. ProducerInterceptors對消息進行攔截
  2. Serializer對消息的key和value進行序列化
  3. Patitioner爲消息選擇合適的Partition
  4. RecordAccumulator收集消息,實現批量發送
  5. Sernder線程從RecordAccumulator中收集消息,實現批量發送
  6. 構造ClientRequest
  7. 將ClientRequest交給NetworkClient,準備發送
  8. NetworkClient將請求放入KafkaChannel
  9. 執行網絡IO,發送請求
  10. 收到響應,調用ClientRequest的回調函數
  11. 調用RecordBatch的回調函數,最終調用每個消息上註冊的回調函數

代碼調用的時序圖如下:
在這裏插入圖片描述
解釋一下上圖的主要流程:

1.1:調用ProducerInterceptors的onSend()方法,對消息進行加工
1.2:實際發送doSend()方法
1.2.1:waitOnMetadata判斷是否要更新元數據,以及wakeUp sender線程去更新元數據
1.2.2:序列化key
1.2.3:序列化value
1.2.4:獲取分區信息
1.2.5:保證記錄的size小於最大的請求size,小於totalSize
1.2.6:將記錄追加到RecordAccumulator中
1.2.7:wakeup sender線程,去發送數據

現在呢,我們大體流程已經都瞭解了,下面我們深入去看下里面涉及到的部分邏輯

3.1 ProducerInterceptors

ProducerInterceptors裏面維護了一個ProducerInterceptor的list。這個類,主要是在消息被髮送到Kafka服務器前,或者接收到了kafka對消息處理信息之後,進行加工處理。
在這裏插入圖片描述
ProducerInterceptor中主要有三個方法
onSend():在消息發送到kafka服務器之前對消息進行處理
onAcknowledgement():在收到kafka服務器響應之後(包含正常響應和異常響應),對信息進行處理,會先於用戶自定義的回調方法
close():關閉時使用

在ProducerInterceptors裏面調用onSend和onAcknowledgement都不會響應錯誤,只會打印錯誤日誌,並繼續執行下一個攔截。所以我們自定義ProducerInterceptor時,需要自己在方法內,校驗,並處理異常邏輯。我們看一下源代碼

public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
        ProducerRecord<K, V> interceptRecord = record;
        for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
            try {
                interceptRecord = interceptor.onSend(interceptRecord);
            } catch (Exception e) {
                // 不傳播攔截器異常,記錄並繼續調用其他攔截器,注意這裏不會返回異常
                if (record != null)
                    log.warn("Error executing interceptor onSend callback for topic: {}, partition: {}", record.topic(), record.partition(), e);
                else
                    log.warn("Error executing interceptor onSend callback", e);
            }
        }
        return interceptRecord;
    }

public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
            try {
                interceptor.onAcknowledgement(metadata, exception);
            } catch (Exception e) {
                // do not propagate interceptor exceptions, just log
                log.warn("Error executing interceptor onAcknowledgement callback", e);
            }
        }
    }

onAcknowledgement方法中會把異常傳遞到實現類中,我們就可以根據這個來判斷,消息是正常返回,還是異常返回了
kafka源碼中有使用ProducerInterceptor的例子,我們來看一下

    private class AppendProducerInterceptor implements ProducerInterceptor<Integer, String> {
        private String appendStr = "";
        private boolean throwExceptionOnSend = false;
        private boolean throwExceptionOnAck = false;

        public AppendProducerInterceptor(String appendStr) {
            this.appendStr = appendStr;
        }

        @Override
        public void configure(Map<String, ?> configs) {
        }

        @Override
        public ProducerRecord<Integer, String> onSend(ProducerRecord<Integer, String> record) {
            //在發送時,統計發送的個數
            onSendCount++;
            //此異常在其他方法中手動設置的
            if (throwExceptionOnSend)
                throw new KafkaException("Injected exception in AppendProducerInterceptor.onSend");

            ProducerRecord<Integer, String> newRecord = new ProducerRecord<>(
                    record.topic(), record.partition(), record.key(), record.value().concat(appendStr));
            return newRecord;
        }

        @Override
        public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
            //統計ack響應的次數
            onAckCount++;
            if (exception != null) {
                //如果存在exception,則代表error,統計error的個數
                onErrorAckCount++;
                // 處理metadata情況
                if (metadata != null && metadata.topic().length() >= 0) {
                    onErrorAckWithTopicSetCount++;
                    if (metadata.partition() >= 0)
                        onErrorAckWithTopicPartitionSetCount++;
                }
            }
            if (throwExceptionOnAck)
                throw new KafkaException("Injected exception in AppendProducerInterceptor.onAcknowledgement");
        }

        @Override
        public void close() {
        }

        // if 'on' is true, onSend will always throw an exception
        public void injectOnSendError(boolean on) {
            throwExceptionOnSend = on;
        }

        // if 'on' is true, onAcknowledgement will always throw an exception
        public void injectOnAcknowledgementError(boolean on) {
            throwExceptionOnAck = on;
        }
    }

總之,上面的例子就是統計發送,發送成功,發送失敗,的消息個數。

3.2 集羣元數據

在前面我們發送消息的例子中,我們只指定了topic的信息,但是我們在發送到服務器的時候,是需要知道,發送的哪個分區上面,哪臺機器,端口號是啥,leader副本是哪個等等。kafka是會把這些分區信息維護在生產者本地的,用Metadata來維護,我們詳細看看這個Metadata是如何來維護這些信息的。
在KafkaProducer中,使用Node,TopicPartition,PartitionInfo封窗了Kafka集羣相關的元數據。

  • Node:表示集羣中的一個節點,Node記錄了這個節點的host,ip,port等信息
  • TopicPartition:表示某Topic的一個分區,其中的topic字段是Topic的名稱,partition字段則是此分區在Topic中的分區編號(ID)
  • PartitionInfo:分區的詳細信息,leader字段記錄了Leader副本所在節點的id,replica字段記錄了全部副本所在的節點信息,inSyncReplicas字段記錄了ISR集合中所有副本所在的節點信息。

通過組合這三個類,我們就可以完整的描述集羣元數據信息,這些元數據保存在cluster中,並按照不同的映射方法進行存放。如下:

    //kafka集羣中的節點信息
    private final List<Node> nodes;
    private final Set<String> unauthorizedTopics;
    //記錄了TopicPartition和PartitionInfo之間的關係
    private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition;
    //記錄了Topic和PartitionInfo的映射關係,可以按照Topic名稱查詢其中全部的partition信息
    private final Map<String, List<PartitionInfo>> partitionsByTopic;
    //topic與PartitionInfo的映射關係,List中存在的分區,必須是有Leader副本的分區
    private final Map<String, List<PartitionInfo>> availablePartitionsByTopic;
    //記錄了Node與PartitionInfo的映射關係,可以按照節點id查詢以上分佈的全部分區的詳細信息
    private final Map<Integer, List<PartitionInfo>> partitionsByNode;
    //BrokerId 與Node節點中的對應關係,方便按照BrokerId進行檢索
    private final Map<Integer, Node> nodesById;

Metadata中封裝了cluster對象,並保存Cluster數據的最後更新時間,版本號,是否需要更新等信息,看下Metadata中的主要屬性

   //兩次發出更新Cluster保存的元數據信息最小的時間差,默認100ms
    private final long refreshBackoffMs;
    //每隔多久就更新一次
    private final long metadataExpireMs;
    //記錄了元數據版本的版本號,Kafka集羣元數據,每更新一次就+1,
    private int version;
    //上一次更新元數據的時間戳,包含失敗
    private long lastRefreshMs;
    //上一次成功更新的時間戳
    private long lastSuccessfulRefreshMs;
    //記錄了topic最新的元數據
    private Cluster cluster;
    private boolean needUpdate;
    //記錄了當前已知的所有topic
    private final Set<String> topics;
    //監聽metaData更新的監聽器集合,在更新cluster中的字段前,會通知listener集合中全部的Listner對象
    private final List<Listener> listeners;
    //是否需要更新全部的topic數據
    private boolean needMetadataForAllTopics;

下面我們重點講一個更新的流程

private long waitOnMetadata(String topic, long maxWaitMs) throws InterruptedException {
        //1.檢查topics集合中是否包含指定的Topic,如果沒有則添加進去
        if (!this.metadata.containsTopic(topic))
            this.metadata.add(topic);

        //2.成功獲取分區的詳細信息
        if (metadata.fetch().partitionsForTopic(topic) != null)
            return 0;

        long begin = time.milliseconds();
        long remainingWaitMs = maxWaitMs;
        //3.如果從metadata中獲得當前topic的partitions爲null,那麼就去更新metadata數據
        while (metadata.fetch().partitionsForTopic(topic) == null) {
            log.trace("Requesting metadata update for topic {}.", topic);
            int version = metadata.requestUpdate();
            //3.1喚醒sender線程
            sender.wakeup();
            //3.2阻塞,等待元數據更新完畢
            metadata.awaitUpdate(version, remainingWaitMs);
            long elapsed = time.milliseconds() - begin;
            //3.3檢測超時時間
            if (elapsed >= maxWaitMs)
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            //3.4檢測權限
            if (metadata.fetch().unauthorizedTopics().contains(topic))
                throw new TopicAuthorizationException(topic);
            remainingWaitMs = maxWaitMs - elapsed;
        }
        return time.milliseconds() - begin;
    }

以上是啓動更新的各種條件,我們再去metadata.awaitUpdate(version, remainingWaitMs);方法中看看他是如何等待更新元數據的

public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException {
        if (maxWaitMs < 0) {
            throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds");
        }
        long begin = System.currentTimeMillis();
        long remainingWaitMs = maxWaitMs;
        //直到當前version>lastVrsion
        while (this.version <= lastVersion) {
            //主線程與Sender線程通過wait/notify同步,更新元數據的操作則交給Sender線程去完成
            if (remainingWaitMs != 0)
                wait(remainingWaitMs);
            long elapsed = System.currentTimeMillis() - begin;
            //是否超時
            if (elapsed >= maxWaitMs)
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            remainingWaitMs = maxWaitMs - elapsed;
        }
    }

while循環中,判斷當前的version是否大於lastVersion,如果大於的話,則代表已經更新成功了。那麼具體是哪裏去更新的呢?又是哪裏獲取更新的結果的呢?我們繼續分析
前面是標記了,我們需要更新metadata,那我們又是在哪裏去發起更新請求的呢?答案在NetworkClient裏面,至於如何會調用到此方法,我們在後面消息發生部分會講到

public List<ClientResponse> poll(long timeout, long now) {
        //更新Metadata
        long metadataTimeout = metadataUpdater.maybeUpdate(now);
        try {
            //執行IO操作
            this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
        } catch (IOException e) {
            log.error("Unexpected error during I/O", e);
        }
        ......
}        

maybeUpdate我們默認是在DefaultMetadataUpdater中,這個類是NetworkClient的內部類,下面這個類主要是判斷是否需要更新MetaData

public long maybeUpdate(long now) {
            //調用Metadata.timeToNextUpdate()方法,其中會檢測needUpdate的值,退避時間,
            // 是否長時間未更新,最終得到一個下次更新集羣元數據的時間戳
            long timeToNextMetadataUpdate = metadata.timeToNextUpdate(now);
            //獲取下次嘗試重新連接服務端的時間戳
            long timeToNextReconnectAttempt = Math.max(this.lastNoNodeAvailableMs + metadata.refreshBackoff() - now, 0);
            //檢測是否已經發送了MetadataRequest請求
            long waitForMetadataFetch = this.metadataFetchInProgress ? Integer.MAX_VALUE : 0;

            // 計算當前距離下次可以發送MetadataRequest請求的時間差
            long metadataTimeout = Math.max(Math.max(timeToNextMetadataUpdate, timeToNextReconnectAttempt),
                    waitForMetadataFetch);

            //允許發送MetadataRequest請求
            if (metadataTimeout == 0) {
                //請注意,此方法的行爲以及對poll()的超時計算都是 高度依賴leastLoadedNode的行爲。
                //找到負載最小的Node,若沒有可用Node,則返回null
                Node node = leastLoadedNode(now);
                //創建並緩存MetadataRequest,等待下次poll()方法纔會真正發送
                maybeUpdate(now, node);
            }

            return metadataTimeout;
        }

如果需要更新的話,那麼就調用maybeUpdate,實際發送請求,調用doSend()將數據寫入到send屬性中,下次調用poll方法就會把數據發送出去了

private void maybeUpdate(long now, Node node) {
            //檢測是否有node可用
            if (node == null) {
                log.debug("Give up sending metadata request since no node is available");
                // 設置lastNoNodeAvailableMs
                this.lastNoNodeAvailableMs = now;
                return;
            }
            String nodeConnectionId = node.idString();

            //檢測是否允許向此Node發送請求
            if (canSendRequest(nodeConnectionId)) {
                this.metadataFetchInProgress = true;
                MetadataRequest metadataRequest;
                //指定需要更新元數據的Topic
                if (metadata.needMetadataForAllTopics())
                    metadataRequest = MetadataRequest.allTopics();
                else
                    metadataRequest = new MetadataRequest(new ArrayList<>(metadata.topics()));
                //將MetadataRequest封裝成ClientRequest
                ClientRequest clientRequest = request(now, nodeConnectionId, metadataRequest);
                log.debug("Sending metadata request {} to node {}", metadataRequest, node.id());
                //緩存請求,在下次poll()操作中會將其發送出去
                doSend(clientRequest, now);
            } else if (connectionStates.canConnect(nodeConnectionId, now)) {
                log.debug("Initialize connection to node {} for sending metadata request", node.id());
                //初始化連接
                initiateConnect(node, now);
            } else {
                //已經成功連接到指定節點,但不能發送請求,則更新lastNoNodeAvailableMs後等待
                this.lastNoNodeAvailableMs = now;
            }
        }

上面我們講完了,如何發送這個流程,下面來看在哪裏處理這個結果的
在NetworkClient中的poll方法中,這個poll方法使用來處理sockets的讀寫事件的,所以在這裏可以拿到服務器返回的Metadata的信息。調用 handleCompletedReceives(responses, updatedNow);處理CompletedReceives隊列。我們看下這個方法

/**
* 處理所有已完成的接收,並使用接收到的響應更新響應列表。
*/
private void handleCompletedReceives(List<ClientResponse> responses, long now) {
        //遍歷completedReceives
        for (NetworkReceive receive : this.selector.completedReceives()) {
            //非關鍵代碼不展示
            ...
            //如果是Metadata更新請求的響應
            // 那麼調用MetadataUpdater.maybeHandleCompleltedReceive()方法處理
            // 重點:MetadataResponse,其中會更新Metadata中記錄的集羣元數據,並喚醒所有等待Metadata 更新完成的線程
            if (!metadataUpdater.maybeHandleCompletedReceive(req, now, body))
                //如果不是MetadataResponse,則創建ClientResponse並添加到responses集合
                responses.add(new ClientResponse(req, now, false, body));
        }
    }

其他代碼先不看,我們看“重點”部分,如果返回是metadata的話,直接調用metadataUpdater.maybeHandleCompletedReceive(req, now, body)來處理。

public boolean maybeHandleCompletedReceive(ClientRequest req, long now, Struct body) {
            short apiKey = req.request().header().apiKey();
            //如果是的Metadata類型的化,我們就解析響應
            if (apiKey == ApiKeys.METADATA.id && req.isInitiatedByNetworkClient()) {
                handleResponse(req.request().header(), body, now);
                return true;
            }
            return false;
        }

看一下具體咋解析的

private void handleResponse(RequestHeader header, Struct body, long now) {
            this.metadataFetchInProgress = false;
            MetadataResponse response = new MetadataResponse(body);
            //Cluster是不可變對象,所以我們創建新的Cluster對象,並覆蓋Metadata.cluster字段
            Cluster cluster = response.cluster();
            Map<String, Errors> errors = response.errors();
            if (!errors.isEmpty())
                log.warn("Error while fetching metadata with correlation id {} : {}", header.correlationId(), errors);

            if (cluster.nodes().size() > 0) {
                //在此方法中,首先通知Metadata上的監聽器,之後更新cluster字段,最後喚醒等待Metadata更新完成的線程
                this.metadata.update(cluster, now);
            } else {
                //更新metadata失敗,只是更新lastRefreshMs字段
                log.trace("Ignoring empty metadata response with correlation id {}.", header.correlationId());
                this.metadata.failedUpdate(now);
            }
        }

簡單來說,就是先新建Cluster對象,然後調用metadata的update方法去更新

public synchronized void update(Cluster cluster, long now) {
        //不再需要更新
        this.needUpdate = false;
        //最後更新時間爲現在
        this.lastRefreshMs = now;
        //上次成功更新時間爲現在
        this.lastSuccessfulRefreshMs = now;
        //當前版本+1
        this.version += 1;

        //監聽
        for (Listener listener: listeners)
            listener.onMetadataUpdate(cluster);
            
        this.cluster = this.needMetadataForAllTopics ? getClusterForCurrentTopics(cluster) : cluster;
        //更新完元數據後,喚醒等待此元數據的所有線程
        notifyAll();
        log.debug("Updated cluster metadata version {} to {}", this.version, this.cluster);
    }

至此,metadata.awaitUpdate中,拿到的version版本就大於lastVersion了,整個更新流程就結束了

3.3 Serialiser&DeseriaLizer

主要就是序列化和反序列化,這個就不詳細展開了

3.4 Partitioner

在3.2中,我們已經更新了元數據信息,下一步就是從Metadata中選擇分區了,分區選擇主要分爲兩個部分

  • 如果用戶指定了partition,那麼校驗用戶指定的partition,校驗合格就直接使用
  • 如果用戶沒有指定,那麼就去計算partition

在KafkaProducer中

private int partition(ProducerRecord<K, V> record, byte[] serializedKey , byte[] serializedValue, Cluster cluster) {
        Integer partition = record.partition();
        //如果record指定了partition,那麼直接使用用戶指定的partition
        if (partition != null) {
            List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
            int lastPartition = partitions.size() - 1;
            //校驗partition是否真實存在
            if (partition < 0 || partition > lastPartition) {
                throw new IllegalArgumentException(String.format("Invalid partition given with record: %d is not in the range [0...%d].", partition, lastPartition));
            }
            return partition;
        }
        //用戶沒有指定partition,則返回的計算的分區
        return this.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue,
            cluster);
    }

用戶沒有指定partition,則去計算分區,在DefaultPartitioner中

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //從Cluster中獲取對應的topic的分區信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        //分區的個數
        int numPartitions = partitions.size();
        //如果消息的key爲null
        if (keyBytes == null) {
            //遞增counter
            int nextValue = counter.getAndIncrement();
            //選擇availablePartitions
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                //nextValue取絕對值,並對可用的分區數取餘
                int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
                return availablePartitions.get(part).partition();
            } else {
                // 沒有可用的分區,就提供一個不可用的分區,隨機提供一個partitionId
                return DefaultPartitioner.toPositive(nextValue) % numPartitions;
            }
        } else {
            // 對key用murumur2算法進行hash,選擇分區
            return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

至此,選擇分區的過程也結束了

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