從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也保存了多次,具體產生過程如下:
具體的情況是,由於網絡原因第三步ack消息回傳的時候,客戶端沒有接收到發送成功確認消息,客戶端會重發。所以這就產生了消息的生產。
如果我們設置retries
等於0,那麼假如在第一步消息就發送失敗了,那麼消息將無法正確的發送到kafka集羣。
冪等發送
如果想發送消息不重複,可以使用kafka的冪等發送,這個功能早在0.11版本中就存在了。
使用冪等發送只需要這樣設置props.put("enable.idempotence", true);
,默認情況下enable.idempotence
爲false
,如果設置了它爲true
,retries
的默認值將爲 Integer.MAX_VALUE
,acks
默認爲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 number
和batch
結束seq number
都相同),那麼該方法便拋出異常,然後由上層方法捕獲到該異常封裝進ProduceResponse
返回。如果batch
不相同,則允許此次寫入,並在寫入完成後更新這些producer
信息。最後再說一點:以上所說的冪等
producer
一直強調的是“精確處理一次”的語義,實際上冪等producer
還有“不亂序”的強語義保證,只不過在0.11版本中這種不亂序主要是通過設置enable.idempotence=true
時強行將max.in.flight.requests.per.connection
設置成1來實現的。這種實現雖然保證了消息不亂序,但也在某種程度上降低了producer
的TPS
。據我所知,這個問題將在1.0.0版本中已然得到解決。
源碼部分的解讀是參見的這篇文章:http://www.mamicode.com/info-detail-2058306.html