Kafka 為什麼快

不假發表於2020-08-14

Kafka 為什麼能那麼快 | Kafka高效讀寫資料的原因

無論 kafka 作為 MQ 也好,作為儲存層也罷,無非就是兩個功能(好簡單的樣子),一是 Producer 生產的資料存到 broker,二是 Consumer 從 broker 讀取資料。那 Kafka 的快也就體現在讀寫兩個方面了,下面我們就聊聊 Kafka 快的原因。

1. 利用 Partition 實現並行處理

我們都知道 Kafka 是一個 Pub-Sub 的訊息系統,無論是釋出還是訂閱,都要指定 Topic。

Topic 只是一個邏輯的概念。每個 Topic 都包含一個或多個 Partition,不同 Partition 可位於不同節點。

一方面,由於不同 Partition 可位於不同機器,因此可以充分利用叢集優勢,實現機器間的並行處理。另一方面,由於 Partition 在物理上對應一個資料夾,即使多個 Partition 位於同一個節點,也可通過配置讓同一節點上的不同 Partition 置於不同的磁碟上,從而實現磁碟間的並行處理,充分發揮多磁碟的優勢。

能並行處理,速度肯定會有提升,多個工人肯定比一個工人乾的快。

可以並行寫入不同的磁碟?那磁碟讀寫的速度可以控制嗎?

那就先簡單扯扯磁碟/IO 的那些事

硬碟效能的制約因素是什麼?如何根據磁碟I/O特性來進行系統設計?

硬碟內部主要部件為磁碟碟片、傳動手臂、讀寫磁頭和主軸馬達。實際資料都是寫在碟片上,讀寫主要是通過傳動手臂上的讀寫磁頭來完成。實際執行時,主軸讓磁碟碟片轉動,然後傳動手臂可伸展讓讀取頭在碟片上進行讀寫操作。磁碟物理結構如下圖所示:

由於單一碟片容量有限,一般硬碟都有兩張以上的碟片,每個碟片有兩面,都可記錄資訊,所以一張碟片對應著兩個磁頭。碟片被分為許多扇形的區域,每個區域叫一個扇區。碟片表面上以碟片中心為圓心,不同半徑的同心圓稱為磁軌,不同碟片相同半徑的磁軌所組成的圓柱稱為柱面。磁軌與柱面都是表示不同半徑的圓,在許多場合,磁軌和柱面可以互換使用。磁碟碟片垂直視角如下圖所示:

圖片來源:commons.wikimedia.org
圖片來源:commons.wikimedia.org

影響磁碟的關鍵因素是磁碟服務時間,即磁碟完成一個I/O請求所花費的時間,它由尋道時間、旋轉延遲和資料傳輸時間三部分構成。

機械硬碟的連續讀寫效能很好,但隨機讀寫效能很差,這主要是因為磁頭移動到正確的磁軌上需要時間,隨機讀寫時,磁頭需要不停的移動,時間都浪費在了磁頭定址上,所以效能不高。衡量磁碟的重要主要指標是IOPS和吞吐量。

在許多的開源框架如 Kafka、HBase 中,都通過追加寫的方式來儘可能的將隨機 I/O 轉換為順序 I/O,以此來降低定址時間和旋轉延時,從而最大限度的提高 IOPS。

感興趣的同學可以看看 磁碟I/O那些事

磁碟讀寫的快慢取決於你怎麼使用它,也就是順序讀寫或者隨機讀寫。

2. 順序寫磁碟

圖片來源:kafka.apache.org
圖片來源:kafka.apache.org

Kafka 中每個分割槽是一個有序的,不可變的訊息序列,新的訊息不斷追加到 partition 的末尾,這個就是順序寫。

很久很久以前就有人做過基準測試:《每秒寫入2百萬(在三臺廉價機器上)》http://ifeve.com/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines/

由於磁碟有限,不可能儲存所有資料,實際上作為訊息系統 Kafka 也沒必要儲存所有資料,需要刪除舊的資料。又由於順序寫入的原因,所以 Kafka 採用各種刪除策略刪除資料的時候,並非通過使用“讀 - 寫”模式去修改檔案,而是將 Partition 分為多個 Segment,每個 Segment 對應一個物理檔案,通過刪除整個檔案的方式去刪除 Partition 內的資料。這種方式清除舊資料的方式,也避免了對檔案的隨機寫操作。

3. 充分利用 Page Cache

引入 Cache 層的目的是為了提高 Linux 作業系統對磁碟訪問的效能。Cache 層在記憶體中快取了磁碟上的部分資料。當資料的請求到達時,如果在 Cache 中存在該資料且是最新的,則直接將資料傳遞給使用者程式,免除了對底層磁碟的操作,提高了效能。Cache 層也正是磁碟 IOPS 為什麼能突破 200 的主要原因之一。

