您好,登錄后才能下訂單哦!
本篇內容介紹了“如何理解Kafka性能”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
65: Redis 和 Kafka 完全是不同作用的中間件,有比較性嗎?
”是的,所以此文講的不是《分布式緩存的選型》,也不是《分布式中間件對比》。我們聚焦于這兩個不同領域的項目對性能的優化,看一看優秀項目對性能優化的通用手段,以及在針對不同場景下的特色的優化方式。
很多人學習了很多東西,了解了很多框架,但在遇到實際問題時,卻常常會感覺到知識不足。這就是沒有將學習到的知識體系化,沒有從具體的實現中抽象出可以行之有效的方法論。
學習開源項目很重要的一點就是歸納,將不同項目的優秀實現總結出方法論,然后演繹到自我的實踐中去。
碼哥:理性、客觀、謹慎是程序員的特點,也是優點,但是很多時候我們也需要帶一點感性,帶一點沖動,這個時候可以幫助我們更快的做決策。「悲觀者正確、樂觀者成功。」希望大家都是一個樂觀地解決問題的人。
從高度抽象的角度來看,性能問題逃不出下面三個方面:
網絡
磁盤
復雜度
對于 Kafka 這種網絡分布式隊列來說,網絡和磁盤更是優化的重中之重。針對于上面提出的抽象問題,解決方案高度抽象出來也很簡單:
并發
壓縮
批量
緩存
算法
知道了問題和思路,我們再來看看,在 Kafka 中,有哪些角色,而這些角色就是可以優化的點:
Producer
Broker
Consumer
是的,所有的問題,思路,優化點都已經列出來了,我們可以盡可能的細化,三個方向都可以細化,如此,所有的實現便一目了然,即使不看 Kafka 的實現,我們自己也可以想到一二點可以優化的地方。
這就是思考方式。提出問題 > 列出問題點 > 列出優化方法 > 列出具體可切入的點 > tradeoff和細化實現。
現在,你也可以嘗試自己想一想優化的點和方法,不用盡善盡美,不用管好不好實現,想一點是一點。
65 哥:不行啊,我很笨,也很懶,你還是直接和我說吧,我白嫖比較行。
65 哥:人家 Redis 是基于純內存的系統,你 kafka 還要讀寫磁盤,能比?
我們不能只知道結論,而不知其所以然。要回答這個問題,就得回到在校時我們學的操作系統課程了。65 哥還留著課本嗎?來,翻到講磁盤的章節,讓我們回顧一下磁盤的運行原理。
65 哥:鬼還留著哦,課程還沒上到一半書就沒了。要不是考試俺眼神好,估計現在還沒畢業。
”看經典大圖:
完成一次磁盤 IO,需要經過尋道、旋轉和數據傳輸三個步驟。
影響磁盤 IO 性能的因素也就發生在上面三個步驟上,因此主要花費的時間就是:
尋道時間:Tseek 是指將讀寫磁頭移動至正確的磁道上所需要的時間。尋道時間越短,I/O 操作越快,目前磁盤的平均尋道時間一般在 3-15ms。
旋轉延遲:Trotation 是指盤片旋轉將請求數據所在的扇區移動到讀寫磁盤下方所需要的時間。旋轉延遲取決于磁盤轉速,通常用磁盤旋轉一周所需時間的 1/2 表示。比如:7200rpm 的磁盤平均旋轉延遲大約為 60*1000/7200/2 = 4.17ms,而轉速為 15000rpm 的磁盤其平均旋轉延遲為 2ms。
數據傳輸時間:Ttransfer 是指完成傳輸所請求的數據所需要的時間,它取決于數據傳輸率,其值等于數據大小除以數據傳輸率。目前 IDE/ATA 能達到 133MB/s,SATA II 可達到 300MB/s 的接口數據傳輸率,數據傳輸時間通常遠小于前兩部分消耗時間。簡單計算時可忽略。
因此,如果在寫磁盤的時候省去尋道、旋轉可以極大地提高磁盤讀寫的性能。
Kafka 采用順序寫文件的方式來提高磁盤寫入性能。順序寫文件,基本減少了磁盤尋道和旋轉的次數。磁頭再也不用在磁道上亂舞了,而是一路向前飛速前行。
Kafka 中每個分區是一個有序的,不可變的消息序列,新的消息不斷追加到 Partition 的末尾,在 Kafka 中 Partition 只是一個邏輯概念,Kafka 將 Partition 劃分為多個 Segment,每個 Segment 對應一個物理文件,Kafka 對 segment 文件追加寫,這就是順序寫文件。
65 哥:為什么 Kafka 可以使用追加寫的方式呢?
”這和 Kafka 的性質有關,我們來看看 Kafka 和 Redis,說白了,Kafka 就是一個Queue,而 Redis 就是一個HashMap。Queue和Map的區別是什么?
Queue 是 FIFO 的,數據是有序的;HashMap數據是無序的,是隨機讀寫的。Kafka 的不可變性,有序性使得 Kafka 可以使用追加寫的方式寫文件。
其實很多符合以上特性的數據系統,都可以采用追加寫的方式來優化磁盤性能。典型的有Redis的 AOF 文件,各種數據庫的WAL(Write ahead log)機制等等。
所以清楚明白自身業務的特點,就可以針對性地做出優化。
65 哥:哈哈,這個我面試被問到過。可惜答得一般般,唉。
我們從 Kafka 的場景來看,Kafka Consumer 消費存儲在 Broker 磁盤的數據,從讀取 Broker 磁盤到網絡傳輸給 Consumer,期間涉及哪些系統交互。Kafka Consumer 從 Broker 消費數據,Broker 讀取 Log,就使用了 sendfile。如果使用傳統的 IO 模型,偽代碼邏輯就如下所示:
readFile(buffer) send(buffer)
如圖,如果采用傳統的 IO 流程,先讀取網絡 IO,再寫入磁盤 IO,實際需要將數據 Copy 四次。
第一次:讀取磁盤文件到操作系統內核緩沖區;
第二次:將內核緩沖區的數據,copy 到應用程序的 buffer;
第三步:將應用程序 buffer 中的數據,copy 到 socket 網絡發送緩沖區;
第四次:將 socket buffer 的數據,copy 到網卡,由網卡進行網絡傳輸。
65 哥:啊,操作系統這么傻嗎?copy 來 copy 去的。
”并不是操作系統傻,操作系統的設計就是每個應用程序都有自己的用戶內存,用戶內存和內核內存隔離,這是為了程序和系統安全考慮,否則的話每個應用程序內存滿天飛,隨意讀寫那還得了。
不過,還有零拷貝技術,英文——Zero-Copy。零拷貝就是盡量去減少上面數據的拷貝次數,從而減少拷貝的 CPU 開銷,減少用戶態內核態的上下文切換次數,從而優化數據傳輸的性能。
常見的零拷貝思路主要有三種:
直接 I/O:數據直接跨過內核,在用戶地址空間與 I/O 設備之間傳遞,內核只是進行必要的虛擬存儲配置等輔助工作;
避免內核和用戶空間之間的數據拷貝:當應用程序不需要對數據進行訪問時,則可以避免將數據從內核空間拷貝到用戶空間;
寫時復制:數據不需要提前拷貝,而是當需要修改的時候再進行部分拷貝。
Kafka 使用到了 mmap 和 sendfile 的方式來實現零拷貝。分別對應 Java 的 MappedByteBuffer 和 FileChannel.transferTo。
使用 Java NIO 實現零拷貝,如下:
FileChannel.transferTo()
在此模型下,上下文切換的數量減少到一個。具體而言,transferTo()方法指示塊設備通過 DMA 引擎將數據讀取到讀取緩沖區中。然后,將該緩沖區復制到另一個內核緩沖區以暫存到套接字。最后,套接字緩沖區通過 DMA 復制到 NIC 緩沖區。
我們將副本數從四減少到三,并且這些副本中只有一個涉及 CPU。我們還將上下文切換的數量從四個減少到了兩個。這是一個很大的改進,但是還沒有查詢零副本。當運行 Linux 內核 2.4 及更高版本以及支持收集操作的網絡接口卡時,后者可以作為進一步的優化來實現。如下所示。
根據前面的示例,調用transferTo()方法會使設備通過 DMA 引擎將數據讀取到內核讀取緩沖區中。但是,使用gather操作時,讀取緩沖區和套接字緩沖區之間沒有復制。取而代之的是,給 NIC 一個指向讀取緩沖區的指針以及偏移量和長度,該偏移量和長度由 DMA 清除。CPU 絕對不參與復制緩沖區。
關于零拷貝詳情,可以詳讀這篇文章零拷貝 (Zero-copy) 淺析及其應用。
producer 生產消息到 Broker 時,Broker 會使用 pwrite() 系統調用【對應到 Java NIO 的 FileChannel.write() API】按偏移量寫入數據,此時數據都會先寫入page cache。consumer 消費消息時,Broker 使用 sendfile() 系統調用【對應 FileChannel.transferTo() API】,零拷貝地將數據從 page cache 傳輸到 broker 的 Socket buffer,再通過網絡傳輸。
leader 與 follower 之間的同步,與上面 consumer 消費數據的過程是同理的。
page cache中的數據會隨著內核中 flusher 線程的調度以及對 sync()/fsync() 的調用寫回到磁盤,就算進程崩潰,也不用擔心數據丟失。另外,如果 consumer 要消費的消息不在page cache里,才會去磁盤讀取,并且會順便預讀出一些相鄰的塊放入 page cache,以方便下一次讀取。
因此如果 Kafka producer 的生產速率與 consumer 的消費速率相差不大,那么就能幾乎只靠對 broker page cache 的讀寫完成整個生產 - 消費過程,磁盤訪問非常少。
65 哥:網絡嘛,作為 Java 程序員,自然是 Netty
”是的,Netty 是 JVM 領域一個優秀的網絡框架,提供了高性能的網絡服務。大多數 Java 程序員提到網絡框架,首先想到的就是 Netty。Dubbo、Avro-RPC 等等優秀的框架都使用 Netty 作為底層的網絡通信框架。
Kafka 自己實現了網絡模型做 RPC。底層基于 Java NIO,采用和 Netty 一樣的 Reactor 線程模型。
Reacotr 模型主要分為三個角色
Reactor:把 IO 事件分配給對應的 handler 處理
Acceptor:處理客戶端連接事件
Handler:處理非阻塞的任務
在傳統阻塞 IO 模型中,每個連接都需要獨立線程處理,當并發數大時,創建線程數多,占用資源;采用阻塞 IO 模型,連接建立后,若當前線程沒有數據可讀,線程會阻塞在讀操作上,造成資源浪費
針對傳統阻塞 IO 模型的兩個問題,Reactor 模型基于池化思想,避免為每個連接創建線程,連接完成后將業務處理交給線程池處理;基于 IO 復用模型,多個連接共用同一個阻塞對象,不用等待所有的連接。遍歷到有新數據可以處理時,操作系統會通知程序,線程跳出阻塞狀態,進行業務邏輯處理
Kafka 即基于 Reactor 模型實現了多路復用和處理線程池。其設計如下:
其中包含了一個Acceptor線程,用于處理新的連接,Acceptor 有 N 個 Processor 線程 select 和 read socket 請求,N 個 Handler 線程處理請求并相應,即處理業務邏輯。
I/O 多路復用可以通過把多個 I/O 的阻塞復用到同一個 select 的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。它的最大優勢是系統開銷小,并且不需要創建新的進程或者線程,降低了系統的資源開銷。
總結: Kafka Broker 的 KafkaServer 設計是一個優秀的網絡架構,有想了解 Java 網絡編程,或需要使用到這方面技術的同學不妨去讀一讀源碼。后續『碼哥』的 Kafka 系列文章也將涉及這塊源碼的解讀。
Kafka Producer 向 Broker 發送消息不是一條消息一條消息的發送。使用過 Kafka 的同學應該知道,Producer 有兩個重要的參數:batch.size和linger.ms。這兩個參數就和 Producer 的批量發送有關。
Kafka Producer 的執行流程如下圖所示:
發送消息依次經過以下處理器:
Serialize:鍵和值都根據傳遞的序列化器進行序列化。優秀的序列化方式可以提高網絡傳輸的效率。
Partition:決定將消息寫入主題的哪個分區,默認情況下遵循 murmur2 算法。自定義分區程序也可以傳遞給生產者,以控制應將消息寫入哪個分區。
Compress:默認情況下,在 Kafka 生產者中不啟用壓縮.Compression 不僅可以更快地從生產者傳輸到代理,還可以在復制過程中進行更快的傳輸。壓縮有助于提高吞吐量,降低延遲并提高磁盤利用率。
Accumulate:Accumulate顧名思義,就是一個消息累計器。其內部為每個 Partition 維護一個Deque雙端隊列,隊列保存將要發送的批次數據,Accumulate將數據累計到一定數量,或者在一定過期時間內,便將數據以批次的方式發送出去。記錄被累積在主題每個分區的緩沖區中。根據生產者批次大小屬性將記錄分組。主題中的每個分區都有一個單獨的累加器 / 緩沖區。
Group Send:記錄累積器中分區的批次按將它們發送到的代理分組。批處理中的記錄基于 batch.size 和 linger.ms 屬性發送到代理。記錄由生產者根據兩個條件發送。當達到定義的批次大小或達到定義的延遲時間時。
Kafka 支持多種壓縮算法:lz4、snappy、gzip。Kafka 2.1.0 正式支持 ZStandard —— ZStandard 是 Facebook 開源的壓縮算法,旨在提供超高的壓縮比 (compression ratio),具體細節參見 zstd。
Producer、Broker 和 Consumer 使用相同的壓縮算法,在 producer 向 Broker 寫入數據,Consumer 向 Broker 讀取數據時甚至可以不用解壓縮,最終在 Consumer Poll 到消息時才解壓,這樣節省了大量的網絡和磁盤開銷。
Kafka 的 Topic 可以分成多個 Partition,每個 Paritition 類似于一個隊列,保證數據有序。同一個 Group 下的不同 Consumer 并發消費 Paritition,分區實際上是調優 Kafka 并行度的最小單元,因此,可以說,每增加一個 Paritition 就增加了一個消費并發。
Kafka 具有優秀的分區分配算法——StickyAssignor,可以保證分區的分配盡量地均衡,且每一次重分配的結果盡量與上一次分配結果保持一致。這樣,整個集群的分區盡量地均衡,各個 Broker 和 Consumer 的處理不至于出現太大的傾斜。
65 哥:那是不是分區數越多越好呢?
”當然不是。
在 kafka 的 broker 中,每個分區都會對照著文件系統的一個目錄。在 kafka 的數據日志文件目錄中,每個日志數據段都會分配兩個文件,一個索引文件和一個數據文件。因此,隨著 partition 的增多,需要的文件句柄數急劇增加,必要時需要調整操作系統允許打開的文件句柄數。
客戶端 producer 有個參數 batch.size,默認是 16KB。它會為每個分區緩存消息,一旦滿了就打包將消息批量發出。看上去這是個能夠提升性能的設計。不過很顯然,因為這個參數是分區級別的,如果分區數越多,這部分緩存所需的內存占用也會更多。
分區越多,每個 Broker 上分配的分區也就越多,當一個發生 Broker 宕機,那么恢復時間將很長。
文件結構Kafka 消息是以 Topic 為單位進行歸類,各個 Topic 之間是彼此獨立的,互不影響。每個 Topic 又可以分為一個或多個分區。每個分區各自存在一個記錄消息數據的日志文件。
Kafka 每個分區日志在物理上實際按大小被分成多個 Segment。
segment file 組成:由 2 大部分組成,分別為 index file 和 data file,此 2 個文件一一對應,成對出現,后綴”.index”和“.log”分別表示為 segment 索引文件、數據文件。
segment 文件命名規則:partion 全局的第一個 segment 從 0 開始,后續每個 segment 文件名為上一個 segment 文件最后一條消息的 offset 值。數值最大為 64 位 long 大小,19 位數字字符長度,沒有數字用 0 填充。
index 采用稀疏索引,這樣每個 index 文件大小有限,Kafka 采用mmap的方式,直接將 index 文件映射到內存,這樣對 index 的操作就不需要操作磁盤 IO。mmap的 Java 實現對應 MappedByteBuffer 。
65 哥筆記:mmap 是一種內存映射文件的方法。即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用 read,write 等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。
”Kafka 充分利用二分法來查找對應 offset 的消息位置:
按照二分法找到小于 offset 的 segment 的.log 和.index
用目標 offset 減去文件名中的 offset 得到消息在這個 segment 中的偏移量。
再次用二分法在 index 文件中找到對應的索引。
到 log 文件中,順序查找,直到找到 offset 對應的消息。
“如何理解Kafka性能”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。