目前項目的日志收集用到了kafka,每天收集幾億條日志數據,梳理了一下kafka在高并發方面的知識,來幫助理解Kafka是如何做到超高并發(百萬級)的,設計很是精妙,尤其是他的暴力輸入輸出美學,深受啟發,并在后續項目中得到了應用。
巧用文件系統
按照kafka官網的話說,不要害怕文件系統。可能很多人一想到磁盤就會想到慢這個字,但是磁盤的性能表現好壞在于你的使用。
我們以kafka官方例子為例,在6個7200rpm SATA RAID-5 陣列的 JBOD 配置上,順序寫入性能約每秒600MB,而隨機寫入的性能約每秒100k,差了6000倍。可以看一下官方給出的性能圖對比

首先順序操作磁盤免去了大量的尋道和旋轉延遲,因為即將訪問的扇區剛好位于上次訪問的扇區后邊,磁頭立刻就找到了扇區。其次順序讀寫是最可預測的,而且操作系統做了大量這方面的優化,操作系統提供了預讀和延遲寫入技術,使得可以以塊整數倍的大小預取數據,并把小塊寫入邏輯,聚合成大塊物理寫入。
mmap技術應用
除了順序操作磁盤,充分利用操作系統將空閑內存轉義到磁盤緩存這個機制,及mmap技術。使用mmap技術,進程可以像操作硬盤一樣操作內存,這種方式可以大幅度提升I/O性能,也能省去用戶空間到內核空間的復制開銷。可能有人會問,為什么程序不自己維護緩存,要交給操作系統,先不說增加了用戶空間到內核空間的拷貝過程,還有一點kafka基于Java開發的,Java中對象內存開銷大,堆內存的增加,GC過程會越來越繁瑣和緩慢,使用頁緩存,可以使程序迅速重啟,緩存也可繼續使用。
長時間保留消息,而不是閱后即焚。在持久化消息上,并沒有使用像MySQL那樣的BTree,因為BTree的操作是O(logN) 磁盤操作尋道時間可能就會達到10毫秒,開銷巨大,kafka直接采用簡單的讀取和追加到文件上,操作為O(1),性能與數據大小分離。廉價的機器照樣能跑出高性能。沒有了頻發的數據刪除操作,相當于又進一步減小了磁盤尋道損耗,而且還能使消費端更加靈活的消費(可以重新消費之前消費過的數據)。
消息批量處理
首先思考一下,有一百條消息,分成一百次通過網絡傳給遠端機器快,還是做好聚合,一次性傳給遠端機器快?如果內部網絡一秒能傳輸10M以上的數據,如果把百萬條消息壓縮在在10M數據上,然后通過網絡一次性傳給遠端,那是不是就相當于完成了百萬級的TPS?
我想上邊的答案非常明顯,顯然是批量處理更快,能更大程度的減少網絡io帶來的延遲。Kafka就采用了這個方式,給你的感覺像是每次在一條一條push消息,其實不然,Kafka會在內存中暫存你的消息,在適當的時機,一下子把所有消息發送出去,也就是一波消息被發走。
消息批量處理并不是只是發送的時候是批量,而是從生產者、broker到消費者一直都是這樣,這個批次的消息在kafka看來好像就是一條消息一樣,生產者在生產端將消息聚合,broker什么都不做,按”一個“消息存儲,最后由消費者拿到消息后解開一條條消費。問題來了,都知道kafka利用offset機制實現從不同位置消費消息,如果消息批量了,我該怎么消費批量里邊的某條呢?看下邊的例子

offset傳36、37還是38是一樣的都是拿圖中38的開頭的消息,消費者拿到這個集體消息后,在取出感興趣的消息即可。
另外為了提升傳輸效率,kafka還做了端到端的消息壓縮,生產者將批量消息壓縮,broker原樣保存壓縮數據,最有有消費者獲取后解壓縮,這又進一步提升了傳輸效率。
總而言之,批量處理和壓縮的目的就是使用相當小的cpu開銷,換取高效的網路傳輸,解決網絡帶寬的瓶頸。
零拷貝
通常情況下,文件數據到套接字傳輸的數據路徑要經過4個步驟:
- 操作系統從磁盤讀取數據到內核空間緩沖區
- 應用程序從內核空間讀取數據到用戶空間緩沖區
- 應用程序將數據寫回內核空間套接字緩沖區
- 操作系統將數據從套接字緩沖區賦值到網絡發送的NIC緩沖區
這四步數據會在數據總線上來來回回被傳輸,而零拷貝(主要函數sendfile)解決了這個問題,只需兩步
- DMA從硬盤讀數據到操作系統內核讀緩沖區
- 根據套接字描述服信息,直接從讀緩沖區里面寫入到網卡的緩沖區
這顯然比通常的四步驟高效的多。想下這個場景,如果消費的足夠快,頁緩存和零拷貝是不是可以讓消費者消費的消息幾乎都是從緩存中拿到的數據,根本不用去讀磁盤(數據還沒來得及從緩存中失效就被消費者拉走了)。
簡潔的消費者邏輯
kafka沒有采用推的方式通知消費者,而是由消費者自己根據消費情況,主動拉取消息,首先服務端沒有了主動推的壓力,另外消費端也變的非常的靈活。如果是拉,還需要再解決一個及時性的問題,堆積的未讀消息多,消費者可以一直拉,但是如果此時沒有消息,如果消息到了怎么能及時讓消費者能知道呢?即便消費者每隔一段時間過來拉取,照樣還是存在時延,kafak為了避免這種情況,在拉取請求中設置了參數,允許消費者請求在“長輪詢”中阻塞,等待數據到達。
在kafka中多個消費者構成一個消費組,這些消費者擁有消費組的共有屬性,例如都能去消費消費組目標topic的消息。
在記錄消費者消費到哪條消息上,更是簡潔,kafka并不為每個消費者維護一個”消費進度“。在kafka中每個topic分為一組完全有序的partitions,消息落地在partitions上,每個partitions在某個時刻只能有一個消費組內的消費者去消費,每個partitions只記錄消費組在本partitions的消費的topic的”消費進度“,消費者來到這個partitions,kafka只需要找到消費者所在的消費組在本partitions的”消費進度“然后給出后邊消息即可。
這有很大的優勢,kafka免去了復雜的消費者消費進度維護邏輯,使其變得相當簡單和易于維護,而且partitions做到了單點(一個消費者)消費,免去了多點帶來的問題。另外需要注意消費者數量盡量和partitions數量一致,這樣每個partitions都能分到一個消費者,如果消費者多于partitions數量,就意味著有的消費者空閑,消費者少于partitions數量,就意味著有的消費者消費多個partitions。
如果想進一步提升消費速度,其中方法之一就是適當的增加partitions的數量,使得更多的消費者去消費。