Rocksdb 通过posix_advise 让内核减少在page_cache的预读
文章目錄
- 1. 問題排查
- 確認(rèn)I/O完全/大多數(shù)來自于rocksdb
- 確認(rèn)此時(shí)系統(tǒng)只使用了rocksdb的Get來讀
- 確認(rèn)每次系統(tǒng)調(diào)用下發(fā)讀的請(qǐng)求大小
- 確認(rèn)是否在內(nèi)核發(fā)生了預(yù)讀
- 2. 問題原因
- 內(nèi)核預(yù)讀機(jī)制
- page_cache_sync_readahead
- ondemand_readahead
- 3. 優(yōu)化
事情起源于 組內(nèi)的分布式kv 系統(tǒng)使用rocksdb過程中發(fā)現(xiàn)磁盤有超過預(yù)期3-4x的讀I/O問題,因?yàn)榻沽薭lock-cache,也就是每一次Get會(huì)對(duì)應(yīng)一次磁盤I/O,剛好上層Get的qps和下層磁盤的r/s 基本吻合,也就是一次讀,讀了超過3個(gè)page。但是實(shí)際寫入的value也就 128B,按照nvme的block size 4K的配置,讀上來3個(gè)page則遠(yuǎn)超過了我們的預(yù)期。
本文涉及的代碼 rocksdb版本是 6.4.6,linux 內(nèi)核版本是 3.10.1
1. 問題排查
看到了嚴(yán)重的讀放大現(xiàn)象之后,接下來一探問題的原因。
確認(rèn)I/O完全/大多數(shù)來自于rocksdb
當(dāng)我們不知道kv系統(tǒng)內(nèi)部如何使用rocksdb的情況下需要確認(rèn)這一些IO的來源。
-
抓I/O 線程棧
-
sudo iotop能直接看到系統(tǒng)中的磁盤i/o來源的線程如果此時(shí)能夠看到具體的線程名稱,確認(rèn)是在讀rocksdb,OK,跳過這一步
-
sudo pstack tid抓取一個(gè)線程棧pstack的邏輯是通過gdb attach進(jìn)去 執(zhí)行bt,多抓幾次就能夠知道I/O線程大多數(shù)的時(shí)間在干嘛
這里發(fā)現(xiàn)確實(shí)是在rocksdb的
Get調(diào)用棧上Thread 1 (process 1201132): #0 0x00007fa59de53f73 in pread64 () from /lib64/libpthread.so.0 #1 0x00000000009020c3 in pread (__offset=56899894, __nbytes=3428, __buf=0x7fa4a83bb670, __fd=<optimized out>) at /usr/include/bits/unistd.h:83 #2 rocksdb::PosixRandomAccessFile::Read(unsigned long, unsigned long, rocksdb::IOOptions const&, rocksdb::Slice*, char*, rocksdb::IODebugContext*) const () at thirdpartty/rocksdb/env/io_posix.cc:453 #3 0x0000000000a074f7 in rocksdb::RandomAccessFileReader::Read(unsigned long, unsigned long, rocksdb::Slice*, char*, bool) const () at thirdpartty/rocksdb/monitoring/statistics.h:127 #4 0x000000000097b8e9 in rocksdb::BlockFetcher::ReadBlockContents() () at thirdpartty/rocksdb/table/format.h:45 #5 0x000000000095bd87 in rocksdb::BlockBasedTable::MaybeReadBlockAndLoadToCache<rocksdb::Block> (this=0xf2083e80, prefetch_buffer=0x0, ro=..., handle=..., uncompression_dict=..., block_entry=0x7fa4a83bcc10, block_type=rocksdb::kData, get_context=0x7fa4a83bd630, lookup_context=0x7fa4a83bcf50, contents=0x0) at thirdpartty/rocksdb/include/rocksdb/cache.h:272 #6 0x000000000095c1d1 in rocksdb::BlockBasedTable::RetrieveBlock<rocksdb::Block> (this=this@entry=0xf2083e80, prefetch_buffer=prefetch_buffer@entry=0x0, ro=..., handle=..., uncompression_dict=..., block_entry=0x7fa4a83bcc10, block_type=rocksdb::kData, get_context=0x7fa4a83bd630, lookup_context=0x7fa4a83bcf50, for_compaction=false, use_cache=true) at thirdpartty/rocksdb/table/block_based/block_based_table_reader.h:585 #7 0x000000000095ef3a in rocksdb::BlockBasedTable::NewDataBlockIterator<rocksdb::DataBlockIter> (this=this@entry=0xf2083e80, ro=..., handle=..., input_iter=input_iter@entry=0x7fa4a83bd190, block_type=block_type@entry=rocksdb::kData, get_context=get_context@entry=0x7fa4a83bd630, lookup_context=0x7fa4a83bcf50, s=..., prefetch_buffer=0x0, for_compaction=false) at thirdpartty/rocksdb/monitoring/perf_step_timer.h:41 #8 0x000000000096a4a1 in rocksdb::BlockBasedTable::Get(rocksdb::ReadOptions const&, rocksdb::Slice const&, rocksdb::GetContext*, rocksdb::SliceTransform const*, bool) () at thirdpartty/rocksdb/table/block_based/block_based_table_reader.cc:3288 #9 0x00000000008a1c0c in rocksdb::TableCache::Get(rocksdb::ReadOptions const&, rocksdb::InternalKeyComparator const&, rocksdb::FileMetaData const&, rocksdb::Slice const&, rocksdb::GetContext*, rocksdb::SliceTransform const*, rocksdb::HistogramImpl*, bool, int) () at thirdpartty/rocksdb/db/table_cache.cc:406
-
-
抓取進(jìn)程都在操作哪一些文件,和上面的進(jìn)程棧信息double check一下
這里需要系統(tǒng)支持eBPF,使用了一個(gè)bcc工具集中的一個(gè)opensnoop命令。opensnoop -p pid能夠看到當(dāng)前進(jìn)程按順序操作的文件,都會(huì)打印出來# opensnoop -p 1200034|grep sst 1200034 rocksdb:low3 6929 0 ../db/13/064388.sst 1200034 rocksdb:low3 14988 0 ../db/13/064389.sst 1200034 rocksdb:low3 18074 0 ../db/13/064390.sst 1200034 rocksdb:low12 3158 0 ../db/11/064350.sst 1200034 rocksdb:low0 9030 0 ../db/31/064494.sst 1200034 rocksdb:low1 10111 0 ../db/19/064495.sst 1200034 rocksdb:low2 1691 0 ../db/0/064570.sst 1200034 test_process/w4- 2879 0 00000000001494113842.sst 1200034 rocksdb:low10 1974 0 ../db/7/064437.sst 1200034 rocksdb:low4 3696 0 ../db/14/064445.sst 1200034 rocksdb:low12 3158 0 ../db/11/064351.sst 1200034 rocksdb:low11 3017 0 ../db/21/064524.sst可以看到除了rocksdb的sst文件之外,還有一個(gè)其他的文件,但是我們主體的讀取都是sst,所以基本能夠確認(rèn)磁盤的讀i/o都是來源于rocksdb.
確認(rèn)此時(shí)系統(tǒng)只使用了rocksdb的Get來讀
到這里,我們能夠確認(rèn)I/O是由rocksdb產(chǎn)生的,可能會(huì)想是不是kv系統(tǒng)中使用rocksdb的方式有問題,他們可能不僅僅是用了Get,在某一個(gè)他們也不清楚的線程里用了迭代器掃描,才產(chǎn)生這么多的I/O,我們想要確認(rèn)這個(gè)問題。這個(gè)時(shí)候之前使用的pstack調(diào)用棧就不夠用了,因?yàn)樗皇且粋€(gè)線程,而我們要用它抓更多的線程比較麻煩,簡單且直觀的辦法就是火焰圖。
現(xiàn)在有大量的I/O,而且rocksdb的操作基本都是on-cpu的,所以直接看on-cpu的火焰圖就非常容易得看到IO調(diào)用棧的來源了。
通過如下腳本立即抓取
#!/bin/sh
DIR=./git clone https://github.com/brendangregg/FlameGraph # clone 火焰圖目錄
if [ $? -ne 0 ]; thenecho "clone FlameGraph failed"exit -1
ficd FlameGraph
sudo perf record -F 99 -p $1 -g -o $DIR/"$1".data -- sleep $2
sudo perf script -i $DIR/"$1".data > $DIR/"$1".perf
stackcollapse-perf.pl $DIR/"$1".perf > $DIR/"$1".folded
flamegraph.pl $DIR/"$1".folded > $DIR/"$1".svgcp $DIR/cpu1.svg ../
會(huì)在當(dāng)前目錄下生成一個(gè)$pid.svg的文件,使用瀏覽器打開即可看到完整的on-cpu調(diào)用棧
看起來確實(shí)是大多數(shù)的cpu都消耗在了rocksdb的Get調(diào)用棧上了,好吧。。。問題躲不開了。
確認(rèn)每次系統(tǒng)調(diào)用下發(fā)讀的請(qǐng)求大小
按照我們對(duì)磁盤讀取的正常邏輯理解,如果讀取的數(shù)據(jù)塊大小小于正常的磁盤塊4k大小的話 從磁盤文件系統(tǒng)下發(fā)的讀取請(qǐng)求會(huì)填充成一個(gè)4k的cache頁面,讀取一個(gè)磁盤的block。
所以為什么它這里會(huì)讀取這么多的block,估算上層的總磁盤帶寬/總qps 可以得到每個(gè)請(qǐng)求大概讀了3-4個(gè)block,而實(shí)際的value大小也就128B,讀取sst的時(shí)候會(huì)把這個(gè)value所在的datablock整個(gè)讀上來,默認(rèn)一個(gè)datablock是4k,這就很奇怪了。
為了確認(rèn)rocksdb側(cè)的pread64系統(tǒng)調(diào)用確實(shí)讀的內(nèi)容比較少,我們要獲取系統(tǒng)調(diào)用每次讀的請(qǐng)求大小,看是不是rocksdb 拼接的請(qǐng)求有問題。
執(zhí)行命令:sudo strace -ttt -T -F -p 1200034 -e trace=pread64 -o strace.txt 追蹤pread64系統(tǒng)調(diào)用,并打印每個(gè)系統(tǒng)調(diào)用的時(shí)間,最后的結(jié)果保存在strace.txt中。
1200069 1619615339.683332 pread64(3686, <unfinished ...>
1200067 1619615339.683750 pread64(23901, <unfinished ...>
1200064 1619615339.683761 pread64(6470, <unfinished ...>
1200060 1619615339.683776 pread64(17541, <unfinished ...>
1200058 1619615339.683784 pread64(15210, <unfinished ...>
1200069 1619615339.686670 <... pread64 resumed> "\0\34\232\1abcdefghij2376553061\1\202\326\216X\0\0\0"..., 3941, 26362727) = 3941 <0.002925>
1200067 1619615339.686696 <... pread64 resumed> "\320\0\0\0\0\0\0\0\341\307\326\315\246I\0\0\6\24\0\0\0abcdefghij3"..., 3950, 30242502) = 3950 <0.002944>
1200064 1619615339.686711 <... pread64 resumed> "`\30\260\200\237\356\354\264\22\21\220\23\336%\0\0\0\0m-\323Y\0\0\0\0\320\0\0\0\0\0"..., 3422, 18387244) = 3422 <0.002949>
1200060 1619615339.686726 <... pread64 resumed> "\311\4\323\32\0\32\6abcdefghij46297319\21\377\377\377\377\377\377"..., 3941, 31861449) = 3941 <0.002948>
1200058 1619615339.686734 <... pread64 resumed> "7,<Qz p&~dHZ!i5kvVUU^Jgc%vA/F;uL"..., 3977, 15684275) = 3977 <0.002949>
...
如果想要確認(rèn)這個(gè)pread64讀的文件句柄是rocksdb的sst文件,可以通過 sudo ls -l /proc/1200034/fd/15210 來看這個(gè)進(jìn)程打開的文件句柄確實(shí)是連結(jié)到了sst文件。
通過上面抓到的pread64系統(tǒng)調(diào)用信息,可以很明顯的發(fā)現(xiàn)系統(tǒng)調(diào)用下發(fā)的是小于4K的請(qǐng)求大小。。。我擦嘞。
立即通過btrace再抓一下磁盤I/O:
$ sudo btrace -a read /dev/nvme0n1
259,0 32 1 0.000000000 1201042 Q RA 5033391024 + 32 [test_process]
259,0 32 2 0.000001625 1201042 G RA 5033391024 + 32 [test_process]
259,0 32 3 0.000001926 1201042 P N [test_process]
259,0 32 4 0.000002279 1201042 U N [test_process] 1
259,0 32 5 0.000003219 1201042 D RA 5033391024 + 32 [test_process]
259,0 33 1 0.000010787 1201046 Q RA 3397669536 + 24 [test_process]
259,0 33 2 0.000012535 1201046 G RA 3397669536 + 32 [test_process]
259,0 33 3 0.000012992 1201046 P N [test_process]
259,0 33 4 0.000013308 1201046 U N [test_process] 1
259,0 33 5 0.000014375 1201046 D RA 3397669536 + 32 [test_process]
259,0 3 1 0.000024160 1201030 Q RA 32733208 + 32 [test_process]
259,0 3 2 0.000025016 1201030 G RA 32733208 + 24 [test_process]
259,0 3 3 0.000025320 1201030 P N [test_process]
259,0 3 4 0.000025660 1201030 U N [test_process] 1
259,0 3 5 0.000026382 1201030 D RA 32733208 + 32 [test_process]
259,0 3 6 0.000032077 843424 C RA 5618458520 + 32 [0]
259,0 32 6 0.000067811 1201120 Q RA 4936712824 + 32 [test_process]
259,0 32 7 0.000068510 1201120 G RA 4936712824 + 32 [test_process]
...
可以看到確實(shí),從磁盤上讀到了很多超過2個(gè)4k的block,也就是系統(tǒng)調(diào)用pread64下發(fā)了小于一個(gè)block的請(qǐng)求,而落到磁盤的I/O達(dá)到了4個(gè)block。。。interesting。
確認(rèn)是否在內(nèi)核發(fā)生了預(yù)讀
只能進(jìn)入內(nèi)核邏輯了,看起來像是操作系統(tǒng)的預(yù)讀邏輯。
順著火焰圖的調(diào)用棧來看,我們的pread系統(tǒng)調(diào)用的調(diào)用棧如下:
sys_pread64vfs_readdo_sync_readxfs_file_aio_readxfs_file_buffered_aio_readgeneric_file_aio_readdo_generic_file_readpage_cache_sync_readahead
實(shí)際由page-cache下發(fā)讀取的頁面?zhèn)€數(shù)是在page_cache_sync_readahead 的參數(shù)中。
這里我們拋開內(nèi)核函數(shù)的邏輯,想要單純確認(rèn)一下do_generic_file_read下發(fā)的請(qǐng)求數(shù)目,可以通過stap來抓取一下這個(gè)函數(shù)的變量
void page_cache_sync_readahead(struct address_space *mapping,struct file_ra_state *ra, struct file *filp,pgoff_t offset, unsigned long req_size);#!/bin/stapprobe kernel.function("page_cache_sync_readahead").call {printf("req_size : %lu\n", $req_size);
}
可以發(fā)現(xiàn)page_cache_sync_readahead這個(gè)函數(shù)的時(shí)候僅僅才1個(gè)頁面。
繼續(xù)向下,看到page_cache_sync_readahead 內(nèi)部實(shí)現(xiàn)有兩個(gè)分支:
void page_cache_sync_readahead(struct address_space *mapping,struct file_ra_state *ra, struct file *filp,pgoff_t offset, unsigned long req_size)
{/* no read-ahead */if (!ra->ra_pages)return;/* be dumb */if (filp && (filp->f_mode & FMODE_RANDOM)) {force_page_cache_readahead(mapping, filp, offset, req_size);return;}/* do read-ahead */ondemand_readahead(mapping, ra, filp, false, offset, req_size);
}
這里后續(xù)會(huì)說明這里的兩個(gè)分支到底作用為何?
不過抓這個(gè)函數(shù)的stap發(fā)現(xiàn)只能讀到1個(gè)page的請(qǐng)求,也就是do_generic_file_read只下發(fā)了1個(gè)page。
通過注釋能夠看到如果要預(yù)讀的話應(yīng)該會(huì)在ondemand_readahead函數(shù)中,結(jié)合火焰圖,這個(gè)函數(shù)預(yù)讀的話最終會(huì)走到ra_submit --> __do_page_cache_readahead 邏輯,同樣的stap抓一下這個(gè)函數(shù)的nr_to_read的參數(shù),發(fā)現(xiàn)確實(shí)預(yù)讀到了3-4個(gè)page。
定位到了函數(shù):ondemand_readahead ,預(yù)讀的多個(gè)頁面就是在這里填充的,而ra_submit只是很薄的一層調(diào)用:
unsigned long ra_submit(struct file_ra_state *ra,struct address_space *mapping, struct file *filp)
{int actual;actual = __do_page_cache_readahead(mapping, filp,ra->start, ra->size, ra->async_size);return actual;
}
到此我們結(jié)合火焰圖的調(diào)用棧知道了具體的哪個(gè)內(nèi)核函數(shù)發(fā)生了預(yù)讀,但是為什么還不清楚。
2. 問題原因
內(nèi)核預(yù)讀機(jī)制
內(nèi)核的預(yù)讀機(jī)制起源是我們還處于大多數(shù)存儲(chǔ)場景都是HDD介質(zhì),磁盤的轉(zhuǎn)動(dòng)和磁頭的尋道消耗太多的時(shí)間,而我們想要在HDD的基礎(chǔ)上提升讀性能,可以減少磁頭移動(dòng)的次數(shù),連續(xù)讀取多個(gè)扇區(qū)的數(shù)據(jù)內(nèi)容就可以。實(shí)際的操作系統(tǒng)中,用戶讀取一個(gè)文件,一般會(huì)從頭讀到尾,這一些文件在磁盤上的存儲(chǔ)都是連續(xù)的扇區(qū),也就是可以利用預(yù)讀來一次讀取多個(gè)扇區(qū),減少磁頭頻繁移動(dòng)尋道。
當(dāng)然,這個(gè)預(yù)讀機(jī)制在我們的NVME上同樣有效,現(xiàn)在大多數(shù)的nvme底層存儲(chǔ)介質(zhì)還是nand ,使用的是浮柵晶體管做存儲(chǔ)單元,通過兩極加正反電壓來控制浮柵層內(nèi)電子的移動(dòng)情況。詳細(xì)的底層nand存儲(chǔ)介質(zhì)原理介紹感興趣的同學(xué)可以參考從NMOS 和 PCM 底層存儲(chǔ)單元 來看NAND和3D XPoint的本質(zhì)區(qū)別。回到要說的預(yù)讀問題,有了預(yù)讀,可以減少一次或者多次針對(duì)nvme的IO,也就能節(jié)省幾十us-幾個(gè)ms 的時(shí)間,極大得提升了系統(tǒng)的響應(yīng)時(shí)間。
預(yù)讀(read-ahead) 算法預(yù)測即將訪問的頁面,并提前將他們讀入到page-cache中,后續(xù)的讀取就不需要產(chǎn)生io了。
主要任務(wù):
- 批量:把小I/O 聚集為大I/O,改善磁盤利用率,提升系統(tǒng)吞吐
- 提前:對(duì)應(yīng)用程序隱藏磁盤的延遲,加快系統(tǒng)響應(yīng)時(shí)間
- 預(yù)測:屬于預(yù)讀算法的核心任務(wù)。前兩個(gè)功能都依賴準(zhǔn)確的預(yù)測能力。包括linux , freeBSD, solaris 等主流操作系統(tǒng)都遵循一個(gè)原則:預(yù)讀僅針對(duì)順序讀模式。這個(gè)模式比較簡單且普遍,提升效果也更明顯,但是隨機(jī)讀模式對(duì)于內(nèi)核來說也是難以預(yù)測的。
觸發(fā)預(yù)讀 的條件:
-
內(nèi)核處理用戶進(jìn)程讀請(qǐng)求時(shí)調(diào)用:
page_cache_sync_readahead或page_cache_async_readahead -
內(nèi)核為文件內(nèi)存映射分配一個(gè)頁面時(shí),即調(diào)用
mmap -
用戶進(jìn)程執(zhí)行系統(tǒng)調(diào)用
readahead -
用戶進(jìn)程在打開文件之后執(zhí)行
posix_fadvise系統(tǒng)調(diào)用 -
用戶進(jìn)程執(zhí)行
madvise()系統(tǒng)調(diào)用,使用MADV_WILLNEEDflag 通知內(nèi)核文件內(nèi)存映射的指定區(qū)域?qū)頃?huì)被訪問
預(yù)讀的過程 是:
預(yù)讀算法的實(shí)現(xiàn)是通過維護(hù)兩個(gè)窗口:當(dāng)前窗口(current window) 和 前進(jìn)窗口(ahead window)
用戶進(jìn)程當(dāng)前訪問的page 都會(huì)在current window中,當(dāng)內(nèi)核判讀用戶進(jìn)程是順序訪問 且 初始訪問頁面是在當(dāng)前窗口時(shí)就檢查前進(jìn)窗口是否建立,如果未建立,則建立一個(gè)新的前進(jìn)窗口,并為對(duì)應(yīng)的文件頁面觸發(fā)讀操作。如果用戶進(jìn)程的讀命中了前進(jìn)窗口的頁面,則將前進(jìn)窗口切換為當(dāng)前窗口。
如上圖,用戶訪問當(dāng)前窗口的時(shí)候會(huì)構(gòu)建前進(jìn)窗口,理想情況下當(dāng)前窗口的頁面都是已經(jīng)被cache住的,而前進(jìn)窗口則還在調(diào)度頁面到cache;當(dāng)然也會(huì)有正在訪問的當(dāng)前窗口的頁面正在調(diào)度的情況。
如果用戶進(jìn)程訪問的頁面命中了前進(jìn)窗口,則前進(jìn)窗口會(huì)切換為當(dāng)前窗口,實(shí)際的前進(jìn)窗口的大小會(huì)根據(jù)命中情況動(dòng)態(tài)調(diào)整,命中到前進(jìn)窗口的page越多,下次創(chuàng)建的前進(jìn)窗口的大小則會(huì)更大一些,否則就會(huì)縮小。
page_cache_sync_readahead
通過前面的問題分析過程,我們知道了在page_cache_sync_readahead的內(nèi)部調(diào)用中發(fā)生了預(yù)讀,用戶下發(fā)的是一個(gè)頁面,在它內(nèi)部卻讀了超過3個(gè)頁面。
看一下這個(gè)函數(shù)的邏輯,參數(shù)含義如下:
- mapping: 文件擁有者的address_space對(duì)象
- ra: 包含此頁面的文件
file_ra_state描述符,持有是否進(jìn)行預(yù)讀的標(biāo)記 - filp: 文件對(duì)象
- offset: 頁面在文件中的起始偏移量
- req_size: 完成當(dāng)前讀操作需要的頁面數(shù)
這個(gè)函數(shù)一般會(huì)在cache-miss的時(shí)候被調(diào)用,即它的調(diào)用者發(fā)現(xiàn)文件page不在cache中,觸發(fā)這個(gè)函數(shù)去讀對(duì)應(yīng)的page,當(dāng)然也包括了預(yù)讀,因?yàn)榍懊嫖覀兺ㄟ^systemtap抓這個(gè)函數(shù)的時(shí)候也只是下發(fā)了一個(gè)page,最后它內(nèi)部的經(jīng)過ondemand_readahead的處理返回了3個(gè)page。
void page_cache_sync_readahead(struct address_space *mapping,struct file_ra_state *ra, struct file *filp,pgoff_t offset, unsigned long req_size)
{/* no read-ahead */// 如果前面填充的readahead-pages是空的話直接返回if (!ra->ra_pages)return;/* be dumb */// 這里 FMODE_RANDOM 是由posix_advise指定的隨機(jī)讀標(biāo)記,不需要預(yù)讀if (filp && (filp->f_mode & FMODE_RANDOM)) {force_page_cache_readahead(mapping, filp, offset, req_size);return;}/* do read-ahead */// 真正執(zhí)行預(yù)讀的邏輯。ondemand_readahead(mapping, ra, filp, false, offset, req_size);
}
可以看到,第二個(gè)分支中 會(huì)判斷文件mode是否為FMODE_RANDOM ,如果是的話就不執(zhí)行預(yù)讀了,僅僅讀當(dāng)前用戶進(jìn)程需要讀的page。這個(gè)標(biāo)記可以由用戶進(jìn)程打開文件的時(shí)候通過posix_advise來指定。
指定的邏輯在posix_advise系統(tǒng)調(diào)用的實(shí)現(xiàn)中:
SYSCALL_DEFINE4(fadvise64_64, int, fd, loff_t, offset, loff_t, len, int, advice) {...case POSIX_FADV_RANDOM:spin_lock(&f.file->f_lock);f.file->f_mode |= FMODE_RANDOM;spin_unlock(&f.file->f_lock);break;...
}
回到我們要討論的預(yù)讀邏輯中,接下來看一下真正執(zhí)行預(yù)讀的函數(shù)ondemand_readahead
ondemand_readahead
這個(gè)函數(shù)主要是根據(jù)傳入的file_ra_state描述符執(zhí)行一些動(dòng)作,函數(shù)參數(shù)還是剛才page_cache_sync_readahead函數(shù)傳入進(jìn)來的。
主體邏輯是
- 首先判斷讀取是否從文件開頭開始,如果是,則初始化預(yù)讀信息。默認(rèn)設(shè)置的是4個(gè)page
- 如果不是文件頭,則判斷是否是順序訪問(連續(xù)讀),如果是,則擴(kuò)大預(yù)讀數(shù)量,一般是上次預(yù)讀數(shù)量x2
- 如果不是順序訪問, 則認(rèn)為是隨機(jī)讀,不適合預(yù)讀,只會(huì)讀取sys_read請(qǐng)求的page數(shù)量。
- 最后調(diào)用
ra_submit提交讀請(qǐng)求
static unsigned long
ondemand_readahead(struct address_space *mapping,struct file_ra_state *ra, struct file *filp,bool hit_readahead_marker, pgoff_t offset,unsigned long req_size)
{unsigned long max = max_sane_readahead(ra->ra_pages);/** start of file*/// 判斷是否是從文件開頭讀取,offset=0// 如果是,則會(huì)調(diào)用get_init_ra_size 初始化預(yù)讀頁面if (!offset)goto initial_readahead;/** It's the expected callback offset, assume sequential access.* Ramp up sizes, and push forward the readahead window.*/// 如果不是從文件開始預(yù)讀,且是順序讀(發(fā)現(xiàn)當(dāng)前的偏移地址是上次讀的起始地址+size)// 通過get_next_ra_size 擴(kuò)大預(yù)讀窗口if ((offset == (ra->start + ra->size - ra->async_size) ||offset == (ra->start + ra->size))) {ra->start += ra->size;ra->size = get_next_ra_size(ra, max);ra->async_size = ra->size;goto readit; // 進(jìn)入ra_submit執(zhí)行實(shí)際的預(yù)讀}/** Hit a marked page without valid readahead state.* E.g. interleaved reads.* Query the pagecache for async_size, which normally equals to* readahead size. Ramp it up and use it as the new readahead size.*/// 發(fā)現(xiàn)當(dāng)前文件被打上了一個(gè)預(yù)讀失效的標(biāo)記,這里默認(rèn)傳入的是falseif (hit_readahead_marker) {pgoff_t start;rcu_read_lock();start = radix_tree_next_hole(&mapping->page_tree, offset+1,max);rcu_read_unlock();if (!start || start - offset > max)return 0;ra->start = start;ra->size = start - offset; /* old async_size */ra->size += req_size;ra->size = get_next_ra_size(ra, max);ra->async_size = ra->size;goto readit;}/** oversize read*/// 大塊預(yù)讀,即sys_read請(qǐng)求的頁面大小超過了最大的預(yù)讀設(shè)置// 重新初始化預(yù)讀窗口,預(yù)讀更多的頁面if (req_size > max)goto initial_readahead;/** sequential cache miss*/// 內(nèi)核重新發(fā)現(xiàn)當(dāng)前讀是順序讀,創(chuàng)建新的當(dāng)前窗口if (offset - (ra->prev_pos >> PAGE_CACHE_SHIFT) <= 1UL)goto initial_readahead;/** Query the page cache and look for the traces(cached history pages)* that a sequential stream would leave behind.*/// 這個(gè)函數(shù)里面發(fā)現(xiàn)不是順序讀,會(huì)返回0,直接不進(jìn)行預(yù)讀了// 后續(xù)的__do_page_cache_readahead 函數(shù)readahead size被設(shè)置為0 了if (try_context_readahead(mapping, ra, offset, req_size, max))goto readit;/** standalone, small random read* Read as is, and do not pollute the readahead state.*/return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);// 初始化當(dāng)前預(yù)讀窗口
initial_readahead:ra->start = offset;ra->size = get_init_ra_size(req_size, max);ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;readit:/** Will this read hit the readahead marker made by itself?* If so, trigger the readahead marker hit now, and merge* the resulted next readahead window into the current one.*/if (offset == ra->start && ra->size == ra->async_size) {ra->async_size = get_next_ra_size(ra, max);ra->size += ra->async_size;}// 實(shí)際進(jìn)行預(yù)讀指定page個(gè)數(shù)的調(diào)用return ra_submit(ra, mapping, filp);
}
3. 優(yōu)化
到這里我們就知道了操作系統(tǒng)的預(yù)讀優(yōu)化主要是針對(duì)順序讀場景,且會(huì)動(dòng)態(tài)調(diào)整預(yù)讀窗口的大小。那回到我們r(jià)ocksdb這里,業(yè)務(wù)下發(fā)的是隨機(jī)讀,但部分讀請(qǐng)求顯然是發(fā)生了預(yù)讀。因?yàn)闇y試的場景是用極少blockcache的,也就是這一些想要預(yù)讀到page-cache的datablock 大多數(shù)都不會(huì)被cache住。
那我們有沒有辦法完全不讓操作系統(tǒng)預(yù)讀呢,減少這個(gè)場景下的預(yù)讀I/O 。
回到rocksdb 讀datablock 的邏輯:
rocksdb::DBImpl::Getrocksdb::DBImpl::GetImplrocksdb::Version::Getrocksdb::TableCache::Getrocksdb::BlockBasedTable::Getrocksdb::BlockBasedTable::NewDataBlockIterator<rocksdb::DataBlockIter>rocksdb::BlockBasedTable::RetrieveBlock<rocksdb::Block>rocksdb::BlockBasedTable::MaybeReadBlockAndLoadToCache<rocksdb::Block>rocksdb::BlockFetcher::ReadBlockContentsrocksdb::RandomAccessFileReader::Read
其中在rocksdb::TableCache::Get 邏輯中需要先找到點(diǎn)查的sst文件,獲取一個(gè)文件handle
會(huì)進(jìn)入到如下邏輯:
TableCache::FindTable // 如果在 block_cache 中找不到,則會(huì)進(jìn)入如下邏輯中。本身我們設(shè)置的blockcache也很小,大多數(shù)的key都找不到TableCache::GetTableReader
在GetTableReader邏輯中主要是創(chuàng)建一個(gè)BlockBasedTable的FileReader。
Status TableCache::GetTableReader(const EnvOptions& env_options,const InternalKeyComparator& internal_comparator, const FileDescriptor& fd,bool sequential_mode, bool record_read_stats, HistogramImpl* file_read_hist,std::unique_ptr<TableReader>* table_reader,const SliceTransform* prefix_extractor, bool skip_filters, int level,bool prefetch_index_and_filter_in_cache) {std::string fname =TableFileName(ioptions_.cf_paths, fd.GetNumber(), fd.GetPathId());std::unique_ptr<RandomAccessFile> file;// 打開傳入的文件Status s = ioptions_.env->NewRandomAccessFile(fname, &file, env_options);RecordTick(ioptions_.statistics, NO_FILE_OPENS);if (s.ok()) {// posix_fadvise 設(shè)置打開文件的讀模式if (!sequential_mode && ioptions_.advise_random_on_open) {file->Hint(RandomAccessFile::RANDOM);}StopWatch sw(ioptions_.env, ioptions_.statistics, TABLE_OPEN_IO_MICROS);// 創(chuàng)建一個(gè)BlockBasedTable的table_readerstd::unique_ptr<RandomAccessFileReader> file_reader(new RandomAccessFileReader(std::move(file), fname, ioptions_.env,record_read_stats ? ioptions_.statistics : nullptr, SST_READ_MICROS,file_read_hist, ioptions_.rate_limiter, ioptions_.listeners));s = ioptions_.table_factory->NewTableReader(TableReaderOptions(ioptions_, prefix_extractor, env_options,internal_comparator, skip_filters, immortal_tables_,level, fd.largest_seqno, block_cache_tracer_),std::move(file_reader), fd.GetFileSize(), table_reader,prefetch_index_and_filter_in_cache);TEST_SYNC_POINT("TableCache::GetTableReader:0");}return s;
}
可以看到在GetTableReader的過程中會(huì)先打開文件,打開之后會(huì)根據(jù)sequential_mode和ioptions_.advise_random_on_open配置來設(shè)置文件的模式。這里默認(rèn)的sequential_mode傳入的時(shí)候是false,所以如果那個(gè)option是true,則會(huì)設(shè)置一個(gè)RANDOM。
其底層是通過posix_fadvise來設(shè)置文件的預(yù)讀模式:
void PosixRandomAccessFile::Hint(AccessPattern pattern) {if (use_direct_io()) {return;}switch (pattern) {case NORMAL:Fadvise(fd_, 0, 0, POSIX_FADV_NORMAL);break;case RANDOM:// 對(duì)fd 下發(fā)RANDOM 標(biāo)記Fadvise(fd_, 0, 0, POSIX_FADV_RANDOM);break;case SEQUENTIAL:Fadvise(fd_, 0, 0, POSIX_FADV_SEQUENTIAL);break;case WILLNEED:Fadvise(fd_, 0, 0, POSIX_FADV_WILLNEED);break;case DONTNEED:Fadvise(fd_, 0, 0, POSIX_FADV_DONTNEED);break;default:assert(false);break;}
}int Fadvise(int fd, off_t offset, size_t len, int advice) {
#ifdef OS_LINUXreturn posix_fadvise(fd, offset, len, advice);
#else(void)fd;(void)offset;(void)len;(void)advice;return 0; // simply do nothing.
#endif
}
到此,我們就知道了通過這里的選項(xiàng) ioptions_.advise_random_on_open = true 能夠讓posix_fadvise設(shè)置內(nèi)核的預(yù)讀建議POSIX_FADV_RANDOM,讓隨機(jī)讀場景不進(jìn)行內(nèi)核的自動(dòng)預(yù)讀。
最后,在page_cache_sync_readahead 中會(huì)進(jìn)入到不進(jìn)行預(yù)讀的邏輯中。
總結(jié)
以上是生活随笔為你收集整理的Rocksdb 通过posix_advise 让内核减少在page_cache的预读的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大家看过咒怨吗?伽椰子有害怕的人或者是物
- 下一篇: blktrace 工具集使用 及其实现原