LevelDB写接口源码剖析

263 阅读8分钟

实现跨平台的env类


graph TD

B[env抽象类] --> C{操作系统宏定义}

C -->|Win| D[env_win]

C -->|POSIX| E[env_posix]

抽象类env:


delete拷贝构造函数和拷贝赋值函数

static Env* Default()用于静态的构造env对象

其他方法主要是于操作系统交互的方法,比如新建文件,删除文件,删除文件夹等,都是virtual修饰,需要子类自己实现

注意:一般函数返回是状态码的话,如果还有要回传的比如指针这些就放在形参里面

Put接口写入过程

写入过程

leveldb提供了put接口供写入kv

leveldb写入的记录是通过构造成writebatch然后调用write写入的

多线程写入在write中通过合并写只写入一次,优化了写性能,本质上是通过构造一个抽象的阻塞队列来限制只有一个writer对象(即一个写线程)执行写,该线程在写入之前会遍历所有写对象要写入的内容并合并,最后执行一次写,然后依次唤醒其他线程并返回状态。

在实现方面主要有几个点:

  1. 构建一个队列用于存放writer对象,并且通过mutex来限制对队列的互斥访问

  2. 每个writer都有对应的信号量,当不是队列front的时候就释放mutex并阻塞等待信号量唤醒

  3. 当被唤醒后就检查是否已经done了,done了返回状态,没有继续写

  4. 正常写入时遍历整个队列的writer对象,要把写入的内容全都合并,一次性写入即可

  5. 写入完成后,依次唤醒阻塞的writer

  6. 要点:不需要全流程阻塞,合并所有统一写入的writer的内容后,标记一下合并写入的最后一个writer,就可以释放mutex,供新的writer进来,在唤醒时,通过标记的最后一个writer为终止,依次唤醒即可

联想到之前实现过的go里面的signalflight机制

需求:多线程访问相同key,如何实现本地阻塞队列限制仅一个线程进入去访问目标主机?

实现:

  1. 构建一个map,用一个mutex限制对map的访问, map的key为string类型,value为一个结构体{val,error,waitgroup},waitgroup跟信号量类似

  2. 每个reader需要先获取mutex,然后添加判断map中是否存在对应key的value了,如果存在则直接调用对应的waitgroup阻塞等待唤醒

  3. 如果没有,则添加value,然后释放mutex,供其他reader访问,然后自己去读远程节点的信息,返回结果后,唤醒阻塞在该信号量的所有reader,并返回读取到的结果,然后就可以加锁把map中的key删除掉了

流程图


graph TD

A[writer获取mutex阻塞直到获取到mutex] --> B(添加到writers队列中)

B --> C{当前w未完成写并且不是队列头}

C -->|是| D[休眠释放mutex等待被唤醒]

D --> |被唤醒后| Q{是否已经done了.如果被合并写后done会被置位true}

Q --> |是| W[返回状态]

Q --> |否| E[正常写入]

C -->|否| E

E --> F{检测是否有足够的空间}

F --> |是| G[1.遍历writers中所有writer合并所有写入的内容到一个writerbatch 2.写 3. 更新合并写的所有writer的状态并唤醒 4.唤醒新的一个writer]

F --> |否| H[会休眠1ms.等待compaction]


Put源码剖析


db_impl.h中的DB_Impl类,继承自DB

成员变量:

mutex //更新所有状态或writers队列时都必须先获取该互斥锁

writers //deque<Writer>队列,写入


db_impl.cc Writer类

// Information kept for every waiting writer

