近期在做on nvme hash引擎相关的事情,对于非全序的数据集的存储需求,相比于我们传统的LSM或者B-tree的数据结构来说 能够减少很多维护全序上的计算/存储资源。当然我们要保证hash场景下的高写入能力,append-only 还是比较友好的选择。
像 Riak的bitcask 基于索引都在内存的hash引擎这种,在后期的针对data-file 的merge 完成之后会将新的key-value-index回填到内存中的hash索引中,这个过程在实际的生产环境对性能有多大的影响还不太好确定。但是,很明显的一点是正确的hash引擎索引在高并发场景中的更新是需要加锁的。而一旦有了排他锁,也就意味着CPU的独占,这个时候用户的读取和插入 就会和merge 之后的index回填发生锁的竞争,从而影响引擎的外部性能。
而同样的问题在以 Wisckey 为首的 LSM-tree key-value 分离策略中尤为明显,包括Titan, rocksdb的BlobDB,BadgerDB 都会面临这样的问题,他们在compaction 之后的回填 大value-index 还需要产生I/O操作,这个代价可能会更高,那他们是怎么解决这个问题的呢?
探索他们的解决办法不一定完全能够借鉴到hash 引擎的实现中,不过是可以提供一个解决思路。
关于Titan的 GC 策略介绍可以参考:Titan GC策略实现
Titan 是 pingcap 早期基于wisckey 做出来的key-value 分离存储引擎,可以作为rocksdb 的一个插件来使用。
它的解决办法是提供一个可配置项gc_merge_rewrite
:
kMergeType
blob-index,这种情况下不需要去执行Callback
了,而是直接写入Merge操作,后续通过compaction 进行 key的blob-index的合并 或者 读请求命中这个key的时候会进行merge。merge请求本省不会携带原本大小的value,所以不会产生较大的写放大,只是在读的时候需要将当前key之前的merge都进行合并,对读性能可能有较大的影响。相关的实现代码可以参考:
void BlobGCJob::BatchWriteNewIndices(BlobFileBuilder::OutContexts& contexts,Status* s) { ...// 关闭merge,调用默认的写入方式if (!gc_merge_rewrite_) { merge_blob_index.EncodeToBase(&index_entry);// Store WriteBatch for rewriting new Key-Index pairs to LSM// 在这个策略下,rewrite_batches_ 最后的消费是通过 Rocksdb::WriteWithCallback实现// 的,在写入的时候会执行 Callback,里面会去查一下key是否存在。GarbageCollectionWriteCallback callback(cfh, ikey.user_key.ToString(),std::move(original_index));callback.value = index_entry;rewrite_batches_.emplace_back(std::make_pair(WriteBatch(), std::move(callback)));auto& wb = rewrite_batches_.back().first;*s = WriteBatchInternal::PutBlobIndex(&wb, cfh->GetID(), ikey.user_key,index_entry);} else { // 开启,rewrite_batches_without_callback_ 的消费过程是 直接写入Merge 类型的keymerge_blob_index.EncodeTo(&index_entry);rewrite_batches_without_callback_.emplace_back(std::make_pair(WriteBatch(), original_index.blob_handle.size));auto& wb = rewrite_batches_without_callback_.back().first;*s = WriteBatchInternal::Merge(&wb, cfh->GetID(), ikey.user_key,index_entry);}...
}
最终对两个数据结构的消费逻辑统一是在RewriteValidKeyToLSM
函数中。
BlobDB 的大体特性可以参考BlobDB 特性及性能测试结果。
因为BlobDB 新版本是社区比较推荐的一个k/v分离的稳定版本,基本的Rocksdb特性都已经支持了,包括trasaction/checkpoint/backup 等这一些不常用但很重要的功能都已经支持了。除了像merge/ingest等更为偏的能力暂时还不支持。
BlobDB的在GC上的一个考虑就不想因为后续频繁的回写处理影响正常的请求。
如果开启了GC enable_blob_garbage_collection
:
kTypeBlobIndex
的key时会进入到GarbageCollectBlobIfNeeded
,因为分离存储的时候lsm中存放的value 是key-index,即这个value能够索引的到blobfile的一个index。主体第二步,也就是想要GC的话会在compaction过程中直接将过期的blob-value直接回收,compaction完成之后 lsm的sst 以及 blob都会被更新到,只需要维护后续的旧的blob回收即可。
代码实现如下:
void CompactionIterator::PrepareOutput() { if (valid_) { if (ikey_.type == kTypeValue) { ExtractLargeValueIfNeeded();} else if (ikey_.type == kTypeBlobIndex) { // 调度GCGarbageCollectBlobIfNeeded();}...
}
void CompactionIterator::GarbageCollectBlobIfNeeded() { ...// 开启GCif (compaction_->enable_blob_garbage_collection()) { BlobIndex blob_index;{ // 1. 获取blobindexconst Status s = blob_index.DecodeFrom(value_);if (!s.ok()) { status_ = s;valid_ = falsereturn;}}if (blob_index.IsInlined() || blob_index.HasTTL()) { status_ = Status::Corruption("Unexpected TTL/inlined blob index");valid_ = false;return;}// 2. 确认当前blob-index 允许参与GCif (blob_index.file_number() >=blob_garbage_collection_cutoff_file_number_) { return;}const Version* const version = compaction_->input_version();assert(version);{ // 3. 解析读出来当前blob数据const Status s =version->GetBlob(ReadOptions(), user_key(), blob_index, &blob_value_);if (!s.ok()) { status_ = s;valid_ = false;return;}}value_ = blob_value_;// 4. 将读出来的blob数据写入到新的blob file,并构造新的 value-index 作为当前lsm-tree// 即将存储的key的value.if (ExtractLargeValueIfNeededImpl()) { return;}ikey_.type = kTypeValue;current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);return;}...
}
这里的读取只会是保留的key的real value,对于那一些要清理的key,则不会读取。为了避免业务峰值触发大量的compaciton以及 GC的读取,GC的触发可以通过SetOption
来动态调整。
个人觉得,BlobDB的GC调度更为简洁高效低成本。
来,我们对比一下GC过程中产生I/O的步骤:
可以看到,blobdb 的第二个步骤是正常的compaction写入逻辑,相比于Titan来说,其实也就只进行了 Titan有效的第二步,少了第一步的点查和第三步的回写。除此之外,Rocksdb的可调性更高一些,可以针对必要的GC时的大value读写进行控制,允许动态调整,从而最大程度得减少了GC对上层请求的性能影响。
具体在 GC 过程中的性能差异会在后续补充上。
Badger 作为 dGraph 社区备受 cgo 折磨之后推出的自研k/v 分离存储引擎,在go 语言中还是非常受欢迎的。
本文仅讨论BadgerDB 在k/v 分离场景的回写策略,对于其测试优于Rocksdb(rocksdb的默认参数) 以及 其相比于Rocksdb 的其他优秀设计暂不展开讨论。
Badger的大value是存放在value log文件中,它很聪明的一点是GC 接口只交给用户来调度,而不是自己内部自主触发,这样的责任划分就非常清晰了,用户自己选择开启关闭GC,来自己承担GC引入的读写问题,真是机智。
当然BadgerDB 这里的GC回写并没有看到太亮眼的设计,就是在对 value log 进行GC的时候和Titan不开启gc_merge_rewrite
逻辑差不多。
回写源代码基本在RunValueLogGC
函数中的rewrite
处理逻辑中,感兴趣的可以看一下。
可以看到为了解决在LSM-tree中大value 不随着compaction一起调度而造成的性能问题,大家可谓是煞费苦心。Titan 尝试做了一些优化,但整体来看还是不尽人意。Rocksdb 的 Blobdb 还是更加成熟,可以说是考虑得很全面了,从实现上看确实有很明显的效果。而BadgerDB的做法更为彻底,这个问题我们不管,交给用户自由调度,因为用户大多数情况还是知道自己的业务什么时候处于高峰,什么时候处于低谷,产生的I/O竞争问题那是你们自己调度造成的,自己解决哈,🐂。
而回到最初的我们 hash-engine 的 hash-index回写问题,其实可以考虑借鉴一下 BlobDB的做法,不过需要接口做的更灵活一些。
前言 最近在读 MyRocks 存储引擎2020年的论文,因为这个存储引擎是在Rocksdb之上进行封装的,并且作为Facebook 内部MySQL的底层引擎,用来解决Innodb的空间利用率低下 和 压缩效率低下的问题。而且MyRocks 在接入他们UDB 之后成功达成了他们的目标:将以万为单位的服务器集群server个数缩减了一...
参考自: https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html 1、maven依赖
springboot redis配置
1、引入maven依赖
json 键值对增加、删除 obj.key='value'; // obj.key=obj[key]=eval("obj."+key); delete obj.key; vue中新增和删除属性 this.$set(object,key,value) this.$delete( object, key ) 触发视图更新 遍历键值 for...
前言 还是回到传统的 LSM-tree 中,我们key-value 写入时以append形态存放到一个data-block中,多个data-block+metablock 之类的数据组织成一个sst。当我们读数据以及compaction的时候读到key 之后则很方便得读取到对应的value,一次I/O能够将key-value完全从磁...
文章目录1. 为什么要有GC2. GC的触发条件3. GC的核心逻辑1. blob file形态2. GC Prepare3. GC pick file4. GC run4. GC 引入的问题5. Titan的测试代码...
【BZOJ4282】慎二的随机数列 Description 间桐慎二是间桐家著名的废柴,有一天,他在学校随机了一组随机数列, 准备使用他那强大的人工智能求出其最长上升子序列,但是天有不测风云,人有旦夕祸福,柳洞一成路过时把间桐慎二的水杯打翻了…… 现在给你一个长度为 n 的整数序列,其中有一些数已经模糊不清了,现在请你任意确定这些整...
CrashReport系统在游戏内测当天出现了异常情况JVM僵死,通过top -p
// 下载blob文件流(暂不支持手机H5唤起下载文件!!!) downloadFile(res: any, fileName: any = '未命名', format: any = '.xlsx') {const blob = new Blob([res]);fileName += format;// for IEif (windo...