被錘了:Acks=all消息也會(huì)丟失?
消息隊(duì)列是面試中一定會(huì)被問(wèn)到的技術(shù)模塊,雖然它在面試題占比不及并發(fā)編程和數(shù)據(jù)庫(kù),但也屬于面試中的關(guān)鍵性問(wèn)題。所以今天我們就來(lái)看一道,MQ 中高頻,但可能會(huì)打破你以往認(rèn)知的一道面試題。
所謂的關(guān)鍵問(wèn)題指的是這道面試題會(huì)影響你整體面試結(jié)果。
我們?cè)诿嬖囅㈥?duì)列(Message Queue,MQ)時(shí),尤其是面試 Kafka 時(shí),經(jīng)常會(huì)被問(wèn)到:如何保證消息不丟失?
那么,我們的回答會(huì)分為以下 3 部分:
- 保證生產(chǎn)者消息不丟失。
- 保證 Kafka 服務(wù)(器端)消息不丟失。
- 保證消費(fèi)者消息不丟失。
只有保證這 3 部分消息都不丟失,才能保證 Kafka 整體消息不丟失。
因?yàn)?Kafka 消息的傳遞流程如下(總共包含 3 部分):
1.如何保證生產(chǎn)者消息不丟失?
那怎么保證生產(chǎn)者消息不丟失呢?
要搞明白這個(gè)事,我們就要先了解一下生產(chǎn)者發(fā)送消息的執(zhí)行流程。
Kafka 生產(chǎn)者發(fā)送消息的執(zhí)行流程如下:
默認(rèn)情況下,所有的消息會(huì)先緩存到 RecordAccumulator 緩存中,再由 Sender 線程拉取消息發(fā)送到 Kafka 服務(wù)器端,通過(guò) RecordAccumulator 和 Sender 線程的協(xié)作,實(shí)現(xiàn)了消息的批量發(fā)送、性能優(yōu)化和異常處理等功能,確保了消息的高效可靠傳輸。
(1)RecordAccumulator 緩存作用
- 暫存消息:RecordAccumulator 是 Kafk a生產(chǎn)者中的一個(gè)關(guān)鍵組件,它充當(dāng)了一個(gè)緩存的角色,用于暫存主線程(Main Thread)發(fā)送過(guò)來(lái)的消息。這些消息在 RecordAccumulato r中等待被 Sender 線程批量發(fā)送。
- 批量發(fā)送:RecordAccumulator 通過(guò)批量收集消息,減少了單個(gè)消息發(fā)送的網(wǎng)絡(luò)請(qǐng)求次數(shù),從而提高了發(fā)送效率。Sender 線程可以從 RecordAccumulator 中批量獲取消息,一次性發(fā)送到 Kafka 集群,減少了網(wǎng)絡(luò)傳輸?shù)馁Y源消耗。
- 性能優(yōu)化:RecordAccumulator的緩存大小可以通過(guò)生產(chǎn)者客戶端參數(shù) buffer.memory 進(jìn)行配置(默認(rèn)值為 32MB)。合理的緩存大小設(shè)置可以平衡內(nèi)存使用與發(fā)送效率,達(dá)到最優(yōu)的性能表現(xiàn)。
- 內(nèi)存管理:如果 RecordAccumulator 的緩存空間被占滿,生產(chǎn)者再次調(diào)用 send() 方法發(fā)送消息時(shí),會(huì)出現(xiàn)阻塞(默認(rèn)阻塞時(shí)間為 60 秒,可通過(guò) max.block.ms 參數(shù)配置)。如果阻塞超時(shí),則會(huì)拋出異常。這種機(jī)制有助于防止生產(chǎn)者因?yàn)闊o(wú)限制地緩存消息而耗盡系統(tǒng)資源。
- ByteBuffer 復(fù)用:為了減少頻繁創(chuàng)建和釋放 ByteBuffer 所造成的資源消耗,RecordAccumulator 內(nèi)部還維護(hù)了一個(gè) BufferPool,用于實(shí)現(xiàn) ByteBuffer 的復(fù)用。特定大小的 ByteBuffer 會(huì)被緩存起來(lái),以便后續(xù)消息發(fā)送時(shí)重復(fù)使用。
(2)Sender 線程作用
- 拉取消息:Sender 線程是 Kafka 生產(chǎn)者中的一個(gè)后臺(tái)線程,它負(fù)責(zé)從 RecordAccumulator 中拉取緩存的消息。Sender 線程會(huì)定期輪詢 RecordAccumulator,檢查是否有新消息需要發(fā)送。
- 批量構(gòu)建請(qǐng)求:當(dāng) Sender 線程發(fā)現(xiàn)有新消息需要發(fā)送時(shí),它會(huì)構(gòu)建一個(gè)或多個(gè) ProducerRequest 請(qǐng)求。每個(gè)請(qǐng)求包含多個(gè)消息,以便進(jìn)行有效的批量發(fā)送。這種批量發(fā)送機(jī)制可以顯著提高網(wǎng)絡(luò)傳輸效率。
- 發(fā)送消息到 Kafka 集群:Sender 線程將構(gòu)建的 ProducerRequest 請(qǐng)求發(fā)送到 Kafka 集群的相應(yīng)分區(qū)。它會(huì)根據(jù)分區(qū)的 Leader 節(jié)點(diǎn)信息,將消息發(fā)送給對(duì)應(yīng)的 Broker 節(jié)點(diǎn)。
- 異常處理:在消息發(fā)送過(guò)程中,可能會(huì)出現(xiàn)網(wǎng)絡(luò)故障、分區(qū)不可用等異常情況。Sender 線程負(fù)責(zé)處理這些異常,例如進(jìn)行重試、重新連接等操作,以確保消息的可靠發(fā)送。
- 狀態(tài)更新:一旦消息被成功接收并記錄在 Kafka Broker 的日志中,Sender 線程會(huì)通知 RecordAccumulator 更新消息的狀態(tài)。這樣,生產(chǎn)者就能夠知道哪些消息已經(jīng)被成功發(fā)送,哪些消息還需要重試發(fā)送。
2.生產(chǎn)者消息丟失的兩種場(chǎng)景
了解了 Kafka 生產(chǎn)者發(fā)送消息的流程之后,我們就能知道在這個(gè)環(huán)節(jié)丟失消息的情況有以下兩種:
- 網(wǎng)絡(luò)抖動(dòng)(消息不可達(dá)):生產(chǎn)者與 Kafka 服務(wù)端之間的鏈路不可達(dá),發(fā)送超時(shí)。此時(shí)各個(gè)節(jié)點(diǎn)的狀態(tài)是正常,但消費(fèi)端就是沒(méi)有消費(fèi)消息,就像消息丟失了一樣。
- 無(wú)消息確認(rèn)(ack):生產(chǎn)者消息發(fā)送之后,無(wú) ack 消息確認(rèn),直接返回消息發(fā)送成功,但消息發(fā)送之后,Kafka 服務(wù)宕機(jī)或掉電了,導(dǎo)致消息丟失。
怎么解決這個(gè)問(wèn)題呢?
(1)網(wǎng)絡(luò)波動(dòng)問(wèn)題處理
網(wǎng)絡(luò)波動(dòng)的話設(shè)置消息重試即可,因?yàn)榫W(wǎng)絡(luò)抖動(dòng)消息不可達(dá),所以只要配置了重試次數(shù),那么就會(huì)消息重試以此來(lái)保證消息不丟失。
在 Spring Boot 項(xiàng)目中,只需要在配置文件 application.yml 中,設(shè)置生產(chǎn)者的重試次數(shù)即可:
spring:
kafka:
producer:
retries: 3
(2)消息確認(rèn)設(shè)置
Kafka 生產(chǎn)者的 ACK(Acknowledgment)機(jī)制是指生產(chǎn)者在發(fā)送消息到 Kafka 集群后,等待確認(rèn)的方式。這個(gè)機(jī)制決定了生產(chǎn)者何時(shí)認(rèn)為消息已經(jīng)成功發(fā)送,并直接影響到消息的可靠性和性能。
Kafka 生產(chǎn)者的 ACK 機(jī)制主要有以下三種類型。
① acks=0
生產(chǎn)者在將消息發(fā)送到網(wǎng)絡(luò)緩沖區(qū)后,立即認(rèn)為消息已被提交,不會(huì)等待任何來(lái)自服務(wù)器的響應(yīng)。這時(shí)設(shè)置的重試次數(shù) retries 無(wú)效。
特點(diǎn):
- 最高性能:由于不需要等待任何確認(rèn),因此具有最高的吞吐量。
- 最低可靠性:消息可能會(huì)在發(fā)送過(guò)程中丟失,生產(chǎn)者無(wú)法知道消息是否成功到達(dá)服務(wù)器。
適用場(chǎng)景:對(duì)消息可靠性要求不高,但追求極致性能的場(chǎng)景。
② acks=1
生產(chǎn)者在將消息發(fā)送到主題的分區(qū) leader 后,等待 leader 的確認(rèn),即認(rèn)為消息已被提交(此時(shí) leader 寫(xiě)入成功,并沒(méi)有刷新到磁盤(pán)),不用等待所有副本的確認(rèn)。
特點(diǎn):
- 中等可靠性和性能:提供了一定程度的可靠性,因?yàn)橹挥蓄I(lǐng)導(dǎo)者副本確認(rèn)消息后生產(chǎn)者才會(huì)收到確認(rèn)。但如果領(lǐng)導(dǎo)者副本在確認(rèn)后發(fā)生故障,而消息還未復(fù)制到其他副本,則消息可能會(huì)丟失。
- 性能與可靠性平衡:在生產(chǎn)者性能和消息可靠性之間提供了一個(gè)折衷方案。
適用場(chǎng)景:適用于傳輸普通日志,允許偶爾丟失少量數(shù)據(jù)的場(chǎng)景。
③ acks=all 或 acks=-1
生產(chǎn)者需要等待所有同步副本(ISR, In-Sync Replicas)都成功寫(xiě)入消息后,才認(rèn)為消息已被提交。
特點(diǎn):
- 最高可靠性:只有當(dāng)所有同步副本都確認(rèn)接收到消息后,生產(chǎn)者才會(huì)收到確認(rèn),確保了消息的可靠性。
- 較低性能:由于需要等待所有同步副本的確認(rèn),因此可能會(huì)導(dǎo)致消息發(fā)送的延遲增加,從而影響性能。
適用場(chǎng)景:適用于對(duì)消息可靠性要求極高的場(chǎng)景,如金融交易等關(guān)鍵任務(wù)應(yīng)用。
在 Spring Boot 項(xiàng)目中,acks 可以在配置文件 application.yml 中設(shè)置:
spring:
kafka:
producer:
acks: all
3.acks=all消息一定不會(huì)丟失嗎?
正常情況下當(dāng)我們?cè)O(shè)置 acks=all 時(shí),其實(shí)是可以保證數(shù)據(jù)不丟失了。但是有一種特殊情況,如果 Topic 只有一個(gè) Partition(分區(qū)時(shí)),也就是只有一個(gè) Leader 節(jié)點(diǎn)時(shí),此時(shí)消息也是會(huì)丟失的。
如果只有一個(gè) Leader 節(jié)點(diǎn),acks=all 的設(shè)置和 acks=1 的設(shè)置效果基本類似,當(dāng) Leader 確認(rèn)消息之后,還沒(méi)來(lái)得及將消息刷到磁盤(pán)之前宕機(jī)了,那么就會(huì)造成消息丟失。
萬(wàn)事必有妖,當(dāng)面試官用疑問(wèn)語(yǔ)句問(wèn)你時(shí),答案基本是否定的。如果是確定的話,面試官可能也就不會(huì)再問(wèn)你了,所以當(dāng)你聽(tīng)到一個(gè)有悖于常識(shí)的問(wèn)題時(shí),先努力思考這個(gè)問(wèn)題還有沒(méi)有其他答案。