在 Linux 的實現中,檔案 Cache 分為兩個層面,一是 Page Cache,另一個 Buffer Cache,每一個 Page Cache 包含若干 Buffer Cache。Page Cache 主要用來作為檔案系統上的檔案資料的快取來用,尤其是針對當程式對檔案有 read/write 操作的時候。Buffer Cache 則主要是設計用來在系統對塊裝置進行讀寫的時候,對塊進行資料快取的系統來使用。

使用 Page Cache 的好處:

  • I/O Scheduler 會將連續的小塊寫組裝成大塊的物理寫從而提高效能

  • I/O Scheduler 會嘗試將一些寫操作重新按順序排好,從而減少磁碟頭的移動時間

  • 充分利用所有空閒記憶體(非 JVM 記憶體)。如果使用應用層 Cache(即 JVM 堆記憶體),會增加 GC 負擔

  • 讀操作可直接在 Page Cache 內進行。如果消費和生產速度相當,甚至不需要通過物理磁碟(直接通過 Page Cache)交換資料

  • 如果程式重啟,JVM 內的 Cache 會失效,但 Page Cache 仍然可用

Broker 收到資料後,寫磁碟時只是將資料寫入 Page Cache,並不保證資料一定完全寫入磁碟。從這一點看,可能會造成機器當機時,Page Cache 內的資料未寫入磁碟從而造成資料丟失。但是這種丟失只發生在機器斷電等造成作業系統不工作的場景,而這種場景完全可以由 Kafka 層面的 Replication 機制去解決。如果為了保證這種情況下資料不丟失而強制將 Page Cache 中的資料 Flush 到磁碟,反而會降低效能。也正因如此,Kafka 雖然提供了 flush.messagesflush.ms 兩個引數將 Page Cache 中的資料強制 Flush 到磁碟,但是 Kafka 並不建議使用。

4. 零拷貝技術

Kafka 中存在大量的網路資料持久化到磁碟(Producer 到 Broker)和磁碟檔案通過網路傳送(Broker 到 Consumer)的過程。這一過程的效能直接影響 Kafka 的整體吞吐量。

作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的許可權。

為了避免使用者程式直接操作核心,保證核心安全,作業系統將虛擬記憶體劃分為兩部分,一部分是核心空間(Kernel-space),一部分是使用者空間(User-space)。

傳統的 Linux 系統中,標準的 I/O 介面(例如read,write)都是基於資料拷貝操作的,即 I/O 操作會導致資料在核心地址空間的緩衝區和使用者地址空間的緩衝區之間進行拷貝,所以標準 I/O 也被稱作快取 I/O。這樣做的好處是,如果所請求的資料已經存放在核心的高速緩衝儲存器中,那麼就可以減少實際的 I/O 操作,但壞處就是資料拷貝的過程,會導致 CPU 開銷。

我們把 Kafka 的生產和消費簡化成如下兩個過程來看

  1. 網路資料持久化到磁碟 (Producer 到 Broker)
  2. 磁碟檔案通過網路傳送(Broker 到 Consumer)
4.1 網路資料持久化到磁碟 (Producer 到 Broker)

傳統模式下,資料從網路傳輸到檔案需要 4 次資料拷貝、4 次上下文切換和兩次系統呼叫。

data = socket.read()// 讀取網路資料 
File file = new File() 
file.write(data)// 持久化到磁碟 
file.flush()

這一過程實際上發生了四次資料拷貝:

  1. 首先通過 DMA copy 將網路資料拷貝到核心態 Socket Buffer
  2. 然後應用程式將核心態 Buffer 資料讀入使用者態(CPU copy)
  3. 接著使用者程式將使用者態 Buffer 再拷貝到核心態(CPU copy)
  4. 最後通過 DMA copy 將資料拷貝到磁碟檔案

DMA(Direct Memory Access):直接儲存器訪問。DMA 是一種無需 CPU 的參與,讓外設和系統記憶體之間進行雙向資料傳輸的硬體機制。使用 DMA 可以使系統 CPU 從實際的 I/O 資料傳輸過程中擺脫出來,從而大大提高系統的吞吐率。

同時,還伴隨著四次上下文切換,如下圖所示

資料落盤通常都是非實時的,kafka 生產者資料持久化也是如此。Kafka 的資料並不是實時的寫入硬碟,它充分利用了現代作業系統分頁儲存來利用記憶體提高 I/O 效率,就是上一節提到的 Page Cache。

對於 kafka 來說,Producer 生產的資料存到 broker,這個過程讀取到 socket buffer 的網路資料,其實可以直接在核心空間完成落盤。並沒有必要將 socket buffer 的網路資料,讀取到應用程式緩衝區;在這裡應用程式緩衝區其實就是 broker,broker 收到生產者的資料,就是為了持久化。

在此特殊場景下:接收來自 socket buffer 的網路資料,應用程式不需要中間處理、直接進行持久化時。可以使用 mmap 記憶體檔案對映。

