leveldb(八):log::Writer写日志

来源:互联网 发布:会声会影x9注册机软件 编辑:程序博客网 时间:2024/05/21 10:30

5 操作Log 1

分析完KV在内存中的存储,接下来就是操作日志。所有的写操作都必须先成功的append到操作日志中,然后再更新内存memtable。这样做有两个有点:1可以将随机的写IO变成append,极大的提高写磁盘速度;2防止在节点down机导致内存数据丢失,造成数据丢失,这对系统来说是个灾难。

在各种高效的存储系统中,这已经是口水技术了。


5.1 格式

在源码下的文档doc/log_format.txt中,作者详细描述了log格式:
The log file contents are a sequence of 32KB blocks.  The only exception is that the tail of thefile may contain a partial block.
Each block consists of a sequence of records:
    block:= record* trailer?
    record :=
    checksum: uint32    // crc32c of type and data[] ; little-endian
    length: uint16       // little-endian
    type: uint8          // One of FULL,FIRST, MIDDLE, LAST
    data: uint8[length]
A record never starts within the last six bytes of a block (since it won'tfit).  Any leftover bytes here form thetrailer, which must consist entirely of zero bytes and must be skipped byreaders.
翻译过来就是:
Leveldb把日志文件切分成了大小为32KB的连续block块,block由连续的log record组成,log record的格式为:
,注意:CRC32, Length都是little-endian的。
Log Type有4种:FULL = 1、FIRST = 2、MIDDLE = 3、LAST = 4。FULL类型表明该log record包含了完整的user record;而user record可能内容很多,超过了block的可用大小,就需要分成几条log record,第一条类型为FIRST,中间的为MIDDLE,最后一条为LAST。也就是:
> FULL,说明该log record包含一个完整的user record;
> FIRST,说明是user record的第一条log record
> MIDDLE,说明是user record中间的log record
> LAST,说明是user record最后的一条log record
翻一下文档上的例子,考虑到如下序列的user records:
   A: length 1000
   B: length 97270
   C: length 8000
A作为FULL类型的record存储在第一个block中;B将被拆分成3条log record,分别存储在第1、2、3个block中,这时block3还剩6byte,将被填充为0;C将作为FULL类型的record存储在block 4中。如图5.1-1所示。
图5.1-1
由于一条logrecord长度最短为7,如果一个block的剩余空间<=6byte,那么将被填充为空字符串,另外长度为7的log record是不包括任何用户数据的。

5.2 写日志

写比读简单,而且写入决定了读,所以从写开始分析。

有意思的是在写文件时,Leveldb使用了内存映射文件,内存映射文件的读写效率比普通文件要高,关于内存映射文件为何更高效,这篇文章写的不错:

http://blog.csdn.net/mg0832058/article/details/5890688


注意:原文说的Leveldb使用了内存映射文件是老版本的实现,不知道为何新版本没有采用了,新版本用的就是普通的fwrite。文章结尾会说。


其中涉及到的类层次比较简单,如图5.2-1.

图5.2-1

注意Write类的成员type_crc_数组,这里存放的为Record Type预先计算的CRC32值,因为Record Type是固定的几种,为了效率。

Writer类只有一个接口,就是AddRecord(),传入Slice参数,下面来看函数实现。

首先取出slice的字符串指针和长度,初始化begin=true,表明是第一条log record。

const char* ptr = slice.data();

size_t left = slice.size();

bool begin = true;

然后进入一个do{}while循环,直到写入出错,或者成功写入全部数据,如下:

S1 首先查看当前block是否<7,如果<7则补位,并重置block偏移。

dest_->Append(Slice("\x00\x00\x00\x00\x00\x00",leftover));

block_offset_ = 0;

S2 计算block剩余大小,以及本次log record可写入数据长度

    const size_t avail =kBlockSize - block_offset_ - kHeaderSize;

    const size_t fragment_length = (left <avail) ? left : avail;

S3 根据两个值,判断log 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;

 S4 调用EmitPhysicalRecord函数,append日志;并更新指针、剩余长度和begin标记。

    s = EmitPhysicalRecord(type, ptr,fragment_length);

    ptr += fragment_length;

    left -= fragment_length;

begin = false;

接下来看看EmitPhysicalRecord函数,这是实际写入的地方,涉及到log的存储格式。函数声明为:StatusWriter::EmitPhysicalRecord(RecordType t, const char* ptr, size_t n)

参数ptr为用户record数据,参数n为record长度,不包含log header。

S1 计算header,并Append到log文件,共7byte格式为:

| CRC32 (4 byte) | payload length lower + high (2 byte) | type (1byte)|

char buf[kHeaderSize];

  buf[4] = static_cast<char>(n& 0xff);

  buf[5] =static_cast<char>(n >> 8);

  buf[6] =static_cast<char>(t);

  // 计算record typepayloadCRC校验值

  uint32_t crc = crc32c::Extend(type_crc_[t], ptr, n);

  crc = crc32c::Mask(crc);        // 空间调整

  EncodeFixed32(buf, crc);

  dest_->Append(Slice(buf,kHeaderSize));

S2 写入payload,并Flush,更新block的当前偏移

    s =dest_->Append(Slice(ptr, n));

    s = dest_->Flush();

    block_offset_ += kHeaderSize +n;

以上就是写日志的逻辑,很直观。

底层日志文件的实现

WritableFile类

class WritableFile { public:  WritableFile() { }  virtual ~WritableFile();  virtual Status Append(const Slice& data) = 0;//写入记录  virtual Status Close() = 0;//关闭文件  virtual Status Flush() = 0;//刷新文件  virtual Status Sync() = 0;//同步文件};


WritableFile类只是作为一个抽象基类,定义了一些纯虚函数作为接口,最终作为父类被继承。 
leveldb中定义的一个子类为class PosixWritableFile:
class PosixWritableFile : public WritableFile { private:  std::string filename_;//要操作的文件名  FILE* file_;//最终要操作的文件 public:  virtual Status Append(const Slice& data) {    size_t r = fwrite_unlocked(data.data(), 1, data.size(), file_);//调用fwrite将数据写入到file_中    return Status::OK();  }  virtual Status Close() {    Status result;    if (fclose(file_) != 0) {//关闭文件      result = IOError(filename_, errno);    }    file_ = NULL;    return result;  }  virtual Status Flush() {    if (fflush_unlocked(file_) != 0) {//刷新文件      return IOError(filename_, errno);    }    return Status::OK();  }  virtual Status Sync() {//同步文件    // Ensure new files referred to by the manifest are in the filesystem.    Status s = SyncDirIfManifest();    if (fflush_unlocked(file_) != 0 ||        fdatasync(fileno(file_)) != 0) {      s = Status::IOError(filename_, strerror(errno));    }    return s;  }};
WritableFile类在写入数据时不会对数据进行任何封装、修改操作,而是直接将数据写入到log文件中。 
因此我们一般插入一个Key-Value对时,首先会调用batch.Put(key, value);将其组织成一条记录,然后调用Write::AddRecord(),在log文件中找到合适的位置,同时为每一条记录增加一个头部,再将其写入到log文件中。然后调用WritableFile的Flush()、Sync()等方法来对log文件进行操作。

我疑问的是:既然每个日志块大小都是确定的,为什么日志文件不采用预分配磁盘块的方式呢,这样能避免磁盘seek的频率?

原创粉丝点击