当前位置: 首页 > news >正文

高频使用RocksDB DeleteRange引起的问题及优化

背景

我们的分布式文件存储系统(polefs)中,元数据子系统部分功能使用到RocksDB。在元数据数据结构设计中,我们基于inode作为主键构建了表模型。一行数据位:

inode为主键,cf1:列1,列2... cf2,:列1,列2,...

但是我们的一行数据是schema-less的。有的列名不是可预测的,他使用用户启用的某些功能或者信息记录。例如:客户端打开文件信息:

inode->clientId(系统分配的值)记录哪个个client打开了这个inode,因此部分列,不是可预测的

这样删除一个文件时,我们只知道一个prefix,并不清楚列名,先scan拿到所有列,然后delete他们是一个方法。

但为了高效支持大量删除文件的场景下,我们为了加速,大量使用了rocksdb的Delete Range功能,是对一个inode内部进行的列删除。因此属于高频操作

RocksDB Delete Range介绍

kv存储实践中,范围删除是一个常见需求。比如我们需要删除所有以特定prefix开头的记录。在RocksDB引入DeleteRange功能之前,实现这样的操作异常繁琐,例如大意:

// DeleteRange之前的实现:低效的Scan+Delete
void delete_by_prefix_old(std::string prefix) {rocksdb::Iterator* it = db->NewIterator(read_options);for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix); it->Next()) {db->Delete(write_options, it->key());  // 逐个删除}delete it;// 时间复杂度:O(N),性能随数据量线性下降
}

而有了DeleteRange之后,同样的操作变得极其高效:

// 使用DeleteRange的实现
void delete_by_prefix_new(std::string prefix) {std::string end = prefix + "\xff";  // 计算范围结束键db->DeleteRange(write_options, db->DefaultColumnFamily(), prefix, end);// 时间复杂度:O(1),无论数据量多大都快速完成
}

性能提升对比

  • 传统方式:10万条数据需要10万次删除操作

  • DeleteRange:只需1次范围删除操作

但是,正如当前工程师们得到的经验:没有银弹。DeleteRange这个在解决上述问题的同时,也伴随着这风险,如果使用不当,会带来严重的负面作用,下面分享下我们的实践。

直接说结论:DeleteRange不可以大量频繁使用,否则会产生两个问题:

  1. Get延迟异常升高。在我们的环境中,他是不断升高,延迟呈现一条直线在上升

  2. 内存不断升高

误用delete range导致的两个问题

问题1:Get延迟异常升高

现象

在大量持续delete range环境中(即使查询的key明确存在于MemTable或BlockCache中),我们的环境下,get的延迟不断增加

原因容易理解,delete range是范围标记。一个key是否失效了,基本上要遍历一遍所有这样的数据。数据越多,get越多越耗时

Get的执行路径,下面代码只描述大意(注意不是严格的):

每个数据本身version(sequence number),基于这个值,有happen before关系。只有新的delete range值,可以作用于老的base值,

下面的伪代码,没有体现这一点。

// RocksDB内部查询路径的复杂性增加
Status Get(const Slice& key) {// 首先检查MemTable中的范围墓碑for (auto* memtable : memtable_list_) {// MemTable也可能包含范围墓碑if (memtable->range_tombstones_.Covers(key)) { // 迭代遍历进行判断 O(n) 算法return Status::NotFound();  // 在MemTable层就被墓碑标记删除}  // 检查MemTable中的常规数据Status s = memtable->Get(key, value, &found);if (found) {return s;}}// 然后检查Immutable MemTables(省略...)// 最后检查SST文件层级中的范围墓碑for (auto& level : levels_) {for (auto& file : level.files) {// 每个SST文件都有自己的范围墓碑列表// 额外的范围检查,即使key存在也要执行!if (file->range_tombstones.Covers(key)) {return Status::NotFound();  // 被SST文件中的墓碑标记删除}// ... 正常的SST文件查询逻辑if (found) {return s;}}}
}

