Linux系統很重要的一個性能提升點就是它的 Pagecache, 因為內存比IO快太多了,所以大家都想進辦法來利用這個cache。 文件系統也不例外,為了達到高性能,文件讀取通常采用預讀來預測用戶的行為,把用戶可能需要的數據預先讀取到cache去,達到高性能的目的。
Linux各個發行版readahead的實現差異很大,我們這裡重點討論2.6.18, RHEL 5U4發行版的行為.文件預讀的實現主要在mm/readahead.c中,代碼才603行。 預讀的流程大概是這樣的,用戶需要文件頁面的時候入口函數do_generic_mapping_read會委托 page_cache_readahead來進行處理。它首先判斷用戶的IO是順序的還是隨機的,如果是隨機的就沒啥好預讀. 如果是順序的話,那麼預讀算法會根據用戶上一次讀取的頁面的使用情況評估出預讀的窗口,決定要讀多少頁面。讀頁面的模塊會先檢查要讀取頁面在 pagecache裡面是否已經存在,如果不存在的話就需要發起IO請求,讀取相應的頁面。
這個預讀的關鍵參數有3個: 用戶的req_size, 預讀算法評估出來的nr_to_read,以及實際上IO讀取的頁面數actual。
接下來我們就是要查看系統是如何運作的,所以我首先寫了個systemtap腳本叫做ratop.stp來獲取這些數據:
$ uname -r
2.6.18-164.el5
$ rpm -i kernel-debuginfo-common-2.6.18-164.el5.x86_64.rpm
$ rpm -i kernel-debuginfo-2.6.18-164.el5.x86_64.rpm
$ cat > ratop.stp
#!/usr/bin/stap -DMAXMAPENTRIES=10240
global total, skip
global req, to_read, actual
global __inode_filename
probe kernel.function("page_cache_readahead")
{
ino = __file_ino($filp)
req[ino]+=$req_size;
total++;
if($ra->flags & 0x2) skip++;
}
probe kernel.function("__do_page_cache_readahead").return
{
ino = __file_ino($filp)
to_read[ino]+= $nr_to_read;
if($return>0) actual[ino]+=$return;
}
probe timer.ms(5000)
{
if(total)
{
foreach( ino in req-)
{
s0+= req[ino];
s1+= to_read[ino]
s2+= actual[ino];
}
printf("\\n%25s, %5s%6d, %5s%6d, %5s%8d, %5s%8d, %5s%8d\\n\\n",
ctime(gettimeofday_s()),
"TOTAL:", total,
"SKIP:", skip,
"REQ:",s0,
"TO_RD:",s1,
"NR_RD:",s2
)
/* print header */
printf("%25s %8s %8s %8s\\n",
"FILENAME","REQ","TO_RD","NR_RD")
foreach( ino in req- limit 20)
printf("%25s %8d %8d %8d\\n", find_filename(ino), req[ino], to_read[ino], actual[ino]);
}
delete total;
delete skip;
delete req;
delete to_read;
delete actual;
}
probe generic.fop.open
{
__inode_filename[ino]= filename
}
function find_filename(ino)
{
return __inode_filename[ino]==""?sprint(ino):__inode_filename[ino];
}
probe begin
{
println("::");
}
CTRL +D
$ chmod +x ratop.stp
$ sudo ./ratop.stp
::
Tue May 31 05:41:37 2011, TOTAL: 2321, SKIP: 0, REQ: 6308, TO_RD: 6308, NR_RD: 1424
FILENAME REQ TO_RD NR_RD
056878.sst 15 15 0
062889.sst 13 13 6
..
其中各個參數含義解釋如下:
TOTAL: 系統共調用了多少次預讀
SKIP: 由於頁面在PAGECACHE中存在,略過多少次預讀
REQ: 用戶准備讀取的頁面數
TO_RD:預讀算法告訴我們要讀取的頁面數
NR_RD:實際IO系統讀取的頁面數
這個腳本每5秒打印下系統目前的預讀情況。
好吧,有了這個工具我們就可以做實驗了。
先在一個終端下運行我們的腳本:
$ sudo ./ratop.stp
::
#等著出數據...
然後在另外一個終端下做實驗:
#准備個數據文件
$ dd if=/dev/zero of=test count=1024 bs=4096
1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 0.008544 seconds, 491 MB/s
#清空pagecache
$ sudo sysctl vm.drop_caches=3
vm.drop_caches = 3
#第一次拷貝
$ cp test junk && sleep 5
#第二次拷貝
$ cp test junk
我們就可以在之前的腳本窗口裡看到下面的信息:
#第一次拷貝test,我們可以看到 用戶要1025個頁面,預讀決定讀1084,但是實際IO讀了1024,很合理,因為當時pagecache是空的
Tue May 31 05:50:21 2011, TOTAL: 1038, SKIP: 0, REQ: 1039, TO_RD: 1320, NR_RD: 1109
FILENAME REQ TO_RD NR_RD
test 1025 1084 1024 cp 3 36 18 ... #第二次拷貝test,我們可以看到 用戶要1025個頁面,預讀決定讀284,但是實際IO讀了0,很合理,因為所有的頁面在pagecache裡面都已經
test 1025 1084 1024
cp 3 36 18
...
#第二次拷貝test,我們可以看到 用戶要1025個頁面,預讀決定讀284,但是實際IO讀了0,很合理,因為所有的頁面在pagecache裡面都已經存在
Tue May 31 05:50:46 2011, TOTAL: 1038, SKIP: 804, REQ: 1039, TO_RD: 328, NR_RD: 0
FILENAME REQ TO_RD NR_RD
test 1025 284 0
cp 3 4 0
...
Linux系統不僅為文件的讀取提供自動預讀,還提供了readahead這樣的系統調用和工具,幫助用戶主動預加載數據,我們演示下:
$ readahead junk
Preloaded 0 files (0 KB) in 5 ms
另外一個窗口說:
Tue May 31 05:57:45 2011, TOTAL: 1044, SKIP: 805, REQ: 1045, TO_RD: 348, NR_RD: 0
FILENAME REQ TO_RD NR_RD
junk 1026 284 0
readahead 3 4 0
Linux還支持對每個設備設定預讀的默認大小,不同的大小可以用來控制預讀的力度,用戶可以自行改變:
$ pwd
/sys/block/sda/queue
$ cat read_ahead_kb
128
$ echo 256 |sudo tee read_ahead_kb
256
後續我會用這個工具分析leveldb數據庫的行為,歡迎關注!
總結: 如果actual讀比用戶req的要多很多, 那麼我們的很多預讀就浪費了,可以考慮減少預讀的大小