kafka2.0-冪等發送(the idempotent producer)_09

從kafka 0.11版本開始,生成者就支持了兩種額外的發送模式 - 冪等發送(the idempotent producer)和事物發送(the transactional producer),可以說這是kafka在支持EOS(exactly-once semantics)上的重要功能。事物發送在後面講。

什麼是冪等?(如果已經瞭解,可跳過)
舉個例子:比如用戶對訂單付款之後,生成了一個付款成功的消息,發送給了訂單系統,訂單系統接收到消息之後,將訂單狀態爲已付款,後來,訂單系統又收到了一個發貨成功的消息,再將訂單狀態更新爲已發貨,但是由於網絡或者是系統的原因,訂單系統再次收到了之前的付款成功的消息,也就是消息重複了,這個在現象在實際應用中也經常出現。訂單系統的處理是,查詢數據庫,發現這個訂單狀態爲已發貨,然後不再更改訂單狀態。這時候,我們可以說訂單處理消息的接口是冪等的,如果訂單再次將狀態更新爲已付款,接口就是非冪等的。

kafka的消息重複發送問題
在以前的kafka的老版本中,是支持消息的同步發送的,但是現在,kafka全部改成了異步發送。其具體過程是
kafkaProducer.send()方法將消息發送到緩衝區中,然後後臺的一個IO線程讀取緩衝區中的數據,將消息發送到對應的broker上。

我們在發送消息的時候,如果設置了retries的次數大於0,就可能一個消息被重複的發送到了broker上,並且broker也保存了多次,具體產生過程如下:
kafka的消息重複發送問題

具體的情況是,由於網絡原因第三步ack消息回傳的時候,客戶端沒有接收到發送成功確認消息,客戶端會重發。所以這就產生了消息的生產。

如果我們設置retries等於0,那麼假如在第一步消息就發送失敗了,那麼消息將無法正確的發送到kafka集羣。

冪等發送
如果想發送消息不重複,可以使用kafka的冪等發送,這個功能早在0.11版本中就存在了。
使用冪等發送只需要這樣設置props.put("enable.idempotence", true);,默認情況下enable.idempotencefalse,如果設置了它爲trueretries的默認值將爲 Integer.MAX_VALUEacks默認爲all
開啓冪等發送之後,其發送過程將會如下:
這裏寫圖片描述

爲了實現Producer的冪等性,Kafka引入了Producer ID(即PID)和Sequence Number

PID:當每個新的Producer在初始化的時候,會分配一個唯一的PID,這個PID對用戶是不可見的。
Sequence Numbler:(對於每個PID,該Producer發送數據的每個<Topic, Partition>都對應一個從0開始單調遞增的Sequence Number
Broker端在緩存中保存了這Sequence Numbler,對於接收的每條消息,如果其序號比Broker緩存中序號大於1則接受它,否則將其丟棄。這樣就可以實現了消息重複提交了。但是,只能保證單個Producer對於同一個<Topic, Partition>的EOS。不能保證同一個Producer一個topic不同的partition冪等。

總而言之,冪等的producer只能保證在同一個session和同一個partition中支持EOS。

源碼解讀

KafkaProducer的構造方法中初始化化了一個IO線程,用來發送producer放在緩存中的消息,如下:

this.sender = new Sender(logContext,
        client,
        this.metadata,
        this.accumulator,
        maxInflightRequests == 1,
        config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
        acks,
        retries,
        metricsRegistry.senderMetrics,
        Time.SYSTEM,
        this.requestTimeoutMs,
        config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG),
        this.transactionManager,
        apiVersions);
String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();

