1、IT系統的IO結構圖

2、應用程式層IO
應用層程式是計算機系統内主動發起IO請求的大戶,但是要知道,計算機内不止有應用程式可以向底層儲存設備主動發起IO請求,其他的,比如檔案系統自身、卷管理層自身、擴充卡驅動層自身等,都可以主動發起IO。當然,隻有應用程式發起的IO才可以修改使用者實體資料内容,而其他角色發起的IO一般隻是對資料進行移動、重分布、校驗、壓縮、加密等動作,并不會修改使用者層面的實際資料内容。
應用程式在讀寫資料的時候一般是直接調用作業系統所提供的檔案系統API來完成檔案資料的讀寫等操作,有的應用程式可以直接調用卷管理層或者擴充卡驅動層API進而直接操控底層的卷或者LUN,比如一些資料庫程式,他們直接操控卷而不需要使用檔案系統提供的功能,它們自己來管理資料在底層卷上的分布。
每個應用程式都會有自己的Buffer用來存取有待處理的資料。 應用程式向檔案系統請求讀資料之後,檔案系統首先将對應的資料從底層卷或磁盤讀入檔案系統自身的Buffer,然後再将資料複制到對應程式的Buffer中。應用程式也可以選擇不适用系統核心緩存,這時FS将IO請求透明的翻譯并轉發給底層處理,傳回的資料将直接由OS放到應用程式Buffer。當應用程式向檔案系統請求寫入資料時,檔案系統會先将應用程式Buffer中對應的資料複制到檔案系統Buffer中,然後在适當的時刻将所有FS Buffer内的
Dirty Page寫入硬碟。同樣如果不使用系統核心緩存,則寫入資料經過FS檔案一塊位址翻譯後直接由OS送出給FS下層處理。檔案系統的動作是可控的,稍後介紹。
同步IO與異步IO
接下來我們來看IO的概念,同步IO和異步IO這個比較簡單;同步的話就是應用程式發出IO請求,然後等待作業系統下層傳回給應用程式資料,然後繼續執行;而所謂的異步IO,就是應用程式發出IO請求後,不必等待傳回資料,直接進行下一步的代碼運作。
IO請求發送到OS核心之後到核心将IO請求對應的資料讀取或者寫入完成這段時間會貢獻為OS的IOWait(即為IO等待時間),IOWait名額一旦升高到高于60%左右的百分比,那麼就需要考慮後端存儲系統所提供的性能是否已經不能滿足應用需求了。
同步IO請求如果不加任何參數的話,一般是操作提供的預設調用方式,也是一般的應用程式首選的IO調用方式。一般情況下,如果遇到資料鍊路速度或者存儲媒體速度很慢,比如通過低速網絡進行IO(Ethernet上的NFS、CIFS等),或者使用低速Flash晶片等,使用異步IO方式是一個很好的選擇。第一是因為異步IO調用可以接連發出多個IO請求,一通發向目标,目标在接收到這些IO請求之後可以一并處理,增加速率。比如SATA硬碟的NCQ功能,
要實作與單線程異步IO類似的結果,可以采用另一種方法,即生成多個線程或程序,每個線程或程序各自進行同步IO調用。然而,維護多個線程或程序需要耗費更多的系統資源,而采用單線程異步IO調用雖然需要複雜的代碼來實作,其相比于多線程同步IO的方式來說來說仍然更加高效。
在Windows系統中,如果應用程式在打開檔案進行讀寫操作時未指定特殊參數,則檔案系統預設是使用自身緩存來加速資料讀寫操作的。并且,這種情況下異步調用多數情況下會自動變為同步調用,其結果就是IO發出後作業系統不會傳回任何消息直到IO完成為止,這段時間内線程處于挂起狀态,為什麼會這樣?有三個原因:
1、預讀和Write Back:檔案系統緩存的機制可以增加IO讀操作的命中率,尤其是小塊連續IO操作,命中率幾乎是百分之百。在這種情況下,每個讀IO操作的響應時間會在微秒級别,是以OS會自動将異步調用轉變為同步調用以便節約異步IO所帶來的系統開銷。
2、盡量保持IO順序:異步模式下,應用程式可以在機關時間内發出若幹IO請求而等待OS批量傳回結果。OS對于異步IO結果的傳回順序可能與IO請求所發出的順序不同,在不使用檔案系統緩存的情況下,OS不能緩存底層傳回的IO結果以便重新對結果排序,隻能夠按照底層傳回的實際順序來将資料傳回給應用程式,而底層裝置比如磁盤在執行IO的時候不一定嚴格按照順序執行,因為檔案系統之下還有多處緩存,IO在這裡可能會被重排,或者有些IO命中了,而有些還需要到存儲媒體中讀取,命中的IO并不一定是先被發送的IO。而應用程式在打開檔案的時候如果沒有給出特殊參數,預設是使用檔案系統緩存的,此時系統核心緩存(也就是檔案系統的緩存)便會嚴格保持IO結果順序的傳回給應用程式,異步調用變為同步模式。
3、系統核心緩存機制和處理容量決定:檔案系統一般使用Memory Mapping的方式來進行IO操作,将映射到緩存中一定數量的page,目标檔案當需要的資料沒有位于對應的page中,便會産生Page Fault,需要将資料從底層媒體讀入記憶體,這個過程OS自身會強行使用同步IO模式向下層存儲發起IO。而OS内 存在一個專門負責處理page Fault情況的Worker線程池,當多個應用程式機關時間内使用異步IO向OS發送大量請求時,一開始OS還可以應付,接收一批IO,然後對其進行異步處理,随着IO大量到來,系統核心緩存命中率逐漸降低,越來越多的Page
Fault将會發生,諸多的Worker線程将會處理Page Fault,線程池也會很快耗盡,此時OS隻能将随後的IO變為同步操作,不再給其回應直到有Worker 線程空閑為止。
導緻Windows将異步強行轉變為同步的原因不隻有系統核心緩存的原因,其他一些原因也可以導緻其發生。在Windows系統中,通路NTFS自身壓縮檔案、通路NTFS自身加密檔案、任何擴充檔案長度的操作都會導緻異步變同步。要實作真正的異步IO效果,最好在打開檔案時給出相關參數,不使用核心檔案系統緩存。不使用核心檔案系統緩存的IO方式一般稱為Direct IO或者DIO,異步IO模式又被稱為AIO,即Asynchronous IO.
在使用iSCSI/NAS等基于TCP/IP的存儲協定時,并且是随機IO環境下,利用Bypass檔案系統緩存的純異步IO模式會大大減少網絡傳輸的開銷,因為如果使用檔案系統緩存,則OS強制變為同步IO模式,IO一個一個的執行,是以每個IO請求就需要被封裝到單獨的TCP包和以太網幀中,在IO量巨大的時候,這種浪費是非常驚人的。舉個例子純異步IO模式下抓包資料檔案隻有120KB,而如果使用FS緩存之後,同樣的IO清單,抓包資料變為490KB,開銷是前者的4倍,這是個絕對不容忽視的地方。
不管是Windows還是Linux都是:使用者程式——OS核心——儲存設備 這種架構,使用者程式和OS核心之間存在一套IO接口。同樣,OS核心與儲存設備之間一樣存在着IO接口,同樣也有同步異步之分。在Windows系統下,OS核心——儲存設備 和 使用者程序——OS核心 這兩個連結之間的IO行為對應,即上層為同步,則底層也同步。上層為異步底層也是異步(不使用FS緩存時;如果使用,有可能異步的轉變為同步的)。 而Linux系統下,上層IO與底層IO不一定是對應關系,
NFS下的緩存和IO機制
同為NAS網絡檔案通路協定,NFS不管在資料包結構還是在互動邏輯上相比CIFS要簡化許多,但是簡化的結果就是不如CIFS強大,CIFS之是以複雜是因為CIFS協定中幾乎可以透傳本地檔案系統的所有參數和屬性,而NFS攜帶的資訊很有限。簡化同樣也帶來了高效,執行類似的操作,NFS互動的資料包在單個包的大小上和整體發包數量都相對CIFS有很大的降低。
與CIFS不同的是,NFS提供諸多更改的參數來控制作業系統核心底層IO行為。
Linux下的NFS比Windows下的CIFS優異,表現在,前者有明顯預讀能力,隻有在特定情況下遊讀懲罰,寫懲罰一點沒。正因為NFS的緩存如此高效,是以在Linux2.6核心中,在mount時并沒有提供Direct IO的選項(核心編譯時被禁止),但是單個程式在Open()的時候依然可以指定O_DIRECT 參數來對單個檔案使用DIO模式。與Windows下CIFS實作方式相同,如果選擇使用了DIO模式,那麼NFS層就會完全透傳程式層的IO請求。
多程序通路下緩存一緻性的解決辦法:
在緩存一緻性方面,NFS相比CIFS來講要差一些。CIFS使用Oplook機制來充分保證檔案的時序一緻性;而對于NFS,除了使用位元組鎖或者幹脆使用DIO模式之外,沒有其他方法能夠在使用緩存的情況下嚴格保證時序一緻性,NFS隻是提供了盡力而為的一緻性保證,而且這種保證全部由用戶端自行實作,NFS服務端在這個過程中不作為,下面我們看一下NFS提供的盡力而為的一緻性保證機制。
比如,有兩個用戶端共同通路同一個NFS服務端上的檔案,而且這兩個用戶端都是用本地NFS緩存,那麼用戶端A首先單開了檔案并且做了預讀,且A本地緩存内還有被緩存的寫資料,此時用戶端B也打開了這個檔案,并且做了預讀,如果被讀入的資料部分恰好是A被緩存的尚未寫入的部分,那麼此時就發生了時序不一緻。而這種情況在CIFS下是不會發生的,因為B打開時,服務端會強制讓A來将自己的寫入緩存Flush,然後才允許B打開,此時B讀入的就是最新的資料。
再回到NFS來,B上某程序打開了這個檔案之後,核心會将檔案的屬性緩存在本地,包括通路時間、建立時間、修改時間、檔案長度等資訊,任何需要讀取檔案資料的操作,都會Cache Hit直到這個Attribute Cache(ac)到達失效時間為止,如果ac達到了失效時間,那麼核心NFS層會向服務端發起一個GETATTR請求來重新取回檔案最新的屬性資訊并緩存在本地,ac失效計時器被置0,重新開始計時,往複執行這個過程,在ac緩存未逾時之前,用戶端不會像服務端發起GETATTR請求,除非收到了某個程序的Open()請求。其他諸如stat指令等讀取檔案屬性的操作,不會觸發GETATTR。
任何時刻,任何針對NFS檔案的Open()操作,核心均會強制觸發一個GETATTR請求被發送至服務端以便取回最新的屬性資料。這樣做是合理的,因為對于Open()操作來說,核心必須提供給這個程序最新的檔案資料,是以必須檢視最新屬性以與ac緩存中的副本對比。如果新取回的屬性資訊中mtime相對于本地緩存的資訊沒有變化,則核心會擅自替代NFS服務端來響應程式的Open(),并且随後程式發起的讀操作也都首先去碰緩存命中,不命中的話再将請求發給服務端,這一點類似于CIFS下的Batch Oplock;但是如果新取回的資料中對應的mtime比緩存副本晚,那麼就證明有其他用戶端的程序修改了這個檔案,也就意味着本地的緩存不能展現目前最新的檔案資料,全部廢棄,是以此時,核心NFS将會将這個Open()請求透傳到伺服器端,随後發生的讀寫資料的過程依舊先Cache
Hit(由于之前緩存廢棄,是以第一次請求一定是不命中的),未命中則從服務端讀取,随着緩存不斷被填充以最新讀入的資料,命中率越來越高,而且直到下次出現同樣的過程之前這些緩存的檔案屬性副本很資料副本不會廢棄。
3、檔案系統層IO
IO離開應用層之後,經由OS相關操作被下到了檔案系統層進行處理,檔案系統最大的任務就是負責在邏輯檔案與底層卷或者磁盤之間做映射,并且維護和優化這些映射資訊。檔案系統還要向上層提供檔案IO通路的API接口,比如打開、讀、寫、屬性修改、裁剪、擴充、鎖等檔案操作。另外,還需要維護緩存,包括預讀、Write Back、Write Through、Flush等操作;還需要維護資料一緻性,比如Log、FSCK等機制;還需要維護檔案權限、Quota等。
可以把一個檔案系統分為上部、中部、下部三個部分。通路接口屬于上部;緩存管理、檔案管理等屬于中部;檔案映射、一緻性保護、底層存儲卷适配等屬于下部。下面我們分别來看看各個部分的功能:
1、檔案系統上部:檔案系統對使用者的表示層屬于上部,比如Linux下的表示法“/root/a.txt”、"/dev/sda1"、“/mnt/nfs”或者Windows下的表示法“D:\a.txt”。。。檔案系統表示層給使用者提供了一種簡潔直覺的檔案目錄,使用者無需關心路徑對應的具體實體處于底層的哪個位置,處于網絡的另一端,還是磁盤的某個磁道扇區。這種FS最頂層的抽象稱為Vitrual File
System(VFS)
檔案系統通路接口層也位于上部。由于接口層直接接受上層IO,而IO又有14種,檔案系統默默接受着這14中IO,包括什麼随機、連續,大塊,小塊什麼的讀。檔案系統一般會自己智能的、自适應的選擇是否需要進行預讀。以加快處理速度。
2、檔案系統中部:緩存位于檔案系統中部。預讀和Write Back是檔案系統的最基本功能,可以參考上文中的示例來了解檔案系統預讀機制。緩存預讀對不通IO類型的優化效果也是不通的。
對于應用程式的寫IO操作,檔案系統使用Write Back模式提高寫IO的響應速度。這種模式下,應用程式的寫IO資料會在被複制到系統核心緩存之後而被通告為完成,而此時FS可能尚未将資料寫入磁盤,是以此時如果系統當機,那麼這塊資料将會丢失,對應的應用程式可能并不知道資料已經丢失進而造成錯誤的邏輯,這種模式在關鍵領域的應用中是要絕對的杜絕的。比如NTFS檔案系統提供了一個參數:FILE_FLAG_WRITE_THROUGH,就是給出這個參數之後,在寫資料的時候,IO會先被複制到系統核心緩存,然後立即寫入磁盤。最後向應用程式傳回完成信号。注意這個參數與FILE_FLAG_NO_BUFFERING不同,後者表示不使用系統核心緩存了,而前者依然使用,隻不過是全部寫到磁盤後才傳回成功信号。
檔案系統還使用另外一種IO優化機制,叫做IO Combination。假設T1時刻有某個IO目标位址為LBA0--1023,被FS收到後暫存于IO Queue中;T2時刻,FS尚未處理這個IO,正好這個時候又有一個與前一個IO類型(讀/寫)的IO被收到,目标位址段位LBA1024--2047。FS将将這個IO追加到IO Queue末尾。T3時刻,FS準備處理Queue中的IO,FS會掃描Queue中一定數量的IO位址,此時FS發現這兩個IO的位址是相鄰的,并且都是讀或者寫類型,則FS會将這兩個IO合并為一個目标位址為LBA0--2047的IO。并且向底層存存儲發起這個IO。待資料傳回之後,FS在将這個大IO的資料按照位址段拆分成兩個IO結果并且分别傳回給請求者。這樣做的目的顯而易見,節約後端IO資源,增加IOPS和帶寬吞吐量。
3、檔案系統下部:檔案系統下部包括:檔案——塊 的映射、Flush機制、日志記錄、FSCK以及與底層卷管理接口等相關操作。
位于FS下部的一個重要的機制是檔案系統的Flush機制。在WB(write back)模式下,FS會暫存寫IO實體資料與檔案系統的Metadata。FS當然不會永久地暫存下去而不寫入磁盤。檔案系統會在适當的條件下将暫存的寫IO資料寫入磁盤,這個過程叫Flush。觸發Flush的條件很多,時間點、應用觸發、FS自身為實作某些功能(如快照)等等等都可以觸發Flush。
檔案系統是系統IO路徑的之歌重要角色,檔案在系統底層存儲卷或者磁盤上的分布算法是重中之重,傳統的檔案系統隻是将底層的卷當做一個連續的扇區空間,并不感覺這個連續空間的實體承載裝置的類型或者數量等,是以也就不知道自己的不同類型的IO行為會給性能帶來身影響。如果我們再FS層對底層的RAID類型和磁盤類型等各種因素做出分析和判斷,然後制定對應的政策,讓檔案能夠按照預期的效果有針對性地在RAID組内進行分布。這樣就會大大提高系統讀寫性能。比如,在格式化檔案系統時,或者在程式調用時,給出顯式參數:盡量保持每個檔案存放在一個屋裡硬碟中,那麼當建立一個檔案的時候,檔案系統便會根據底層RAID的Stripe邊界來計算将哪些扇區位址段配置設定給這個檔案,進而讓其實體地隻分布到一個此磁盤中,這種檔案分布方式在需要并行通路大齡檔案時是非常有意義的,因為底層RAID的盲并發度很低,如果在FS層面手動地将每個檔案隻分布一個磁盤上,那麼N個磁盤組成的TAID組理論上就可以并發N個針對檔案的讀操作,寫操作并發值仍相對很低,但是至少可以保證為理論最大值。
總之、檔案系統必須與底層完美配合才能獲得最大性能。