每个Get操作都需要遍历所有可能覆盖查询键的范围墓碑,即使数据就在内存中!

问题2:内存不可控增长

进程中的内存使用时间持续上升,配置的BlockCache和WriteBufferManager无法限制住内存

测试发现31%的内存被 处理DeleteRange记录的Iterator持有。

内存增长机制

class RangeTombstoneSet {std::vector<RangeTombstone> tombstones_;// 每个DeleteRange操作都会添加新的范围墓碑// 这些墓碑在Compaction前会一直占用内存
};// 内存中的层级结构
class VersionStorageInfo {std::vector<FileMetaData*> level_files_;// 每个SST文件都包含RangeTombstoneList// 大量DeleteRange导致这些列表不断膨胀
};

问题复现的一个例子:例如

// 不好的用法:频繁的DeleteRange
class MetadataManager {void cleanup_expired_metadata() {while (true) {auto expired_ranges = find_expired_ranges();for (auto& range : expired_ranges) {// 重点是这里:频繁的小范围DeleteRangedb_->DeleteRange(options_, cf_, range.start, range.end);}std::this_thread::sleep_for(60s);}}
};

这种大量执行delete range 使用模式会导致:

  1. 范围墓碑碎片化:大量小范围墓碑分散在各个SST文件

  2. 查询路径复杂化:每个查询都要检查众多范围墓碑

  3. 内存累积效应:墓碑数据在Compaction前无法释放,如上图。 理论上,执行compaction会释放,但是我们的例子里,没有释放,这是我们的一个待研究的点,是rocksdb本身有bug还是我们在使用rocksdb的方式上(如配置选项)上还有问题? TODO

优化

找到了问题,我们的解决方法很简单,消灭所有的delete range操作

我们梳理和分析自身的系统,把所有delete本身精细的分散到具体的操作上,最终把delete range操作全部换成了delete,同时不影响性能

总结

DeleteRange是一个强大的工具,但必须谨慎使用,它不适合高频操作

  1. 最上,不使用delete range是最好的

  2. 其次,低频使用

  3. 最下,无法避免,需要控制使用,同时需要定期Compaction:帮助清理范围墓碑碎片

http://www.dtcms.com/a/482024.html

相关文章:

  • for是什么意思?从foreign、forest谈起
  • 网站开发设计工程师网上注册公司申请入口
  • ARM 总线技术 —— AHB
  • .NET 程序自动更新的回忆总结
  • 自然语言处理笔记
  • 通州网站建设如何做信用网站截图
  • 网站空间控制网络服务采购
  • 方法合集——第七章
  • 定制衣柜厂柔性生产:客户需求拆解、板材切割与组装工序协同路径
  • 厦门外贸网站建设 之家wordpress菜单与顶部互换
  • openrewrite 的rewrite.yml 编写注意事项
  • 系统架构的平衡之道
  • 考研10.2笔记
  • Linux:传输层协议
  • 北京做网站建设的公司有哪些优化网站哪个好
  • 搭建网站工具抚州公司做网站
  • RK3588 + 银河麒麟部署 swarm 集群指南-续(自己应用程序部署)
  • 为什么我选择用 Rust 构建全栈后台管理系统?
  • 一篇文章讲清 UPD协议 与 TCP协议
  • 武邑网站建设价格wordpress 8小时
  • SSM高校职称申报系统337gs(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 深度解析:Linux sudo权限配置中的 %wheel ALL=(ALL:ALL) ALL 到底是什么意思?
  • d3.js:学习积累
  • ESLint
  • 大米CMS支付漏洞复现报告
  • SAP MM采购申请审批接口分享
  • 自定义类型:结构体、联合和枚举
  • iOS 是开源的吗?苹果系统的封闭与开放边界全解析(含开发与开心上架(Appuploader)实战)
  • 网站建设费 项目经费通用网址通用网站查询
  • 知道网站域名怎么联系wordpress插件的安装目录下