Memory Mapped Files:簡稱 mmap,也有叫 MMFile 的,使用 mmap 的目的是將核心中讀緩衝區(read buffer)的地址與使用者空間的緩衝區(user buffer)進行對映。從而實現核心緩衝區與應用程式記憶體的共享,省去了將資料從核心讀緩衝區(read buffer)拷貝到使用者緩衝區(user buffer)的過程。它的工作原理是直接利用作業系統的 Page 來實現檔案到實體記憶體的直接對映。完成對映之後你對實體記憶體的操作會被同步到硬碟上。

使用這種方式可以獲取很大的 I/O 提升,省去了使用者空間到核心空間複製的開銷。

mmap 也有一個很明顯的缺陷——不可靠,寫到 mmap 中的資料並沒有被真正的寫到硬碟,作業系統會在程式主動呼叫 flush 的時候才把資料真正的寫到硬碟。Kafka 提供了一個引數——producer.type 來控制是不是主動flush;如果 Kafka 寫入到 mmap 之後就立即 flush 然後再返回 Producer 叫同步(sync);寫入 mmap 之後立即返回 Producer 不呼叫 flush 就叫非同步(async),預設是 sync。

零拷貝(Zero-copy)技術指在計算機執行操作時,CPU 不需要先將資料從一個記憶體區域複製到另一個記憶體區域,從而可以減少上下文切換以及 CPU 的拷貝時間。

它的作用是在資料包從網路裝置到使用者程式空間傳遞的過程中,減少資料拷貝次數,減少系統呼叫,實現 CPU 的零參與,徹底消除 CPU 在這方面的負載。

目前零拷貝技術主要有三種型別

  • 直接I/O:資料直接跨過核心,在使用者地址空間與I/O裝置之間傳遞,核心只是進行必要的虛擬儲存配置等輔助工作;
  • 避免核心和使用者空間之間的資料拷貝:當應用程式不需要對資料進行訪問時,則可以避免將資料從核心空間拷貝到使用者空間
  • mmap
  • sendfile
  • splice && tee
  • sockmap
  • copy on write:寫時拷貝技術,資料不需要提前拷貝,而是當需要修改的時候再進行部分拷貝。
4.2 磁碟檔案通過網路傳送(Broker 到 Consumer)

傳統方式實現:先讀取磁碟、再用 socket 傳送,實際也是進過四次 copy

buffer = File.read 
Socket.send(buffer)

這一過程可以類比上邊的生產訊息:

  1. 首先通過系統呼叫將檔案資料讀入到核心態 Buffer(DMA 拷貝)
  2. 然後應用程式將記憶體態 Buffer 資料讀入到使用者態 Buffer(CPU 拷貝)
  3. 接著使用者程式通過 Socket 傳送資料時將使用者態 Buffer 資料拷貝到核心態 Buffer(CPU 拷貝)
  4. 最後通過 DMA 拷貝將資料拷貝到 NIC Buffer

Linux 2.4+ 核心通過 sendfile 系統呼叫,提供了零拷貝。資料通過 DMA 拷貝到核心態 Buffer 後,直接通過 DMA 拷貝到 NIC Buffer,無需 CPU 拷貝。這也是零拷貝這一說法的來源。除了減少資料拷貝外,因為整個讀檔案 - 網路傳送由一個 sendfile 呼叫完成,整個過程只有兩次上下文切換,因此大大提高了效能。

Kafka 在這裡採用的方案是通過 NIO 的 transferTo/transferFrom 呼叫作業系統的 sendfile 實現零拷貝。總共發生 2 次核心資料拷貝、2 次上下文切換和一次系統呼叫,消除了 CPU 資料拷貝

5. 批處理

在很多情況下,系統的瓶頸不是 CPU 或磁碟,而是網路IO。

因此,除了作業系統提供的低階批處理之外,Kafka 的客戶端和 broker 還會在通過網路傳送資料之前,在一個批處理中累積多條記錄 (包括讀和寫)。記錄的批處理分攤了網路往返的開銷,使用了更大的資料包從而提高了頻寬利用率。

6. 資料壓縮

Producer 可將資料壓縮後傳送給 broker,從而減少網路傳輸代價,目前支援的壓縮演算法有:Snappy、Gzip、LZ4。資料壓縮一般都是和批處理配套使用來作為優化手段的。

小總結 | 下次面試官問我 kafka 為什麼快,我就這麼說

  • partition 並行處理
  • 順序寫磁碟,充分利用磁碟特性
  • 利用了現代作業系統分頁儲存 Page Cache 來利用記憶體提高 I/O 效率
  • 採用了零拷貝技術
  • Producer 生產的資料持久化到 broker,採用 mmap 檔案對映,實現順序的快速寫入
  • Customer 從 broker 讀取資料,採用 sendfile,將磁碟檔案讀到 OS 核心緩衝區後,轉到 NIO buffer進行網路傳送,減少 CPU 消耗

相關文章