图是一种存储实体,及实体之间关系的数据结构,而图数据库则是一个使用图结构进行语义查询的数据库。因此,选择正确合适的图数据库能帮助企业快速了解数据间关系,深挖背后的价值。
图数据库不同于传统的关系型数据库(RDBMS)。关系型数据库主要由单个或多个二维表组成,表中的行和列存储数据。
而图数据库的设计初衷是让企业专注数据集内的数据关系,除了具备高可用、高性能等特性,能对海量数据进行存储的能力也至关重要。
图片来自于官网
传统的关系型数据库和图数据库无论是在模型,存储以及查询优化上都存在极大的差异。比如社交用户关系中的2度查询请求,传统关系型数据库处理起来至少是秒级别的,3度查询更差甚至无法支持。
对比而言,图数据库能够轻松支持这类场景,性能往往能够轻松的达到传统关系型数据库的十倍乃至几十倍。这种性能的差异并非简单的调优问题,而是更深层次的数据库建模以及内核层面决定的。因此,图数据库在基因层面更适合高度连接数据集的处理。
今天我们介绍的是目前得物在使用的一款开源图数据库产品Nebula Graph。Nebula Graph 由三种服务构成:Graph 服务、Meta 服务和 Storage 服务,是一种存储与计算分离的架构。Graph 服务负责处理计算请求,Storage 服务负责存储数据,Meta 服务负责数据管理。
每个服务都有可执行的二进制文件和对应进程,用户可以使用这些二进制文件在一个或多个计算机上部署 Nebula Graph 集群。
下图展示了 Nebula Graph 集群的架构。
图片来自于官网
1. Storage interface 层,Storage 服务的最上层,定义了一系列和图相关的 API。
2. Consensus 层,Storage 服务的中间层,实现了 Multi Group Raft,保证强一致性和高可用性。
3. Store Engine 层,Storage 服务的最底层,是一个单机版本地存储引擎Rocksdb
1. RocksDB是一个高性能的单机KV存储,Nebula 一个图Space对应一个RocksDB实例。
2. 图存储的主要数据是点和边,Nebula Graph 将点和边的信息存储为 key(RocksDB key),同时将点和边的属性信息存储在 value 中(RocksDB value)。
3. RocksDB架构图,写入数据直接写Memtable,memtable是内存块。
图片来自于官网
LSM磁盘上的文件被分成多层进行组织,我们叫他们Level-1, Level-2,等等,或者简单的L1,L2,等等。一个特殊的层,Level-0(L0),会包含刚从内存memtable落盘的数据。每个层(除了Level0)都是一个独立的排序结果,为了加快LSM查询速度引入 Block_Cache缓存和 Indexes and bloom filters缓存。
图片来自于官网
所有非0层都有目标大小。压缩的目标是限制这些层的数据大小。大小目标通常是指数增加。
图片来自于官网
一直以来使用都没什么问题,最近业务方通过Nebula Exchange Spark应用将其他存储中的数据导入到Nebula Graph中,数据导入一定量后出现Nebula-Storage 使用内存一直增长,直至Nebula-Storage OOM,通知业务服务暂停,重新启动时依然OOM。确认不是由于业务方导致的问题,问题发生在本身存储问题上。所以根据我们的猜想,我们需要进一步去排查相关问题。
通过Linux top 命令很快定位到是由于 nebula-storaged内存使用过多。
查看 Nebula 官网文档之后,对内存的容量调整,主要是对RocksDB内存占用调整。之前已经提到过 Nebula 底层存储引擎用的 Rocksdb。
修改nebula-storaged.conf 配置文件中的 --rocksdb_block_cache 进行调整,设置为 4M 大小,但是结果显示依旧同之前一样占用大量内存。继而继续调整配置项,设置enable_partitioned_index_filter=true,问题依旧没有解决。
RocksDB内存占用主要有 Block cache,Indexes and bloom filters,Memtables
1. Memtables占固定大小,不会有很多内存占用。"write_buffer_size":"67108864","max_write_buffer_number":"4"。总占用64M*4。
2. 设置nebula图 rocksdb_block_cache 对应 RocksDB block_cache 实例缓存未压缩块。总占用4M。
3. enable_partitioned_index_filter 对应 pin_l0_filter_and_index_blocks_in_cache,在 Nebula 中只对 RocksDB 第 L0 层 SST 文件开启了索引(index block)和布隆过滤(filter block)缓存,在其他层不进行缓存。
配置完 enable_partitioned_index_filter=true 理论上存储不会占用很多内存,但是内存占用量仍旧持续不减。
程序运行时的内存泄漏问题在很多场景下都相当难以排查,因为这类问题通常难以预测,也很难通过静态代码梳理的方式定位。内存剖析技术就是解决此类问题。 内存剖析通常指对应用程序的堆分配进行收集或采样,来向我们报告程序的内存使用情况,以便分析内存占用原因或定位内存泄漏根源。
nebula-storaged的内存分配器是jemalloc,jemalloc 提供了一个 jeprof工具, jeprof 工具收集内存分配信息。
生成profile文件(代码如下)
生成 jeprof.*heap 文件进行内存占用量分析参考命令如下:
MALLOC_CONF="prof:true,lg_prof_interval:26" LD_PRELOAD="/usr/lib64/libjemalloc.so.2" /usr/local/nebula/bin/nebula-storaged --flagfile /usr/local/nebula/etc/nebula-storaged.conf
通过 jeprof 读取生成的 jeprof.*heap 文件
jeprof /usr/local/nebula/bin/nebula-storaged jeprof.5327.*
(jeprof) top
Total: 18903464.5 MB
13371403.1 70.7% 70.7% 13371403.1 70.7% rocksdb::UncompressBlockContentsForCompressionType
5503821.4 29.1% 99.9% 18875224.5 99.9% rocksdb::BlockFetcher::ReadBlockContents
5923.5 0.0% 99.9% 6518.4 0.0% rocksdb::LRUCacheShard::Insert
4704.0 0.0% 99.9% 4704.0 0.0% std::__cxx11::basic_string::_M_mutate
3257.7 0.0% 99.9% 10664.1 0.1% rocksdb::BlockBasedTable::PutDataBlockToCache
2964.1 0.0% 99.9% 3261.7 0.0% std::__detail::_Map_base::operator[]
2085.1 0.0% 100.0% 2085.1 0.0% rocksdb::VersionBuilder::Rep::ApplyFileAddition
1714.6 0.0% 100.0% 1714.6 0.0% std::_Rb_tree::_M_insert_unique
1483.0 0.0% 100.0% 1783.5 0.0% nebula::meta::MetaClient::loadSessions
888.5 0.0% 100.0% 18892273.5 99.9% rocksdb::BlockBasedTable::Open
这里我们看到上面 RocksDB UncompressBlockContentsForCompressionType 占用内存很高,将该提示符去 RocksDB 的 GitHub 检索下,看能发现什么?在 GitHub 的 issue:https://github.com/facebook/rocksdb/issues/4112 中有用户通过设置参数 max_open_files 为 20 降低了内存。所以我们也来试试。
然而设置完 max_open_files 为 20 之后,内存占用依旧高。继续用 jeprof 来分析下内存占用;
这里提下 jeprof 生成的内存 profile 支持 PDF、SVG 等多种格式。由于 jeprof 生成的 PDF 可看到调用链,因此这里我采用了 PDF 格式。生成PDF调用图命令参考如下:
生成内存调用图(代码如下)
jeprof --pdf /usr/local/nebula/bin/nebula-storaged jeprof.5327.* > nebula-storaged.pdf
从生成的 PDF 中,我发现是 PartitionIndexReader、PartitionedFilterBlockReader 进程占用内存高。
完整调用链如下:
通过谷歌检索 rocksdb PartitionIndexReader high memory,好的,找到了一个相关连接:http://rocksdb.org/blog/2017/05/12/partitioned-index-filter.html。
来看看这个 url 里面有什么——"cache_index_and_filter_blocks,通过设置cache_index_and_filter_blocks:false,再重启 nebula-storage 进程,内存终于降下去了。
查看 RocksDB 官网:https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB 这块对 “RocksDB memory used” 内容的讲解:如果你设置 cache_index_and_filter_blocks 为 true,索引和过滤块会被存储在块缓存(和rocksdb_block_cache内存共用),跟其他数据块一起;如果设置成 cache_index_and_filter_blocks:false时,是由max_open_files控制,如果为-1会加载所有索引块和过滤快,当然性能也是最好的。
但设置 cache_index_and_filter_blocks:true会降低性能,这里在开启缓存之后,尝试调整参数找到性能平衡点:通过调大 block_size,增加块大小,降低块的数量,故而线性减少索引的大小。
尝试调大 block_size 到 32768,降低 max_open_files 到 50000,重启 nebula-storage:很好,内存使用率保持在比较低的水平。
查看Rocksdb每层分布
cat /data/nebula/data/storage/nebula/3/data/LOG | grep -B 10 "Compaction Stats"
通过此命令得到Rocksdb SST文件每层存储的数据量,数据文件个数,以及数据量大小enable_partitioned_index_filter=true (只缓存L0层数据),所以也解释了官方给的参数不能有效降低内存。
** Compaction Stats [default] **
Level Files Size Score Read(GB) Rn(GB) Rnp1(GB) Write(GB) Wnew(GB) Moved(GB) W-Amp Rd(MB/s) Wr(MB/s) Comp(sec) CompMergeCPU(sec) Comp(cnt) Avg(sec) KeyIn KeyDrop
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
L0 3/0 2.99 KB 0.8 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.5 0.00 0.00 1 0.001 0 0
L1 6/0 243.56 MB 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.00 0.00 0 0.000 0 0
L2 50/0 2.48 GB 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.00 0.00 0 0.000 0 0
L3 416/0 24.97 GB 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.00 0.00 0 0.000 0 0
L4 1000/0 60.57 GB 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.00 0.00 0 0.000 0 0
Sum 1475/0 88.25 GB 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 1.5 0.00 0.00 1 0.001 0 0
Int 0/0 0.00 KB 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.00 0.00 0 0.000 0 0
从剖析内存可知是由于index block和filter block占用了大量内存,通过sst_dump分析sst文件,index block size 占用大量空间,cache_index_and_filter_blocks:false max_open_files=-1是会加载所有index block 和 filter block 说明原因在此:是由于RocksDB key过大导致。
Process /data/nebula/data/storage/nebula/3/data/276924.sst
Sst file format: block-based
from [] to []
Table Properties:
------------------------------
# data blocks: 148152
# entries: 921192
# deletions: 0
# merge operands: 0
# range deletions: 0
raw key size: 5550181800
raw average key size: 6025.000000
raw value size: 24862198
raw average value size: 26.989160
data block size: 67109354
index block size (user-key? 1, delta-value? 1): 253536473
# index partitions: 27700
top-level index size: 83699470
filter block size: 0
(estimated) table size: 320645827
filter policy name: rocksdb.BuiltinBloomFilter
prefix extractor name: nullptr
column family ID: 0
column family name: default
comparator name: leveldb.BytewiseComparator
merge operator name: nullptr
property collectors names: []
SST file compression algo: LZ4
SST file compression options: window_bits=-14; level=32767; strategy=0; max_dict_bytes=0; zstd_max_train_bytes=0; enabled=0;
creation time: 1647261423
time stamp of earliest key: 0
file creation time: 1647829598
DB identity: 9c8dda9b-4572-4b01-b6a0-ae2f800f7c3c
DB session identity: 4VZKQ83CS1EQUAAGO0VT
Raw user collected properties
------------------------------
# rocksdb.block.based.table.index.type: 0x02000000
# rocksdb.block.based.table.prefix.filtering: 0x30
# rocksdb.block.based.table.whole.key.filtering: 0x30
# rocksdb.deleted.keys: 0x00
# rocksdb.merge.operands: 0x00
(1)问题复现,通过复原参数,一重启nebula-storaged内存一直增长,几分钟内就nebula-storge进程由于内存不够OOM。
nebula-storge默认rocksdb参数(代码如下)
############## rocksdb Options ##############
# rocksdb DBOptions in json, each name and value of option is a string, given as "option_name":"option_value" separated by comma
--rocksdb_db_options={"max_subcompactions":"4","max_background_jobs":"8"}
# rocksdb ColumnFamilyOptions in json, each name and value of option is string, given as "option_name":"option_value" separated by comma
--rocksdb_column_family_options={"disable_auto_compactions":"false","write_buffer_size":"67108864","max_write_buffer_number":"4","max_bytes_for_level_base":"268435456",}
# rocksdb BlockBasedTableOptions in json, each name and value of option is string, given as "option_name":"option_value" separated by comma
--rocksdb_block_based_table_options={"block_size":"8192"}
(2)修改参数后重启,修改 "max_open_files":"50000"、"block_size":"32768",内存保持在正常水位线。
############## rocksdb Options ##############
# rocksdb DBOptions in json, each name and value of option is a string, given as "option_name":"option_value" separated by comma
--rocksdb_db_options={"max_subcompactions":"4","max_background_jobs":"8","max_open_files":"50000"}
# rocksdb ColumnFamilyOptions in json, each name and value of option is string, given as "option_name":"option_value" separated by comma
#--rocksdb_column_family_options={"disable_auto_compactions":"false","write_buffer_size":"67108864","max_write_buffer_number":"4","max_bytes_for_level_base":"268435456",}
# rocksdb BlockBasedTableOptions in json, each name and value of option is string, given as "option_name":"option_value" separated by comma
--rocksdb_block_based_table_options={"block_size":"32768"}
(3)经上面测试。修改nebula-storaged RocksDB "max_open_files":"50000"、"block_size":"32768" 可以使nebula-storaged内存保持在合理水位,保证nebula-storaged稳定性。
Nebula-Graph已在投放后端游戏、风控业务线使用,带来业务价值和性能优势。DBA团队会继续加大研究Nebula图数据存储。图数据库优势可将多个看似无关联的数据集连接起来,挖掘藏于数据集背后的关联关系。通过图数据库挖掘数据背后关联关系为企业带来无可估量的商用价值 ,图数据库是实时推荐、金融风控、知识图谱等业务的最佳选择。
https://zhuanlan.zhihu.com/p/434825941
https://github.com/facebook/rocksdb/wiki/RocksDB-Overview
Storage 服务配置 - Nebula Graph Database 手册
https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Leak-Checking
Leveled Compaction · facebook/rocksdb Wiki
https://github.com/johnzeng/rocksdb-doc-cn/blob/master/doc/Leveled-Compaction.md
https://docs.nebula-graph.com.cn/3.0.2/
*文/刘谦
限时活动:
上期(0415)活动中奖者名单公示:
文化周边:O**y
文化伞:E**c
随行杯:Loy**e
得物渔夫帽:会飞**子、*希
(注意:由于上海目前快递不支持发货,预计发出时间为5月底,如有疑问,可以微信公众号后台留言)
本期活动:
转发任意一条得物技术公众号文章,后台回复「得物」即可以参与抽奖,开奖时间:2022年4月22日 20:00。