從Apache Kafka 重溫文件高效讀寫

me87re 9年前發布 | 30K 次閱讀 消息系統 Apache Kafka

0. Overview

卡夫卡說:不要害怕文件系統

它就那么簡簡單單地用順序寫的普通文件,借力于Linux內核的Page Cache,不(顯式)用內存,勝用內存,完全沒有別家那樣要同時維護內存中數據、持久化數據的煩惱——只要內存足夠,生產者與消費者的速度也沒有差上太多,讀寫便都發生在Page Cache中,完全沒有同步的磁盤訪問。

整個IO過程,從上到下分成文件系統層(VFS+ ext3)、 Page Cache 層、通用數據塊層、 IO調度層、塊設備驅動層。 這里借著Apache Kafka的由頭,將Page Cache層與IO調度層重溫一遍,記一篇針對Linux kernel 2.6的科普文。

 

1. Page Cache

1.1 讀寫空中接力

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(可以放心使用60G內存了),而且即使Kafka重啟了,Page Cache還依然在。

 

1.2 后臺異步flush的策略

這是大家最需要關心的,因為不能及時flush的話,OS crash(不是應用crash) 可能引起數據丟失,Page Cache瞬間從朋友變魔鬼。

當然,Kafka不怕丟,因為它的持久性是靠replicate保證,重啟后會從原來的replicate follower中拉缺失的數據。

內核線程pdflush負責將有dirty標記的頁面,發送給IO調度層。內核會為每個磁盤起一條pdflush線程,每5秒(/proc/sys/vm/dirty_writeback_centisecs)喚醒一次,根據下面三個參數來決定行為:

1. 如果page dirty的時間超過了30秒(/proc/sys/vm/dirty_expire_centiseconds,單位是百分之一秒),就會被刷到磁盤,所以crash時最多丟30秒左右的數據。

2. 如果dirty page的總大小已經超過了10%(/proc/sys/vm/dirty_background_ratio)的可用內存(cat /proc/meminfo里 MemFree+ Cached - Mapped),則會在后臺啟動pdflush 線程寫盤,但不影響當前的write(2)操作。增減這個值是最主要的flush策略里調優手段。

3. 如果wrte(2)的速度太快,比pdflush還快,dirty page 迅速漲到 20%(/proc/sys/vm/dirty_ratio)的總內存(cat /proc/meminfo里的MemTotal),則此時所有應用的寫操作都會被block,各自在自己的時間片里去執行flush,因為操作系統認為現在已經來不及寫盤了,如果crash會丟太多數據,要讓大家都冷靜點。這個代價有點大,要盡量避免。在Redis2.8以前,Rewrite AOF就經常導致這個大面積阻塞,現在已經改為Redis每32Mb先主動flush()一下了。

詳細的文章可以看: The Linux Page Cache and pdflush
 

1.3 主動flush的方式

對于重要數據,應用需要自己觸發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。
 

1.4 Page Cache的清理策略

當內存滿了,就需要清理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隊列的尾端開始清理,直到清理出足夠的內存。

 

1.5 預讀策略

根據清理策略,Apache Kafka里如果消費者太慢,堆積了幾十G的內容,Cache還是會被清理掉的。這時消費者就需要讀盤了。

內核這里又有個動態自適應的預讀策略,每次讀請求會嘗試預讀更多的內容(反正都是一次讀操作)。內核如果發現一個進程一直使用預讀數據,就會增加預讀窗口的大小(最小16K,最大128K),否則會關掉預讀窗口。連續讀的文件,明顯適合預讀。

 

2. IO調度層

如果所有讀寫請求都直接發給硬盤,對傳統硬盤來說太殘忍了。IO調度層主要做兩個事情,合并和排序。 合并是將相同和相鄰扇區(每個512字節)的操作合并成一個,比如我現在要讀扇區1,2,3,那可以合并成一個讀扇區1-3的操作。排序就是將所有操作按扇區方向排成一個隊列,讓磁盤的磁頭可以按順序移動,有效減少了機械硬盤尋址這個最慢最慢的操作。

排序看上去很美,但可能造成嚴重的不公平,比如某個應用在相鄰扇區狂寫盤,其他應用就都干等在那了,pdflush還好等等沒所謂,讀請求都是同步的,耗在那會很慘。

所有又有多種算法來解決這個問題,其中內核2.6的默認算法是CFQ(完全公正排隊),把總的排序隊列拆分成每個發起讀寫的進程自己有一條排序隊列,然后以時間片輪轉調度每個隊列,輪流從每個進程的隊列里拿出若干個請求來執行(默認是4)。

在Apache Kafka里,消息的讀寫都發生在內存中,真正寫盤的就是那條pdflush內核線程,因為都是順序寫,即使一臺服務器上有多個Partition文件,經過合并和排序后都能獲得很好的性能,或者說,Partition文件的個數并不影響性能,不會出現文件多了變成隨機讀寫的情況。

如果是SSD硬盤,沒有尋址的花銷,排序好像就沒必要了,但合并的幫助依然良多,所以還有另一種只合并不排序的NOOP算法可供選擇。

題外話

另外,硬盤上還有一塊幾十M的緩存,硬盤規格上的外部傳輸速率(總線到緩存)與內部傳輸速率(緩存到磁盤)的區別就在此......IO調度層以為已經寫盤了,其實可能依然沒寫成,斷電的話靠硬盤上的電池或大電容保命......

</div> 來自:http://calvin1978.blogcn.com/articles/kafkaio.html

 本文由用戶 me87re 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!