面試官竟然問我訊息佇列為啥會丟失訊息?幸虧我總結了全套八股文

一燈架構 發表於 2022-06-22
面試
一個挺著啤酒肚,身穿格子衫,髮際線嚴重後移的中年男子,手拿著保溫杯,胳膊夾著MacBook向你走來,看樣子是架構師級別。

面試開始,直入正題。

面試官: 我看到你的簡歷上寫著專案中用到了訊息佇列,還用的是kafka,你有遇到過訊息佇列丟失訊息的情況嗎?

我: [疑問] 訊息佇列還能丟失訊息?那誰還用訊息佇列!你是不是搞錯了?我沒遇到過丟失訊息的情況,也沒考慮過這個問題。

面試官: 嗯...,小夥子,看來有些面試套路,你還是不太懂。今天面試就先到這裡吧!給你的簡歷,我送你下樓。

我去!面試還有啥套路?
能不能少一點套路,多一點真誠!
難道都要去背一遍八股文才能參加面試?
好吧,我去瞅一眼一燈總結的面試八股文。

我: 訊息佇列傳送訊息和消費訊息的過程,共分為三段,生產過程、服務端持久化過程、消費過程,如下圖所示。

image-20220424203515816.png

這三個過程都有可能弄丟訊息。

面試官: 嗯,訊息丟失的具體原因是什麼?怎麼防止丟失訊息呢?

我: 我詳細說一下這種情況:

一、生產過程丟失訊息

丟失原因:一般可能是網路故障,導致訊息沒有傳送出去。

解決方案:重發就行了。

由於kafka為了提高效能,採用了非同步傳送訊息。我們只有獲取到傳送結果,才能確保訊息傳送成功。
有兩個方案可以獲取傳送結果。

一種是kafka把傳送結果封裝在Future物件中,我可以使用Future的get方法同步阻塞獲取結果。

Future<RecordMetadata> future = producer.send(new ProducerRecord<>(topic, message));
try {
    RecordMetadata recordMetadata = future.get();
    if (recordMetadata != null) {
        System.out.println("傳送成功");
    }
} catch (Exception e) {
    e.printStackTrace();
}

另一種是使用kafka的callback函式獲取返回結果。

producer.send(new ProducerRecord<>(topic, message), new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        if (exception == null) {
            System.out.println("傳送成功");
        } else {
            System.out.println("傳送失敗");
        }
    }
});

如果傳送失敗了,有兩種重試方案:

  1. 手動重試
    在catch邏輯或else邏輯中,再呼叫一次send方法。如果還不成功怎麼辦?
    在資料庫中建一張異常訊息表,把失敗訊息存入表中,然後搞個非同步任務重試,便於控制重試次數和間隔時間。
  2. 自動重試
    kafka支援自動重試,設定引數如下,當叢集Leader選舉中或者Follower數量不足等原因返回失敗時,就可以自動重試。

    # 設定重試次數為3
    retries = 3
    # 設定重試間隔為100ms
    retry.backoff.ms = 100

    一般我們不會用kafka自動重試,因為超過重試次數,還是會返回失敗,還需要我們手動重試。

二、服務端持久化過程丟失訊息

為了保證效能,kafka採用的是非同步刷盤,當我們傳送訊息成功後,Broker節點在刷盤之前當機了,就會導致訊息丟失。

當然我們也可以設定刷盤頻率:

# 設定每1000條訊息刷一次盤
flush.messages = 1000
# 設定每秒刷一次盤
flush.ms = 1000

先普及一下kafka叢集的架構模型:

kafka叢集由多個broker組成,一個broker就是一個節點(機器)。
一個topic有多個partition(分割槽),每個partition分佈在不同的broker上面,可以充分利用分散式機器效能,擴容時只需要加機器、加partition就行了。

image-20220419202536379.png

一個partition又有多個replica(副本),有一個leader replica(主副本)和多個follower replica(從副本),這樣設計是為了保證資料的安全性。

傳送訊息和消費訊息都在leader上面,follower負責定時從leader上面拉取訊息,只有follower從leader上面把這條訊息拉取回來,才算生產者傳送訊息成功。

kafka為了加快持久化訊息的效能,把效能較好的follower組成一個ISR列表(in-sync replica),把效能較差的follower組成一個OSR列表(out-of-sync replica),ISR+OSR=AR(assigned repllicas)。
如果某個follower一段時間沒有向leader拉取訊息,落後leader太多,就把它移出ISR,放到OSR之中。
如果某個follower追上了leader,又會把它重新放到ISR之中。
如果leader掛掉,就會從ISR之中選一個follower做leader。

image-20220424210547625.png

為了提升持久化訊息效能,我們可以進行一些設定:

# 如果follower超過一秒沒有向leader拉取訊息,就把它移出ISR列表
rerplica.lag.time.max.ms = 1000
# 如果follower落後leader一千條訊息,就把它移出ISR列表
rerplica.lag.max.messages = 1000

# 至少保證ISR中有3個follower
min.insync.replicas = 3

# 非同步訊息,不需要leader確認,立即給生產者返回傳送成功,丟失訊息概率較大
asks = 0
# leader把訊息寫入本地日誌中,不會等所有follower確認,就給生產者返回傳送成功,小概率丟失訊息
asks = 1
# leader需要所有ISR中follower確認,才給生產者返回傳送成功,不會丟失訊息
asks = -1 或者 asks = all

三、消費過程丟失訊息

kafka中有個offset的概念,consumer從partition中拉取訊息,consumer本地處理完成後需要commit一下offset,表示消費完成,下次就不會再拉取到這條訊息。
所以我們需要關閉自動commit offset的配置,防止consumer拉到訊息後,服務當機,導致訊息丟失。

enable.auto.commit = false

面試官: 還得是你,就你總結的全,我都想不那麼全,明天來上班吧,薪資double。

本文知識點總結:

image-20220424230304143.png

文章持續更新,可以微信搜一搜「 一燈架構 」第一時間閱讀更多技術乾貨。