levedb 导入 mysql_LevelDB的操作日志

LevelDB将用户写入的数据临时存储在MemTable中,内存数据足够多后才排序写入SSTable文件(避免写出过小的文件)。那么如果在写磁盘之前,系统崩溃了怎么办呢?作为数据库系统,最基本的要求就是保证数据持久,即向用户承诺已写成功的数据不会丢失,无论是系统重启还是进程退出。经典的解决方案是在向用户返回写操作成功之前,首先在磁盘上将这个写操作记录在日志文件中。一旦写日志成功,后续操作便可以放心的只在内存中进行了。当数据库因为任何原因重启时,它会首先重播日志文件中的操作记录,保证之前承诺过的操作不会丢失。

写操作分为两种:插入和删除。LevelDB允许用户原子地执行若干插入和删除,或者全部成功或者全部失败。这些写操作放在一起称作write batch,write batch是日志记录中的最小单位。

b5686ddeb3fe23a714892abd686cf5cc.png

图1:日志中的一条记录,即一个write batch(格式详见include/write_batch.h和db/write_batch.cc)

每个写操作的格式如下:日志文件是由若干个32KB的定长数据块组成的,每个块头有整个块的校验和。过长的write batch会被拆开保存到几个连续的数据块中。LevelDB的作者在文档中提到了使用定长数据块的好处(doc/log_format.txt),笔者在此简单复述和解释一下 :

若日志文件部分损坏,我们可以直接定位到下一个32KB的数据块,继续读接下来的数据,不至于整个日志文件读不出。

容易对数据进行切分,特别适合用于MapReduce。MapReduce需要在不读出文件内容的情况下对文件按chunk进行切分,典型的chunk长度是64MB,32KB对齐的数据块自然也是64MB对齐的。考虑到过长的write batch会跨chunk边界分布在的连续的块中,这里需要特殊处理一下。

处理大记录时不需要分配额外的缓冲区。这里不太明白,无论在写数据还是从重播日志,都需要将数据复制到MemTable中。(原文:We do not need to put extra buffering for large records)

坏处是这样做会造成空间浪费:

一个32KB定长数据块只存储一个小write batch,比如key/value都只有几字节。

没有压缩。

好在日志文件占空间较小,并且是在SSTable写入磁盘后就会被删除,所以造成的空间浪费并不严重。

保证日志文件正确无误非常十分重要。在系统崩溃或掉电后所有内存数据都会丢失(包括还在系统page cache中的数据),在Linux系统中,只有fsync()成功后的数据才保证写入磁盘不丢失。在响应用户读写请求的流程中,fsync()是一个耗时的操作。在传统机械硬盘和ext3文件系统环境下,开启fsync后顺序写32KB数据每秒仅能完成百次左右,这直接影响数据库的写入性能。

首先不是所有的数据库数据都重要到立即fsync()的程序(比如在桌面应用的本地数据就不值得)。LevelDB为用户提供了一个开关控制每个写操作是否需要进行fsync()。但到目前的版本为止,LevelDB都没有提供周期性触发fsync()的折衷方案(MySQL和MongoDB都可以使用这个方案,典型的触发周期是几百亳秒)。

另一方面,服务器应用中通常写入操作是多线程同时进行的。为了防止写日志在高并发下成为瓶颈,LevelDB将不同线程同时产生的日志数据聚合在一起写入文件并进行一次fsync(),而非对每个线程写的数据进行单独fsync()。这个做法称为group commit。这样一来,虽然每秒完成的fsync()数量无法突破硬件限制,但实际上每次写入的数据量包含更多数据,对用户来说这样也就可以提高写入性能。顺便提一句:这部分代码写得短小精悍,值得学习(db/db_impl.cc中的DBImpl::Write()方法,或见下文)。

0eb181d6e51b5809bdc43f462a4658fb.png

图2:线程1正在写日志,线程2和线程3正在等待锁

group-commit-2.png

图3:线程1已完成,线程2和线程3的将日志合并在一起写入文件,只需进行一次write()和fsync()

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

Writer w(&mutex_);

w.batch = my_batch;

w.sync = options.sync;

w.done = false;

MutexLock l(&mutex_);

writers_.push_back(&w); // 并发写的线程在这里排队

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

w.cv.Wait();

}

if (w.done) {

return w.status;

}

// 排在队首的线程负责将数据聚合起来写入日志。

// 只有排在队首的线程会进入这里。其它线程则等待被通知。

// May temporarily unlock and wait.

Status status = MakeRoomForWrite(my_batch == NULL);

uint64_t last_sequence = versions_->LastSequence();

Writer* last_writer = &w;

if (status.ok() && my_batch != NULL) { // NULL batch is for compactions

WriteBatch* updates = BuildBatchGroup(&last_writer); // 将排在队首的几个batch聚合在一起

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

last_sequence += WriteBatchInternal::Count(updates);

// 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)); // 写日志

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

status = logfile_->Sync();

}

if (status.ok()) {

status = WriteBatchInternal::InsertInto(updates, mem_); // 写MemTable

}

mutex_.Lock();

}

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

versions_->SetLastSequence(last_sequence);

}

// 通知这些在等待的线程,我们已经将日志写完了

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;

}

// Notify new head of write queue

if (!writers_.empty()) {

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

}

return status;

}

Like this:

Like Loading...

Related

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值