本文主要講解Linux IO調度層的三種模式:cfp、deadline和noop,並給出各自的優化和適用場景建議。
IO調度發生在Linux內核的IO調度層。這個層次是針對Linux的整體IO層次體系來說的。從read()或者write()系統調用的角度來說,Linux整體IO體系可以分為七層,它們分別是:
有一個已經整理好的[Linux IO結構圖],非常經典,一圖勝千言:
我們今天要研究的內容主要在IO調度這一層。
它要解決的核心問題是,如何提高塊設備IO的整體性能?這一層也主要是針對機械硬盤結構而設計的。
眾所周知,機械硬盤的存儲介質是磁盤,磁頭在盤片上移動進行磁道尋址,行為類似播放一張唱片。
這種結構的特點是,順序訪問時吞吐量較高,但是如果一旦對盤片有隨機訪問,那麼大量的時間都會浪費在磁頭的移動上,這時候就會導致每次IO的響應時間變長,極大的降低IO的響應速度。
磁頭在盤片上尋道的操作,類似電梯調度,實際上在最開始的時期,Linux把這個算法命名為Linux電梯算法,即:
如果在尋道的過程中,能把順序路過的相關磁道的數據請求都“順便”處理掉,那麼就可以在比較小影響響應速度的前提下,提高整體IO的吞吐量。
這就是我們為什麼要設計IO調度算法的原因。
目前在內核中默認開啟了三種算法/模式:noop,cfq和deadline。嚴格算應該是兩種:
因為第一種叫做noop,就是空操作調度算法,也就是沒有任何調度操作,並不對io請求進行排序,僅僅做適當的io合並的一個fifo隊列。
目前內核中默認的調度算法應該是cfq,叫做完全公平隊列調度。這個調度算法人如其名,它試圖給所有進程提供一個完全公平的IO操作環境。
注:請大家一定記住這個詞語,cfq,完全公平隊列調度,不然下文就沒法看了。
cfq為每個進程創建一個同步IO調度隊列,並默認以時間片和請求數限定的方式分配IO資源,以此保證每個進程的IO資源占用是公平的,cfq還實現了針對進程級別的優先級調度,這個我們後面會詳細解釋。
查看和修改IO調度算法的方法是:
cfq是通用服務器比較好的IO調度算法選擇,對桌面用戶也是比較好的選擇。
但是對於很多IO壓力較大的場景就並不是很適應,尤其是IO壓力集中在某些進程上的場景。
因為這種場景我們需要更多的滿足某個或者某幾個進程的IO響應速度,而不是讓所有的進程公平的使用IO,比如數據庫應用。
deadline調度(最終期限調度)就是更適合上述場景的解決方案。deadline實現了四個隊列:
不久前,內核還是默認標配四種算法,還有一種叫做as的算法(Anticipatory scheduler),預測調度算法。一個高大上的名字,搞得我一度認為Linux內核都會算命了。
結果發現,無非是在基於deadline算法做io調度的之前等一小會時間,如果這段時間內有可以合並的io請求到來,就可以合並處理,提高deadline調度的在順序讀寫情況下的數據吞吐量。
其實這根本不是啥預測,我覺得不如叫撞大運調度算法,當然這種策略在某些特定場景差效果不錯。
但是在大多數場景下,這個調度不僅沒有提高吞吐量,還降低了響應速度,所以內核干脆把它從默認配置裡刪除了。畢竟Linux的宗旨是實用,而我們也就不再這個調度算法上多費口舌了。
cfq是內核默認選擇的IO調度隊列,它在桌面應用場景以及大多數常見應用場景下都是很好的選擇。
如何實現一個所謂的完全公平隊列(Completely Fair Queueing)?
首先我們要理解所謂的公平是對誰的公平?從操作系統的角度來說,產生操作行為的主體都是進程,所以這裡的公平是針對每個進程而言的,我們要試圖讓進程可以公平的占用IO資源。
那麼如何讓進程公平的占用IO資源?我們需要先理解什麼是IO資源。當我們衡量一個IO資源的時候,一般喜歡用的是兩個單位,一個是數據讀寫的帶寬,另一個是數據讀寫的IOPS。
帶寬就是以時間為單位的讀寫數據量,比如,100Mbyte/s。而IOPS是以時間為單位的讀寫次數。在不同的讀寫情境下,這兩個單位的表現可能不一樣,但是可以確定的是,兩個單位的任何一個達到了性能上限,都會成為IO的瓶頸。
從機械硬盤的結構考慮,如果讀寫是順序讀寫,那麼IO的表現是可以通過比較少的IOPS達到較大的帶寬,因為可以合並很多IO,也可以通過預讀等方式加速數據讀取效率。
當IO的表現是偏向於隨機讀寫的時候,那麼IOPS就會變得更大,IO的請求的合並可能性下降,當每次io請求數據越少的時候,帶寬表現就會越低。
從這裡我們可以理解,針對進程的IO資源的主要表現形式有兩個:進程在單位時間內提交的IO請求個數和進程占用IO的帶寬。
其實無論哪個,都是跟進程分配的IO處理時間長度緊密相關的。
有時業務可以在較少IOPS的情況下占用較大帶寬,另外一些則可能在較大IOPS的情況下占用較少帶寬,所以對進程占用IO的時間進行調度才是相對最公平的。
即,我不管你是IOPS高還是帶寬占用高,到了時間咱就換下一個進程處理,你愛咋樣咋樣。
所以,cfq就是試圖給所有進程分配等同的塊設備使用的時間片,進程在時間片內,可以將產生的IO請求提交給塊設備進行處理,時間片結束,進程的請求將排進它自己的隊列,等待下次調度的時候進行處理。這就是cfq的基本原理。
當然,現實生活中不可能有真正的“公平”,常見的應用場景下,我們很肯能需要人為的對進程的IO占用進行人為指定優先級,這就像對進程的CPU占用設置優先級的概念一樣。
所以,除了針對時間片進行公平隊列調度外,cfq還提供了優先級支持。每個進程都可以設置一個IO優先級,cfq會根據這個優先級的設置情況作為調度時的重要參考因素。
優先級首先分成三大類:RT、BE、IDLE,它們分別是實時(Real Time)、最佳效果(Best Try)和閒置(Idle)三個類別,對每個類別的IO,cfq都使用不同的策略進行處理。另外,RT和BE類別中,分別又再劃分了8個子優先級實現更細節的QOS需求,而IDLE只有一個子優先級。
另外,我們都知道內核默認對存儲的讀寫都是經過緩存(buffer/cache)的,在這種情況下,cfq是無法區分當前處理的請求是來自哪一個進程的。
只有在進程使用同步方式(sync read或者sync wirte)或者直接IO(Direct IO)方式進行讀寫的時候,cfq才能區分出IO請求來自哪個進程。
所以,除了針對每個進程實現的IO隊列以外,還實現了一個公共的隊列用來處理異步請求。
當前內核已經實現了針對IO資源的cgroup資源隔離,所以在以上體系的基礎上,cfq也實現了針對cgroup的調度支持。
總的來說,cfq用了一系列的數據結構實現了以上所有復雜功能的支持,大家可以通過源代碼看到其相關實現,文件在源代碼目錄下的block/cfq-iosched.c。
在此,我們對整體數據結構做一個簡要描述:首先,cfq通過一個叫做cfq_data的數據結構維護了整個調度器流程。在一個支持了cgroup功能的cfq中,全部進程被分成了若干個contral group進行管理。
每個cgroup在cfq中都有一個cfq_group的結構進行描述,所有的cgroup都被作為一個調度對象放進一個紅黑樹中,並以vdisktime為key進行排序。
vdisktime這個時間紀錄的是當前cgroup所占用的io時間,每次對cgroup進行調度時,總是通過紅黑樹選擇當前vdisktime時間最少的cgroup進行處理,以保證所有cgroups之間的IO資源占用“公平”。
當然我們知道,cgroup是可以對blkio進行資源比例分配的,其作用原理就是,分配比例大的cgroup占用vdisktime時間增長較慢,分配比例小的vdisktime時間增長較快,快慢與分配比例成正比。
這樣就做到了不同的cgroup分配的IO比例不一樣,並且在cfq的角度看來依然是“公平“的。
選擇好了需要處理的cgroup(cfq_group)之後,調度器需要決策選擇下一步的service_tree。
service_tree這個數據結構對應的都是一系列的紅黑樹,主要目的是用來實現請求優先級分類的,就是RT、BE、IDLE的分類。每一個cfq_group都維護了7個service_trees,其定義如下:
其中service_tree_idle就是用來給IDLE類型的請求進行排隊用的紅黑樹。
而上面二維數組,首先第一個維度針對RT和BE分別各實現了一個數組,每一個數組中都維護了三個紅黑樹,分別對應三種不同子類型的請求,分別是:SYNC、SYNC_NOIDLE以及ASYNC。
我們可以認為SYNC相當於SYNC_IDLE並與SYNC_NOIDLE對應。idling是cfq在設計上為了盡量合並連續的IO請求以達到提高吞吐量的目的而加入的機制,我們可以理解為是一種“空轉”等待機制。
空轉是指,當一個隊列處理一個請求結束後,會在發生調度之前空等一小會時間,如果下一個請求到來,則可以減少磁頭尋址,繼續處理順序的IO請求。
為了實現這個功能,cfq在service_tree這層數據結構這實現了SYNC隊列,如果請求是同步順序請求,就入隊這個service tree,如果請求是同步隨機請求,則入隊SYNC_NOIDLE隊列,以判斷下一個請求是否是順序請求。
所有的異步寫操作請求將入隊ASYNC的service tree,並且針對這個隊列沒有空轉等待機制。
此外,cfq還對SSD這樣的硬盤有特殊調整,當cfq發現存儲設備是一個ssd硬盤這樣的隊列深度更大的設備時,所有針對單獨隊列的空轉都將不生效,所有的IO請求都將入隊SYNC_NOIDLE這個service tree。
每一個service tree都對應了若干個cfq_queue隊列,每個cfq_queue隊列對應一個進程,這個我們後續再詳細說明。
cfq_group還維護了一個在cgroup內部所有進程公用的異步IO請求隊列,其結構如下:
異步請求也分成了RT、BE、IDLE這三類進行處理,每一類對應一個cfq_queue進行排隊。
BE和RT也實現了優先級的支持,每一個類型有IOPRIO_BE_NR這麼多個優先級,這個值定義為8,數組下標為0-7。
我們目前分析的內核代碼版本為Linux 4.4,可以看出,從cfq的角度來說,已經可以實現異步IO的cgroup支持了,我們需要定義一下這裡所謂異步IO的含義,它僅僅表示從內存的buffer/cache中的數據同步到硬盤的IO請求,而不是aio(man 7 aio)或者linux的native異步io以及libaio機制,實際上這些所謂的“異步”IO機制,在內核中都是同步實現的(本質上馮諾伊曼計算機沒有真正的“異步”機制)。
我們在上面已經說明過,由於進程正常情況下都是將數據先寫入buffer/cache,所以這種異步IO都是統一由cfq_group中的async請求隊列處理的。
那麼為什麼在上面的service_tree中還要實現和一個ASYNC的類型呢?
這當然是為了支持區分進程的異步IO並使之可以“完全公平”做准備喽。
實際上在最新的cgroup v2的blkio體系中,內核已經支持了針對buffer IO的cgroup限速支持,而以上這些可能容易混淆的一堆類型,都是在新的體系下需要用到的類型標記。
新體系的復雜度更高了,功能也更加強大,但是大家先不要著急,正式的cgroup v2體系,在Linux 4.5發布的時候會正式跟大家見面。
我們繼續選擇service_tree的過程,三種優先級類型的service_tree的選擇就是根據類型的優先級來做選擇的,RT優先級最高,BE其次,IDLE最低。就是說,RT裡有,就會一直處理RT,RT沒了再處理BE。
每個service_tree對應一個元素為cfq_queue排隊的紅黑樹,而每個cfq_queue就是內核為進程(線程)創建的請求隊列。
每一個cfq_queue都會維護一個rb_key的變量,這個變量實際上就是這個隊列的IO服務時間(service time)。
這裡還是通過紅黑樹找到service time時間最短的那個cfq_queue進行服務,以保證“完全公平”。
選擇好了cfq_queue之後,就要開始處理這個隊列裡的IO請求了。這裡的調度方式基本跟deadline類似。
cfq_queue會對進入隊列的每一個請求進行兩次入隊,一個放進fifo中,另一個放進按訪問扇區順序作為key的紅黑樹中。
默認從紅黑樹中取請求進行處理,當請求的延時時間達到deadline時,就從紅黑樹中取等待時間最長的進行處理,以保證請求不被餓死。
這就是整個cfq的調度流程,當然其中還有很多細枝末節沒有交代,比如合並處理以及順序處理等等。
理解整個調度流程有助於我們決策如何調整cfq的相關參數。所有cfq的可調參數都可以在/sys/class/block/sda/queue/iosched/目錄下找到,當然,在你的系統上,請將sda替換為相應的磁盤名稱。我們來看一下都有什麼: