从LevelDB SnapShot到内存快照的思考

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。

引言

SnapShot技术在数据库领域的重要性不言而喻,这种记录整个数据库某一时间点全部视图并快速恢复的能力非常重要,这相当于是一颗后悔药。存储网络行业协会SNIA(StorageNetworking Industry Association)对于Snapshot的定义是:

A point in time copy of a defined collection of data.

而维基对SnapShot的定义是:

a snapshot is the state of a system at a particular point in time.

我们可以看出Snapshot本身是什么并不重要,可以是其所表示的数据的一个副本,也可以是数据的一个复制品,重要的是其记录了系统某一个时间点的状态,这其实类似与Git,LevelDB中的Version以及容器分层的概念,其实就是一个版本控制而已。但是上面术语中定义的SnapShot本身的粒度可能与Git更为类似,因为这两者的版本概念更为具体,这里的版本是我们可以直接恢复到的某一个手动执行SnapShot的时间点。

而LevelDB中Version的概念是去标识SSTable结构的变动,信息存储在 MANIFEST 文件中,这里的粒度就更细了,当然粒度更细的就是所谓的PITR(Point-in-time recovery)了,可以恢复至任意时间点的数据库状态。

扯了一大堆,其实我真正想谈的其实是SnapShot本身的开销问题,不管是Raft算法的SnapShot也好,还是Redis的RDB也罢,本身的开销其实是不小的,尤其是Redis的RDB,会直接Fork当前主进程,然后会序列化当前的内存状态,当内存数据多了以后这显然是一个既消耗CPU又消耗磁盘带宽的行为,虽然有ROW。经典的Fork分为两步,Fork时资源复制时复制页表项并且将其都设置为只读,缺页异常中识别COW引发的错误并实际分配资源实现地址空间隔离。父进程内存占用过大的时候开始页表项和vma内容虽然共享,但是本身对象的复制开销也非常大[10](好文),其他私有数据也需要拷贝,后面缺页时还是需要拷贝[8][9].

Raft的Snapshot就不提了,虽然TiKV对Snapshot做了特殊的处理,但是这个处理其实只是在收发的时候引入SnapChunk,利用了gRPC的Stream发送来防止把SnapShot一次载入内存的情况,其实在进行SnapShot的时候还是比较慢,毕竟需要把日志数据存入磁盘,序列化本身的的CPU消耗以及磁盘IO都是需要考虑的。

本质上的问题来源于大量的磁盘IO以及对于CPU资源的消耗,这些都可能会影响到工作线程从而使得服务时延出现毛刺,你可能会说都已经使用了SnapShot了,这种开销显然无法避免啊,其实是可以的。首先简单思考,需要dump到磁盘的原因是因为我们磁盘上没有存储已有数据,假如已经有的话我们是否可以利用呢?

显然在现代数据库数据库中MVCC和LSM Tree的出现频率已经非常高了,我们能否基于这两个机制的冗余数据做到零开销的内存快照呢?其实LevelDB中的SnapShot我个人认为实现的非常优秀,其可以几乎零开销的生成一个SnapShot(分配一个SSequenceNumber而已),虽然这个快照是只读的。

LevelDB SnapShot

我们来看一看LevelDB的SnapShot是如何实现的,首先前置知识是SSTable中Block的数据被组织为这样:
在这里插入图片描述
其中的key会被压缩,但是每分隔16个就会全量存储,后面restarts记录的是这些全量存储的offset。

其中key的数据被组织为这样:

// klength varint32
// userkey char[klength]
// tag (sequence + type) uint64

也就是所有的key其实都带着一个sequence。

然后我们再来看看InternalKeyComparator::Compare

int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
  // 首先根据user key按升序排列
  // 然后根据sequence number按降序排列
  // 最后根据value type按降序排列
  int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
  if (r == 0) {
    const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
    const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
    if (anum > bnum) {
      r = -1;
    } else if (anum < bnum) {
      r = +1;
    }
  }
  return r;
}

LevelDB获取SnapShot的接口为Snapshot* GetSnapshot(),我们来看看其实现:

typedef uint64_t SequenceNumber;

class SnapshotImpl : public Snapshot {
 public:
  SnapshotImpl(SequenceNumber sequence_number)
      : sequence_number_(sequence_number) {}

  SequenceNumber sequence_number() const { return sequence_number_; }

 private:
  friend class SnapshotList;

  // SnapshotImpl is kept in a doubly-linked circular list. The SnapshotList
  // implementation operates on the next/previous fields direcly.
  SnapshotImpl* prev_;
  SnapshotImpl* next_;

  const SequenceNumber sequence_number_;

#if !defined(NDEBUG)
  SnapshotList* list_ = nullptr;
#endif  // !defined(NDEBUG)
};

可以看到其实只有1个uin64_t类型的成员,根据这个SequenceNumber和上面的比较运算符,我们就可以在Get的时候拼接出一个带着某个SnapShot的SequenceNumber的key,然后去取数据了,这部分代码我们在DBImpl::Get也可以找到:

  SequenceNumber snapshot;
  if (options.snapshot != nullptr) {
    snapshot =
        static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
  } else {
    snapshot = versions_->LastSequence();
  }

