RocksDB 內(nèi)存超限問(wèn)題剖析
一、背景
1.1 前言
在現(xiàn)代數(shù)據(jù)庫(kù)系統(tǒng)中,RocksDB 作為一種高性能的鍵值存儲(chǔ)引擎,廣泛應(yīng)用于需要高吞吐量和低延遲的場(chǎng)景。然而,在使用過(guò)程中觀察到 RocksDB 的內(nèi)存使用常常超出預(yù)設(shè)的閾值,這一現(xiàn)象對(duì)系統(tǒng)的穩(wěn)定性和可用性構(gòu)成了嚴(yán)重威脅。
RocksDB 提供了通過(guò) block-cache-size 參數(shù)來(lái)控制緩存使用的機(jī)制。開(kāi)發(fā)者可以通過(guò)以下代碼片段設(shè)置緩存大小:
std::shared_ptr<rocksdb::Cache> cache = rocksdb::NewLRUCache(cache_size, -1, true);
然而,實(shí)際應(yīng)用中發(fā)現(xiàn),RocksDB 的內(nèi)存占用往往超出了設(shè)定的 cache_size 值。這種內(nèi)存使用的不可預(yù)測(cè)性導(dǎo)致了內(nèi)存分配的失控,甚至觸發(fā)了程序的 OOM(Out of Memory)錯(cuò)誤,嚴(yán)重影響了服務(wù)的連續(xù)性和可靠性。
有部分開(kāi)發(fā)者報(bào)告了相似的內(nèi)存超額使用問(wèn)題,該問(wèn)題在 GitHub 社區(qū)也引起了廣泛關(guān)注。
1.2 內(nèi)存分析流程
在分析內(nèi)存的過(guò)程中,可以搭配許多 Linux 的命令工具來(lái)進(jìn)行。以下是一套內(nèi)存分析的基本思路:
圖片來(lái)源:https://learn.lianglianglee.com/
1、可以先用 free 和 top,查看系統(tǒng)整體的內(nèi)存使用情況。
2、再用 vmstat 和 pidstat,查看一段時(shí)間的趨勢(shì),從而判斷出內(nèi)存問(wèn)題的類(lèi)型。
3、最后進(jìn)行詳細(xì)分析,比如內(nèi)存分配分析、緩存/緩沖區(qū)分析、具體進(jìn)程的內(nèi)存使用分析等。
其中,第一步和第二步可以觀察到內(nèi)存問(wèn)題的現(xiàn)象,而最難的往往是第三步,對(duì)內(nèi)存的使用情況進(jìn)行分析。第三步中需要結(jié)合業(yè)務(wù)代碼,對(duì)問(wèn)題的根因提出假設(shè),然后配合一些工具來(lái)驗(yàn)證假設(shè)。分析的過(guò)程更像在做實(shí)驗(yàn):提出假設(shè),收集數(shù)據(jù),驗(yàn)證假設(shè),得出結(jié)論。下文中,也會(huì)搭配內(nèi)存工具進(jìn)行分析,供讀者參考。
二、問(wèn)題描述
在前文所述的 RocksDB 內(nèi)存使用問(wèn)題背景下,我們業(yè)務(wù)生產(chǎn)環(huán)境遭遇了相似的挑戰(zhàn)。應(yīng)用程序采用 glibc 的 ptmalloc 作為內(nèi)存分配器。在程序中,存在兩個(gè) RocksDB 實(shí)例,分別用于存儲(chǔ)不同類(lèi)型的數(shù)據(jù)。根據(jù)配置,兩個(gè)實(shí)例的 block-cache-size 分別被設(shè)定為4GB和8GB。然而,實(shí)際的內(nèi)存消耗量遠(yuǎn)遠(yuǎn)超出了這一預(yù)設(shè)值,導(dǎo)致整體內(nèi)存使用量顯著高于預(yù)期。
通過(guò)執(zhí)行 free -g 命令,監(jiān)測(cè)到程序的內(nèi)存使用量達(dá)到了59GB,這一數(shù)值已經(jīng)接近了物理服務(wù)器的內(nèi)存容量閾值。此外,通過(guò)定期執(zhí)行 vmstat 3 命令,觀察到自服務(wù)啟動(dòng)以來(lái),內(nèi)存使用量持續(xù)上升,直至接近100%的使用率。這一現(xiàn)象表明,系統(tǒng)內(nèi)存已極度緊張,存在觸發(fā) OOM(Out of Memory)錯(cuò)誤的風(fēng)險(xiǎn)。
鑒于當(dāng)前內(nèi)存使用情況,確認(rèn)了內(nèi)存管理問(wèn)題的存在,并認(rèn)識(shí)到需要進(jìn)一步結(jié)合源代碼進(jìn)行深入分析,以識(shí)別內(nèi)存使用異常的根本原因,并探索相應(yīng)的優(yōu)化措施。
名稱(chēng) | 信息 |
機(jī)器配置 | 32C64G 物理機(jī) |
內(nèi)存使用量 | 59G |
RocksDB 實(shí)例數(shù)量 | 2 |
每個(gè) RocksDB 實(shí)例文件夾大小 | 實(shí)例1:190G、實(shí)例2:180G |
內(nèi)存分配器 | glibc ptmalloc |
block_cache設(shè)置 | 實(shí)例1:4G、實(shí)例2:8G |
三、分析過(guò)程
3.1 內(nèi)存泄露分析
以下分析均在內(nèi)部測(cè)試環(huán)境中進(jìn)行,使用的是16C32G的機(jī)器。起初,懷疑 RocksDB 存在內(nèi)存泄露,會(huì)不斷申請(qǐng)內(nèi)存并且不會(huì)回收。
分析內(nèi)存泄露的常用工具有 valgrind、memleak、strace、jemalloc 的 jeprof。這里用到的工具是 jemalloc 的 jeprof。jeprof 的原理主要是在內(nèi)存的 malloc 和 free 的地方進(jìn)行監(jiān)控并收集數(shù)據(jù),使用時(shí)可以設(shè)置定期打印數(shù)據(jù)。
通過(guò) RocksDB 提供的的 db.getProperty() 方法對(duì)各個(gè)模塊占用內(nèi)存情況進(jìn)行取值,結(jié)果如下:
rocksdb.estimate-table-readers-mem: 16014055104 // 重點(diǎn)關(guān)注
rocksdb.block-cache-usage: 1073659024 // 重點(diǎn)關(guān)注
發(fā)現(xiàn)主要占用內(nèi)存的地方有兩個(gè):block-cache-usage 和 estimate-table-readers-mem。這兩個(gè)屬性分別對(duì)應(yīng)了 RocksDB 中的 block_cache 以及 indexs/filters。
但是隨著時(shí)間的推移,block_cache 和 indexs/filters 會(huì)達(dá)到一個(gè)均衡點(diǎn),不再增加上漲。與 RocksDB 存在內(nèi)存泄露的假設(shè)不相符。
進(jìn)一步分析 RocksDB 分配內(nèi)存的調(diào)用堆棧,由于 glibc ptmalloc 無(wú)法打印調(diào)用堆棧,將 glibc ptmalloc 切換成了 jemalloc,通過(guò) jeprof 進(jìn)行內(nèi)存調(diào)用堆棧的打印,以下是 jemalloc 的安裝方法:
# 用jemalloc 對(duì)于服務(wù)來(lái)說(shuō)沒(méi)有改造成本。
# 可以直接使用LD_PRELOAD=/usr/local/lib/libjemalloc.so這種動(dòng)態(tài)鏈接的方式去植入
# 前提是Linux機(jī)器上需要先安裝jemalloc:
wget https://github.com/jemalloc/jemalloc/archive/5.1.0.tar.gz tar zxvf jemalloc-5.1.0.tar.gz
cd jemalloc-5.1.0/
./autogen.sh
./configure --prefix=/usr/local/jemalloc-5.1.0 --enable-prof
make && make install_bin install_include install_lib
上述命令中,--enable-prof 代表開(kāi)啟 jemalloc 的 jeprof 功能。
安裝完成后,通過(guò) LD_PRELOAD 命令來(lái)開(kāi)啟 jemalloc 的 malloc 和 free。LD_PRELOAD 的原理是直接使用 jemalloc 的 malloc 和 free 方法替換掉 glibc 的 malloc/free。
通過(guò)以下命令啟動(dòng)程序:
export MALLOC_CONF="prof:true,lg_prof_interval:29"
LD_PRELOAD=/usr/local/jemalloc-5.1.0/lib/libjemalloc.so ./process_start
上述命令中 export MALLOC_CONF="prof:true,lg_prof_interval:29" 代表開(kāi)啟 jeprof 的信息捕獲,內(nèi)存每次上漲 2的29次方 btyes (512MB) 便記錄一次信息。最終輸出了結(jié)果,可以通過(guò)以下命令將結(jié)果轉(zhuǎn)成調(diào)用堆棧圖:
jeprof --show_bytes --pdf ./process_start jeprof.34447.0.f.heap > result.pdf
最終觀察堆棧圖(只截取了部分)發(fā)現(xiàn),RocksDB 正常調(diào)用分配內(nèi)存的方法:rocksdb::AllocateBlock,沒(méi)有觀察到有內(nèi)存泄露的情況。
3.2 系統(tǒng) glibc ptmalloc 分析
搜索了很多類(lèi)似的問(wèn)題,發(fā)現(xiàn)也有開(kāi)發(fā)者都遇到了 glibc 內(nèi)存分配不釋放的問(wèn)題,便懷疑是否是 glibc 的內(nèi)存分配不合理導(dǎo)致的。目前線上環(huán)境 glibc 的版本是2.17。
查看了線上機(jī)器的 /proc/meminfo,大部分內(nèi)存主要用在了程序申請(qǐng)的棧內(nèi)存和堆內(nèi)存中,可以看到下圖中 Active(anon) 匿名內(nèi)存占用了52G,這部分內(nèi)存申請(qǐng)后沒(méi)有被釋放。
glibc 申請(qǐng)的內(nèi)存均屬于這部分內(nèi)存。
其次,通過(guò) pmap -X pid 查看進(jìn)程的內(nèi)存塊,發(fā)現(xiàn)有很多64MB的內(nèi)存段。
為什么會(huì)創(chuàng)建這么多的64M的內(nèi)存區(qū)域?這個(gè)跟 glibc 的內(nèi)存分配器有關(guān)系。glibc 每次進(jìn)行 mmap 分配時(shí)申請(qǐng)內(nèi)存的大小在64位系統(tǒng)上默認(rèn)為64MB。
此時(shí)便進(jìn)一步提出了新的假設(shè):是否因?yàn)?glibc 的內(nèi)存分配機(jī)制不合理,導(dǎo)致內(nèi)存不斷申請(qǐng),但是不釋放資源?
分析 glibc 分配的內(nèi)存情況,可以使用 glibc 提供的接口:
malloc_info(https://man7.org/linux/man-pages/man3/malloc_info.3.html
The malloc_info() function exports an XML string that describes the current state of the memory-allocation implementation in the caller. The string is printed on the file stream stream. The exported string includes information about all arenas.
以下為 malloc_info 的接口定義。該接口會(huì)將內(nèi)存分配的情況直接以 XML 的形式輸出到文件中。
#include <malloc.h>
int malloc_info(int options, FILE *stream);
在程序中添加內(nèi)存信息打印的代碼,每隔一段時(shí)間觸發(fā)一次打印:
FILE *filePointer;
filePointer = fopen("mem_info.log", "a");
if (filePointer != nullptr) {
malloc_info(0, filePointer);
fclose(filePointer);
}
以下為 malloc_info 輸出的內(nèi)容(截取部分內(nèi)容):
<malloc versinotallow="1">
<heap nr="0">
<sizes>
<size from="17" to="32" total="32" count="1"/>
<size from="33" to="48" total="48" count="1"/>
<size from="81" to="96" total="1824" count="19"/>
<size from="97" to="112" total="112" count="1"/>
<size from="33" to="33" total="42636" count="1292"/>
// ....
</sizes>
<total type="fast" count="22" size="2016"/>
<total type="rest" count="5509" size="33761685"/>
<system type="current" size="230117376"/>
<system type="max" size="230117376"/>
<aspace type="total" size="230117376"/>
<aspace type="mprotect" size="230117376"/>
</heap>
XML 內(nèi)容闡述:
1.nr 即 arena,通常一個(gè)線程一個(gè),線程間會(huì)相互爭(zhēng)搶 arena。
2.<size from="17" to="32" total="32" count="1"/>
大小在一定范圍內(nèi)的內(nèi)存,會(huì)放到一個(gè)鏈表里,這就是其中一個(gè)鏈表。from 是內(nèi)存下限,to是上限,上面的意思是內(nèi)存分配在 [17,32] 范圍內(nèi)的空閑內(nèi)存總共有32個(gè)。
3.<total type="fast" count="22" size="2016"/>
即 fastbin 這鏈表當(dāng)前有22個(gè)空閑內(nèi)存塊,大小為2016字節(jié)。
4.<total type="rest" count="5500" size="33761685"/>
除 fastbin 以外,所有鏈表空閑的內(nèi)存數(shù)量,以及內(nèi)存大小,此處內(nèi)存塊數(shù)量為5509,大小為33761685字節(jié)。
因此,fast 和 rest 加起來(lái)為當(dāng)前 glibc 中空閑的未歸還給操作系統(tǒng)的內(nèi)存。通過(guò)命令 awk 將文件中所有 fast 和 rest 占用的內(nèi)存加起來(lái)后,發(fā)現(xiàn)約為 4G 。
當(dāng)前 RocksDB 進(jìn)程的內(nèi)存使用量為20.48G,上述提到 block-cache-usage 和 estimate-table-readers-mem 加起來(lái)只有15.9G (1073659024 bytes + 16014055104 bytes)。相當(dāng)于中間差距還有4G左右。剛好和 glibc 占用的空閑內(nèi)存相吻合。
最終確認(rèn)是由于 glibc 的 ptmalloc 內(nèi)存管理器申請(qǐng)內(nèi)存不回收,導(dǎo)致了機(jī)器內(nèi)存緊張。
四、問(wèn)題解決
發(fā)現(xiàn)是 glibc ptmalloc 的問(wèn)題之后,解決也相對(duì)簡(jiǎn)單,業(yè)內(nèi)有更好的 ptmalloc 替代方案,如 jemalloc 以及 tcmalloc。
將 jemalloc 應(yīng)用到線上環(huán)境之后發(fā)現(xiàn),確實(shí)像預(yù)期那樣,內(nèi)存的使用相比于 ptmalloc 更少,此前,機(jī)器的內(nèi)存一直維持在高位,使用 jemalloc 之后,內(nèi)存的使用下降了1/4(從95%+下降到80%+),隨著內(nèi)存地釋放,有更多的內(nèi)存可用于處理請(qǐng)求,IO和CPU的使用率就降低了,下圖是內(nèi)存、磁盤(pán)IO使用率以及 CPU 空閑率的對(duì)比圖。
在相關(guān)性能指標(biāo)得到優(yōu)化之后,服務(wù)可用性以及RT也得到了提升。
五、總結(jié)
在進(jìn)行內(nèi)存超量使用問(wèn)題的分析過(guò)程中,最初懷疑是 RocksDB 存在一些內(nèi)存管理不合理的地方導(dǎo)致了內(nèi)存超量使用。然而,經(jīng)過(guò)深入研究和分析,發(fā)現(xiàn)實(shí)際的原因主要由 glibc的 ptmalloc 內(nèi)存回收機(jī)制所導(dǎo)致。整個(gè)分析過(guò)程較為繁瑣,需要結(jié)合一些合適的內(nèi)存分析工具,逐層深入,不斷假設(shè)并驗(yàn)證猜想。
總的來(lái)說(shuō),內(nèi)存超量使用問(wèn)題得到了解釋?zhuān)渤晒鉀Q。通過(guò)逐步深入,持續(xù)假設(shè)和驗(yàn)證,最終找到了真正的問(wèn)題所在,希望能為讀者在解決類(lèi)似問(wèn)題上提供一些靈感和思路。
六、參考文獻(xiàn)
- https://github.com/facebook/rocksdb/wiki/Partitioned-Index-Filters
- https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB
- http://jemalloc.net/jemalloc.3.htmlhttps://paper.seebug.org/papers/Archive/refs/heap/glibc%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86ptmalloc%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90.pdf
- https://man7.org/linux/man-pages/man3/malloc_info.3.html