Kafka 雜談

2023-05-26 12:01:20

開始之前

首先,此篇文章會有很多地方會和 RocketMQ 比較,不太熟悉 RocketMQ 可以去看看我之前寫的RocketMQ基礎概念剖析&原始碼解析,先有個大概的印象,可能會幫助你更好的理解 Kafka。

概覽

什麼是 Kafka?

這裡先給出結論,我不太希望在解釋概念 X 的時候,說到「為了瞭解 X,我們需要先了解一下 Y」,閱讀的人思緒會被遷到另一個地方。既然小標題裡說了要解釋什麼是 Kafka,那麼我們就只說什麼是 Kafka。

專業點講,Kafka 是一個開源的分散式事件流的平臺。通俗點講,Kafka 就是一個訊息佇列。

事件流的定義

這才是一個正常的拋概念的順序,而不是「我們要了解 Kafka,就需要先了解一下 事件流...」

怎麼理解這個事件流呢?拿人來類比的話,你可以簡單的把它理解成人的中樞神經系統,它是人體神經系統最主要的部分。中樞神經接收全身各個部位的資訊輸入,然後再發出命令,讓身體執行適當的反應。甚至可以說,神經系統可以控制整個生物的行為。

通過這個類比相信你能夠理解件流的重要性。

而切回到技術視角來看,事件流其實就是從各種型別的資料來源收取實時資料。對應到我們平時對訊息佇列的用途來說,可以理解為有很多個不同的、甚至說不同種類的生產者,都能夠向同一個 Topic 寫入訊息。

收集到這些事件流後,Kafka 會將它們持久化起來,然後根據需要,將這些事件路由給不同的目標。也換個角度理解,一個 Topic 中所存放的訊息(或者說事件)可以被不同的消費者消費。

事件流的用途

現在我們知道了事件流的重要性,上面也拿中樞神經系統做了對比,我們清楚中樞神經系統可以做些什麼,那麼事件流呢?它能拿來做啥呢?

舉例來說,像我們平時網購東西,上面會顯示你的快遞現在走到哪裡了。這就是通過事件流來實時跟蹤、監控汽車、卡車或者船隻,在物流、汽車行業這樣用的比較多;比如,持續的捕獲、分析來自物聯網裝置或者其他裝置的感測器資料;通過監測住院病人的資料,來預測病人的病情變化等等這些。

那這個跟 kafka 有啥關係呢?因為除了這些,還有一個比較重要的用途那就是作為一個資料平臺、事件驅動架構的基石,而 Kakfa 剛好就是這麼一個平臺。

Kafka 由來

這塊,之前的文章有過介紹,為了避免贅述我就直接貼過來了

Kafka 最初來自於 LinkedIn,是用於做紀錄檔收集的工具,採用Java和Scala開發。其實那個時候已經有 ActiveMQ了,但是在當時 ActiveMQ 沒有辦法滿足 LinkedIn 的需求,於是 Kafka 就應運而生。

在 2010 年底,Kakfa 的0.7.0被開源到了Github上。到了2011年,由於 Kafka 非常受關注,被納入了 Apache Incubator,所有想要成為 Apache 正式專案的外部專案,都必須要經過 Incubator,翻譯過來就是孵化器。旨在將一些專案孵化成完全成熟的 Apache 開源專案。

你也可以把它想象成一個學校,所有想要成為 Apache 正式開源專案的外部專案都必須要進入 Incubator 學習,並且拿到畢業證,才能走入社會。於是在 2012 年,Kafka 成功從 Apache Incubator 畢業,正式成為 Apache 中的一員。

Kafka 擁有很高的吞吐量,單機能夠抗下十幾w的並行,而且寫入的效能也很高,能夠達到毫秒級別。而且 Kafka的功能較為簡單,就是簡單的接收生產者的訊息,消費者從 Kafka 消費訊息。

