計算機主要由CPU、總線、I/O設備、內存、硬盤等組成,見下圖:

- cpu由控制器(CU)和運算器(ALU)組成,相當于計算機大腦。用來解釋計算機指令和處理計算數(shù)據(jù)。
- 總線:相當于傳遞信息的高速公路,計算機各大部件之間的信息交流都會通過總線。
- io設備: 計算機與外界的信息交互的載體。鼠標,鍵盤,磁盤,聲卡,網(wǎng)卡等都屬于io設備
- 內存:即圖中的主存儲器,運行速度比磁盤快得多。主要用來解決磁盤和cpu之間處理速度差距過大的問題。所有cpu運算所需的數(shù)據(jù)都是從內存獲得。但是內存中的數(shù)據(jù)在斷電時會丟失。
- 硬盤:用于持久化存儲計算機運行所需數(shù)據(jù)。但是由于它的機械結構,運行速度慢,所以必須通過運行速度快的內存將數(shù)據(jù)傳遞給cpu。磁盤具有容量大,速度慢,斷電數(shù)據(jù)不丟失。
我們在評估一個服務端系統(tǒng)瓶頸的時候,通常把服務分為io密集型和計算密集型。
- 計算密集型是指cpu的計算單元處理速度達到瓶頸,可以通過增加cpu核心,提高cpu主頻來增大計算能力。目前cpu主頻已經(jīng)超過3GHZ,服務器核心也從2核心,4核增大到32核,64核甚至128核。
-
io密集型是指磁盤處理io操作的速度達到瓶頸。上面介紹過,為了解決cpu處理速度和硬盤處理速度的巨大差異,我們引入了內存作為數(shù)據(jù)中轉站,好讓cpu不致于等待硬盤而浪費寶貴的cpu資源。以下是cpu-內存-磁盤的示意圖,不完全準確,但是能清晰表達這里的意思:
cpu-內存-磁盤關系示意圖.png系統(tǒng)表現(xiàn)為io密集型的意思就是:cpu工廠產(chǎn)出的數(shù)據(jù)雖然能很快的放到內存這個小倉庫,但是硬盤這個大倉庫的處理速度實在太慢,導致內存小倉庫收到cpu工廠運過來的數(shù)據(jù)太多而裝滿,cpu工廠和內存小倉庫不得不停下工作,等待硬盤大倉庫從內存小倉庫運出部分數(shù)據(jù)。
那么硬盤io速度為什么這么慢?操作系統(tǒng)有沒有做對應的優(yōu)化?應用程序有沒有做對應的優(yōu)化?
一,硬盤io的物理過程
磁盤由一些旋轉著的金屬碟片和一個裝在步進馬達上的讀寫頭組成。


磁盤完成一個io過程所花費的時間,由尋道時間,旋轉延遲和數(shù)據(jù)傳輸三部分時間組成:
1,尋道時間
Tseek是指將讀寫磁頭移動至正確的磁道上所需要的時間,目前磁盤的平均尋道時間一般在3-15ms
2,旋轉延遲
Trotation是指盤片旋轉將請求數(shù)據(jù)所在的扇區(qū)移動到讀寫磁盤下方所需要的時間。旋轉延遲取決于磁盤轉速,通常用磁盤旋轉一周所需時間的1/2表示。比如:7200rpm的磁盤平均旋轉延遲大約為60*1000/7200/2 = 4.17ms,而轉速為15000rpm的磁盤其平均旋轉延遲為2ms。
機械硬盤的連續(xù)讀寫性能很好,但隨機讀寫性能很差。這主要是因為隨機讀寫的尋道時間和旋轉延遲比較高,磁頭需要不停的移動來尋找正確的磁道,然后磁片也需要旋轉來找到正確的分區(qū),所以性能不高。
3,數(shù)據(jù)傳輸
Ttransfer是指完成傳輸所請求的數(shù)據(jù)所需要的時間,它取決于數(shù)據(jù)傳輸率,其值等于數(shù)據(jù)大小除以數(shù)據(jù)傳輸率。目前IDE/ATA能達到133MB/s,SATA II可達到300MB/s的接口數(shù)據(jù)傳輸率,數(shù)據(jù)傳輸時間通常遠小于前兩部分消耗時間。
二,操作系統(tǒng)對硬盤io的封裝優(yōu)化
類似于網(wǎng)絡的分層結構,下圖顯示了Linux系統(tǒng)中對于磁盤的一次讀請求在核心空間中所要經(jīng)歷的層次模型:

對于磁盤的一次讀請求,首先經(jīng)過虛擬文件系統(tǒng)層(VFS Layer),其次是具體的文件系統(tǒng)層(例如Ext2),接下來是Cache層(Page Cache Layer)、通用塊層(Generic Block Layer)、I/O調度層(I/O Scheduler Layer)、塊設備驅動層(Block Device Driver Layer),最后是物理塊設備層(Block Device Layer)。
1,虛擬文件系統(tǒng)層(VFS Layer)
VFS(Virtual File System)虛擬文件系統(tǒng)是一種軟件機制,更確切的說扮演著文件系統(tǒng)管理者的角色,與它相關的數(shù)據(jù)結構只存在于物理內存當中。它的作用是:屏蔽下層具體文件系統(tǒng)操作的差異,為上層的操作提供一個統(tǒng)一的接口。正是因為有了這個層次,Linux中允許眾多不同的文件系統(tǒng)共存并且對文件的操作可以跨文件系統(tǒng)而執(zhí)行。
VFS中包含著向物理文件系統(tǒng)轉換的一系列數(shù)據(jù)結構,如VFS超級塊、VFS的Inode、各種操作函數(shù)的轉換入口等。Linux中VFS依靠四個主要的數(shù)據(jù)結構來描述其結構信息,分別為超級塊、索引結點、目錄項和文件對象。
- 超級塊(Super Block):超級塊對象表示一個文件系統(tǒng)。它存儲一個已安裝的文件系統(tǒng)的控制信息,包括文件系統(tǒng)名稱(比如Ext2)、文件系統(tǒng)的大小和狀態(tài)、塊設備的引用和元數(shù)據(jù)信息(比如空閑列表等等)。VFS超級塊存在于內存中,它在文件系統(tǒng)安裝時建立,并且在文件系統(tǒng)卸載時自動刪除。同時需要注意的是對于每個具體的文件系統(tǒng)來說,也有各自的超級塊,它們存放于磁盤。
- 索引結點(Inode):索引結點對象存儲了文件的相關元數(shù)據(jù)信息,例如:文件大小、設備標識符、用戶標識符、用戶組標識符等等。Inode分為兩種:一種是VFS的Inode,一種是具體文件系統(tǒng)的Inode。前者在內存中,后者在磁盤中。所以每次其實是將磁盤中的Inode調進填充內存中的Inode,這樣才是算使用了磁盤文件Inode。當創(chuàng)建一個文件的時候,就給文件分配了一個Inode。一個Inode只對應一個實際文件,一個文件也會只有一個Inode。
- 目錄項(Dentry):引入目錄項對象的概念主要是出于方便查找文件的目的。不同于前面的兩個對象,目錄項對象沒有對應的磁盤數(shù)據(jù)結構,只存在于內存中。一個路徑的各個組成部分,不管是目錄還是普通的文件,都是一個目錄項對象。如,在路徑/home/source/test.java中,目錄 /, home, source和文件 test.java都對應一個目錄項對象。VFS在查找的時候,根據(jù)一層一層的目錄項找到對應的每個目錄項的Inode,那么沿著目錄項進行操作就可以找到最終的文件。
- 文件對象(File):文件對象描述的是進程已經(jīng)打開的文件。因為一個文件可以被多個進程打開,所以一個文件可以存在多個文件對象。一個文件對應的文件對象可能不是惟一的,但是其對應的索引節(jié)點和目錄項對象肯定是惟一的。
2, Ext2文件系統(tǒng)
具體的文件系統(tǒng),linux常用的有Ext2, Ext3, NFS, NTFS等文件系統(tǒng),對用戶透明,不介紹。
3,Page Cache層
引入Cache層的目的是為了提高Linux操作系統(tǒng)對磁盤訪問的性能。層在內存中緩存了磁盤上的部分數(shù)據(jù)。當數(shù)據(jù)的請求到達時,如果在Cache中存在該數(shù)據(jù)且是最新的,則直接將數(shù)據(jù)傳遞給用戶程序,免除了對底層磁盤的操作,提高了性能。磁盤Cache有兩大功能:預讀和回寫。
預讀
預讀其實就是利用了局部性原理,有同步和異步兩種情況。具體過程是:
- 對于每個文件的第一個次請求,系統(tǒng)讀入所請求的頁面并讀入緊隨其后的少數(shù)幾個頁面(通常是三個頁面),這時的預讀稱為同步預讀。
- 對于第二次讀請求,
- 如果所讀頁面不在Cache中,即不在前次預讀的頁中,則表明文件訪問不是順序訪問,系統(tǒng)繼續(xù)采用同步預讀;
- 如果所讀頁面在Cache中,則表明前次預讀命中,操作系統(tǒng)把預讀頁的大小擴大一倍,此時預讀過程是異步的,應用程序可以不等預讀完成即可返回,只要后臺慢慢讀頁面即可,這時的預讀稱為異步預讀。
- 任何接下來的讀請求都會處于兩種情況之一:第一種情況是所請求的頁面處于預讀的頁面中,這時繼續(xù)進行異步預讀;第二種情況是所請求的頁面處于預讀頁面之外,這時系統(tǒng)就要進行同步預讀。
回寫
回寫是通過暫時將數(shù)據(jù)存在Cache里,然后統(tǒng)一異步寫到磁盤中。
之前還有疑問:如果進程不斷的對一個文件頻繁的每次寫一點數(shù)據(jù),那系統(tǒng)不斷的做用戶態(tài)和內核態(tài)數(shù)據(jù)交換,效率豈不是非常低。原來操作系統(tǒng)考慮到了這個問題,在這里做了page cache優(yōu)化。
通過這種異步的數(shù)據(jù)I/O模式解決了程序中的計算速度和數(shù)據(jù)存儲速度不匹配的鴻溝,減少了訪問底層存儲介質的次數(shù),使存儲系統(tǒng)的性能大大提高。
回寫機制存在的問題是回寫不及時引發(fā)數(shù)據(jù)丟失(可由sync|fsync解決),回寫期間讀I/O性能很差。
4,通用塊層
不太了解,不介紹。
5,io調度層
I/O調度層的功能是管理塊設備的請求隊列。即接收通用塊層發(fā)出的I/O請求,緩存請求并試圖合并相鄰的請求。并根據(jù)設置好的調度算法,回調驅動層提供的請求處理函數(shù),以處理具體的I/O請求。
如果簡單地以內核產(chǎn)生請求的次序直接將請求發(fā)給塊設備的話,那么塊設備性能肯定讓人難以接受,因為磁盤尋址是整個計算機中最慢的操作之一。為了優(yōu)化尋址操作,內核不會一旦接收到I/O請求后,就按照請求的次序發(fā)起塊I/O請求。為此Linux實現(xiàn)了幾種I/O調度算法,算法基本思想就是通過合并和排序I/O請求隊列中的請求,以此大大降低所需的磁盤尋道時間和旋轉延遲,從而一定程度上克服隨機io的缺點,來提高整體I/O性能。
6,塊設備驅動層
驅動層中的驅動程序對應具體的物理塊設備。它從上層中取出I/O請求,并根據(jù)該I/O請求中指定的信息,通過向具體塊設備的設備控制器發(fā)送命令的方式,來操縱設備傳輸數(shù)據(jù)
三,開源軟件利用操作系統(tǒng)io特性的設計技巧
下面以開源系統(tǒng)闡述一些基于磁盤I/O特性的設計技巧。
1,追加寫
kafka就是采用了追加寫,最直接的證明就是Kafka源碼中只調用了FileChannel.write(ByteBuffer),而沒有調用過帶offset參數(shù)的write方法,說明它不會執(zhí)行隨機寫操作。順序讀寫就減少了尋道和旋轉的時間,從而提高了kafka的整體讀寫tps。還有另外一種聲音,就是kafka的整體讀寫tps決定因素,除了順序讀寫外,還包括基于mmap的零拷貝技術,pagecache,利用java NIO技術等的綜合因素。
2,小文件合并
- 首先減少了大量元數(shù)據(jù),提高了元數(shù)據(jù)的檢索和查詢效率,降低了文件讀寫的I/O操作延時。
- 其次將可能連續(xù)訪問的小文件一同合并存儲,增加了文件之間的局部性,將原本小文件間的隨機訪問變?yōu)榱隧樞蛟L問,大大提高了性能。
- 合并存儲能夠有效的減少小文件存儲時所產(chǎn)生的磁盤碎片問題,提高了磁盤的利用率。
- 合并之后小文件的訪問流程也有了很大的變化,由原來許多的open操作轉變?yōu)榱藄eek操作,定位到大文件具體的位置即可。如何尋址這個大文件中的小文件呢?其實就是利用一個旁路數(shù)據(jù)庫來記錄每個小文件在這個大文件中的偏移量和長度等信息。其實
小文件合并的策略本質上就是通過分層的思想來存儲元數(shù)據(jù)。中控節(jié)點存儲一級元數(shù)據(jù),也就是大文件與底層塊的對應關系;數(shù)據(jù)節(jié)點存放二級元數(shù)據(jù),也就是最終的用戶文件在這些一級大塊中的存儲位置對應關系,經(jīng)過兩級尋址來讀寫數(shù)據(jù)。
淘寶的TFS就采用了小文件合并存儲的策略。TFS中默認Block大小為64M,每個塊中會存儲許多不同的小文件,但是這個塊只占用一個Inode。假設一個Block為64M,數(shù)量級為1PB。那么NameServer上會有 1 * 1024 * 1024 * 1024 / 64 = 16.7M個Block。假設每個Block的元數(shù)據(jù)大小為0.1K,則占用內存不到2G。在TFS中,文件名中包含了Block ID和File ID,通過Block ID定位到具體的DataServer上,然后DataServer會根據(jù)本地記錄的信息來得到File ID所在Block的偏移量,從而讀取到正確的文件內容。
3,元數(shù)據(jù)管理優(yōu)化
元數(shù)據(jù)信息包括名稱、文件大小、設備標識符、用戶標識符、用戶組標識符等等。
- 在小文件系統(tǒng)中可以對元數(shù)據(jù)信息進行精簡,僅保存足夠的信息即可。元數(shù)據(jù)精簡可以減少元數(shù)據(jù)通信延時,同時相同容量的Cache能存儲更多的元數(shù)據(jù),從而提高元數(shù)據(jù)使用效率。
- 可以在文件名中就包含元數(shù)據(jù)信息,從而減少一個元數(shù)據(jù)的查詢操作。TFS中文件命名就隱含了位置信息等部分元數(shù)據(jù)。
- 針對特別小的一些文件,可以采取元數(shù)據(jù)和數(shù)據(jù)并存的策略,將數(shù)據(jù)直接存儲在元數(shù)據(jù)之中,通過減少一次尋址操作從而大大提高性能。在Rerserfs中,對于小于1KB的小文件,Rerserfs可以將數(shù)據(jù)直接存儲在Inode中。
