文章收錄地址:Java-Bang
專注于系統(tǒng)架構媳荒、高可用尊蚁、高性能、高并發(fā)類技術分享
Kafka 依賴于文件系統(tǒng)(更底層地來說就是磁盤)來存儲和緩存消息吃嘿。在我們的印象中祠乃,對于各個存儲介質(zhì)的速度認知大體同下圖所示的相同,層級越高代表速度越快兑燥。很顯然亮瓷,磁盤處于一個比較尷尬的位置,這不禁讓我們懷疑 Kafka 采用這種持久化形式能否提供有競爭力的性能降瞳。在傳統(tǒng)的消息中間件 RabbitMQ 中嘱支,就使用內(nèi)存作為默認的存儲介質(zhì),而磁盤作為備選介質(zhì)力崇,以此實現(xiàn)高吞吐和低延遲的特性斗塘。然而,事實上磁盤可以比我們預想的要快亮靴,也可能比我們預想的要慢馍盟,這完全取決于我們?nèi)绾问褂盟?br>
有關測試結果表明,一個由6塊 7200r/min 的 RAID-5 陣列組成的磁盤簇的線性(順序)寫入速度可以達到 600MB/s茧吊,而隨機寫入速度只有 100KB/s贞岭,兩者性能相差6000倍。操作系統(tǒng)可以針對線性讀寫做深層次的優(yōu)化搓侄,比如預讀(read-ahead瞄桨,提前將一個比較大的磁盤塊讀入內(nèi)存)和后寫(write-behind,將很多小的邏輯寫操作合并起來組成一個大的物理寫操作)技術讶踪。順序?qū)懕P的速度不僅比隨機寫盤的速度快芯侥,而且也比隨機寫內(nèi)存的速度快,如下圖所示。
頁緩存的魅力
Kafka 在設計時采用了文件追加的方式來寫入消息柱查,即只能在日志文件的尾部追加新的消息廓俭,并且也不允許修改已寫入的消息,這種方式屬于典型的順序?qū)懕P的操作唉工,所以就算Kafka使用磁盤作為存儲介質(zhì)研乒,它所能承載的吞吐量也不容小覷。但這并不是讓 Kafka 在性能上具備足夠競爭力的唯一因素淋硝,我們不妨繼續(xù)分析雹熬。
頁緩存是操作系統(tǒng)實現(xiàn)的一種主要的磁盤緩存,以此用來減少對磁盤 I/O 的操作谣膳。具體來說竿报,就是把磁盤中的數(shù)據(jù)緩存到內(nèi)存中,把對磁盤的訪問變?yōu)閷?nèi)存的訪問参歹。為了彌補性能上的差異仰楚,現(xiàn)代操作系統(tǒng)越來越“激進地”將內(nèi)存作為磁盤緩存,甚至會非常樂意將所有可用的內(nèi)存用作磁盤緩存犬庇,這樣當內(nèi)存回收時也幾乎沒有性能損失,所有對于磁盤的讀寫也將經(jīng)由統(tǒng)一的緩存侨嘀。
當一個進程準備讀取磁盤上的文件內(nèi)容時臭挽,操作系統(tǒng)會先查看待讀取的數(shù)據(jù)所在的頁(page)是否在頁緩存(pagecache)中,如果存在(命中)則直接返回數(shù)據(jù)咬腕,從而避免了對物理磁盤的 I/O 操作欢峰;如果沒有命中,則操作系統(tǒng)會向磁盤發(fā)起讀取請求并將讀取的數(shù)據(jù)頁存入頁緩存涨共,之后再將數(shù)據(jù)返回給進程纽帖。
同樣,如果一個進程需要將數(shù)據(jù)寫入磁盤举反,那么操作系統(tǒng)也會檢測數(shù)據(jù)對應的頁是否在頁緩存中懊直,如果不存在,則會先在頁緩存中添加相應的頁火鼻,最后將數(shù)據(jù)寫入對應的頁室囊。被修改過后的頁也就變成了臟頁,操作系統(tǒng)會在合適的時間把臟頁中的數(shù)據(jù)寫入磁盤魁索,以保持數(shù)據(jù)的一致性融撞。
Linux 操作系統(tǒng)中的 vm.dirty_background_ratio 參數(shù)用來指定當臟頁數(shù)量達到系統(tǒng)內(nèi)存的百分之多少之后就會觸發(fā) pdflush/flush/kdmflush 等后臺回寫進程的運行來處理臟頁,一般設置為小于10的值即可粗蔚,但不建議設置為0尝偎。與這個參數(shù)對應的還有一個 vm.dirty_ratio 參數(shù),它用來指定當臟頁數(shù)量達到系統(tǒng)內(nèi)存的百分之多少之后就不得不開始對臟頁進行處理鹏控,在此過程中致扯,新的 I/O 請求會被阻擋直至所有臟頁被沖刷到磁盤中趁窃。對臟頁有興趣的讀者還可以自行查閱 vm.dirty_expire_centisecs、vm.dirty_writeback.centisecs 等參數(shù)的使用說明急前。
對一個進程而言醒陆,它會在進程內(nèi)部緩存處理所需的數(shù)據(jù),然而這些數(shù)據(jù)有可能還緩存在操作系統(tǒng)的頁緩存中裆针,因此同一份數(shù)據(jù)有可能被緩存了兩次刨摩。并且,除非使用 Direct I/O 的方式世吨,否則頁緩存很難被禁止澡刹。此外,用過 Java 的人一般都知道兩點事實:對象的內(nèi)存開銷非常大耘婚,通常會是真實數(shù)據(jù)大小的幾倍甚至更多罢浇,空間使用率低下;Java 的垃圾回收會隨著堆內(nèi)數(shù)據(jù)的增多而變得越來越慢沐祷∪卤眨基于這些因素,使用文件系統(tǒng)并依賴于頁緩存的做法明顯要優(yōu)于維護一個進程內(nèi)緩存或其他結構赖临,至少我們可以省去了一份進程內(nèi)部的緩存消耗胞锰,同時還可以通過結構緊湊的字節(jié)碼來替代使用對象的方式以節(jié)省更多的空間。如此兢榨,我們可以在32GB的機器上使用28GB至30GB的內(nèi)存而不用擔心 GC 所帶來的性能問題嗅榕。
此外,即使 Kafka 服務重啟吵聪,頁緩存還是會保持有效凌那,然而進程內(nèi)的緩存卻需要重建。這樣也極大地簡化了代碼邏輯吟逝,因為維護頁緩存和文件之間的一致性交由操作系統(tǒng)來負責帽蝶,這樣會比進程內(nèi)維護更加安全有效。
Kafka 中大量使用了頁緩存澎办,這是 Kafka 實現(xiàn)高吞吐的重要因素之一嘲碱。雖然消息都是先被寫入頁緩存,然后由操作系統(tǒng)負責具體的刷盤任務的局蚀,但在 Kafka 中同樣提供了同步刷盤及間斷性強制刷盤(fsync)的功能麦锯,這些功能可以通過 log.flush.interval.messages、log.flush.interval.ms 等參數(shù)來控制琅绅。
同步刷盤可以提高消息的可靠性扶欣,防止由于機器掉電等異常造成處于頁緩存而沒有及時寫入磁盤的消息丟失。不過筆者并不建議這么做,刷盤任務就應交由操作系統(tǒng)去調(diào)配料祠,消息的可靠性應該由多副本機制來保障骆捧,而不是由同步刷盤這種嚴重影響性能的行為來保障。
Linux 系統(tǒng)會使用磁盤的一部分作為 swap 分區(qū)髓绽,這樣可以進行進程的調(diào)度:把當前非活躍的進程調(diào)入 swap 分區(qū)敛苇,以此把內(nèi)存空出來讓給活躍的進程。對大量使用系統(tǒng)頁緩存的 Kafka 而言顺呕,應當盡量避免這種內(nèi)存的交換枫攀,否則會對它各方面的性能產(chǎn)生很大的負面影響。
我們可以通過修改 vm.swappiness 參數(shù)(Linux 系統(tǒng)參數(shù))來進行調(diào)節(jié)株茶。vm.swappiness 參數(shù)的上限為100来涨,它表示積極地使用 swap 分區(qū),并把內(nèi)存上的數(shù)據(jù)及時地搬運到 swap 分區(qū)中启盛;vm.swappiness 參數(shù)的下限為0蹦掐,表示在任何情況下都不要發(fā)生交換(vm.swappiness = 0 的含義在不同版本的 Linux 內(nèi)核中不太相同,這里采用的是變更后的最新解釋)僵闯,這樣一來卧抗,當內(nèi)存耗盡時會根據(jù)一定的規(guī)則突然中止某些進程。筆者建議將這個參數(shù)的值設置為1棍厂,這樣保留了 swap 的機制而又最大限度地限制了它對 Kafka 性能的影響颗味。