既然 Kafka 作為一個高可用的平臺,那麼肯定需要對訊息進行持久化,不然一旦重啟,所有的訊息就都丟了。那 Kafka 是怎麼做的持久化呢?

設計

持久化

當然是磁碟了,並且還是強依賴磁碟

不瞭解的可能會認為:「磁碟?不就是那個很慢很慢的磁碟?」這種速度級的儲存裝置是怎麼樣和 Kafka 這樣的高效能資料平臺沾上邊的?

確實我們會看到大量關於磁碟的描述,就是慢。但實際上,磁碟同時集快、慢於一身,其表現具體是快還是慢,還得看我們如何使用它。

舉個例子,我們可能都聽過,記憶體的順序 IO 是慢於記憶體的隨機 IO 的,確實是這樣。磁碟自身的隨機 IO 和順序 IO 也有非常大的差異。比如在某些情況下,磁碟順序寫的速度可能是 600MB/秒,而對於磁碟隨機寫的速度可能才 100KB/秒,這個差異達到了恐怖的 6000 倍。

對磁碟的一些原理感興趣可以看看我之前寫的文章

Kafka 其實就是用實際行動來告訴我們「Don't fear the filesystem」,現在順序寫、讀的效能表現是很穩定的,並且我們的大哥作業系統也對此進行了大量的優化。

瞭解了持久化,解決了訊息的存、取問題,還有什麼更重要呢?

效率

當然是效率,持久化能保證你的資料不丟,這可能只做到了一半,如果對訊息的處理效率不高,仍然不能滿足實際生產環境中海量的資料請求。

舉個例子,現在請求一個系統的一個頁面都有可能會產生好幾十條訊息,這個在複雜一些的系統裡絲毫不誇張。如果投遞、消費的效率不提上去,會影響到整個核心鏈路。

影響效率的大頭一半來說有兩個:

  • 大量零散的小 IO
  • 大量的資料拷貝

這也是為啥大家都要搞 Buffer,例如 MySQL 裡有 Log Buffer,作業系統也有自己的 Buffer,這就是要把儘量減少和磁碟的互動,減少小 IO 的產生,提高效率。

比如說,Consumer 現在需要消費 Broker 上的某條訊息,Broker 就需要將此訊息從磁碟中讀取出來,再通過 Socket 將訊息傳送給 Consumer。那通常拷貝一個檔案再傳送會涉及到哪些步驟?

  • 使用者態切換到核心態,作業系統將訊息從磁碟中讀取到核心緩衝區
  • 核心態切換到使用者態,應用將核心緩衝區的資料 Copy 到使用者緩衝區
  • 使用者態切換到核心態,應用將使用者緩衝區的內容 Copy 到 Socket 緩衝區
  • 將資料庫 Copy 到網路卡,網路卡會將資料傳送出去
  • 核心態切換到使用者態

可能你看文字有點懵逼,簡單總結就是,涉及到了 4 次態的切換,4 次資料的拷貝,2次系統呼叫

紅色的是態的切換,綠色的是資料拷貝。

不清楚什麼是使用者態、核心態的可以去看看《使用者態和核心態的區別》

態的切換、資料的拷貝,都是耗時的操作,那 Kafka 是怎麼解決這個問題的呢?

其實就是我們常說的零拷貝了,但是不要看到零就對零拷貝有誤解,認為就是一次都沒有拷貝,那你想想,不拷貝怎麼樣把磁碟的資料讀取出來呢?

所謂的零拷貝是指資料在使用者態、核心態之間的拷貝次數是 0

最初,從磁碟讀取資料的時候是在核心態。

最後,將讀取到的資料傳送出去的時候也在核心態。

那讀取——傳送這中間,是不是就沒有必要再將資料從核心態拷貝到使用者態了?Linux 裡封裝好的系統呼叫 sendfile 就已經幫我們做了這件事了。

簡單描述一下:「在從磁碟將資料讀取到核心態的緩衝區內之後(也就是 pagecache),直接將其拷貝到網路卡里,然後傳送。」

