Kafka 雜談

開始之前

首先,此篇文章會有很多地方會和 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的全棧筆記」

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章