相信各位小伙伴之前或多或少接觸過消息隊列,比較知名的包含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 服務端的高性能設計3.2.1 Reactor網絡通信模型kafka相比其他消息中間件最出彩的地方在于他的高吞吐量,那么對于服務端來說每秒的請求壓力將會巨大,需要有一個優秀的網絡通信機制來處理海量的請求。如果 IO 有所研究的同學,應該清楚:Reactor 模式正是采用了很經典的 IO 多路復用技術,它可以復用一個線程去處理大量的 Socket 連接,從而保證高性能。Netty 和 Redis 為什么能做到十萬甚至百萬并發?它們其實都采用了 Reactor 網絡通信模型。
1.kafka網絡通信層架構
從圖中可以看出,SocketServer和KafkaRequestHandlerPool是其中最重要的兩個組件:
SocketServer:主要實現了 Reactor 模式,用于處理外部多個 Clients(這里的 Clients 指的是廣義的 Clients,可能包含 Producer、Consumer 或其他 Broker)的并發請求,并負責將處理結果封裝進 Response 中,返還給 ClientsKafkaRequestHandlerPool: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-copy1)實現原理零拷貝并不是沒有拷貝數據,而是減少用戶態/內核態的切換次數以及CPU拷貝的次數。零拷貝實現有多種方式,分別是
mmap+writesendfile在服務端那里,我們已經知道了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讀取數據,就是由此實現。
@Overridepublic long transferFrom(FileChannel fileChannel, long position, long count) throws IOException { return fileChannel.transferTo(position, count, socketChannel);}tips: transferTo 和 transferFrom 并不保證一定能使用零拷貝。實際上是否能使用零拷貝與操作系統相關,如果操作系統提供 sendfile 這樣的零拷貝系統調用,則這兩個方法會通過這樣的系統調用充分利用零拷貝的優勢,否則并不能通過這兩個方法本身實現零拷貝。
4 總結文章第一部分為大家講解了高性能常見的優化手段,從”秘籍”和”道法”兩個方面來詮釋高性能設計之路該如何走,并引申出計算和IO兩個優化方向。
文章第二部分是kafka內部高性能的具體設計——分別從生產者、服務端、消費者來進行全方位講解,包括其設計、使用及相關原理。
希望通過這篇文章,能夠使大家不僅學習到相關方法論,也能明白其方法論具體的落地方案,一起學習,一起成長。
作者:京東物流 李鵬
來源:京東云開發者社區
免責聲明:以上內容為本網站轉自其它媒體,相關信息僅為傳遞更多信息之目的,不代表本網觀點,亦不代表本網站贊同其觀點或證實其內容的真實性。如稿件版權單位或個人不想在本網發布,可與本網聯系,本網視情況可立即將其撤除。