struct DBImpl::Writer {

explicit Writer(port::Mutex* mu)

: batch(nullptr), sync(false), done(false), cv(mu) {}

Status status; //返回状态

WriteBatch* batch; //写入对象,leveldb把写入内容封装成该对象

bool sync; //用于判断是否需要刷盘

bool done; //用于判断是否执行完毕

port::CondVar cv; //条件变量

};


Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {

Writer w(&mutex_);

w.batch = updates;

w.sync = options.sync; //默认是false

w.done = false;

//RAII的思想,构造即初始化,即构造的时候上锁,析构的时候解锁

//writers_是全局唯一的队列deque<Writer>,使用该队列需要先加锁,锁也是全局唯一的

MutexLock l(&mutex_);

writers_.push_back(&w);

//如果未写入并且不是队列头则阻塞等待

//这里应该是构建了一个阻塞队列,只放一个进去写,其他的等待,然后写入后再依次唤醒其他阻塞

//writer,直接返回状态即可

while (!w.done && &w != writers_.front()) {

w.cv.Wait();//Wait会释放mutex

}

//如果已经done了则返回状态

if (w.done) {

return w.status;

}

//所以只有未写入且属于队列头的writer才能执行以下逻辑

// May temporarily unlock and wait.

//MakeRoomForWrite 清理空间及各方条件合适后供写入

Status status = MakeRoomForWrite(updates == nullptr);

uint64_t last_sequence = versions_->LastSequence();

Writer* last_writer = &w; //w是当前的deque.front(),last_writer初始化为头,在BuildBatchGroup中会逐步被更新为最后一个writer

if (status.ok() && updates != nullptr) { // nullptr batch is for compactions

//这里的逻辑是把所有其他wirter的batch都合并到一起,合并写只需要写入一次

WriteBatch* write_batch = BuildBatchGroup(&last_writer);

WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);

last_sequence += WriteBatchInternal::Count(write_batch);

// Add to log and apply to memtable. We can release the lock

// during this phase since &w is currently responsible for logging

// and protects against concurrent loggers and concurrent writes

// into mem_.

{

//释放mutex,这里为什么要释放?

//释放了的话,如果有新的writer就可以加入到writers中

//这里有新的writer加入到writers也没有关系,因为已经记录了当前被合并写的最后一个writer了,为last_writer

//在后续的唤醒操作时,只唤醒到last_writer即可,这个也算是神来之笔了

mutex_.Unlock();

status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));

bool sync_error = false;

if (status.ok() && options.sync) {

//Sync是刷盘

status = logfile_->Sync();

if (!status.ok()) {

sync_error = true;

}

}

if (status.ok()) {

//写入到mem中

status = WriteBatchInternal::InsertInto(write_batch, mem_);

}

mutex_.Lock();

if (sync_error) {

// The state of the log file is indeterminate: the log record we

// just added may or may not show up when the DB is re-opened.

// So we force the DB into a mode where all future writes fail.

RecordBackgroundError(status);

}

}

if (write_batch == tmp_batch_) tmp_batch_->Clear();

versions_->SetLastSequence(last_sequence);

}

//依次唤醒队列中阻塞的所有writer

while (true) {

Writer* ready = writers_.front();

writers_.pop_front();

if (ready != &w) {

ready->status = status;

ready->done = true;

ready->cv.Signal();

}

if (ready == last_writer) break; //注意这里只唤醒到合并写的最后一个writer

}

// 注意需要重新唤醒一个新的writer,因为中间释放了锁,所以有新的writer进来并且因为不是front而被休眠

// Notify new head of write queue

if (!writers_.empty()) {

writers_.front()->cv.Signal();

}

return status;

}


MakeRoomForWrite逻辑

// REQUIRES: mutex_ is held

// REQUIRES: this thread is currently at the front of the writer queue

Status DBImpl::MakeRoomForWrite(bool force) {

mutex_.AssertHeld();

assert(!writers_.empty());

bool allow_delay = !force;

Status s;

//注意这里是while(true),所以如果break才会退出,如果没有break还会重新判断

/*

1、允许延迟并且当前level0文件数量超过了规定的缓速数量,则释放mutex(释放后允许新的writer进入writers,但这些新的writer因为不是front所以仍然会休眠),

然后本线程睡1ms,并且设置不允许再休眠了,醒后重新获取锁,虽然重新进入循环判断。这样做的好处是文件数量太多的情况下,把CPU资源腾出来给compaction线程,尽量把文件数降低。

2、 如果非强制并且当前内存使用量小于分配量,说明现在内存有空间,所以直接break返回即可。updates==nullptr->force为true

3、 imm不为nullptr,说明mem满了转换成immumem了,阻塞等地后天compaction完成,再写入

4、 如果当前level0文件已经达到规定的暂停触发数量,则休眠等待compaction完成,注意这个值跟1的缓速数量的区别

5、 其他情况,说明当前mem没有内存,也没有imm,后台也没有在compaction,则当前是初始化状态,则创建log,创建mem,新建后台线程负责后台compaction

*/

while (true) {

//bg_error_是一个db对象的状态变量

if (!bg_error_.ok()) {

// Yield previous error

s = bg_error_;

break;

} else if (allow_delay && versions_->NumLevelFiles(0) >=

config::kL0_SlowdownWritesTrigger) {

//如果允许延迟并且当前的level0文件数大于等于最大限制数

//此时会延迟写入,而是把CPU让给合并线程,从而减少文件数量

//解锁,休眠1ms,每个线程最多休眠一次

// We are getting close to hitting a hard limit on the number of

// L0 files. Rather than delaying a single write by several

// seconds when we hit the hard limit, start delaying each

// individual write by 1ms to reduce latency variance. Also,

// this delay hands over some CPU to the compaction thread in

// case it is sharing the same core as the writer.

//写入接近硬件限制最大的LO文件数量了。

//当达到硬件限制时,不是延迟单个writer几秒,而是对每个writer都延迟1ms,从而减少延迟差异

//如果共享同个CPU核心,这样延迟可以把cpu资源让给合并县城

mutex_.Unlock();

env_->SleepForMicroseconds(1000);

allow_delay = false; // Do not delay a single write more than once

mutex_.Lock();

} else if (!force &&

(mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {

// There is room in current memtable

//如果非强制写入,。。。

break;

} else if (imm_ != nullptr) {

// We have filled up the current memtable, but the previous

// one is still being compacted, so we wait.

//如果当前有immu内存表对象,即当前已经填满了一个,在生成另一个mem,则等待

Log(options_.info_log, "Current memtable full; waiting...\n");

background_work_finished_signal_.Wait();//db对象唯一条件变量,应该在更新完mem后会唤醒

} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {

// There are too many level-0 files.

//此时是不允许延迟了,但仍然有太多level0文件,则休眠,等合并完被唤醒

Log(options_.info_log, "Too many L0 files; waiting...\n");

background_work_finished_signal_.Wait();

} else {

// Attempt to switch to a new memtable and trigger compaction of old

//尝试选择一个新的mem表并且触发旧表的minor合并操作

assert(versions_->PrevLogNumber() == 0);//断言没有log文件则执行下面逻辑

uint64_t new_log_number = versions_->NewFileNumber();//申请一个新的文件号

WritableFile* lfile = nullptr;

s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);//申请一个新的log文件

if (!s.ok()) {

// Avoid chewing through file number space in a tight loop.

versions_->ReuseFileNumber(new_log_number);

break;

}

//删除日志文件写对象

delete log_;

delete logfile_;

//重新复制日志写对象

logfile_ = lfile;

logfile_number_ = new_log_number;

log_ = new log::Writer(lfile);

//mem转immutable_mem

imm_ = mem_;

//原子量,用了释放写,让其他线程知道

has_imm_.store(true, std::memory_order_release);

//重新申请mem

mem_ = new MemTable(internal_comparator_);

//引用+1

mem_->Ref();

force = false; // Do not force another compaction if have room

MaybeScheduleCompaction();

}

}

return s;

}

创建compaction的逻辑


/*

后台线程是一个队列,只要队列里面有后台线程,就会依次拿出来执行,执行完后就删除

所以compaction的核心逻辑BGWork里面首先执行了compaction,然后再重新构建一个线程加入到后台线程队列中

从而实现的不断的compaction

*/

void DBImpl::MaybeScheduleCompaction() {

mutex_.AssertHeld();

if (background_compaction_scheduled_) {

// Already scheduled

//当前已经在compaction

} else if (shutting_down_.load(std::memory_order_acquire)) {

// DB is being deleted; no more background compactions

} else if (!bg_error_.ok()) {

// Already got an error; no more changes

} else if (imm_ == nullptr && manual_compaction_ == nullptr &&

!versions_->NeedsCompaction()) {

// No work to be done

} else {

//标志位设置为true,表示执行后台合并中,然后创建一个后台线程用于执行compaction,加入到后台线程队列中

background_compaction_scheduled_ = true;

//DBImpl::BGWork就是执行逻辑,一个静态函数,this是参数

env_->Schedule(&DBImpl::BGWork, this);

}

}

void DBImpl::BGWork(void* db) {

reinterpret_cast<DBImpl*>(db)->BackgroundCall();

}

void DBImpl::BackgroundCall() {

MutexLock l(&mutex_);

assert(background_compaction_scheduled_);

if (shutting_down_.load(std::memory_order_acquire)) {

// No more background work when shutting down.

} else if (!bg_error_.ok()) {

// No more background work after a background error.

} else {

BackgroundCompaction();

}

background_compaction_scheduled_ = false;

// Previous compaction may have produced too many files in a level,

// so reschedule another compaction if needed.

MaybeScheduleCompaction();

background_work_finished_signal_.SignalAll();

}