導讀
我們還是按照上一篇博文的那三個函數順序往下走:Put
,Get
和Delete
,以自上而下的視角一點一點剖析leveldb。那么本篇就主要講Put的實現。Put的實現講的還是偏上層的,因為Put可能引發compaction,而compaction等細節內容還是需要專門一篇文章才能描述清楚。那么我們開始吧!
流程
還記得我們上一篇講解的Put的流程是咋樣的嗎?
Put操作首先將操作記錄寫入log文件,然后寫入memtable,返回寫成功。整體來看是這樣,但是會引發下面的問題:
1. 寫log的時候是實時刷到磁盤的嗎?
2. 寫入的時候memtable過大了咋辦?
3. 同時多個線程并發寫咋辦?
......
下面的分析中就會面對這些問題,有些答案很清晰,有些涉及到超級多細節。
step by step
在開始之前,我們知道Write操作是要記錄到log文件中的。那么一個記錄它的格式是怎樣的呢?看圖:
這里特別解釋一下,Delete操作也是通過Put實現的,只是圖中的類型字段是0,而正常Write的操作類型是1,由此區分寫操作和刪除操作!
step 1
很簡單吧。這里講解一下那三個參數:
- WriteOptions:提供一些寫操作的配置項,例如要不要寫log的時候馬上flush磁盤
- key和value就是對應的keyValue,slice只是作者自己封裝的char數組存儲數據而已。<b>大牛喜歡把所有東西都封裝一下,賦予數據結構意義!</b>這在大的工程里面是很有意義的,既方便操作,也方便思考(這樣就不用思考底層的真實的char數組還是啥)。
step 2
這里的WriteBatch的意思就是多個寫并起來操作的意思。這里可以學習一個思維,支持多key寫,單key寫就是key為1的特例。底層完全按照WriteBatch的實現方式
step 3
下面是真實的各種復雜邏輯的操作了。下面的代碼有點長,所以基本都在代碼中注釋講解了。部分調用函數的細節下面會繼續,請耐心欣賞,畢竟人家也不是幾分鐘就寫出來了,哈哈:
Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) {
Writer w(&mutex_);
w.batch = my_batch;
w.sync = options.sync; //log是否馬上刷到磁盤,如果false會有數據丟失的風險
w.done = false; //標記寫入是否完成
//串行化writer,如果有其他writer在執行則進入隊列等待被喚醒執行
MutexLock l(&mutex_);
writers_.push_back(&w);
while (!w.done && &w != writers_.front()) {
w.cv.Wait();
}
//writer的任務可能被其他writer幫忙執行了
//如果是則直接返回
if (w.done) {
return w.status;
}
// May temporarily unlock and wait.
Status status = MakeRoomForWrite(my_batch == NULL); //寫入前的各種檢查
//是否該停寫,是否該切memtable,是否該compact
//獲取本次寫入的版本號,其實就是個uint64
uint64_t last_sequence = versions_->LastSequence();
Writer* last_writer = &w;
//這里writer還是隊列中第一個,由于下面會隊列前面的writers也可能合并起來,所以last_writer指針會指向被合并的最后一個writer
if (status.ok() && my_batch != NULL) { // NULL batch is for compactions
WriteBatch* updates = BuildBatchGroup(&last_writer); //這里會把writers隊列中的其他適合的寫操作一起執行
WriteBatchInternal::SetSequence(updates, last_sequence + 1); //把版本號寫入batch中
last_sequence += WriteBatchInternal::Count(updates); //updates如果合并了n條操作,版本號也會向前跳躍n
// 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_.Unlock();
status = log_->AddRecord(WriteBatchInternal::Contents(updates)); //寫log了,不容易啊,等那么久
bool sync_error = false;
if (status.ok() && options.sync) { //馬上要flush到磁盤
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
if (status.ok()) {
status = WriteBatchInternal::InsertInto(updates, mem_); //恩,是時候插入memtable中了
}
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 (updates == tmp_batch_) tmp_batch_->Clear();
versions_->SetLastSequence(last_sequence);
}
while (true) { //在這里喚醒已經幫它干完活的writer線程,讓它早早回家,別傻傻等了
Writer* ready = writers_.front();
writers_.pop_front();
if (ready != &w) {
ready->status = status;
ready->done = true;
ready->cv.Signal();
}
if (ready == last_writer) break;
}
// Notify new head of write queue
if (!writers_.empty()) { //喚醒還沒干活的等待的第一位,叫醒它自己去干活,老子干完了,哈哈,典型FIFO,公平
writers_.front()->cv.Signal();
}
return status;
}
嗯,核心的write代碼已經在這里欣賞完了。基本邏輯分為五步:
- 隊列化請求
- 寫入的前期檢查和保證
- 按格式組裝數據為二進制
- 寫入log文件和memtable
- 喚醒隊列的其他人去干活,自己返回
這個函數的內容就這么多,但是有些內容調用我們還沒有講,下面會選擇部分重中之重的講。例如添加前的檢查保證,compaction。
step 4 MakeRoomForWrite函數
我們在step 3的時候知道了MakeRoomForWrite函數的存在,就是寫入前的各種檢查,例如:
- memtable滿了達到大小的限制了沒?沒有直接可以插入
- memtable達到太大之后需要切換為Imuable memtable,這時候需要檢查舊的Imuable memtable已經load到磁盤沒
- 由于Imuable需要load入磁盤,level 0的文件數超過限制沒
。。。。。。
在進入邏輯代碼之前需要解釋一個概念:<b>compaction</b>
<b>compaction</b>是指:Imuable memtablecompact到level 0 或者level n compact 到level n + 1, 就是數據向搞level流動的操作,這個操作會保證數據在level中是全局有序的,除了level 0.
這里只是簡單列舉幾個需要考慮的邏輯,具體檢查看下面MakeRoomForWrite的實現:
Status DBImpl::MakeRoomForWrite(bool force) { // force代表需要強制compaction與否
mutex_.AssertHeld(); //保證進入該函數前已經加鎖
assert(!writers_.empty());
bool allow_delay = !force; //allow_delay代表compaction可以延后
Status s;
while (true) {
if (!bg_error_.ok()) { //如果后臺任務已經出錯,直接返回錯誤
// Yield previous error
s = bg_error_;
break;
} else if (//level0的文件數限制超過8,睡眠1ms,簡單等待后臺任務執行
allow_delay &&
versions_->NumLevelFiles(0) >= config::kL0_SlowdownWritesTrigger) { //level 0超過8個sst文件了
// 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.
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)) { //memtable的大小限制,default為4MB
// There is room in current memtable
break;
} else if (imm_ != NULL) {
// We have filled up the current memtable, but the previous
// one is still being compacted, so we wait.
Log(options_.info_log, "Current memtable full; waiting...\n"); //等待之前的imuable memtable完成compact到level0
bg_cv_.Wait(); // bg_cv_為后臺程序的條件變量,后臺程序就是做compact的
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) { //level0的文件數超過12,強制等待
// There are too many level-0 files.
Log(options_.info_log, "Too many L0 files; waiting...\n");
bg_cv_.Wait();
} else { //下面是切換到新的memtable和觸發舊的進行compaction
// Attempt to switch to a new memtable and trigger compaction of old
assert(versions_->PrevLogNumber() == 0);
uint64_t new_log_number = versions_->NewFileNumber();
WritableFile* lfile = NULL;
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;
}
//刪除舊的log對象分配新的
delete log_;
delete logfile_;
logfile_ = lfile;
logfile_number_ = new_log_number;
log_ = new log::Writer(lfile);
imm_ = mem_; //切換memtable到Imuable memtable
has_imm_.Release_Store(imm_);
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
force = false; // Do not force another compaction if have room
MaybeScheduleCompaction(); //如果需要進行compaction,后臺執行
}
}
return s;
}
由上面的代碼可知,這是一個while循環,直到確保數據庫可以寫入了才會返回。
流程大概是:
1. 后臺任務有無出現錯誤?出現錯誤直接返回錯誤。(compaction是后臺線程執行的)
2. level 0 的文件數超過8個,則等待1s再繼續執行,因為level 0的文件數目需要嚴格控制
3. 如果memtable的大小小于4MB(默認值,可以修改),直接返回可以插入;
4. 到達4說明memtable已經滿了,這時候需要切換為Imuable memtable。所以這時候需要等待舊的Imuable memtable compact到level 0,進入等待
5. 到達5說明舊的Imuable memtable已經compact到level 0了,這時候假如level 0的文件數目到達了12個,也需要等待
6. 到達6說明舊的Imuable memtable已經compact到磁盤了,level 0的文件數目也符合要求,這時候就可以生成新的memtable用于數據的寫入了。
由此我們知道這里并沒有涉及到具體的compaction是如何進行的,只是保證memtable可以寫入和可能觸發后臺的compaction。至于compaction的邏輯,后面需要專門的一篇文章才能說明清楚。
好了,MakeRoomForWrite函數就說這么多。
step 5 BuildBatchGroup函數
還有一個函數我覺得可以說一下,那就是BuildBatchGroup函數。前面我們說到了writer會被隊列化,而只有排在第一位的writer才會往下執行。而writer都是樂于助人的,它有可能會把排在它后面的writer的活也拿過來一起干了。那么這段幫忙的邏輯就是在BuildBatchGroup函數中。
請看這個函數做了寫啥:
WriteBatch* DBImpl::BuildBatchGroup(Writer** last_writer) {
assert(!writers_.empty());
Writer* first = writers_.front(); //當前執行的writer兄弟
WriteBatch* result = first->batch;
assert(result != NULL);
size_t size = WriteBatchInternal::ByteSize(first->batch); // 計算要自己要寫入的byte大小
// Allow the group to grow up to a maximum size, but if the
// original write is small, limit the growth so we do not slow
// down the small write too much.
//計算maxsize,避免自己幫太多忙了導致寫入數據過大
size_t max_size = 1 << 20;
if (size <= (128<<10)) {
max_size = size + (128<<10);
}
*last_writer = first;
std::deque<Writer*>::iterator iter = writers_.begin();
++iter; // Advance past "first"
for (; iter != writers_.end(); ++iter) {
Writer* w = *iter;
if (w->sync && !first->sync) { //sync類型不同,你的活我不幫你了
// Do not include a sync write into a batch handled by a non-sync write.
break;
}
if (w->batch != NULL) {
size += WriteBatchInternal::ByteSize(w->batch);
if (size > max_size) { //這個幫忙數據量過大了,我也不幫忙了
// Do not make batch too big
break;
}
// Append to *result
if (result == first->batch) {
// Switch to temporary batch instead of disturbing caller's batch
result = tmp_batch_;
assert(WriteBatchInternal::Count(result) == 0);
WriteBatchInternal::Append(result, first->batch); //來吧,你的活我可以幫你干了
}
WriteBatchInternal::Append(result, w->batch);
}
*last_writer = w;
}
return result;
}
看上面代碼,這段代碼的邏輯比較簡單。就是遍歷隊列后面的writer們,一直到遇到幫不了忙的writer就返回了。那么幫不幫忙的標準是啥?
兩個標準:
- sync類型是否一樣(我不需要馬上flush到磁盤而你要,你的活還是自己干吧)
- 寫入的數據量是不是過大了?(避免單次寫入數據量太大)
只要沒有符合這兩個限制條件,就可以幫忙,合并多條write操作為一條操作!
總結
哇嗚,看了這么多代碼,是不是對level db的write操作有了一定的了解了。其實就是文章開始說的,
- 先寫入log文件
- 再寫入memtable
這兩個邏輯而已。然后在寫入之前需要保證可以寫入啊,不然內存會跪的。然后這個檢查會導致后臺一系列的compaction可能。
至于什么多個操作合并為一個操作,完全是優化,為了更高的性能!
記住了,我們講完了Put操作,其實Delete操作也已經講完了。為什么?不知道的好好看上面的正文內容吧。下一篇文章,我們將迎來Get函數,我們會一起進入level db的查找數據之旅!