Page Cache 的同步
廣義上Cache的同步方式有兩種,即Write Through(寫穿)
和Write back(寫回)
. 從名字上就能看出這兩種方式都是從寫操作的不同處理方式引出的概念(純讀的話就不存在Cache一致性了,不是麼)。對應到Linux的Page Cache
上所謂Write Through
就是指write(2)
操作將數據拷貝到Page Cache
後立即和下層進行同步的寫操作,完成下層的更新後才返回,可以理解爲寫穿透page cache直抵磁盤。而Write back
正好相反,指的是寫完Page Cache
就可以返回了,可以理解爲寫到page cache就返回了。Page Cache
到下層的更新操作是異步進行的。
Linux下Buffered IO
默認使用的是Write back
機制,即文件操作的寫只寫到Page Cache
就返回,之後Page Cache
到磁盤的更新操作是異步進行的。Page Cache
中被修改的內存頁稱之爲髒頁(Dirty Page),髒頁在特定的時候被一個叫做pdflush(Page Dirty Flush)
的內核線程寫入磁盤,寫入的時機和條件如下:
- 當空閒內存低於一個特定的閾值時,內核必須將髒頁寫回磁盤,以便釋放內存。
- 當髒頁在內存中駐留時間超過一個特定的閾值時,內核必須將超時的髒頁寫回磁盤。
- 用戶進程調用
sync(2)
、fsync(2)
、fdatasync(2)
系統調用時,內核會執行相應的寫回操作。
刷新策略由以下幾個參數決定(數值單位均爲1/100秒):
# flush每隔5秒執行一次
默認是寫回方式,如果想指定某個文件是寫穿方式呢?即寫操作的可靠性壓倒效率的時候,能否做到呢?當然能,除了之前提到的fsync(2)
之類的系統調用外,在open(2)
打開文件時,傳入O_SYNC
這個flag即可實現。這裏給篇參考文章[5],不再贅述(更好的選擇是去讀TLPI相關章節)。
文件讀寫遭遇斷電時,數據還安全嗎?相信你有自己的答案了。使用O_SYNC
或者fsync(2)
刷新文件就能保證安全嗎?現代磁盤一般都內置了緩存,代碼層面上也只能講數據刷新到磁盤的緩存了。當數據已經進入到磁盤的高速緩存時斷電了會怎麼樣?這個恐怕不能一概而論了。不過可以使用hdparm -W0
命令關掉這個緩存,相應的,磁盤性能必然會降低。
文件操作與鎖
當多個進程/線程對同一個文件發生寫操作的時候會發生什麼?如果寫的是文件的同一個位置呢?這個問題討論起來有點複雜了。首先write(2)
調用不是原子操作,不要被TLPI的中文版5.2章節的第一句話誤導了(英文版也是有歧義的,作者在這裏給出了勘誤信息)。當多個write(2)
操作對一個文件的同一部分發起寫操作的時候,情況實際上和多個線程訪問共享的變量沒有什麼區別。按照不同的邏輯執行流,會有很多種可能的結果。也許大多數情況下符合預期,但是本質上這樣的代碼是不可靠的。
特別的,文件操作中有兩個操作是內核保證原子的。分別是open(2)
調用的O_CREAT
和O_APPEND
這兩個flag屬性。前者是文件不存在就創建,後者是每次寫文件時把文件遊標移動到文件最後追加寫(NFS等文件系統不保證這個flag)。有意思的問題來了,以O_APPEND
方式打開的文件write(2)
操作是不是原子的?文件遊標的移動和調用寫操作是原子的,那寫操作本身會不會發生改變呢?有的開源軟件比如apache寫日誌就是這樣寫的,這是可靠安全的嗎?坦白講我也不清楚,有人說Then O_APPEND is atomic and write-in-full for all reasonably-sized> writes to regular files.
但是我也沒有找到很權威的說法。這裏給出一個郵件列表上的討論,可以參考下[6]。今天先放過去,後面有時間的話專門研究下這個問題。如果你能給出很明確的說法和證明,還望不吝賜教。
Linux下的文件鎖有兩種,分別是flock(2)
的方式和fcntl(2)
的方式,前者源於BSD,後者源於System V,各有限制和應用場景。老規矩,TLPI上講的很清楚的這裏不贅述。我個人是沒有用過文件鎖的,系統設計的時候一般會避免多個執行流寫一個文件的情況,或者在代碼邏輯上以mutex加鎖,而不是直接加鎖文件本身。數據庫場景下這樣的操作可能會多一些(這個純屬臆測),這就不是我瞭解的範疇了。
磁盤的性能測試
在具體的機器上跑服務程序,如果涉及大量IO的話,首先要對機器本身的磁盤性能有明確的瞭解,包括不限於IOPS、IO Depth等等。這些數據不僅能指導系統設計,也能幫助資源規劃以及定位系統瓶頸。比如我們知道機械磁盤的連續讀寫性能一般不會超過120M/s,而普通的SSD磁盤隨意就能超過機械盤幾倍(商用SSD的連續讀寫速率達到2G+/s不是什麼新鮮事)。另外由於磁盤的工作原理不同,機械磁盤需要旋轉來尋找數據存放的磁道,所以其隨機存取的效率受到了“尋道時間”的嚴重影響,遠遠小於連續存取的效率;而SSD磁盤讀寫任意扇區可以認爲是相同的時間,隨機存取的性能遠遠超過機械盤。所以呢,在機械磁盤作爲底層存儲時,如果一個線程寫文件很慢的話,多個線程分別去寫這個文件的各個部分能否加速呢?不見得吧?如果這個文件很大,各個部分的尋道時間帶來極大的時間消耗的話,效率就很低了(先不考慮Page Cache
)。SSD呢?可以明確,設計合理的話,SSD多線程讀寫文件的效率會高於單線程。當前的SSD盤很多都以高併發的讀取爲賣點的,一個線程壓根就喂不飽一塊SSD盤。一般SSD的IO Depth都在32甚至更高,使用32或者64個線程才能跑滿一個SSD磁盤的帶寬(同步IO情況下)。
具體的SSD原理不在本文計劃內,這裏給出一篇詳細的參考文章[7]。有時候一些文章中所謂的STAT磁盤一般說的就是機械盤(雖然STAT本身只是一個總線接口)。接口會影響存儲設備的最大速率,基本上是STAT -> PCI-E -> NVMe
的發展路徑,具體請自行Google瞭解。
具體的設備一般使用fio
工具[8]來測試相關磁盤的讀寫性能。fio的介紹和使用教程有很多[9],不再贅述。這裏不想貼性能數據的原因是存儲介質的發展實在太快了,一方面不想貼某些很快就過時的數據以免讓初學者留下不恰當的第一印象,另一方面也希望讀寫自己實踐下fio命令。
前文提到存儲介質的原理會影響程序設計,我想稍微的解釋下。這裏說的“影響”不是說具體的讀寫能到某個速率,程序中就依賴這個數值,換個工作環境就性能大幅度降低(當然,爲專門的機型做過優化的結果很可能有這個副作用)。而是說根據存儲介質的特性,程序的設計起碼要遵循某個設計套路。舉個簡單的例子,SATA機械盤的隨機存取很慢,那系統設計時,就要儘可能的避免隨機的IO出現,儘可能的轉換成連續的文件存取來加速運行。比如Google的LevelDB就是轉換隨機的Key-Value寫入爲Binlog(連續文件寫入)+ 內存插入MemTable(內存隨機讀寫可以認爲是O(1)的性能),之後批量dump到磁盤(連續文件寫入)。這種LSM-Tree
的設計便是合理的利用了存儲介質的特性,做到了最大化的性能利用(磁盤換成SSD也依舊能有很好的運行效率)。