Kafka如果丟了消息,怎么處理的?

image.png

Kafka存在丟消息的問題,消息丟失會發生在Broker,Producer和Consumer三種。Java面試寶典PDF完整版

Broker

Broker丟失消息是由于Kafka本身的原因造成的,kafka為了得到更高的性能和吞吐量,將數據異步批量的存儲在磁盤中。消息的刷盤過程,為了提高性能,減少刷盤次數,kafka采用了批量刷盤的做法。即,按照一定的消息量,和時間間隔進行刷盤。這種機制也是由于linux操作系統決定的。將數據存儲到linux操作系統種,會先存儲到頁緩存(Page cache)中,按照時間或者其他條件進行刷盤(從page cache到file),或者通過fsync命令強制刷盤。數據在page cache中時,如果系統掛掉,數據會丟失。

image

Broker在linux服務器上高速讀寫以及同步到Replica

上圖簡述了broker寫數據以及同步的一個過程。broker寫數據只寫到PageCache中,而pageCache位于內存。這部分數據在斷電后是會丟失的。pageCache的數據通過linux的flusher程序進行刷盤。刷盤觸發條件有三:

  • 主動調用sync或fsync函數

  • 可用內存低于閥值

  • dirty data時間達到閥值。dirty是pagecache的一個標識位,當有數據寫入到pageCache時,pagecache被標注為dirty,數據刷盤以后,dirty標志清除。

Broker配置刷盤機制,是通過調用fsync函數接管了刷盤動作。從單個Broker來看,pageCache的數據會丟失。

Kafka沒有提供同步刷盤的方式。同步刷盤在RocketMQ中有實現,實現原理是將異步刷盤的流程進行阻塞,等待響應,類似ajax的callback或者是java的future。下面是一段rocketmq的源碼。

GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
service.putRequest(request);
boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout()); // 刷盤

也就是說,理論上,要完全讓kafka保證單個broker不丟失消息是做不到的,只能通過調整刷盤機制的參數緩解該情況。比如,減少刷盤間隔,減少刷盤數據量大小。時間越短,性能越差,可靠性越好(盡可能可靠)。這是一個選擇題。

為了解決該問題,kafka通過producer和broker協同處理單個broker丟失參數的情況。一旦producer發現broker消息丟失,即可自動進行retry。除非retry次數超過閥值(可配置),消息才會丟失。此時需要生產者客戶端手動處理該情況。那么producer是如何檢測到數據丟失的呢?是通過ack機制,類似于http的三次握手的方式。

The number of acknowledgments the producer requires the leader to have received before considering a request complete. This controls the durability of records that are sent. The following settings are allowed: acks=0 If set to zero then the producer will not wait for any acknowledgment from the server at all. The record will be immediately added to the socket buffer and considered sent. No guarantee can be made that the server has received the record in this case, and the retries configuration will not take effect (as the client won’t generally know of any failures). The offset given back for each record will always be set to -1. acks=1 This will mean the leader will write the record to its local log but will respond without awaiting full acknowledgement from all followers. In this case should the leader fail immediately after acknowledging the record but before the followers have replicated it then the record will be lost. acks=allThis means the leader will wait for the full set of in-sync replicas to acknowledge the record. This guarantees that the record will not be lost as long as at least one in-sync replica remains alive. This is the strongest available guarantee. This is equivalent to the acks=-1 setting. kafka.apache.org/20/document…
以上的引用是kafka官方對于參數acks的解釋(在老版本中,該參數是request.required.acks)。

  • acks=0,producer不等待broker的響應,效率最高,但是消息很可能會丟。

  • acks=1,leader broker收到消息后,不等待其他follower的響應,即返回ack。也可以理解為ack數為1。此時,如果follower還沒有收到leader同步的消息leader就掛了,那么消息會丟失。按照上圖中的例子,如果leader收到消息,成功寫入PageCache后,會返回ack,此時producer認為消息發送成功。但此時,按照上圖,數據還沒有被同步到follower。如果此時leader斷電,數據會丟失。

  • acks=-1,leader broker收到消息后,掛起,等待所有ISR列表中的follower返回結果后,再返回ack。-1等效與 all 。這種配置下,只有leader寫入數據到pagecache是不會返回ack的,還需要所有的ISR返回“成功”才會觸發ack。如果此時斷電,producer可以知道消息沒有被發送成功,將會重新發送。如果在follower收到數據以后,成功返回ack,leader斷電,數據將存在于原來的follower中。在重新選舉以后,新的leader會持有該部分數據。數據從leader同步到follower,需要2步:

  1. 數據從pageCache被刷盤到disk。因為只有disk中的數據才能被同步到replica。

  2. 數據同步到replica,并且replica成功將數據寫入PageCache。在producer得到ack后,哪怕是所有機器都停電,數據也至少會存在于leader的磁盤內。

上面第三點提到了ISR的列表的follower,需要配合另一個參數才能更好的保證ack的有效性。ISR是Broker維護的一個“可靠的follower列表”,in-sync Replica列表,broker的配置包含一個參數:min.insync.replicas。該參數表示ISR中最少的副本數。如果不設置該值,ISR中的follower列表可能為空。此時相當于acks=1。