這裡嚴格上來說還有 offset 的拷貝,但影響太小可以忽略不就,就先不討論

你會發現,這裡也應證了我上面說的「零拷貝並不是說沒有拷貝」。算下來,零拷貝總共也有 2 次態的切換,2 次資料的拷貝。但這已經能大大的提升效率了。

到此為止,我們聊到了訊息已經被傳送出去了,接下來就是消費者接收到這條訊息然後開始處理了。那這部分會有效率問題嗎?

答案是肯定的,隨著現在的計算機發展,系統的瓶頸很多時候已經不是 CPU 或者磁碟了,而是網路頻寬。對頻寬不理解的你就把頻寬理解成一條路的寬度。路寬了,就能同時容納更多的車行進,堵車的概率也會小一些。

那在路寬不變的基礎上,我們要怎麼樣跑更多的車呢?讓車變小(現實中別這麼幹,手動狗頭)。

換句話說,就是要對傳送給 Consumer 的資訊進行壓縮。並且,還不能是來一條壓縮一條,為啥呢?因為同型別的一批訊息之間會有大量的重複,將這一批進行壓縮能夠極大的減少重複,而相反,壓縮單條訊息效果並不理想,因為你沒有辦法提取公共冗餘的部分。Kafka 通過批次處理來對訊息進行批次壓縮。

Push vs Pull

關於這個老生常談的問題,確實可以簡單的聊聊。我們都知道 Consumer 消費資料,無非就是 pull 或者 push。可能在大多數的情況下,這兩個沒啥區別,但實際上大多數情況下還是用的 pull 的方式。

那為啥是 pull?

假設現在是採取的 push 的方式,那麼當 Broker 內部出現了問題,向 Consumer push 的頻率降低了,此時作為消費方是不是隻能乾著急。想象一下,現在產生了訊息堆積,我們確啥也幹不了,只能等著 Broker 恢復了繼續 push 訊息到 Consumer。

那如果是 pull 我們怎麼解決呢?我們可以新增消費者,以此來增加消費的速率。當然新增消費者並不總是有效,例如在 RocketMQ 中,消費者的數量如果大於了 MessageQueue 的數量,多出來的這部分消費者是無法消費訊息的,資源就被白白浪費了。

Kafka 中的 Partition 也是同理,在新增消費者的時候,也需要注意消費者、Partition 的數量。

除此之外,採用 pull 能使 Consumer 更加的靈活,能夠根據自己的情況決定什麼時候消費,消費多少。

關於消費

這個問題其實在訊息系統裡也很經典。

Consumer 從 Broker 里拉取資料消費,那 Consumer 如何知道自己消費到哪兒了?Broker 如何知道 Consumer 消費到哪兒了?雙方如何達成共識?

我們假設,Broker 在收到 Consumer 的拉取訊息請求並行送之後,就將剛剛傳送的訊息給刪除了,這樣 OK 嗎?

廢話,這當然不行,假設 Broker 把訊息發給 Consumer 了,但由於 Consumer 掛了並沒有收到這些訊息,那這些訊息就會丟失。

所以才有了我們都熟悉的 ACK(Acknowlegement)機制,Broker 在將訊息發出後,將其標識為「已傳送|未消費」,Broker 會等待 Consumer 返回一個 ACK,然後再將剛剛的訊息標識為「已消費」。

這個機制在一定程度上解決了上面說的訊息丟失的問題,但事情總有雙面性, ACK 機制又引入了新的問題。

舉個例子,假設 Consumer 收到了、並且正確的消費了訊息,但偏偏就是在返回 ACK 時出了問題,導致 Broker 沒有收到。則在 Broker 側,訊息的狀態仍然是「已傳送|未消費」,下次 Consumer 來拉,仍然會拉取到這條訊息,此時就發生了重複消費

歡迎 wx 關注「SH的全棧筆記」