Linux性能優化11:磁碟IO的調度模型
這一篇講磁碟IO的模型。
[Linux文件IO子系統]
Linux的文件IO子系統是Linux中最複雜的一個子系統(沒有之一)。讀者可以參考以下這個圖:https://www.thomas-krenn.com/de/wikiDE/images/2/2d/Linux-storage-stack-diagram_v4.0.pdf
如果你懶得跳過去,這裡貼一個:
存儲系統可以認為有兩個部分,第一個部分站在用戶的角度,提供讀寫的介面。這裡的名稱空間是以流為中心的。第二部分站在存儲設備的角度,提供讀寫介面,這裡的名稱空間是以塊為中心。插在兩者中間提供承接的是文件系統(不是指VFS,我指的是Ext4這樣的文件系統)。
簡單理解,你的應用程序發出一個讀寫請求,文件系統負責定位這個讀寫請求的位置,換成塊設備的塊,然後把這個請求發到設備上,把文件寫入內存中,你的應用程序就從內存中獲得數據。
所以,你會發現,上下兩個部分是個非同步的運行模型,對上半部分來說,不到沒有辦法的時候(比如你第一次要磁碟中的數據),讓數據一直留在內存中是最好的。因為誰知道你後面還會不會修改這個地方呢?如果你還要讀寫,同步到磁碟中不是白乾了?所以,數據什麼時候和磁碟同步,是個獨立的邏輯,和你的應用的要求(比如主動sync),內存空間的大小(比如你Pagecache佔據太多的內存了),都有關係。
這個文檔不是討論Cache部分的行為,這個部分理解到這裡為止。稍深入一點的介紹可以看這裡:Linux 中 mmap() 函數的內存映射問題理解? - in nek 的回答。這部分的模型是純軟體模型,用一般熱點方法分析就可以了。
本文主要關注下半部分的通用部分的模型,也就是上面圖中Block Layer的運行模型。
[Block Layer調度模型]
數據從Pagecache同步到磁碟上,發出的請求稱為一個request,一個request包含一組bio,每個bio包含要同步的數據pages,你要把Page和磁碟的數據進行同步。
和網路子系統不同,磁碟的調度是有要求的,不是說你發一個page,我就幫你寫進去,你再發一個page,我就給你再寫一個進去。你要寫磁碟的一個地方,磁碟要先把磁頭物理上移動到那個軌道上,然後才能寫,你讓磁頭這樣移來移去的,磁碟的性能就很難看了。
Linux的IO調度器稱為evelator(電梯),因為Linus開始實現這個系統的時候,使用的就是電梯演算法。
坐過電梯很容易理解什麼是電梯演算法,電梯的演算法是:電梯總是從一個方向,把人送到有需要的最高的位置,然後反過來,把人送到有需要的最低一個位置。這樣效率是最高的,因為電梯不用根據先後順序,不斷調整方向,走更多的冤枉路。
為了實現這個演算法,我們需要一個plug的概念。這個概念類似馬桶的沖水器,你先把沖水器用塞子堵住,然後開始接水,等水滿了,你一次把塞子拔掉,沖水器中的水就一次衝出去了。在真正沖水之前,你就有機會把數據進行合併,排序,保證你的「電梯」可以從一頭走到另一頭,然後從另一頭回來。
我們前面講IO系統的時候就提過磁碟調度子系統的ftrace跟蹤,這裡我們深入看看blktrace跟蹤到的事件的含義:
請求相關
- Q - queued:bio請求進入調度
- G - get request:分配request
- I - inserted:request進入io調度器
調度相關
- B - bounced:硬體無法訪問內存,需要把內存降低到硬體可訪問
- M - back merge:請求和前一個從後面合併
- F - front merge:請求和前一個從前面合併
- X - split:請求分析為多個request(很可能是因為硬體不支持太大的請求)
- A - remap:基於分區等,重新映射request的位置
- R - requeue: Request重新回到調度隊列
- S - sleep:調度器進入休眠P - plug:調度隊列插入設備(準備合併)
- U - unplug:調度隊列離開設備(全部一次寫入設備中)
- T - unplug due to timer超時,而不是數據足夠發起的unplug
發出相關
- C - complete:完成一個request的調度(無論成功還是失敗)
- D - issued:發送到設備,這個是從下層硬體驅動發起的
我們通過對這些事件的跟蹤,對照硬體的特性大概就可以知道運行的模型是否正常了。
[多調度器支持]
但情況千變萬化,不是每種磁碟,每個場景都可以用一樣的演算法。所以,現在Linux可以支持多個IO調度器,你可以給每個磁碟制定不同的調度演算法,這個在/sys/block/<dev>/queue/scheduler中設置。它的用戶介面設計得很好,你看看它的內容就明白我的意思了:
noop [deadline] cfq
上面三個是我的PC上支持的三個演算法,其中被選中的演算法是deadline,寫另一個名字進去可以選定另一個調度演算法。
我們簡單理解一下這三個演算法:
noop是no operation,就是不調度的演算法,有什麼請求都直接寫下去。這通常用於兩種情形:你的磁碟是比如SSD那樣的內存存儲設備,根本不需要調度,往下寫就對了。第二種情形是你的磁碟比較高級,自帶調度器,OS不需要自作聰明,有什麼請求直接往下扔就好了。這兩種情況就應該選noop演算法
deadline是一個改良的電梯演算法,基本上和電梯演算法一樣,但加了一條,如果部分請求等太久了(deadline到了,默認讀請求500ms,寫請求5s),電梯就要立即給我掉頭,先處理這個請求。
CFQ,呵呵,這個名字是不是有種特別熟悉的感覺?對了,它就是我們前面我們講CPU調度器時提到的CFS,完全公平調度器。這個演算法按任務分成多個隊列,按隊列的「完全公平」進行調度。
利用這個演算法,可以通過ionice設定每個任務不同的優先順序,提供給調度器進行分級調度。例如:
ionice -c1 -n3 dd if=/dev/zero of=test.hd bs=4096 count=1000n
這個命令要求後面的dd命令按三級實時策略進行調度(實時策略需要root許可權)。
ionice不需要CFS就可以工作,只是沒有CFQ,策略並不能很好得到執行。通常我們在Desktop上使用deadline,在伺服器上用QFQ。
對調優來說,切換調度器和相關調度參數是最簡單的方案。更多的其他考慮,那就是看你怎麼跟蹤了。
[多隊列]
另一方面,一個塊設備一個隊列的策略,面對新的存儲設備,也開始變得不合時宜了。我們這樣來體會一下這個問題:
普通的物理硬碟,每秒可以響應幾百個request(IOPS)
SSD1,這個大約是6萬
SSD2,~9萬
SSD3,~50萬
SSD4,~60萬
SSD5,~78.5萬
一個隊列?開玩笑!
一個隊列會帶來如下問題:
1. 全部IO中斷會集中到一個CPU上
2. 不同NUMA節點也會集中到一個CPU上
3. 所有CPU訪問一個隊列會造成前面提到的Amdahl模型的spinlock問題
所以,和網路一樣,現在塊設備子系統也支持多隊列模型,稱為mq-blk。也和隊列網卡一樣,這個特性需要你的硬體支持多隊列,如果你使用scsi庫,你需要你的設備支持mq-scsi。使用m-blk後,io調度器直接作用在每個queue上。
[AIO]
AIO的名字叫非同步IO,其實本質上並非是對文件系統的非同步封裝,而是從塊設備層直接出訪問介面,只是這個介面恰好是非同步的,所以才叫AIO。
AIO的實現在內核fs/aio.c中,現在好像還沒有對應的庫封裝(請注意,posix的aio(7)文檔描述的AIO介面是POSIX庫自己對文件系統的封裝,而Linux內核的AIO現在沒有庫的封裝,需要用戶自己做系統調用來實現。除非特別指出,本文涉及的AIO介面都指向Linux內核的AIO,而不是Posix的AIO。
AIO包括如下系統調用:
io_setup() 創建一個context上下文
io_destroy()刪除context
io_submit() 發出命令
io_get_events() 接收命令(事件)
io_cancel() 取消
這個介面不用過多解釋,猜都能猜到怎麼用(具體細節自己上網搜),這裡想說明的是調度模型。本質上,AIO的「事件」就是我們前面分析塊設備介面中的一個「命令」(讀寫都可以),一個io_submit()就是一次plug(),unplug()的過程,也就是你發起一個submit,發出一組iocb,每個iocb就是一個讀寫操作,submit的時候就先plug,把請求堵住,然後把iocb灌進去,然後unplug,把請求發出去。
所以,對AIO的跟蹤不會比一般的跟蹤複雜,因為都是跟蹤塊設備層的方法。
推薦閱讀: