RocksDB源码学习(五): 写(一)-框架

RocksDB 的写流程比读流程要复杂很多,其中涉及到了多线程写入与 RocksDB 采用的很多优化,比如 unordered_write、pipelined_write 等等,还有一些可选策略,比如 2pc、parallel 等等,所以写入过程的分支非常多,如果直接磕源码会很没方向。

本篇博客将先理清 RocksDB 的基本写入框架,不考虑 pipelined_write 优化,并且不讨论源码实现,用以为后续的源码分析做前言。


框架

RocksDB 的写入口为 DBImpl::WriteImpl(),这个函数相当复杂,写操作的所有分支流程基本都被封装进去了,该函数的核心入参为一个 WriteBatch* 和一个 WriteOptions& 。RocksDB 每个线程发起的写事务均以一个 WriteBatch 对象为载体,WriteBatch 记录了要写入的所有数据。接着,WriteBatch 会被封装为一个 WriteThread::Writer 结构体,该结构体将存储 WriteBatch、WriteOptions 中的配置以及前后向指针等等,所以一个写线程配备一个 Writer。可以看下这个结构的源码:

  struct Writer {
    WriteBatch* batch;
    bool sync;
    bool no_slowdown;
    bool disable_wal;
    Env::IOPriority rate_limiter_priority;
    bool disable_memtable;
    size_t batch_cnt;  // if non-zero, number of sub-batches in the write batch
    size_t protection_bytes_per_key;
    PreReleaseCallback* pre_release_callback;
    PostMemTableCallback* post_memtable_callback;
    uint64_t log_used;  // log number that this batch was inserted into
    uint64_t log_ref;   // log number that memtable insert should reference
    WriteCallback* callback;
    bool made_waitable;          // records lazy construction of mutex and cv
    std::atomic<uint8_t> state;  // write under StateMutex() or pre-link
    WriteGroup* write_group;
    SequenceNumber sequence;  // the sequence number to use for the first key
    Status status;
    Status callback_status;  // status returned by callback->Callback()

    std::aligned_storage<sizeof(std::mutex)>::type state_mutex_bytes;
    std::aligned_storage<sizeof(std::condition_variable)>::type state_cv_bytes;
    Writer* link_older;  // read/write only before linking, or as leader
    Writer* link_newer;  // lazy, read/write only before linking, or as leader
    // ...
  }

简单的来讲,该结构记录了如下信息:

  • 本次 write 的配置,比如是否要做 sync,要写入的 WAL 日志编号等等
  • 本次要 write 的数据,即 WriteBatch 对象
  • 本次 write 所属的 WiteGroup
  • 本次 write 的第一个数据对象的 sequence number
  • 前置 Writer
  • 后置 Writer

WriteGroup 构建

因为随时都会有接连不断的写请求涌入,为了处理并发, RocksDB 将多个 Writer 对象用链表串起来,组成一个WiteGroup,一个 WriteGroup 会选出一个 Leader 来管理当前 group 的写过程。而任意时刻只会有一个WriteGroup被写入。

每个 Writer 写入前要先加入一个 WriteGroup。加入的过程其实将 Writer 对象加入到 WriteGroup 链表尾端。每个 DB 对象一次只能存在一个 WriteGroup,只有当该 WriteGroup 全部写入完成后,才会开始建立新的 WriteGroup。

首先关注下 WriteGroup 的建立过程。

在 DBImpl::WriteImpl() 封装每一个 Writer 之后,都会把它加进当前 DB 对象的 Writer 的单向链表中,不妨把该该链表称为 WriteLink(实际没有这个结构),link_older 就是该链表的指针,由后往前指。而这个 WriteLink 并不是 WriteGroup,二者是包含关系,每次生成 WriteGroup 都是从 WriteLink 的 Leader 开始连续选择一定数量的 Writer 构成的子双向链表。在 WriteLink 中,由 newest_writer_ 代表最后一个 Writer,在 WriteGroup 中,由 last_writer 代表最后一个 Writer。

Writer 加入 WriteLink 的过程,由 WriteThread::JoinBatchGroup() 实现,主要完成两点作用:

  • 如果加入时链表为空,则将该 Writer 标记为 Leader;
  • 如果加入时链表不为空,则阻塞等待该 Writer 的状态被设置;

WriteGroup 的建立位于 WriteThread::EnterAsBatchGroupLeader() 中,由 Leader 执行,它会首先遍历整个 WriteLink,把单向链表变成双向链表,由 WriteThread::CreateMissingNewerLinks() 实现。接着,重新从 Leader 开始往后遍历,逐个加入 WriteGroup,直到遇见一个 Writer 的配置与 Leader 的配置不符,就算作构建完成。因此,一个 WriteGroup 中所有的 Writer 都是配置相符合的。

WriteGroup与WriteLink

WriteGroup 构建完毕后,由 Leader 来处理其向 WAL 以及 Memtable 中的写入,具体怎么写入先略过。

当本次 WriteGroup 写入完毕后,它需要做一些收尾工作,由 Leader 完成,主要为两点:

  • 更新全局的 sequence number;
  • 设置新的 WriteGroup

第一点由 VersionSet::SetLastSequence() 实现,第二点由 WriteThread::ExitAsBatchGroupLeader() 来实现,这里只关注后者。虽然 WriteThread::CreateMissingNewerLinks() 已经将单向链表变为双向链表,但是在 WriteGroup 写入的过程中,可能有新的 Writer 加入 WriteLink 中,而新的这一段仍然是单向链表,所以 Exit 会重新调用一遍 WriteThread::CreateMissingNewerLinks() 来完善双向链表。接着,设置下一个 WriteGroup 的新Leader(为本组最后一个 Writer 的后置 Writer),并将新 Leader 的前置指针设为 null。

新的WriteGroup

上述流程的源码分析在下一篇博客会详细说明:WriteGroup 源码分析。接下来,我们关注 WriteGroup 的写入。

WriteGroup 写入

这里不考虑 pipelined_write,不考虑 2pc。WriteGroup 的写入分为两步,一是写入 WAL,二是写入 memtable。前者由 DBImpl::WriteToWAL() 实现,后者主要由 WriteBatchInternal::InsertInto() 实现。

WAL

WAL 主要的功能是当 RocksDB 异常退出后,能够恢复出错前的内存中(memtable)数据,因此 RocksDB 默认是每次用户写都会刷新数据到 WAL。每次当当前 WAL 对应的 memtable 刷新到磁盘之后,都会新建一个WAL,即一个 WAL 对应一个memtable。

每一个 WAL 最后都会被写入对应的 WAL 文件中,所有的 WAL 文件均保存在 WA L目录(options.wal_dir),为了保证数据的状态,所有的 WAL 文件的名字都是按照顺序的(log_number)。

写 WAL 可以简单分为三个阶段,第一阶段往 WritableFileWriter 的 buf_ 里面写,第二阶段写到系统缓存,第三阶段是将系统缓存刷盘,也就是让写文件操作落盘。WAL 写入的具体流程会在源码分析时详细说明:WAL 写源码分析(待填坑

memtable

在 RocksDB 中,每个 CF 都有自己的 memtable,互不影响。每当 WriteGroup 写完 WAL 之后,就会向 memtable 中去写。具体的写入流程将分为两种情况, !parallel 和 parallel,由 allow_concurrent_memtable_write 来决定。

在没有并行的情况下,Leader 全权负责整个 WriteGroup 的写入,在并行的情况下,Leader 会唤醒该 WriteGroup 中的所有 Writer,然后各自负责自己的写入,并发执行。不管哪种情况,写memtable的入口函数为WriteBatchInternal::InsertInto(),它会根据传入的是 WriteGroup、Writer 还是 WriteBatch 来进行重载。

memtable 写入的具体流程会在源码分析时详细说明:

总结

这里贴两张总结图,是网上找的,版本为 2018年7月11日版本(commit:35b38a232c1d357a7a885b9b4b8442e24a8433d7 ),不过我没搜到这一版。原图所在仓库:https://github.com/wisehead/myrocks_notes

1、当开启 allow_concurrent_memtable_write 并且关闭 pipelined_write 的写入流程图:

parallel写入流程

2、从 WriteImpl() 开始的函数调用链图,图片太大了,地址如下:

https://s1.ax1x.com/2022/10/25/xRT6JA.jpg

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据Valgrind提供的信息,可以得出以下分析: 这段Valgrind信息表示在程序运行结束时,有24字节的内存块是明确丢失的。这是在294条记录中的第68条记录。 这个内存块的分配是通过`operator new`函数进行的,具体是在`vg_replace_malloc.c`文件的`operator new(unsigned long, std::nothrow_t const&)`函数中进行的。这个函数用于分配内存,并且使用了`std::nothrow_t`参数,表示在分配失败时不抛出异常。 这个内存块的丢失发生在`libstdc++.so.6.0.19`库文件中的`__cxa_thread_atexit`函数中。这个函数是C++标准库中的一个线程退出钩子函数,用于在线程退出时执行清理操作。 进一步跟踪,这个内存块的丢失是在`librocksdb.so.6.20.3`库文件中的`rocksdb::InstrumentedMutex::Lock()`函数中发生的。这个函数是RocksDB数据库引擎的一个锁操作函数,用于获取互斥锁。 在调用堆栈中,可以看到这个内存块丢失是在RocksDB数据库引擎的后台合并线程(Background Compaction)中发生的。具体是在`rocksdb::DBImpl::BackgroundCallCompaction()`和`rocksdb::DBImpl::BGWorkCompaction()`函数中进行的合并操作。 最后,从调用堆栈中可以看到,这个内存块的丢失是在后台线程中发生的。这是在`librocksdb.so.6.20.3`库文件中的`rocksdb::ThreadPoolImpl::Impl::BGThread()`和`rocksdb::ThreadPoolImpl::Impl::BGThreadWrapper()`函数中执行的。 综上所述,根据Valgrind的信息分析,这段代码中存在一个明确的内存泄漏问题,24字节的内存块在后台合并线程中丢失。需要进一步检查代码,确保在合适的时机释放这些内存块,以避免资源泄漏和潜在的问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值