inux系统很重要的一个性能提升点就是它的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请求,读取相应的页面。还有个路径就是在文件mmap缺页的时候filemap_nopage调用do_page_cache_readahead进行预读, 不过这个路径在通常的环境里概率不高.
这个预读的关键参数有3个: 用户的req_size, 预读算法评估出来的nr_to_read,以及实际上IO读取的页面数actual。
接下来我们就是要查看系统是如何运作的,所以我首先写了个systemtap脚本叫做ratop.stp来获取这些数据:
$ uname -r |
2.6.18-164.el5 |
|
$ sudo rpm -i kernel-debuginfo-common-2.6.18-164.el5.x86_64.rpm |
$ sudo rpm -i kernel-debuginfo-2.6.18-164.el5.x86_64.rpm |
$ cat > ratop.stp |
#!/usr/bin/stap -DMAXMAPENTRIES=10240 -DMAXACTION=9999 |
global total, skip |
global req, to_read, actual |
global __inode_filename |
//generic_file_aio_read path |
probe kernel. function ( "page_cache_readahead" ) { |
ino = __file_ino($filp) |
req[ino] += $req_size; |
total++; |
if ($ra->flags & 0x2) skip++; |
} |
//mmap path |
probe kernel. function ( "do_page_cache_readahead" ) { |
ino = __file_ino($filp) |
req[ino]+=$nr_to_read; |
} |
// |
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 timer.s(180) { |
delete __inode_filename |
} |
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里面都已经存在 |
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这样的系统调用和工具,帮助用户主动预加载数据,我们演示下:
$ sudo sysctl vm.drop_caches=3 |
vm.drop_caches = 3 |
$ cat > file .lst |
test |
junk |
CTRL+D |
$ readahead file .lst |
Preloaded 2 files (409600 KB) in 863 ms |
$ cp test /dev/null |
另外一个窗口说:
# 说明文件确实被预加载了。 |
Tue May 31 08:51:08 2011, TOTAL: 1038, SKIP: 804, REQ: 1039, TO_RD: 392, NR_RD: 29 |
FILENAME REQ TO_RD NR_RD |
test 1025 284 0 |
cp 3 36 18 |
Linux还支持对每个设备设定预读的默认大小,不同的大小可以用来控制预读的力度,用户可以自行改变:
$ pwd |
/sys/block/sda/queue |
$ cat read_ahead_kb |
128 |
$ echo 256 | sudo tee read_ahead_kb |
256 |
后续我会用这个工具分析leveldb数据库的行为,欢迎关注!
总结: 如果actual读比用户req的要多很多, 那么我们的很多预读就浪费了,可以考虑减少预读的大小。
玩得开心!
PS. 附上不分文件统计的版本,方便使用:
$ cat ra.stp |
global req, to_read, actual |
//generic_file_aio_read path |
probe kernel.function("page_cache_readahead") { |
req += $req_size; |
} |
//mmap path |
probe kernel.function("do_page_cache_readahead") { |
req += $nr_to_read; |
} |
// |
probe kernel.function("__do_page_cache_readahead").return { |
to_read += $nr_to_read; |
if($return>0) actual += $return; |
} |
probe timer.s(5) { |
if(req) { |
printf("%25s, %5s%8d, %5s%8d, %5s%8d\n", |
ctime(gettimeofday_s()), |
"REQ:", req, |
"TO_RD:", to_read, |
"NR_RD:", actual |
) |
delete req; |
delete to_read; |
delete actual; |
} |
} |
probe begin { |
println("::"); |
} |
Post Footer automatically generated by wp-posturl plugin for wordpress.