Sender實現了Runable接口,是IO線程主體所在,從kafka0.11版本開始,它實現了冪等和事物,所以主要實現看Sender.run方法。

    /** sender線程的主體 */
    public void run() {
        log.debug("Starting Kafka producer I/O thread.");

        // main loop, runs until close is called
        while (running) {
            try {
                run(time.milliseconds());
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }

        log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records.");

        // okay we stopped accepting requests but there may still be
        // requests in the accumulator or waiting for acknowledgment,
        // wait until these are completed.
        while (!forceClose && (this.accumulator.hasUndrained() || this.client.inFlightRequestCount() > 0)) {
            try {
                run(time.milliseconds());
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }
        if (forceClose) {
            // We need to fail all the incomplete batches and wake up the threads waiting on
            // the futures.
            log.debug("Aborting incomplete batches due to forced shutdown");
            this.accumulator.abortIncompleteBatches();
        }
        try {
            this.client.close();
        } catch (Exception e) {
            log.error("Failed to close network client", e);
        }

        log.debug("Shutdown of Kafka producer I/O thread has completed.");
    }

    /**
     * Run a single iteration of sending
     *
     * @param now The current POSIX time in milliseconds
     */
    void run(long now) {
        if (transactionManager != null) {
            try {
                if (transactionManager.shouldResetProducerStateAfterResolvingSequences())
                    // Check if the previous run expired batches which requires a reset of the producer state.
                    transactionManager.resetProducerId();

                if (!transactionManager.isTransactional()) {
                    // this is an idempotent producer, so make sure we have a producer id
                    maybeWaitForProducerId();
                } else if (transactionManager.hasUnresolvedSequences() && !transactionManager.hasFatalError()) {
                    transactionManager.transitionToFatalError(new KafkaException("The client hasn't received acknowledgment for " +
                            "some previously sent messages and can no longer retry them. It isn't safe to continue."));
                } else if (transactionManager.hasInFlightTransactionalRequest() || maybeSendTransactionalRequest(now)) {
                    // as long as there are outstanding transactional requests, we simply wait for them to return
                    client.poll(retryBackoffMs, now);
                    return;
                }

                // do not continue sending if the transaction manager is in a failed state or if there
                // is no producer id (for the idempotent case).
                if (transactionManager.hasFatalError() || !transactionManager.hasProducerId()) {
                    RuntimeException lastError = transactionManager.lastError();
                    if (lastError != null)
                        maybeAbortBatches(lastError);
                    client.poll(retryBackoffMs, now);
                    return;
                } else if (transactionManager.hasAbortableError()) {
                    accumulator.abortUndrainedBatches(transactionManager.lastError());
                }
            } catch (AuthenticationException e) {
                // This is already logged as error, but propagated here to perform any clean ups.
                log.trace("Authentication exception while processing transactional request: {}", e);
                transactionManager.authenticationFailed(e);
            }
        }

        long pollTimeout = sendProducerData(now);
        client.poll(pollTimeout, now);
    }

如果是冪等發送,就要求有一個producderID,主要看這個方法maybeWaitForProducerId();

private void maybeWaitForProducerId() {
    //如果沒有produceId並且,transactionManager沒有error那就一直自旋。
    while (!transactionManager.hasProducerId() && !transactionManager.hasError()) {
        try {
            Node node = awaitLeastLoadedNodeReady(requestTimeoutMs);
            if (node != null) {
                ClientResponse response = sendAndAwaitInitProducerIdRequest(node);
                InitProducerIdResponse initProducerIdResponse = (InitProducerIdResponse) response.responseBody();
                Errors error = initProducerIdResponse.error();
                if (error == Errors.NONE) {
                    ProducerIdAndEpoch producerIdAndEpoch = new ProducerIdAndEpoch(
                            initProducerIdResponse.producerId(), initProducerIdResponse.epoch());
                    transactionManager.setProducerIdAndEpoch(producerIdAndEpoch);
                    return;
                } else if (error.exception() instanceof RetriableException) {
                    log.debug("Retriable error from InitProducerId response", error.message());
                } else {
                    transactionManager.transitionToFatalError(error.exception());
                    break;
                }
            } else {
                log.debug("Could not find an available broker to send InitProducerIdRequest to. " +
                        "We will back off and try again.");
            }
        } catch (UnsupportedVersionException e) {
            transactionManager.transitionToFatalError(e);
            break;
        } catch (IOException e) {
            log.debug("Broker {} disconnected while awaiting InitProducerId response", e);
        }
        log.trace("Retry InitProducerIdRequest in {}ms.", retryBackoffMs);
        time.sleep(retryBackoffMs);
        metadata.requestUpdate();
    }
}
  • awaitLeastLoadedNodeReady方法
    這個方法是隨機尋找一個負載最低的broker,也就是說,獲取producerID可由任意的broker完成處理。

Kafka在zk中新引入了一個節點:/latest_producer_id_block,broker啓動時提前預分配一段PID,當前是0~999,即提前分配出1000個PID來,當PID超過了999,則目前會按照1000的步長重新分配,依次遞增,如下圖所示:

這裏寫圖片描述

broker在內存中還保存了下一個待分配的PID。這樣,當broker端接收到初始化PID的請求後,它會比較下一個PID是否在當前預分配的PID範圍:若是則直接返回;否則再次預分配下一批的PID。現在我們來討論下爲什麼這個請求所有broker都能響應——原因就在於集羣中所有broker啓動時都會啓動一個叫TransactionCoordinator的組件,該組件能夠執行預分配PID塊和分配PID的工作,而所有broker都使用/latest_producer_id_block節點來保存PID塊,因此任意一個broker都能響應這個請求。

  • sendAndAwaitInitProducerIdRequest方法
      這個就是發送初始化PID請求的方法,注意當前是同步等待返回結果,即Sender線程會無限阻塞直到broker端返回response(當然依然會受制於request.timeout.ms參數的影響)。

      得到PID之後,Sender線程會調用RecordAccumulator.drain()提取當前可發送的消息,在該方法中會將PID,Seq number等信息封裝進消息batch中,具體代碼參見:RecordAccumulator.drain()。一旦獲取到消息batch後,Sender線程開始構建ProduceRequest請求然後發送給broker端。至此producer端的工作就算告一段落了。

  • broker端是如何響應producer請求
      實際上,broker最重要的事情就是要區別某個PID的同一個消息batch是否重複發送了。因此在消息被寫入到leader底層日誌之前必須要先做一次判斷,即producer請求中的消息batch是否已然被處理過。如果請求中包含的消息batch與最近一次成功寫入的batch相同(即PID相同,batch起始seq numberbatch結束seq number都相同),那麼該方法便拋出異常,然後由上層方法捕獲到該異常封裝進ProduceResponse返回。如果batch不相同,則允許此次寫入,並在寫入完成後更新這些producer信息。

      最後再說一點:以上所說的冪等producer一直強調的是“精確處理一次”的語義,實際上冪等producer還有“不亂序”的強語義保證,只不過在0.11版本中這種不亂序主要是通過設置enable.idempotence=true時強行將max.in.flight.requests.per.connection設置成1來實現的。這種實現雖然保證了消息不亂序,但也在某種程度上降低了producerTPS。據我所知,這個問題將在1.0.0版本中已然得到解決。

源碼部分的解讀是參見的這篇文章:http://www.mamicode.com/info-detail-2058306.html

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