8.1 基本原理
"LOG文件在LevelDb中的主要作用是系統(tǒng)故障恢復時,能夠保證不會丟失數(shù)據(jù)。因為在將記錄寫入內存的Memtable之前,會先寫入Log文件,這樣即使系統(tǒng)發(fā)生故障,Memtable中的數(shù)據(jù)沒有來得及Dump到磁盤的SSTable文件,LevelDB也可以根據(jù)log文件恢復內存的Memtable數(shù)據(jù)結構內容,不會造成系統(tǒng)丟失數(shù)據(jù),在這點上LevelDb和Bigtable是一致的。" --- 數(shù)據(jù)分析與處理之二(Leveldb 實現(xiàn)原理)
8.2. 日志文件
8.2.1 數(shù)據(jù)結構
日志中的每條記錄由Record Header + Record Content組成,其中Header大小為kHeaderSize(7字節(jié)),由CRC(4字節(jié)) + Size(2字節(jié)) + Type(1字節(jié))三部分組成。除此之外才是content的真正內容:

日志文件的基礎部件很簡單,只需要能夠創(chuàng)建文件、追加操作、實時刷新數(shù)據(jù)即可。為了做到跨平臺、解耦,LevelDB還是對此做了封裝。Leveldb命名空間下,有一個名為log的子命名空間,其下有Writer、Reader兩個實現(xiàn)類。按前幾節(jié)的命名規(guī)則,Writer其實是一個Builder,它對外提供了唯一的AddRecord方法用于追加操作記錄。
8.2.2 Writer
在log命名空間中,包含一個Writer用于日志操作,其只有一個Append方法,這和日志的定位相同,定義如下:
class Writer
{
explicit Writer(WritableFile *dest);
Writer(WritableFile *dest, uint64_t dest_length);
...
Status AddRecord(const Slice &slice);
...
};
外部創(chuàng)建一個WritableFile,通過構造函數(shù)傳遞給Writer。AddRecord按上述的結構完善record并添加到日志文件中。
Status Writer::AddRecord(const Slice& slice) {
const char* ptr = slice.data();
size_t left = slice.size();
// Fragment the record if necessary and emit it. Note that if slice
// is empty, we still want to iterate once to emit a single
// zero-length record
Status s;
bool begin = true;
do {
//1. 當前塊剩余大小
const int leftover = kBlockSize - block_offset_;
assert(leftover >= 0);
//2. 剩余大小不足,占位
if (leftover < kHeaderSize)
{
// Switch to a new block
if (leftover > 0)
{
// Fill the trailer (literal below relies on kHeaderSize being 7)
assert(kHeaderSize == 7);
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
}
block_offset_ = 0;
}
// Invariant: we never leave < kHeaderSize bytes in a block.
assert(kBlockSize - block_offset_ - kHeaderSize >= 0);
const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
//3. 當前塊存儲的空間大小
const size_t fragment_length = (left < avail) ? left : avail;
//4. Record Type
RecordType type;
const bool end = (left == fragment_length);
if (begin && end) {
type = kFullType;
}
else if (begin) {
type = kFirstType;
}
else if (end) {
type = kLastType;
}
else {
type = kMiddleType;
}
//5. 寫入文件
s = EmitPhysicalRecord(type, ptr, fragment_length);
ptr += fragment_length;
left -= fragment_length;
begin = false;
} while (s.ok() && left > 0);
return s;
}
- 當前Block剩余大小不足以填充Record Header時,以"\x00\x00\x00\x00\x00\x00"占位。
- 當Block無法完整記錄一條Record時,通過type信息標識該record在當前block中的區(qū)塊信息,以便讀取時可根據(jù)type拼接出完整的record。
- EmitPhysicalRecord向Block中插入Record數(shù)據(jù),每條記錄append之后會執(zhí)行一次flush。
8.3 創(chuàng)建日志的時機
在LevelDB中,日志文件和memtable是配對的,在任何數(shù)據(jù)寫入Memtable之前都會先寫入日志文件。除此之外,日志文件別無它用。
因此,日志文件的創(chuàng)建時和Memtable的創(chuàng)建時機也必然一致,這點對于我們理解日志文件至關重要。那么,Memtable在何時會創(chuàng)建呢?
8.3.1 數(shù)據(jù)庫啟動
如果我們創(chuàng)建了一個新數(shù)據(jù)庫,或者數(shù)據(jù)庫上次運行的所有日志都已經歸檔到Level0狀態(tài)。此時,需要為本次數(shù)據(jù)庫進程創(chuàng)建新的Memtable以及日志文件,代碼邏輯如下:
Status DB::Open(const Options &options, const std::string &dbname,
DB **dbptr)
{
*dbptr = NULL;
//創(chuàng)建新的數(shù)據(jù)庫實例
DBImpl *impl = new DBImpl(options, dbname);
impl->mutex_.Lock();
VersionEdit edit;
//恢復到上一次關閉時的狀態(tài)
// Recover handles create_if_missing, error_if_exists
bool save_manifest = false;
Status s = impl->Recover(&edit, &save_manifest);
if (s.ok() && impl->mem_ == NULL)
{
//創(chuàng)建新的memtable及日志文件
// Create new log and a corresponding memtable.
//分配日志文件編號及創(chuàng)建日志文件
uint64_t new_log_number = impl->versions_->NewFileNumber();
WritableFile *lfile;
s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
&lfile);
if (s.ok())
{
edit.SetLogNumber(new_log_number);
impl->logfile_ = lfile;
impl->logfile_number_ = new_log_number;
//文件交由log::Writer做追加操作
impl->log_ = new log::Writer(lfile);
//創(chuàng)建MemTable
impl->mem_ = new MemTable(impl->internal_comparator_);
impl->mem_->Ref();
}
}
......
}
創(chuàng)建日志文件前,需要先給日志文件起一個名字,此處使用日志編號及數(shù)據(jù)庫名稱拼接而成,例如:
數(shù)據(jù)庫名稱為AiDb,編號為324時,日志文件名稱為AiDb000324.log
8.3.2 插入數(shù)據(jù)
如果插入數(shù)據(jù)時,當前的memtable容量達到設定的options_.write_buffer_size,此時觸發(fā)新的memtable創(chuàng)建,并將之前的memtable轉為imm,同時構建新的日志文件。
uint64_t new_log_number = versions_->NewFileNumber();
WritableFile *lfile = NULL;
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
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;
//創(chuàng)建日志文件
log_ = new log::Writer(lfile);
imm_ = mem_;
has_imm_.Release_Store(imm_);
//創(chuàng)建memtable
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
force = false; // Do not force another compaction if have room
MaybeScheduleCompaction();
8.3.3 數(shù)據(jù)庫恢復
數(shù)據(jù)庫啟動時首先完成數(shù)據(jù)庫狀態(tài)恢復,日志恢復過程中,如果為最后一個日志文件,且配置為日志重用模式(options_.reuse_logs=true)時,創(chuàng)建新的日志文件。但和其他場景不同的是,這里的日志文件是“繼承性”的,也就是說部分內容是上次遺留下來的。來看實現(xiàn):
// See if we should keep reusing the last log file.
if (status.ok() && options_.reuse_logs && last_log && compactions == 0)
{
assert(logfile_ == NULL);
assert(log_ == NULL);
assert(mem_ == NULL);
uint64_t lfile_size;
if (env_->GetFileSize(fname, &lfile_size).ok() &&
env_->NewAppendableFile(fname, &logfile_).ok())
{
Log(options_.info_log, "Reusing old log %s \n", fname.c_str());
log_ = new log::Writer(logfile_, lfile_size);
logfile_number_ = log_number;
if (mem != NULL)
{
mem_ = mem;
mem = NULL;
}
else
{
// mem can be NULL if lognum exists but was empty.
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
}
}
}
8.5 總結
日志文件本身的定位是清晰的,實現(xiàn)也不復雜。原本Current、Manifest與Log打算一起備注,但要搞清楚Manifest,LevelDB的版本機制必定要搞清楚,而這本身又是很豐富的內容。
轉載請注明;【隨安居士】http://www.itdecent.cn/p/d1bb2e2ceb4c