image.png

如上圖中:

  • acks=0,總耗時f(t) = f(1)。

  • acks=1,總耗時f(t) = f(1) + f(2)。

  • acks=-1,總耗時f(t) = f(1) + max( f(A) , f(B) ) + f(2)。

性能依次遞減,可靠性依次升高。

Producer

Producer丟失消息,發生在生產者客戶端。

為了提升效率,減少IO,producer在發送數據時可以將多個請求進行合并后發送。被合并的請求咋發送一線緩存在本地buffer中。緩存的方式和前文提到的刷盤類似,producer可以將請求打包成“塊”或者按照時間間隔,將buffer中的數據發出。通過buffer我們可以將生產者改造為異步的方式,而這可以提升我們的發送效率。

但是,buffer中的數據就是危險的。在正常情況下,客戶端的異步調用可以通過callback來處理消息發送失敗或者超時的情況,但是,一旦producer被非法的停止了,那么buffer中的數據將丟失,broker將無法收到該部分數據。又或者,當Producer客戶端內存不夠時,如果采取的策略是丟棄消息(另一種策略是block阻塞),消息也會被丟失。抑或,消息產生(異步產生)過快,導致掛起線程過多,內存不足,導致程序崩潰,消息丟失。

image.png

producer采取批量發送的示意圖

image

異步發送消息生產速度過快的示意圖

根據上圖,可以想到幾個解決的思路:

  • 異步發送消息改為同步發送消。或者service產生消息時,使用阻塞的線程池,并且線程數有一定上限。整體思路是控制消息產生速度。

  • 擴大Buffer的容量配置。這種方式可以緩解該情況的出現,但不能杜絕。

  • service不直接將消息發送到buffer(內存),而是將消息寫到本地的磁盤中(數據庫或者文件),由另一個(或少量)生產線程進行消息發送。相當于是在buffer和service之間又加了一層空間更加富裕的緩沖層。

Consumer

Consumer消費消息有下面幾個步驟:

  1. 接收消息

  2. 處理消息

  3. 反饋“處理完畢”(commited)

Consumer的消費方式主要分為兩種:

  • 自動提交offset,Automatic Offset Committing

  • 手動提交offset,Manual Offset Control

Consumer自動提交的機制是根據一定的時間間隔,將收到的消息進行commit。commit過程和消費消息的過程是異步的。也就是說,可能存在消費過程未成功(比如拋出異常),commit消息已經提交了。此時消息就丟失了。

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
// 自動提交開關
props.put("enable.auto.commit", "true");
// 自動提交的時間間隔,此處是1s
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
        // 調用poll后,1000ms后,消息狀態會被改為 committed
 ConsumerRecords<String, String> records = consumer.poll(100);
 for (ConsumerRecord<String, String> record : records)
  insertIntoDB(record); // 將消息入庫,時間可能會超過1000ms
}

上面的示例是自動提交的例子。如果此時,insertIntoDB(record)發生異常,消息將會出現丟失。接下來是手動提交的例子:

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
// 關閉自動提交,改為手動提交
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
final int minBatchSize = 200;
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
while (true) {
        // 調用poll后,不會進行auto commit
   ConsumerRecords<String, String> records = consumer.poll(100);
   for (ConsumerRecord<String, String> record : records) {
    buffer.add(record);
   }
   if (buffer.size() >= minBatchSize) {
    insertIntoDb(buffer);
                // 所有消息消費完畢以后,才進行commit操作
    consumer.commitSync();
    buffer.clear();
   }
 }

將提交類型改為手動以后,可以保證消息“至少被消費一次”(at least once)。但此時可能出現重復消費的情況,重復消費不屬于本篇討論范圍。

上面兩個例子,是直接使用Consumer的High level API,客戶端對于offset等控制是透明的。也可以采用Low level API的方式,手動控制offset,也可以保證消息不丟,不過會更加復雜。Java面試寶典PDF完整版

  try {
     while(running) {
         ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
         for (TopicPartition partition : records.partitions()) {
             List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
             for (ConsumerRecord<String, String> record : partitionRecords) {
                 System.out.println(record.offset() + ": " + record.value());
             }
             long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
             // 精確控制offset
             consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
         }
     }
  } finally {
    consumer.close();
  }
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • Kafka存在丟消息的問題,消息丟失會發生在Broker,Producer和Consumer三種。 Broker ...
    long_c2b7閱讀 1,400評論 0 1
  • outline kafka是什么 基本概念和整體架構 producer端 消息的存儲機制 consumer端 Ka...
    neo_ng閱讀 2,239評論 0 0
  • 什么是Kafka Kafka是一款分布式消息發布和訂閱系統,它的特點是高性能、高吞吐量。 最早設計的目的是作為Li...
    WEIJAVA閱讀 8,537評論 4 76
  • kafka的定義:是一個分布式消息系統,由LinkedIn使用Scala編寫,用作LinkedIn的活動流(Act...
    時待吾閱讀 5,343評論 1 15
  • 推薦指數: 6.0 書籍主旨關鍵詞:特權、焦點、注意力、語言聯想、情景聯想 觀點: 1.統計學現在叫數據分析,社會...
    Jenaral閱讀 5,734評論 0 5