Rocksdb 通过posix_advise 让内核减少在page_cache的预读
文章目錄
- 1. 問題排查
- 確認I/O完全/大多數來自于rocksdb
- 確認此時系統只使用了rocksdb的Get來讀
- 確認每次系統調用下發讀的請求大小
- 確認是否在內核發生了預讀
- 2. 問題原因
- 內核預讀機制
- page_cache_sync_readahead
- ondemand_readahead
- 3. 優化
事情起源于 組內的分布式kv 系統使用rocksdb過程中發現磁盤有超過預期3-4x的讀I/O問題,因為禁止了block-cache,也就是每一次Get會對應一次磁盤I/O,剛好上層Get的qps和下層磁盤的r/s 基本吻合,也就是一次讀,讀了超過3個page。但是實際寫入的value也就 128B,按照nvme的block size 4K的配置,讀上來3個page則遠超過了我們的預期。
本文涉及的代碼 rocksdb版本是 6.4.6,linux 內核版本是 3.10.1
1. 問題排查
看到了嚴重的讀放大現象之后,接下來一探問題的原因。
確認I/O完全/大多數來自于rocksdb
當我們不知道kv系統內部如何使用rocksdb的情況下需要確認這一些IO的來源。
-
抓I/O 線程棧
-
sudo iotop能直接看到系統中的磁盤i/o來源的線程如果此時能夠看到具體的線程名稱,確認是在讀rocksdb,OK,跳過這一步
-
sudo pstack tid抓取一個線程棧pstack的邏輯是通過gdb attach進去 執行bt,多抓幾次就能夠知道I/O線程大多數的時間在干嘛
這里發現確實是在rocksdb的
Get調用棧上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
-
-
抓取進程都在操作哪一些文件,和上面的進程棧信息double check一下
這里需要系統支持eBPF,使用了一個bcc工具集中的一個opensnoop命令。opensnoop -p pid能夠看到當前進程按順序操作的文件,都會打印出來# 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文件之外,還有一個其他的文件,但是我們主體的讀取都是sst,所以基本能夠確認磁盤的讀i/o都是來源于rocksdb.
確認此時系統只使用了rocksdb的Get來讀
到這里,我們能夠確認I/O是由rocksdb產生的,可能會想是不是kv系統中使用rocksdb的方式有問題,他們可能不僅僅是用了Get,在某一個他們也不清楚的線程里用了迭代器掃描,才產生這么多的I/O,我們想要確認這個問題。這個時候之前使用的pstack調用棧就不夠用了,因為它只是一個線程,而我們要用它抓更多的線程比較麻煩,簡單且直觀的辦法就是火焰圖。
現在有大量的I/O,而且rocksdb的操作基本都是on-cpu的,所以直接看on-cpu的火焰圖就非常容易得看到IO調用棧的來源了。
通過如下腳本立即抓取
#!/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 ../
會在當前目錄下生成一個$pid.svg的文件,使用瀏覽器打開即可看到完整的on-cpu調用棧
看起來確實是大多數的cpu都消耗在了rocksdb的Get調用棧上了,好吧。。。問題躲不開了。
確認每次系統調用下發讀的請求大小
按照我們對磁盤讀取的正常邏輯理解,如果讀取的數據塊大小小于正常的磁盤塊4k大小的話 從磁盤文件系統下發的讀取請求會填充成一個4k的cache頁面,讀取一個磁盤的block。
所以為什么它這里會讀取這么多的block,估算上層的總磁盤帶寬/總qps 可以得到每個請求大概讀了3-4個block,而實際的value大小也就128B,讀取sst的時候會把這個value所在的datablock整個讀上來,默認一個datablock是4k,這就很奇怪了。
為了確認rocksdb側的pread64系統調用確實讀的內容比較少,我們要獲取系統調用每次讀的請求大小,看是不是rocksdb 拼接的請求有問題。
執行命令:sudo strace -ttt -T -F -p 1200034 -e trace=pread64 -o strace.txt 追蹤pread64系統調用,并打印每個系統調用的時間,最后的結果保存在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>
...
如果想要確認這個pread64讀的文件句柄是rocksdb的sst文件,可以通過 sudo ls -l /proc/1200034/fd/15210 來看這個進程打開的文件句柄確實是連結到了sst文件。
通過上面抓到的pread64系統調用信息,可以很明顯的發現系統調用下發的是小于4K的請求大小。。。我擦嘞。
立即通過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]
...
可以看到確實,從磁盤上讀到了很多超過2個4k的block,也就是系統調用pread64下發了小于一個block的請求,而落到磁盤的I/O達到了4個block。。。interesting。
確認是否在內核發生了預讀
只能進入內核邏輯了,看起來像是操作系統的預讀邏輯。
順著火焰圖的調用棧來看,我們的pread系統調用的調用棧如下:
sys_pread64vfs_readdo_sync_readxfs_file_aio_readxfs_file_buffered_aio_readgeneric_file_aio_readdo_generic_file_readpage_cache_sync_readahead
實際由page-cache下發讀取的頁面個數是在page_cache_sync_readahead 的參數中。
這里我們拋開內核函數的邏輯,想要單純確認一下do_generic_file_read下發的請求數目,可以通過stap來抓取一下這個函數的變量
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);
}
可以發現page_cache_sync_readahead這個函數的時候僅僅才1個頁面。
繼續向下,看到page_cache_sync_readahead 內部實現有兩個分支:
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);
}
這里后續會說明這里的兩個分支到底作用為何?
不過抓這個函數的stap發現只能讀到1個page的請求,也就是do_generic_file_read只下發了1個page。
通過注釋能夠看到如果要預讀的話應該會在ondemand_readahead函數中,結合火焰圖,這個函數預讀的話最終會走到ra_submit --> __do_page_cache_readahead 邏輯,同樣的stap抓一下這個函數的nr_to_read的參數,發現確實預讀到了3-4個page。
定位到了函數:ondemand_readahead ,預讀的多個頁面就是在這里填充的,而ra_submit只是很薄的一層調用:
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;
}
到此我們結合火焰圖的調用棧知道了具體的哪個內核函數發生了預讀,但是為什么還不清楚。
2. 問題原因
內核預讀機制
內核的預讀機制起源是我們還處于大多數存儲場景都是HDD介質,磁盤的轉動和磁頭的尋道消耗太多的時間,而我們想要在HDD的基礎上提升讀性能,可以減少磁頭移動的次數,連續讀取多個扇區的數據內容就可以。實際的操作系統中,用戶讀取一個文件,一般會從頭讀到尾,這一些文件在磁盤上的存儲都是連續的扇區,也就是可以利用預讀來一次讀取多個扇區,減少磁頭頻繁移動尋道。
當然,這個預讀機制在我們的NVME上同樣有效,現在大多數的nvme底層存儲介質還是nand ,使用的是浮柵晶體管做存儲單元,通過兩極加正反電壓來控制浮柵層內電子的移動情況。詳細的底層nand存儲介質原理介紹感興趣的同學可以參考從NMOS 和 PCM 底層存儲單元 來看NAND和3D XPoint的本質區別?;氐揭f的預讀問題,有了預讀,可以減少一次或者多次針對nvme的IO,也就能節省幾十us-幾個ms 的時間,極大得提升了系統的響應時間。
預讀(read-ahead) 算法預測即將訪問的頁面,并提前將他們讀入到page-cache中,后續的讀取就不需要產生io了。
主要任務:
- 批量:把小I/O 聚集為大I/O,改善磁盤利用率,提升系統吞吐
- 提前:對應用程序隱藏磁盤的延遲,加快系統響應時間
- 預測:屬于預讀算法的核心任務。前兩個功能都依賴準確的預測能力。包括linux , freeBSD, solaris 等主流操作系統都遵循一個原則:預讀僅針對順序讀模式。這個模式比較簡單且普遍,提升效果也更明顯,但是隨機讀模式對于內核來說也是難以預測的。
觸發預讀 的條件:
-
內核處理用戶進程讀請求時調用:
page_cache_sync_readahead或page_cache_async_readahead -
內核為文件內存映射分配一個頁面時,即調用
mmap -
用戶進程執行系統調用
readahead -
用戶進程在打開文件之后執行
posix_fadvise系統調用 -
用戶進程執行
madvise()系統調用,使用MADV_WILLNEEDflag 通知內核文件內存映射的指定區域將來會被訪問
預讀的過程 是:
預讀算法的實現是通過維護兩個窗口:當前窗口(current window) 和 前進窗口(ahead window)
用戶進程當前訪問的page 都會在current window中,當內核判讀用戶進程是順序訪問 且 初始訪問頁面是在當前窗口時就檢查前進窗口是否建立,如果未建立,則建立一個新的前進窗口,并為對應的文件頁面觸發讀操作。如果用戶進程的讀命中了前進窗口的頁面,則將前進窗口切換為當前窗口。
如上圖,用戶訪問當前窗口的時候會構建前進窗口,理想情況下當前窗口的頁面都是已經被cache住的,而前進窗口則還在調度頁面到cache;當然也會有正在訪問的當前窗口的頁面正在調度的情況。
如果用戶進程訪問的頁面命中了前進窗口,則前進窗口會切換為當前窗口,實際的前進窗口的大小會根據命中情況動態調整,命中到前進窗口的page越多,下次創建的前進窗口的大小則會更大一些,否則就會縮小。
page_cache_sync_readahead
通過前面的問題分析過程,我們知道了在page_cache_sync_readahead的內部調用中發生了預讀,用戶下發的是一個頁面,在它內部卻讀了超過3個頁面。
看一下這個函數的邏輯,參數含義如下:
- mapping: 文件擁有者的address_space對象
- ra: 包含此頁面的文件
file_ra_state描述符,持有是否進行預讀的標記 - filp: 文件對象
- offset: 頁面在文件中的起始偏移量
- req_size: 完成當前讀操作需要的頁面數
這個函數一般會在cache-miss的時候被調用,即它的調用者發現文件page不在cache中,觸發這個函數去讀對應的page,當然也包括了預讀,因為前面我們通過systemtap抓這個函數的時候也只是下發了一個page,最后它內部的經過ondemand_readahead的處理返回了3個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指定的隨機讀標記,不需要預讀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);
}
可以看到,第二個分支中 會判斷文件mode是否為FMODE_RANDOM ,如果是的話就不執行預讀了,僅僅讀當前用戶進程需要讀的page。這個標記可以由用戶進程打開文件的時候通過posix_advise來指定。
指定的邏輯在posix_advise系統調用的實現中:
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;...
}
回到我們要討論的預讀邏輯中,接下來看一下真正執行預讀的函數ondemand_readahead
ondemand_readahead
這個函數主要是根據傳入的file_ra_state描述符執行一些動作,函數參數還是剛才page_cache_sync_readahead函數傳入進來的。
主體邏輯是
- 首先判斷讀取是否從文件開頭開始,如果是,則初始化預讀信息。默認設置的是4個page
- 如果不是文件頭,則判斷是否是順序訪問(連續讀),如果是,則擴大預讀數量,一般是上次預讀數量x2
- 如果不是順序訪問, 則認為是隨機讀,不適合預讀,只會讀取sys_read請求的page數量。
- 最后調用
ra_submit提交讀請求
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// 如果是,則會調用get_init_ra_size 初始化預讀頁面if (!offset)goto initial_readahead;/** It's the expected callback offset, assume sequential access.* Ramp up sizes, and push forward the readahead window.*/// 如果不是從文件開始預讀,且是順序讀(發現當前的偏移地址是上次讀的起始地址+size)// 通過get_next_ra_size 擴大預讀窗口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; // 進入ra_submit執行實際的預讀}/** 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.*/// 發現當前文件被打上了一個預讀失效的標記,這里默認傳入的是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*/// 大塊預讀,即sys_read請求的頁面大小超過了最大的預讀設置// 重新初始化預讀窗口,預讀更多的頁面if (req_size > max)goto initial_readahead;/** sequential cache miss*/// 內核重新發現當前讀是順序讀,創建新的當前窗口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.*/// 這個函數里面發現不是順序讀,會返回0,直接不進行預讀了// 后續的__do_page_cache_readahead 函數readahead size被設置為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);// 初始化當前預讀窗口
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;}// 實際進行預讀指定page個數的調用return ra_submit(ra, mapping, filp);
}
3. 優化
到這里我們就知道了操作系統的預讀優化主要是針對順序讀場景,且會動態調整預讀窗口的大小。那回到我們rocksdb這里,業務下發的是隨機讀,但部分讀請求顯然是發生了預讀。因為測試的場景是用極少blockcache的,也就是這一些想要預讀到page-cache的datablock 大多數都不會被cache住。
那我們有沒有辦法完全不讓操作系統預讀呢,減少這個場景下的預讀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 邏輯中需要先找到點查的sst文件,獲取一個文件handle
會進入到如下邏輯:
TableCache::FindTable // 如果在 block_cache 中找不到,則會進入如下邏輯中。本身我們設置的blockcache也很小,大多數的key都找不到TableCache::GetTableReader
在GetTableReader邏輯中主要是創建一個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 設置打開文件的讀模式if (!sequential_mode && ioptions_.advise_random_on_open) {file->Hint(RandomAccessFile::RANDOM);}StopWatch sw(ioptions_.env, ioptions_.statistics, TABLE_OPEN_IO_MICROS);// 創建一個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的過程中會先打開文件,打開之后會根據sequential_mode和ioptions_.advise_random_on_open配置來設置文件的模式。這里默認的sequential_mode傳入的時候是false,所以如果那個option是true,則會設置一個RANDOM。
其底層是通過posix_fadvise來設置文件的預讀模式:
void PosixRandomAccessFile::Hint(AccessPattern pattern) {if (use_direct_io()) {return;}switch (pattern) {case NORMAL:Fadvise(fd_, 0, 0, POSIX_FADV_NORMAL);break;case RANDOM:// 對fd 下發RANDOM 標記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
}
到此,我們就知道了通過這里的選項 ioptions_.advise_random_on_open = true 能夠讓posix_fadvise設置內核的預讀建議POSIX_FADV_RANDOM,讓隨機讀場景不進行內核的自動預讀。
最后,在page_cache_sync_readahead 中會進入到不進行預讀的邏輯中。
總結
以上是生活随笔為你收集整理的Rocksdb 通过posix_advise 让内核减少在page_cache的预读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大家看过咒怨吗?伽椰子有害怕的人或者是物
- 下一篇: 上海欢乐谷万圣节夜场期间地铁接驳车最晚到