NVMe SSD具有高效能、低時延等優點,是目前儲存行業的研究熱點之一,但在光鮮的效能下也同樣存在一些沒有廣為人知的問題,而這些問題其實對於一個生產系統而言至關重要,例如:
- QoS無法做到100%保證;
- 讀寫混合情況下,與單獨讀相比,效能下降嚴重,且讀長尾延遲比較嚴重;
所以如何利用好NVMe盤的效能,並更好的為業務服務,我們需要從硬體,Linux核心等多個角度去剖析和解決。
從核心中NVMe IO框架來看其中存在的問題
當前Linux核心中對NVMe SSD的訪問是透過MQ框架來實現的,接入NVMe驅動後直接略過IO排程器,具體實現上來說是從block layer中的通用塊層回呼make_request從而打通上下層IO通路。示意圖如下,這裡面有幾個關鍵的點:
IO傳送過程
MQ的框架提升效能最主要的將鎖的粒度按照硬體佇列進行拆分,並與底層SSD的佇列進行系結,理想的情況每一個CPU都有對應的硬體傳送SQ與響應CQ,這樣可以併發同時彼此之前無影響。按照NVMe SPEC協議中的標準,硬體最多支援64K個佇列,所以理想情況下硬體佇列個數將不會是我們需要擔心的地方。但是實際情況又如何呢?由於硬體佇列的增加會給NVMe SSD帶來功耗的增加,所以不同的廠商在設計硬體佇列個數時的考量是不同的,比如intel P3600支援32個佇列,intel最新的P4500支援16384個,但是SUMSUNG PM963卻只支援到8個。那麼當CPU的個數超過硬體的佇列個數,就會出現多個CPU共用一個硬體佇列的情況,對效能就會產生影響。
下麵使用SUMSUNG PM963做一個簡單的測試:
#!/bin/bash
fio -name=test1 -filename=/dev/nvme11n1 -thread=1 -bs=4k -rw=randread -iodepth=256 -direct=1 -numjobs=1 -ioengine=libaio -group_reporting -runtime=60 -cpus_allowed=1 –output=urgent.txt &
fio -name=test2 -filename=/dev/nvme11n1 -thread=1 -bs=4k -rw=randread -iodepth=256 -direct=1 -numjobs=1 -ioengine=libaio -group_reporting -runtime=60 -cpus_allowed=10 –output=high.txt &
測試結果為
read : io=59198MB, bw=986.61MB/s, iops=252567, runt= 60002msec
read : io=59197MB, bw=986.62MB/s, iops=252571, runt= 60001msec
也就是整個IOPS可以達到50w
如果使用同一個硬體佇列
#!/bin/bash
fio -name=test1 -filename=/dev/nvme11n1 -thread=1 -bs=4k -rw=randread -iodepth=256 -direct=1 -numjobs=1 -ioengine=libaio -group_reporting -runtime=60 -cpus_allowed=1 –output=urgent.txt &
fio -name=test2 -filename=/dev/nvme11n1 -thread=1 -bs=4k -rw=randread -iodepth=256 -direct=1 -numjobs=1 -ioengine=libaio -group_reporting -runtime=60 -cpus_allowed=2 –output=high.txt &
結果為:
read : io=51735MB, bw=882937KB/s, iops=220734, runt= 60001msec
read : io=52411MB, bw=894461KB/s, iops=223615, runt= 60001msec
整個IOPS只有44w
,效能下降12%
,主要原因是多個CPU共用硬體佇列進行傳送的時候會有自旋鎖爭搶的影響。所以對於共用硬體佇列的情況下,如何系結CPU是需要根據業務的特點來確定的。
IO響應過程
IO響應過程中最主要問題是中斷的balance,由於預設linux中並沒有對NVMe的中斷進行有效的系結,所以不同的系結策略會帶來截然不同的效能資料。不過在我們的實際測試中,雖然我們沒有做中斷的系結,但是貌似不管是效能還是穩定性的下降並沒有那麼嚴重,什麼原因呢?根據我們的分析,這裡面最主要的原因是(後面也會提到),我們並沒有大壓力的使用NVMe盤,所以實際的應用場景壓力以及佇列深度並不大。
從應用本身的IO Pattern來看使用NVMe問題
我們在評測一個NVMe SSD的效能的時候,往往都是透過benchmark工具,例如見1, 見2。
然而這些測試的結果與業務實際使用NVMe SSD看到的效能相比差距很大。原因是因為這些效能測試存在兩個比較大的誤區,因而並不能反映生產系統的真實情況。
1. 片面誇大了NVMe盤的效能
從上面兩篇文章中的測試中我們可以看到,大多數壓測中使用的佇列深度為128,並且是用libaio這樣的非同步IO樣式來下發IO。但是在實際應用場景中很少有這麼大的佇列深度。在這種場景下,根據“色子效應”,並不會將NVMe盤的併發效能充分發揮出來。
2. 低估了NVMe的長尾延遲
然而在另外一些場景下,佇列深度又會非常高(比如到1024甚至更高),在這種情況下NVMe SSD帶來的QoS長尾延遲影響比上面的benchmark的測試又嚴重得多。
所以綜合起來看,這種評測選擇了一個看上去沒啥大用的場景做了測試,所以得出的結果也對我們實際的應用基本沒有參考價值。那麼問題出在什麼地方麼?
問題分析
首先讓我們再次強調一下一般評測文章中benchmark進行的測試場景的特點:
- 大多是
fio工具
,開啟libaio引擎
增加IO壓力 佇列深度到128或者256
在這種場景下確實基本都可以將NVMe盤的壓力打滿。
在展開分析問題的原因之前,我們先看看Linux內核的IO棧。
在實際應用中,VFS提供給應用的介面,從IO的特點來分類,大致上可以分為兩類:direct IO與buffer IO。使用direct IO的業務大多在應用本身就已經做了一層cache,不依賴OS提供的page cache。其他的情況大多使用的都是buffer IO。linux kernel中的block layer透過REQ_SYNC與~REQ_SYNC這兩種不同的標誌來區分這兩類IO。大家常用的direct IO這個型別,核心要保證這次IO操作的資料落盤,並且當響應傳回以後,應用程式才能夠認為這次IO操作是完成的。所以是使用了這裡的REQ_SYNC標誌,而對應的buffer IO,則大量使用了~REQ_SYNC的標誌。讓我們一個一個看過去。
direct IO
由於在實際使用方式中AIO還不夠成熟,所以大多使用direct IO。但是direct IO是一種SYNC樣式,並且完全達不到測試用例中128路併發AIO的效果。
這主要兩個方面原因:
direct io在下發過程中可能會使用檔案粒度的鎖inode->i_mutex
進行互斥。
101 static ssize_t
102 ext4_file_dio_write(struct kiocb *iocb, const struct iovec *iov,
103 unsigned long nr_segs, loff_t pos)
104 {
105 struct file *file = iocb->ki_filp;
106 struct inode *inode = file->f_mapping->host;
107 struct blk_plug plug;
108 int unaligned_aio = 0;
109 ssize_t ret;
110 int overwrite = 0;
111 size_t length = iov_length(iov, nr_segs);
112
……….
122
123 BUG_ON(iocb->ki_pos != pos);
124
125 mutex_lock(&inode-;>i_mutex);
127
前面說的IO SYNC樣式
1052 static inline ssize_t
1053 do_blockdev_direct_IO(int rw,
struct kiocb *iocb,
………..
1283 if (retval != -EIOCBQUEUED)
1284 dio_await_completion(dio);
也就是說,我們很難透過direct IO來達到壓滿NVMe盤的目的。如果一定要打滿NVMe盤,那麼一方面要提高行程併發,另外一方面還要提高多行程多檔案的併發。而這是生產系統中很難滿足的。
buffer IO
我們再來看看buffer IO的特點。下麵我使用了比較簡單的fio透過buffer IO的樣式下發,而且透過rate限速,我們發現平均下來每秒的資料量不到100MB,整個IO的特點如下:
fio -name=test1 -filename=/home/nvme/test1.txt -thread=1 -bs=4k -rw=randwrite -iodepth=32 -numjobs=32 -ioengine=sync -size=10G -group_reporting -runtime=600 -rate=200m -thinktime=1000
FUNC COUNT
submit_bio 113805
FUNC COUNT
submit_bio 143703
FUNC COUNT
submit_bio 154
FUNC COUNT
submit_bio 86
FUNC COUNT
submit_bio 78
FUNC COUNT
submit_bio 96
FUNC COUNT
submit_bio 124
抓取了下submit_bio在每秒的呼叫次數並分析可以得出,buffer IO在下刷的時候並不會考慮QD的多少,而是類似aio那樣,kworker將需要下發的臟頁都會bio形式下發,而且不需要等待某些bio傳回。註意這裡面有一個細節從qusize
觀察到IO最大值986,並沒有達到百K,或者幾十K,這個原因是由本身MQ的框架中tag機制nr_request
決定,目前單Q設定預設值一般為1024。總之buffer IO這樣特點的結果就是突發量的高iops的寫入,buffer IO對於應用程式來說是不可見的,因為這是linux kernel的本身的刷臟頁行為。但是它帶給應用的影響確實可見的,筆者曾經總結過非同步IO的延時對長尾的影響,如下圖所示,分別是buffer IO與direct IO在相同頻寬下延時表現,可以看出這延遲長尾比我們簡單的透過fio benchmark測試嚴重的多,特別是盤開始做GC的時候,抖動更加嚴重;而且隨著盤的容量用著越來越多,GC的影響越來越大,長尾的影響也是越來越嚴重。
在HDD的時代上面的問題同樣會存在,但是為什麼沒有那麼嚴重,原因主要是HDD大多使用CFQ排程器,其中一個特性是同步、非同步IO佇列分離。並且在排程過程中同步優先順序比較高,在排程搶佔、時間片等都是同步優先。
解決問題
前面描述了使用NVMe硬碟的嚴重性,下麵介紹一下如何解決這些問題。
(1)MQ系結的問題,需要根據當前業務的特點,如果硬體的佇列小於當前CPU的個數,儘量讓核心業務上跑的行程分散在系結不同硬體佇列的CPU上,防止IO壓力大的時候鎖資源的競爭。
(2)中斷系結CPU,建議下發的SQ的CPU與響應的CQ的CPU保持一致,這樣各自CPU來處理自己的事情,互相業務與中斷不幹擾。
(3)解決direct IO狀態下長尾延遲,因為長尾延遲是本身NVMe SSD Controller帶來,所以解決這個問題還是要從控制器入手,使用的方法有WRR(Weight Round Robin),這個功能在當前主流廠商的最新的NVMe SSD中已經支援。
(4)解決buffer IO狀態下長尾延遲,可以透過控制NVMe SSD處理的QD來解決,使用的NVME多佇列IO排程器,充分利用了MQ框架,根據同步寫、讀延遲動態調整非同步IO的佇列,很好的解決buffer io帶來的長尾延遲。