1 前言
相信各位小伙伴之前或多或少接觸過消息隊列,比較知名的包含 Rocket MQ 和 Kafka,在京東內部使用的是自研的消息中間件 JMQ,從 JMQ2 升級到 JMQ4 的也是帶來了性能上的明顯提升,并且 JMQ4 的底層也是參考 Kafka 去做的設計。在這里我會給大家展示 Kafka 它的高性能是如何設計的,大家也可以學習相關方法論將其利用在實際項目中,也許下一個頂級項目就在各位的代碼中產生了。
2 如何理解高性能設計
2.1 高性能設計的” 秘籍”
先拋開 kafka,咱們先來談論一下高性能設計的本質,在這里借用一下網上的一張總結高性能的思維導圖:
從中可以看到,高性能設計的手段還是非常多,從” 微觀設計” 上的無鎖化、序列化,到” 宏觀設計” 上的緩存、存儲等,可以說是五花八門,令人眼花繚亂。但是在我看來本質就兩點:計算和 IO。下面將從這兩點來淺析一下我認為的高性能的” 道”。
2.2 高性能設計的” 道法”
2.2.1 計算上的” 道”
計算上的優化手段無外乎兩種方式:1. 減少計算量 2. 加快單位時間的計算量
減少計算量:比如用索引來取代全局掃描、用同步代替異步、通過限流來減少請求處理量、采用更高效的數據結構和算法等。(舉例:mysql 的 BTree,redis 的跳表等)
加快單位時間的計算量:可以利用 CPU 多核的特性,比如用多線程代替單線程、用集群代替單機等。(舉例:多線程編程、分治計算等)
2.2.2 IO 上的” 道”
IO 上的優化手段也可以從兩個方面來體現:1. 減少 IO 次數或者 IO 數據量 2. 加快 IO 速度
減少 IO 次數或者 IO 數據量:比如借助系統緩存或者外部緩存、通過零拷貝技術減少 IO 復制次數、批量讀寫、數據壓縮等。
加快 IO 速度:比如用磁盤順序寫代替隨機寫、用 NIO 代替 BIO、用性能更好的 SSD 代替機械硬盤等。
3 kafka 高性能設計
理解了高性能設計的手段和本質之后,我們再來看看 kafka 里面使用到的性能優化方法。各類消息中間件的本質都是一個生產者 - 消費者模型,生產者發送消息給服務端進行暫存,消費者從服務端獲取消息進行消費。也就是說 kafka 分為三個部分:生產者 - 服務端 - 消費者,我們可以按照這三個來分別歸納一下其關于性能優化的手段,這些手段也會涵蓋在我們之前梳理的腦圖里面。
3.1 生產者的高性能設計
3.1.1 批量發送消息
之前在上面說過,高性能的” 道” 在于計算和 IO 上,咱們先來看看在 IO 上 kafka 是如何做設計的。 IO 上的優化
kafka 是一個消息中間件,數據的載體就是消息,如何將消息高效的進行傳遞和持久化是 kafka 高性能設計的一個重點。基于此分析 kafka 肯定是 IO 密集型應用,producer 需要通過網絡 IO 將消息傳遞給 broker,broker 需要通過磁盤 IO 將消息持久化,consumer 需要通過網絡 IO 將消息從 broker 上拉取消費。
網絡 IO 上的優化:producer->broker 發送消息不是一條一條發送的,kafka 模式會有個消息發送延遲機制,會將一批消息進行聚合,一口氣打包發送給 broker,這樣就成功減少了 IO 的次數。除了傳輸消息本身以外,還要傳輸非常多的網絡協議本身的一些內容(稱為 Overhead),所以將多條消息合并到一起傳輸,可有效減少網絡傳輸的 Overhead,進而提高了傳輸效率。
磁盤 IO 上的優化:大家知道磁盤和內存的存儲速度是不同的,在磁盤上操作的速度是遠低于內存,但是在成本上內存是高于磁盤。kafka 是面向大數據量的消息中間件,也就是說需要將大批量的數據持久化,這些數據放在內存上也是不現實。那 kafka 是怎么在磁盤 IO 上進行優化的呢?在這里我先直接給出方法,具體細節在后文中解釋(它是借助于一種磁盤順序寫的機制來提升寫入速度)。
3.1.2 負載均衡
1.kafka 負載均衡設計
Kafka 有主題(Topic)概念,他是承載真實數據的邏輯容器,主題之下還分為若干個分區,Kafka 消息組織方式實際上是三級結構:主題 - 分區 - 消息。主題下的每條消息只會在某一個分區中,而不會在多個分區中被保存多份。
Kafka 這樣設計,使用分區的作用就是提供負載均衡的能力,對數據進行分區的主要目的就是為了實現系統的高伸縮性(Scalability)。
不同的分區能夠放在不同的節點的機器上,而數據的讀寫操作也都是針對分區這個粒度進行的,每個節點的機器都能獨立地執行各自分區讀寫請求。我們還可以通過增加節點來提升整體系統的吞吐量。Kafka 的分區設計,還可以實現業務級別的消息順序的問題。
2. 具體分區策略
所謂的分區策略是指決定生產者將消息發送到那個分區的算法。Kafka 提供了默認的分區策略是輪詢,同時 kafka 也支持用戶自己制定。
輪詢策略:也稱為 Round-robin 策略,即順序分配。輪詢的優點是有著優秀的負載均衡的表現。
隨機策略:雖然也是追求負載均衡,但總體表現差于輪詢。
消息鍵劃分策略:還要一種是為每條消息配置一個 key,按消息的 key 來存。Kafka 允許為每條消息指定一個 key。一旦指定了 key ,那么會對 key 進行 hash 計算,將相同的 key 存入相同的分區中,而且每個分區下的消息都是有序的。key 的作用很大,可以是一個有著明確業務含義的字符串,也可以是用來表征消息的元數據。
其他的分區策略:基于地理位置的分區。可以從所有分區中找出那些 Leader 副本在某個地理位置所有分區,然后隨機挑選一個進行消息發送。
3.1.3 異步發送
1. 線程模型
之前已經說了 kafka 是選擇批量發送消息來提升整體的 IO 性能,具體流程是 kafka 生產者使用批處理試圖在內存中積累數據,主線程將多條消息通過一個 ProduceRequest 請求批量發送出去,發送的消息暫存在一個隊列 (RecordAccumulator) 中,再由 sender 線程去獲取一批數據或者不超過某個延遲時間內的數據發送給 broker 進行持久化。
優點:
可以提升 kafka 整體的吞吐量,減少網絡 IO 的次數;
提高數據壓縮效率 (一般壓縮算法都是數據量越大越能接近預期的壓縮效果);
缺點:
數據發送有一定延遲,但是這個延遲可以由業務因素來自行設置。
3.1.4 高效序列化
1. 序列化的優勢
Kafka 消息中的 Key 和 Value,都支持自定義類型,只需要提供相應的序列化和反序列化器即可。因此,用戶可以根據實際情況選用快速且緊湊的序列化方式(比如 ProtoBuf、Avro)來減少實際的網絡傳輸量以及磁盤存儲量,進一步提高吞吐量。
2. 內置的序列化器
org.apache.kafka.common.serialization.StringSerializer;
org.apache.kafka.common.serialization.LongSerializer;
org.apache.kafka.common.serialization.IntegerSerializer;
org.apache.kafka.common.serialization.ShortSerializer;
org.apache.kafka.common.serialization.FloatSerializer;
org.apache.kafka.common.serialization.DoubleSerializer;
org.apache.kafka.common.serialization.BytesSerializer;
org.apache.kafka.common.serialization.ByteBufferSerializer;
org.apache.kafka.common.serialization.ByteArraySerializer;
3.1.5 消息壓縮
1. 壓縮的目的
壓縮秉承了用時間換空間的經典 trade-off 思想,即用 CPU 的時間去換取磁盤空間或網絡 I/O 傳輸量,Kafka 的壓縮算法也是出于這種目的。并且通常是:數據量越大,壓縮效果才會越好。
因為有了批量發送這個前期,從而使得 Kafka 的消息壓縮機制能真正發揮出它的威力(壓縮的本質取決于多消息的重復性)。對比壓縮單條消息,同時對多條消息進行壓縮,能大幅減少數據量,從而更大程度提高網絡傳輸率。 2. 壓縮的
方法
想了解 kafka 消息壓縮的設計,就需要先了解 kafka 消息的格式:
Kafka 的消息層次分為:消息集合(message set)和消息(message);一個消息集合中包含若干條日志項(record item),而日志項才是真正封裝消息的地方。
Kafka 底層的消息日志由一系列消息集合 - 日志項組成。Kafka 通常不會直接操作具體的一條條消息,他總是在消息集合這個層面上進行寫入操作。
每條消息都含有自己的元數據信息,kafka 會將一批消息相同的元數據信息給提升到外層的消息集合里面,然后再對整個消息集合來進行壓縮。批量消息在持久化到 Broker 中的磁盤時,仍然保持的是壓縮狀態,最終是在 Consumer 端做了解壓縮操作。
壓縮算法效率對比
Kafka 共支持四種主要的壓縮類型:Gzip、Snappy、Lz4 和 Zstd,具體效率對比如下:
3.2 服務端的高性能設計
kafka 相比其他消息中間件最出彩的地方在于他的高吞吐量,那么對于服務端來說每秒的請求壓力將會巨大,需要有一個優秀的網絡通信機制來處理海量的請求。如果 IO 有所研究的同學,應該清楚:Reactor 模式正是采用了很經典的 IO 多路復用技術,它可以復用一個線程去處理大量的 Socket 連接,從而保證高性能。Netty 和 Redis 為什么能做到十萬甚至百萬并發?它們其實都采用了 Reactor 網絡通信模型。
1.kafka 網絡通信層架構
從圖中可以看出,SocketServer 和 KafkaRequestHandlerPool 是其中最重要的兩個組件:
SocketServer:主要實現了 Reactor 模式,用于處理外部多個 Clients(這里的 Clients 指的是廣義的 Clients,可能包含 Producer、Consumer 或其他 Broker)的并發請求,并負責將處理結果封裝進 Response 中,返還給 Clients
KafkaRequestHandlerPool:Reactor 模式中的 Worker 線程池,里面定義了多個工作線程,用于處理實際的 I/O 請求邏輯。
2. 請求流程
Clients 或其他 Broker 通過 Selector 機制發起創建連接請求。(NIO 的機制,使用 epoll)
Processor 線程接收請求,并將其轉換成可處理的 Request 對象。
Processor 線程將 Request 對象放入共享的 RequestChannel 的 Request 隊列。
KafkaRequestHandler 線程從 Request 隊列中取出待處理請求,并進行處理。
KafkaRequestHandler 線程將 Response 放回到對應 Processor 線程的 Response 隊列。
Processor 線程發送 Response 給 Request 發送方。
3.2.2 Kafka 的底層日志結構
基本結構的展示
Kafka 是一個 Pub-Sub 的消息系統,無論是發布還是訂閱,都須指定 Topic。Topic 只是一個邏輯的概念。每個 Topic 都包含一個或多個 Partition,不同 Partition 可位于不同節點。同時 Partition 在物理上對應一個本地文件夾 (也就是個日志對象 Log),每個 Partition 包含一個或多個 Segment,每個 Segment 包含一個數據文件和多個與之對應的索引文件。在邏輯上,可以把一個 Partition 當作一個非常長的數組,可通過這個 “數組” 的索引(offset)去訪問其數據。 2.Partition 的并行處理能力
一方面,topic 是由多個 partion 組成,Producer 發送消息到 topic 是有個負載均衡機制,基本上會將消息平均分配到每個 partion 里面,同時 consumer 里面會有個 consumer group 的概念,也就是說它會以組為單位來消費一個 topic 內的消息,一個 consumer group 內包含多個 consumer,每個 consumer 消費 topic 內不同的 partion,這樣通過多 partion 提高了消息的接收和處理能力
另一方面,由于不同 Partition 可位于不同機器,因此可以充分利用集群優勢,實現機器間的并行處理。并且 Partition 在物理上對應一個文件夾,即使多個 Partition 位于同一個節點,也可通過配置讓同一節點上的不同 Partition 置于不同的 disk drive 上,從而實現磁盤間的并行處理,充分發揮多磁盤的優勢。
3. 過期消息的清除
Kafka 的整個設計中,Partition 相當于一個非常長的數組,而 Broker 接收到的所有消息順序寫入這個大數組中。同時 Consumer 通過 Offset 順序消費這些數據,并且不刪除已經消費的數據,從而避免了隨機寫磁盤的過程。
由于磁盤有限,不可能保存所有數據,實際上作為消息系統 Kafka 也沒必要保存所有數據,需要刪除舊的數據。而這個刪除過程,并非通過使用 “讀 - 寫” 模式去修改文件,而是將 Partition 分為多個 Segment,每個 Segment 對應一個物理文件,通過刪除整個文件的方式去刪除 Partition 內的數據。這種方式清除舊數據的方式,也避免了對文件的隨機寫操作。
3.2.3 樸實高效的索引
1. 稀疏索引
可以從上面看到,一個 segment 包含一個.log 后綴的文件和多個 index 后綴的文件。那么這些文件具體作用是干啥的呢?并且這些文件除了后綴不同文件名都是相同,為什么這么設計?
.log 文件:具體存儲消息的日志文件
.index 文件:位移索引文件,可根據消息的位移值快速地從查詢到消息的物理文件位置
.timeindex 文件:時間戳索引文件,可根據時間戳查找到對應的位移信息
.txnindex 文件:已中止事物索引文件
除了.log 是實際存儲消息的文件以外,其他的幾個文件都是索引文件。索引本身設計的原來是一種空間換時間的概念,在這里 kafka 是為了加速查詢所使用。kafka 索引不會為每一條消息建立索引關系,這個也很好理解,畢竟對一條消息建立索引的成本還是比較大的,所以它是一種稀疏索引的概念,就好比我們常見的跳表,都是一種稀疏索引。
kafka 日志的文件名一般都是該 segment 寫入的第一條消息的起始位移值 baseOffset,比如 000000000123.log,這里面的 123 就是 baseOffset,具體索引文件里面紀錄的數據是相對于起始位移的相對位移值 relativeOffset,baseOffset 與 relativeOffse 的加和即為實際消息的索引值。假設一個索引文件為:00000000000000000100.index,那么起始位移值即 100,當存儲位移為 150 的消息索引時,在索引文件中的相對位移則為 150 - 100 = 50,這么做的好處是使用 4 字節保存位移即可,可以節省非常多的磁盤空間。(ps:kafka 真的是極致的壓縮了數據存儲的空間)
2. 優化的二分查找算法
kafka 沒有使用我們熟知的跳表或者 B+Tree 結構來設計索引,而是使用了一種更為簡單且高效的查找算法:二分查找。但是相對于傳統的二分查找,kafka 將其進行了部分優化,個人覺得設計的非常巧妙,在這里我會進行詳述。
在這之前,我先補充一下 kafka 索引文件的構成:每個索引文件包含若干條索引項。不同索引文件的索引項的大小不同,比如 offsetIndex 索引項大小是 8B,timeIndex 索引項的大小是 12B。
這里以 offsetIndex 為例子來詳述 kafka 的二分查找算法:
1)普通二分查找
offsetIndex 每個索引項大小是 8B,但操作系統訪問內存時的最小單元是頁,一般是 4KB,即 4096B,會包含了 512 個索引項。而找出在索引中的指定偏移量,對于操作系統訪問內存時則變成了找出指定偏移量所在的頁。假設索引的大小有 13 個頁,如下圖所示:
由于 Kafka 讀取消息,一般都是讀取最新的偏移量,所以要查詢的頁就集中在尾部,即第 12 號頁上。根據二分查找,將依次訪問 6、9、11、12 號頁。
當隨著 Kafka 接收消息的增加,索引文件也會增加至第 13 號頁,這時根據二分查找,將依次訪問 7、10、12、13 號頁。
可以看出訪問的頁和上一次的頁完全不同。之前在只有 12 號頁的時候,Kafak 讀取索引時會頻繁訪問 6、9、11、12 號頁,而由于 Kafka 使用了mmap來提高速度,即讀寫操作都將通過操作系統的 page cache,所以 6、9、11、12 號頁會被緩存到 page cache 中,避免磁盤加載。但是當增至 13 號頁時,則需要訪問 7、10、12、13 號頁,而由于 7、10 號頁長時間沒有被訪問(現代操作系統都是使用 LRU 或其變體來管理 page cache),很可能已經不在 page cache 中了,那么就會造成缺頁中斷(線程被阻塞等待從磁盤加載沒有被緩存到 page cache 的數據)。在 Kafka 的官方測試中,這種情況會造成幾毫秒至 1 秒的延遲。 2)kafka 優化的二分查找
Kafka 對二分查找進行了改進。既然一般讀取數據集中在索引的尾部。那么將索引中最后的 8192B(8KB)劃分為 “熱區”(剛好緩存兩頁數據),其余部分劃分為 “冷區”,分別進行二分查找。這樣做的好處是,在頻繁查詢尾部的情況下,尾部的頁基本都能在 page cahce 中,從而避免缺頁中斷。
下面我們還是用之前的例子來看下。由于每個頁最多包含 512 個索引項,而最后的 1024 個索引項所在頁會被認為是熱區。那么當 12 號頁未滿時,則 10、11、12 會被判定是熱區;而當 12 號頁剛好滿了的時候,則 11、12 被判定為熱區;當增至 13 號頁且未滿時,11、12、13 被判定為熱區。假設我們讀取的是最新的消息,則在熱區中進行二分查找的情況如下:
當 12 號頁未滿時,依次訪問 11、12 號頁,當 12 號頁滿時,訪問頁的情況相同。當 13 號頁出現的時候,依次訪問 12、13 號頁,不會出現訪問長時間未訪問的頁,則能有效避免缺頁中斷。
3.mmap 的使用
利用稀疏索引,已經基本解決了高效查詢的問題,但是這個過程中仍然有進一步的優化空間,那便是通過 mmap(memory mapped files) 讀寫上面提到的稀疏索引文件,進一步提高查詢消息的速度。
究竟如何理解 mmap?前面提到,常規的文件操作為了提高讀寫性能,使用了 Page Cache 機制,但是由于頁緩存處在內核空間中,不能被用戶進程直接尋址,所以讀文件時還需要通過系統調用,將頁緩存中的數據再次拷貝到用戶空間中。
1)常規文件讀寫
app 拿著 inode 查找讀取文件
address_space 中存儲了 inode 和該文件對應頁面緩存的映射關系
頁面緩存缺失,引發缺頁異常
通過 inode 找到磁盤地址,將文件信息讀取并填充到頁面緩存
頁面緩存處于內核態,無法直接被 app 讀取到,因此要先拷貝到用戶空間緩沖區,此處發生內核態和用戶態的切換
tips:這一過程實際上發生了四次數據拷貝。首先通過系統調用將文件數據讀入到內核態 Buffer(DMA 拷貝),然后應用程序將內存態 Buffer 數據讀入到用戶態 Buffer(CPU 拷貝),接著用戶程序通過 Socket 發送數據時將用戶態 Buffer 數據拷貝到內核態 Buffer(CPU 拷貝),最后通過 DMA 拷貝將數據拷貝到 NIC Buffer。同時,還伴隨著四次上下文切換。
2)mmap 讀寫模式
調用內核函數 mmap (),在頁表 (類比虛擬內存 PTE) 中建立了文件地址和虛擬地址空間中用戶空間的映射關系
讀操作引發缺頁異常,通過 inode 找到磁盤地址,將文件內容拷貝到用戶空間,此處不涉及內核態和用戶態的切換
tips:采用 mmap 后,它將磁盤文件與進程虛擬地址做了映射,并不會招致系統調用,以及額外的內存 copy 開銷,從而提高了文件讀取效率。具體到 Kafka 的源碼層面,就是基于 JDK nio 包下的 MappedByteBuffer 的 map 函數,將磁盤文件映射到內存中。只有索引文件的讀寫才用到了 mmap。
3.2.4 消息存儲 - 磁盤順序寫
對于我們常用的機械硬盤,其讀取數據分 3 步:
尋道;
尋找扇區;
讀取數據;
前兩個,即尋找數據位置的過程為機械運動。我們常說硬盤比內存慢,主要原因是這兩個過程在拖后腿。不過,硬盤比內存慢是絕對的嗎?其實不然,如果我們能通過順序讀寫減少尋找數據位置時讀寫磁頭的移動距離,硬盤的速度還是相當可觀的。一般來講,IO 速度層面,內存順序 IO > 磁盤順序 IO > 內存隨機 IO > 磁盤隨機 IO。這里用一張網上的圖來對比一下相關 IO 性能:
Kafka 在順序 IO 上的設計分兩方面看:
LogSegment 創建時,一口氣申請 LogSegment 最大 size 的磁盤空間,這樣一個文件內部盡可能分布在一個連續的磁盤空間內;
.log 文件也好,.index 和.timeindex 也罷,在設計上都是只追加寫入,不做更新操作,這樣避免了隨機 IO 的場景;
3.2.5 Page Cache 的使用
為了優化讀寫性能,Kafka 利用了操作系統本身的 Page Cache,就是利用操作系統自身的內存而不是 JVM 空間內存。這樣做的好處有:
避免 Object 消耗:如果是使用 Java 堆,Java 對象的內存消耗比較大,通常是所存儲數據的兩倍甚至更多。
避免 GC 問題:隨著 JVM 中數據不斷增多,垃圾回收將會變得復雜與緩慢,使用系統緩存就不會存在 GC 問題
相比于使用 JVM 或 in-memory cache 等數據結構,利用操作系統的 Page Cache 更加簡單可靠。
首先,操作系統層面的緩存利用率會更高,因為存儲的都是緊湊的字節結構而不是獨立的對象。
其次,操作系統本身也對于 Page Cache 做了大量優化,提供了 write-behind、read-ahead 以及 flush 等多種機制。
再者,即使服務進程重啟,JVM 內的 Cache 會失效,Page Cache 依然可用,避免了 in-process cache 重建緩存的過程。
通過操作系統的 Page Cache,Kafka 的讀寫操作基本上是基于內存的,讀寫速度得到了極大的提升。
3.3 消費端的高性能設計
3.3.1 批量消費
生產者是批量發送消息,消息者也是批量拉取消息的,每次拉取一個消息 batch,從而大大減少了網絡傳輸的 overhead。在這里 kafka 是通過 fetch.min.bytes 參數來控制每次拉取的數據大小。默認是 1 字節,表示只要 Kafka Broker 端積攢了 1 字節的數據,就可以返回給 Consumer 端,這實在是太小了。我們還是讓 Broker 端一次性多返回點數據吧。
并且,在生產者高性能設計目錄里面也說過,生產者其實在 Client 端對批量消息進行了壓縮,這批消息持久化到 Broker 時,仍然保持的是壓縮狀態,最終在 Consumer 端再做解壓縮操作。
3.3.2 零拷貝 - 磁盤消息文件的讀取
1.zero-copy 定義
零拷貝并不是不需要拷貝,而是減少不必要的拷貝次數。通常是說在 IO 讀寫過程中。
零拷貝字面上的意思包括兩個,“零” 和 “拷貝”:
“拷貝”:就是指數據從一個存儲區域轉移到另一個存儲區域。
“零” :表示次數為 0,它表示拷貝數據的次數為 0。
實際上,零拷貝是有廣義和狹義之分,目前我們通常聽到的零拷貝,包括上面這個定義減少不必要的拷貝次數都是廣義上的零拷貝。其實了解到這點就足夠了。
我們知道,減少不必要的拷貝次數,就是為了提高效率。那零拷貝之前,是怎樣的呢?
2. 傳統 IO 的流程
做服務端開發的小伙伴,文件下載功能應該實現過不少了吧。如果你實現的是一個 web 程序 ,前端請求過來,服務端的任務就是:將服務端主機磁盤中的文件從已連接的 socket 發出去。關鍵實現代碼如下:
while((n = read(diskfd, buf, BUF_SIZE)) > 0) write(sockfd, buf , n); 傳統的 IO 流程,包括 read 和 write 的過程。
read:把數據從磁盤讀取到內核緩沖區,再拷貝到用戶緩沖區
write:先把數據寫入到 socket 緩沖區,最后寫入網卡設備 流程圖如下:
用戶應用進程調用 read 函數,向操作系統發起 IO 調用,上下文從用戶態轉為內核態(切換 1)
DMA 控制器把數據從磁盤中,讀取到內核緩沖區。
CPU 把內核緩沖區數據,拷貝到用戶應用緩沖區,上下文從內核態轉為用戶態(切換 2) ,read 函數返回
用戶應用進程通過 write 函數,發起 IO 調用,上下文從用戶態轉為內核態(切換 3)
CPU 將用戶緩沖區中的數據,拷貝到 socket 緩沖區
DMA 控制器把數據從 socket 緩沖區,拷貝到網卡設備,上下文從內核態切換回用戶態(切換 4) ,write 函數返回
從流程圖可以看出,傳統 IO 的讀寫流程 ,包括了 4 次上下文切換(4 次用戶態和內核態的切換),4 次數據拷貝(兩次 CPU 拷貝以及兩次的 DMA 拷貝 ),什么是 DMA 拷貝呢?我們一起來回顧下,零拷貝涉及的操作系統知識點。
3. 零拷貝相關知識點
1)內核空間和用戶空間
操作系統為每個進程都分配了內存空間,一部分是用戶空間,一部分是內核空間。內核空間是操作系統內核訪問的區域,是受保護的內存空間,而用戶空間是用戶應用程序訪問的內存區域。以 32 位操作系統為例,它會為每一個進程都分配了 4G (2 的 32 次方) 的內存空間。
內核空間:主要提供進程調度、內存分配、連接硬件資源等功能
用戶空間:提供給各個程序進程的空間,它不具有訪問內核空間資源的權限,如果應用程序需要使用到內核空間的資源,則需要通過系統調用來完成。進程從用戶空間切換到內核空間,完成相關操作后,再從內核空間切換回用戶空間。
2)用戶態 & 內核態
如果進程運行于內核空間,被稱為進程的內核態
如果進程運行于用戶空間,被稱為進程的用戶態。
3)上下文切換 cpu 上下文
CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。而程序計數器,則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。它們都是 CPU 在運行任何任務前,必須的依賴環境,因此叫做 CPU 上下文。
cpu 上下文切換
它是指,先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程序計數器)保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的新位置,運行新任務。
一般我們說的上下文切換 ,就是指內核(操作系統的核心)在 CPU 上對進程或者線程進行切換。進程從用戶態到內核態的轉變,需要通過系統調用 來完成。系統調用的過程,會發生 CPU 上下文的切換 。 4)DMA 技術
DMA,英文全稱是 Direct Memory Access ,即直接內存訪問。DMA 本質上是一塊主板上獨立的芯片,允許外設設備和內存存儲器之間直接進行 IO 數據傳輸,其過程不需要 CPU 的參與 。
我們一起來看下 IO 流程,DMA 幫忙做了什么事情。
可以發現,DMA 做的事情很清晰啦,它主要就是幫忙 CPU 轉發一下 IO 請求,以及拷貝數據 。 之所以需要 DMA,主要就是效率,它幫忙 CPU 做事情,這時候,CPU 就可以閑下來去做別的事情,提高了 CPU 的利用效率。 4.kafka 消費的 zero-copy
1)實現原理
零拷貝并不是沒有拷貝數據,而是減少用戶態 / 內核態的切換次數以及 CPU 拷貝的次數。零拷貝實現有多種方式,分別是
mmap+write
sendfile
在服務端那里,我們已經知道了 kafka 索引文件使用的 mmap 來進行零拷貝優化的,現在告訴你 kafka 消費者在讀取消息的時候使用的是 sendfile 來進行零拷貝優化。
linux 2.4 版本之后,對 sendfile 做了優化升級,引入 SG-DMA 技術,其實就是對 DMA 拷貝加入了 scatter/gather 操作,它可以直接從內核空間緩沖區中將數據讀取到網卡。使用這個特點搞零拷貝,即還可以多省去一次 CPU 拷貝 。
sendfile+DMA scatter/gather 實現的零拷貝流程如下:
用戶進程發起 sendfile 系統調用,上下文(切換 1)從用戶態轉向內核態。
DMA 控制器,把數據從硬盤中拷貝到內核緩沖區。
CPU 把內核緩沖區中的文件描述符信息 (包括內核緩沖區的內存地址和偏移量)發送到 socket 緩沖區
DMA 控制器根據文件描述符信息,直接把數據從內核緩沖區拷貝到網卡
上下文(切換 2)從內核態切換回用戶態 ,sendfile 調用返回。
可以發現,sendfile+DMA scatter/gather 實現的零拷貝,I/O 發生了 2 次用戶空間與內核空間的上下文切換,以及 2 次數據拷貝。其中 2 次數據拷貝都是包 DMA 拷貝 。這就是真正的 零拷貝(Zero-copy) 技術,全程都沒有通過 CPU 來搬運數據,所有的數據都是通過 DMA 來進行傳輸的。
2)底層實現 Kafka 數據傳輸通過 TransportLayer 來完成,其子類 PlaintextTransportLayer 通過 Java NIO 的 FileChannel 的 transferTo 和 transferFrom 方法實現零拷貝。底層就是 sendfile。消費者從 broker 讀取數據,就是由此實現。
@Override public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException { return fileChannel.transferTo(position, count, socketChannel); } tips:transferTo 和 transferFrom 并不保證一定能使用零拷貝。實際上是否能使用零拷貝與操作系統相關,如果操作系統提供 sendfile 這樣的零拷貝系統調用,則這兩個方法會通過這樣的系統調用充分利用零拷貝的優勢,否則并不能通過這兩個方法本身實現零拷貝。
4 總結
文章第一部分為大家講解了高性能常見的優化手段,從” 秘籍” 和” 道法” 兩個方面來詮釋高性能設計之路該如何走,并引申出計算和 IO 兩個優化方向。 文章第二部分是 kafka 內部高性能的具體設計 —— 分別從生產者、服務端、消費者來進行全方位講解,包括其設計、使用及相關原理。 希望通過這篇文章,能夠使大家不僅學習到相關方法論,也能明白其方法論具體的落地方案,一起學習,一起成長。
審核編輯:劉清
-
多路復用器
+關注
關注
9文章
873瀏覽量
65300 -
網絡通信
+關注
關注
4文章
801瀏覽量
29812 -
Hash算法
+關注
關注
0文章
43瀏覽量
7383 -
負載均衡器
+關注
關注
0文章
18瀏覽量
2585
原文標題:從Kafka中學習高性能系統如何設計
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論