【Free Style】kafka 解密:破除單機topic數多性能下降魔咒 (上)
版權歸PUMA項目組所有,轉載請聲明,多謝。

kakfa大規模集群能力在前面已給大家分享過,Kafka作為消息總線,在支撐云千萬tps上千節點的集群能力非常出色,本文繼續對業界關于單機多topic的性能瓶頸點問題(比如:https://yq.aliyun.com/articles/62832?spm=5176.100239.blogcont25379.8.KMUH1L),國內某云使用RocketMQ,性能對比唱衰Kafka 64分區時出現性能拐點,為此我們表示不服,從理論到實測數據,一步步揭開所謂單機性能拐點的秘密及解決之道。
1?????簡介
本文分析了Kafka分區數量的支持情況,通過一系列的測試和分析需要確認分區的數目是否受到限制,并找到解決方案。
本文讀者包含分布式消息總線系統的開發者、測試者,以及應用該系統的客戶產品
硬件約束:2285服務器2臺
軟件約束:無
2?????需求背景
為什么要擴展Kafka分區數量呢?主要有以下幾點:
1.????分區的數目關系消息隊列的并行度,消息發送是往分區里發送的,每個消費者都只能對每個分區啟動1個線程獲取數據。所以分區是消息隊列并行度的最大保證。
2.????消息總線需要多業務接入,每個業務都要創建自己的Topic和分區,必然對隊列數目有需求(隊列數目包含Topic和分區)
3.????終端有個IM的需求,是做一個類似一個微信和旺旺一樣的聊天工具。如果用消息總線來做,那么要求消息隊列為每個用戶分配一個隊列,這樣至少要支持幾億的消息隊列,這個在目前的消息隊列中完全沒辦法支持。
4.????當消息隊列作為公有云的服務提供的時候,我們分析每個用戶會獨占一個隊列(分區或Topic),同樣會對分區數量帶來要求。
正是這些需求和原因導致我們將分區個數作為了MQ的一個度量指標。同時從網上的一些分析,Kafka最多支持64個分區性能就下降得很厲害,為了驗證和解決分區數量限制的問題,我們就有本文檔的分析。
3?????分析
3.1???原理分析
3.1.1????基本概念
分區是Kafka中的最重要的概念之一,消息發送和接收都是按照分區為單位來處理的。
分區的幾個主要作用如下:
1、 Producer(消息發送者)的往消息Server的寫入并發數與分區數成正比。
2、 Consumer(消息消費者)消費某個Topic的并行度與分區數保持一致,假設分區數是20,那么Consumer的消費并行度最大為20。
3、?每個Topic由固定數量的分區數組成,分區數的多少決定了單臺Broker能支持的Topic數量,Topic數量又決定了支持的業務數量。
簡單的說,有多少分區,就有多少對應的文件讀寫,Kafka的每個分區對應一個文件:
每條消息都被append到該Partition中,屬于順序寫磁盤,因此效率非常高,經驗證,順序寫磁盤效率比隨機寫內存還要高,這是Kafka高吞吐率的一個很重要的保證。
在對消息存儲和緩存時,Kafka使用了文件系統。
Kafka的設計基于一種非常簡單的指導思想:不是要在內存中保存盡可能多的數據,在需要時將這些數據刷新(flush)到文件系統,而是要做完全相反的事情。所有數據都要立即寫入文件系統中持久化的日志中,但不進行刷新數據的任何調用。實際中這樣做意味著,數據被傳輸到OS內核的頁面緩存中了,OS隨后會將這些數據刷新到磁盤。
大家普遍為“磁盤很慢”,因而人們都對持久化(persistent structure)結構能夠提供說得過去的性能抱有懷疑態度。實際上,同人們的期望值相比,磁盤可以說是既很慢又很快,這取決決于磁盤的使用方式。設計的很好的磁盤結構可以和網絡一樣快。在一個由6個7200rpm的SATA硬盤組成的RAID-5磁盤陣列上,線性寫入(linear write)的速度大約是600MB/秒,但隨機寫入卻只有100k/秒,其中的差距接近6000倍。
Kafka并沒有在內存中創建緩沖區,然后再向磁盤write的方法,而是直接使用了PageCache。
OS在文件系統的讀寫上已經做了太多的優化,PageCache就是其中最重要的一種方法,詳細的說明請看3.1.2章節。
直接使用PageCache有如下幾個好處:
1)減少內存開銷:?Java對象的內存開銷(overhead)非常大,往往是對象中存儲的數據所占內存的兩倍以上。
2)避免GC問題:Java中的內存垃圾回收會隨著堆內數據不斷增長而變得越來越不明確,回收所花費的代價也會越來越大。
3)簡單可靠:OS會調用所有的空閑內存作為PageCache,并在其上做了大量的優化:預讀,后寫,flush管理等,這些都不用應用層操心,而是由OS自動完成。
由于這些因素,使用文件系統并依賴于PageCache頁面緩存要優于自己在內存中維護一個緩存或者什么其他別的結構。
3.1.2????文件讀寫分析
Kafka借力于Linux內核的Page Cache,不(顯式)用內存,勝用內存,完全沒有別家那樣要同時維護內存中數據、持久化數據的煩惱——只要內存足夠,生產者與消費者的速度也沒有差上太多,讀寫便都發生在Page Cache中,完全沒有同步的磁盤訪問。
Linux總會把系統中還沒被應用使用的內存挪來給Page Cache,在命令行輸入free,或者cat /proc/meminfo,"Cached"的部分就是Page Cache。
Page Cache中每個文件是一棵Radix樹(基樹),節點由4k大小的Page組成,可以通過文件的偏移量快速定位Page。
當寫操作發生時,它只是將數據寫入Page Cache中,并將該頁置上dirty標志。
當讀操作發生時,它會首先在Page Cache中查找內容,如果有就直接返回了,沒有的話就會從磁盤讀取文件再寫回Page Cache。
可見,只要生產者與消費者的速度相差不大,消費者會直接讀取之前生產者寫入Page Cache的數據,大家在內存里完成接力,根本沒有磁盤訪問。
而比起在內存中維護一份消息數據的傳統做法,這既不會重復浪費一倍的內存,Page Cache又不需要GC(可以放心使用大把內存了),而且即使Kafka重啟了,Page Cache還依然在。
這是大家最需要關心的,因為不能及時flush的話,OS crash(不是應用crash)?可能引起數據丟失,Page Cache瞬間從朋友變魔鬼。
當然,Kafka不怕丟,因為它的持久性是靠replicate保證,重啟后會從原來的replicate follower中拉缺失的數據。
內核線程pdflush負責將有dirty標記的頁面,發送給IO調度層。內核會為每個磁盤起一條pdflush線程,每5秒(/proc/sys/vm/dirty_writeback_centisecs)喚醒一次,根據下面三個參數來決定行為:
1.????/proc/sys/vm/dirty_expire_centiseconds:如果page dirty的時間超過了30秒(單位是10ms),就
會被刷到磁盤,所以crash時最多丟30秒左右的數據。
2.????/proc/sys/vm/dirty_background_ratio:如果dirty page的總大小已經超過了10%的可用內存(cat
/proc/meminfo里?MemFree+ Cached - Mapped),則會在后臺啟動pdflush?線程寫盤,但不影響
當前的write(2)操作。增減這個值是最主要的flush策略里調優手段。
3.?????/proc/sys/vm/dirty_ratio:如果wrte(2)的速度太快,比pdflush還快,dirty page?迅速漲到?10%
的總內存(cat /proc/meminfo里的MemTotal),則此時所有應用的寫操作都會被block,各自在自
己的時間片里去執行flush,因為操作系統認為現在已經來不及寫盤了,如果crash會丟太多數據,
要讓大家都冷靜點。這個代價有點大,要盡量避免。在Redis2.8以前,Rewrite AOF就經常導致
這個大面積阻塞,現在已經改為Redis每32Mb先主動flush()一下了。
詳細的文章可以看:http://www.westnet.com/~gsmith/content/linux-pdflush.htm
對于重要數據,應用需要自己觸發flush保證寫盤。
1.????調用fsync()?和?fdatasync()
fsync(fd)將屬于該文件描述符的所有dirty page的寫入請求發送給IO調度層。
fsync()總是同時flush文件內容與文件元數據,?而fdatasync()只flush文件內容與后續操作必須
的文件元數據。元數據含時間戳,大小等,大小可能是后續操作必須,而時間戳就不是必須的。
因為文件的元數據保存在另一個地方,所以fsync()總是觸發兩次IO,性能要差一點。
2.????打開文件時設置O_SYNC,O_DSYNC標志或O_DIRECT標志
O_SYNC、O_DSYNC標志表示每次write后要等到flush完成才返回,效果等同于write()后緊接一個fsync()或fdatasync(),不過按APUE里的測試,因為OS做了優化,性能會比自己調write() + fsync()好一點,但與只是write相比就慢很多了。O_DIRECT標志表示直接IO,完全跳過Page Cache。不過這也放棄了讀文件時的Cache,必須每次讀取磁盤文件。而且要求所有IO請求長度,偏移都必須是底層扇區大小的整數倍。所以使用直接IO的時候一定要在應用層做好Cache。
Kafka的默認機制中,fsync的間隔時間和消息個數都是最大值,所以基本上都是依賴OS層面的flush。
當內存滿了,就需要清理Page Cache,或把應用占的內存swap到文件去。有一個swappiness的參數(/proc/sys/vm/swappiness)決定是swap還是清理page cache,值在0到100之間,設為0表示盡量不要用swap,這也是很多優化指南讓你做的事情,因為默認值居然是60,Linux認為Page Cache更重要。
Page Cache的清理策略是LRU的升級版。如果簡單用LRU,一些新讀出來的但可能只用一次的數據會占滿了LRU的頭端。因此將原來一條LRU隊列拆成了兩條,一條放新的Page,一條放已經訪問過好幾次的Page。Page剛訪問時放在新LRU隊列里,訪問幾輪了才升級到舊LRU隊列(想想JVM Heap的新生代老生代)。清理時就從新LRU隊列的尾端開始清理,直到清理出足夠的內存。
Linux?提供了這樣一個參數min_free_kbytes,用來確定系統開始回收內存的閥值,控制系統的空閑內存。值越高,內核越早開始回收內存,空閑內存越高。
根據清理策略,Apache Kafka里如果消費者太慢,堆積了幾十G的內容,Cache還是會被清理掉的。這時消費者就需要讀盤了。
內核這里又有個動態自適應的預讀策略,每次讀請求會嘗試預讀更多的內容(反正都是一次讀操作)。內核如果發現一個進程一直使用預讀數據,就會增加預讀窗口的大小,否則會關掉預讀窗口。連續讀的文件,明顯適合預讀。
IO調度層主要做兩個事情,合并和排序。
合并是將相同和相鄰扇區(每個512字節)的操作合并成一個,比如現在要讀扇區1,2,3,那可以合并成一個讀扇區1-3的操作。
排序就是將所有操作按扇區方向排成一個隊列,讓磁盤的磁頭可以按順序移動,有效減少了機械硬盤尋址這個最慢最慢的操作。
排序看上去很美,但可能造成嚴重的不公平,比如某個應用在相鄰扇區狂寫盤,其他應用就都干等在那了,pdflush還好等等沒所謂,讀請求都是同步的,耗在那會很慘。所有又有多種算法來解決這個問題,其中內核2.6的默認算法是CFQ(完全公正排隊)。
3.1.3????原理分析結論
1、Kafka使用文件系統來交換消息,性能是否比使用內存來交換消息的系統要低很多?
在Apache Kafka里,消息的讀寫都發生在內存中(Pagecache),真正寫盤的就是那條pdflush內核線程,根本不在Kafka的主流程中,讀操作大多數會命中Pagecache,同時由于預讀機制存在,所以性能非常好,從原理上有保證的。
2、?每個分區一個文件,那么多個分區會有多個文件同時讀寫,是否會極大的降低性能?
1)????????首先,由于Kafka讀寫流程是發生在PageCache中,后臺的flush不在主流程中觸發,所以正常情況下理論上是沒有影響的,除非PageCache占用內存過大,或是釋放導致讀寫消耗Kafka進程的CPU時間。
2)????????再次,文件都是順序讀寫,OS層面有預讀和后寫機制,即使一臺服務器上有多個Partition文件,經過合并和排序后都能獲得很好的性能,不會出現文件多了變成隨機讀寫的情況,但是當達到相當多的數量之后,也會存在一定的影響。
3)????????當PageCache過大,大量觸發磁盤I/O的時候,超過了/proc/sys/vm/dirty_ratio,Flush會占用各個應用自己的CPU時間,會對主流程產生影響,讓主流程變慢。
3.2???性能測試
3.2.1????測試環境
同時啟動消息發送和消費。
指標說明:
TPS
在客戶端側由代碼統計的每秒發送和接收到的消息個數。
wai
系統因為io導致的進程wait。再深一點講就是:這時候系統在做io,導致沒有進程在干活,cpu在執行idle進程空轉,所以說iowait的產生要滿足兩個條件,一是進程在等io,二是等io時沒有進程可運行。
%util
使用iostat測試得到。Percentage of CPU time during which I/O requests ? were issued to the device (bandwidth utilization for the device). Device ? saturation occurs when this value is close to 100%。一秒中有百分之多少的時間用于 I/O 操作,或者說一秒中有多少時間 ? I/O 隊列是非空的。如果 %util 接近 100%,說明產生的I/O請求太多,I/O系統已經滿負荷,該磁盤可能存在瓶頸。
IOPS
該設備每秒的傳輸次數(Indicate the number of ? transfers per second that were issued to the device.)。“一次傳輸”意思是“一次I/O請求”。多個邏輯請求可能會被合并為“一次I/O請求”。“一次傳輸”請求的大小是未知的。IOPS=reads+writes
reads
在用例運行過程中,每秒讀io的個數均值
writes
在用例運行過程中,每秒寫io的個數均值
從流程來看,影響性能的幾個主要點為:客戶端,網絡,服務器端CPU,內存,文件系統(磁盤)。
3.2.2????單硬盤測試
當系統只有一個硬盤的時候,所有的分區文件集中在一個磁盤上,測試數據如下:
從數據來看,cpu,內存,網絡問題都不大。
隨著分區數目的增加,吞吐量會下降。從圖上來看,2000的分區下,生產者和消費者TPS下降幅度已經較大了。1000左右的分區TPS浮動不大。
備注:Consumer的TPS下降的原因,根據JProfile分析,主要是客戶端問題,后面章節有專門分析。
從磁盤的IOPS來看,超過1000之后,會急劇上升,3000之后因為TPS的下降,反而有所下降。
特別注意的是:讀操作一直為0,證明基本上所有的Consumer消息都是從PageCache中獲取的。
接下來,看一下util和wai,超過1000之后,util會急劇上升,到了3000左右,基本滿負荷了。
這個和TPS是能對上的。
根據以上的測試數據,我們可以得到一個初步的結論:
1、?當分區超過了2000之后,util急劇上升,磁盤性能會成為瓶頸,導致Producer TPS下降。
2、?由于Read操作一直為0,證明磁盤對Consumer的影響不大。Consumer的TPS下降是客戶端原因。(后面的章節給出證據)
建議:單臺服務器單硬盤下,分區數量不超過2000,推薦值在1000以下。
3.2.3????多硬盤測試
啟動8個磁盤,1個磁盤跑zookeeper和OS,其他7個磁盤分擔所有的分區文件。
Util和IOPS是7個硬盤加起來的值,實際上每個硬盤的負荷是平均分擔的。
1、可以看到,3000分區下,Producer的TPS很平穩,實際上還要超過單硬盤下單分區的TPS。
證明追加硬盤的方法可以明顯的提高TPS。
2、Consumer TPS,在多硬盤下其實和單硬盤是一致的,超過2000分區之后,一直都是18w左右。證明硬盤并不是Consumer的瓶頸。
磁盤的IOPS和Util是隨分區增加而增加的,但是實際上被7個硬盤平均分擔,每個硬盤的負荷除以7之后很小了。
當分區達到5000之后,TPS會下降,但是磁盤負荷還遠沒有達到瓶頸。
CPU,網絡,內存也同樣沒有問題。
Kafka
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。