当然最大的问题在于Compaction的时候可能会把某些老旧的key删除掉,这个问题很好解决,代码在DoCompactionWork中,也就是在compaction中判断哪些key需要丢弃的时候引入对于snapshot的判断:

	// 是否可以丢掉当前kv对,默认是否
    bool drop = false;
    if (!ParseInternalKey(key, &ikey)) {
      // Do not hide error keys
      current_user_key.clear();
      has_current_user_key = false;
      last_sequence_for_key = kMaxSequenceNumber;
    } else {
      if (!has_current_user_key ||
          user_comparator()->Compare(ikey.user_key, Slice(current_user_key)) !=
              0) {
        // 该user_key是第一次出现
        current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
        has_current_user_key = true;
        //因为第一次出现的user_key不允许删除,所有将last_sequence_for_key设为最大值
        last_sequence_for_key = kMaxSequenceNumber;
      }

      if (last_sequence_for_key <= compact->smallest_snapshot) {
        // 已经有相同user_key出现了,并且上一个user_key的sequenceNumber还小于等于
        // compact->smallest_snapshot,注意直到遇到第二个user_key的sequenceNumber
        // 小于等于smallest_snapshot才能丢弃
        drop = true;  // (A)
      } else if (ikey.type == kTypeDeletion &&
                 ikey.sequence <= compact->smallest_snapshot &&
                 // 在下面的层级中这个key不存在了
                 compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
        //当前kv是距离最小版本smallest_snapshot最近的user_key,但因为它是条删除操作,并且
        //没有已经比它还老的user_key了。所有可以丢弃掉。
        //对于已经存在的key在第一个条件中已经被删除了
        // For this user key:
        // (1) there is no data in higher levels
        // (2) data in lower levels will have larger sequence numbers
        // (3) data in layers that are being compacted here and have
        //     smaller sequence numbers will be dropped in the next
        //     few iterations of this loop (by rule (A) above).
        // Therefore this deletion marker is obsolete and can be dropped.
        drop = true;
      }

      last_sequence_for_key = ikey.sequence;
    }

这样即可保证Snapshot存在时比其SequenceNumber小的数据中最新的那一个不会被删,从而保证SnapShot可以读到最新的数据。

MVCC的启发

这里其实给了我新的想法,这种方法能否利用在MVCC中呢,因为MVCC中也有冗余数据,我们完全可以基于时间来做SnapShot,不过这要求MVCC的垃圾回收需要重新设计,因为其需要考虑到已有的Snapshot,如果做的好,这项工作可以使得基于MVCC的数据库都可以立马拥有完美的零开销的内存快照功能。

这其实很好理解,因为处于RR隔离级别来看,事务眼中确实就是一个不可变的视图,为什么我们不能把这看作一个SnapShot呢。

我认为这是一个很有价值的思考,我想后续可以找机会调研下这项工作。

可回滚数据结构

第一次学习到这个概念实在学习HAMT(Hash Array Mapped Trie)时了解到的[4],具体可以参考[5]进行学习,这其实也是一种forkless的思路。

COW与ROW

COW(Copy-On-Write)与ROW(Redirect-on-write)是经典的SnapShot的实现方案,感觉上就是以Page为单位去做SnapShot了,这其实数据抽象(也就是如何用到我们的代码中)也需要一些时间,但是确实是很有用的,因为把一次很大的资源消耗均摊到多次操作中了,基本上就是一种惰性的思路,具体可以参考[2][6]。

在[6]的评论区提到NetApp有一种很优秀的SnapShot的实现方案,但是看了[7]以后,觉得原理类似与ROW类似。

当然Redis的RDB本质上就是一种COW的实现,在Fork以后直接在子进程执行数据的RDB操作,代码在rdb.c中可以看到。

Cgroup

前面提到Fork的方案会带来一些问题,如果我们已有的代码无法支持我们像是LevelDB一样做这种轻量级的快照,我们怎么才能保证快照的过程不影响工作线程呢,显然资源隔离的方案是一个看起来不错的实现,我们可以起一个进程,对其隔离资源,然后通过工作线程的LOG去做SnapShot,显然这也是一种可行的做法。

总结

从SnapShot到PITR,这种企业级的能力值得我们花时间去思考,如何更快速,更不影响工作线程,还不大幅增加系统的写放大的实现SnapShot是一个很有价值的问题。

参考:

  1. TiKV 源码解析系列文章(十)Snapshot 的发送和接收
  2. 揭秘:存储快照的实现
  3. Snapshot (computer storage) wiki
  4. 抛弃哈希表?持久化数据结构HAMT探究
  5. Functional Go: 持久化数据结构简介
  6. 存储系统的快照技术
  7. NetApp Snapshot Technology
  8. Linux 系统调用 —— fork()内核源码剖析
  9. linux fork COW机制分析
  10. Linux fork那些隐藏的开销
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!
在Python中,"import leveldb"是用于导入LevelDB的Python开发包,使得我们可以在代码中使用LevelDB的功能和方法。 当我们在代码中使用"import leveldb"时,我们可以通过创建一个LevelDB对象来打开一个数据库连接,从而可以对数据库进行读写操作。然而,如果我们尝试在已经打开的连接上再次打开连接,就会引发错误。这是因为LevelDB只允许一个进程同时持有一个数据库的锁定。如果数据库已经被其他进程锁定,就会出现"leveldb.LevelDBError: IO error: lock /var/tmp/ldb1.ldb/LOCK: already held by process"的错误提示。 此外,LevelDB还提供了一些API用法。在使用LevelDB之前,我们可以包装相关的import语句和Options对象来打开和关闭数据库连接,以及其他操作。具体的API用法可以参考LevelDB的官方文档或相关教程。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [LevelDB的Python开发包 py-leveldb基本使用方法的代码](https://blog.csdn.net/weixin_43896490/article/details/121946555)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [leveldb 的部署和使用](https://blog.csdn.net/Moolight_shadow/article/details/119276763)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [leveldb:LevelDB到Java的端口](https://download.csdn.net/download/weixin_42098892/18545599)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李兆龙的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值