1 存储的可靠性
对于存储系统而言,最重要的无外乎可靠性,即存下去的东西不能丢失,如果存储不具备可靠性或者可靠性很差,那存储系统的意义有多大呢?
levelDB中保证存储可靠性的组件是Redo Log(重做日志,RLog),RLog在存储系统中比较常见,其发挥作用的关键是数据在写入实际的存储系统之前,首先写入RLog,如果后续实际的存储系统发生了丢失数据的情况,则通过RLog对数据进行恢复。
在存储引擎中,为了加快读取和写入的速度,会在内存中设置若干个缓冲区,数据写入时通常会先写入缓冲区,然后直接返回,后续会由一个后台线程按照策略对数据进行刷盘操作。这样的流程其实会产生不可靠行为,其在于数据首先写入内存,然后直接将写入成功的消息返回客户端。而后客户端已知数据已经写入成功,但此时数据仍然存储在内存中,如果后续进程发生意外,数据则会丢失。所以设置RLog的意义就在于避免进程发生意外时造成的数据丢失问题。
但是与此而来的另一个问题是,此时数据要先写入RLog,而RLog为了要保证可靠性,必然也要写磁盘,此时整个存储系统写入的瓶颈岂不是落在了RLog上?确实如此,但是RLog的设计相对于存储实际数据的存储系统而言,更加简单,且一次RLog的数据较少,写入采用Append的方式,能够极大得增加效率。因此对于缓冲区而言,RLog的效率确实不如,但是这也是为了保证可靠性的无奈之举。
2 levelDB的日志文件结构
上图是levelDB RLog文件的存储结构,RLog文件由若干个32KB的block组成,每个block中又包含若干个Record以及Trailer,而每个Record由Header+Data组成,Header又由checksum+length+type组成。
其中type包括如下类型
- KFullType
- KFirstType
- KMiddleType
- KLastType
3 读写日志流程
3.1 写日志流程
如上图,写日志的流程如下
- 按照32KB为单位写入数据,如果当前32KB block中剩余空间不足7B,那么直接开辟一个新的block进行存储,并将该block中的剩余空间写入0(Trailer)
- 判断当前写入的block的type类型
- 构造header,并将header与slice数据一同写入文件中
整个写入流程并没有特别的操作,需要注意的点如下:
- levelDB为了提升crc32的计算效率,在log::Writer初始化时,就计算出了type的crc32值,之后只计算data的crc32
- crc32和length都是小端存储,比如在header对应的buf中,length是这样写入的
buf[4] = static_cast<char>(length & 0xff)
buf[5] = static_cast<char>(length >> 8)
第5、6个byte用于存储长度,其中第5个byte存储length的低位数据,第6个byte用于存储length的高位数据,也即低地址存储低位数据,高地址存储高位数据
- 当32KB的block中剩余的空间小于一个header(7B)的存储空间时,余下内容填充为Trailer,即全部置0
3.2 读日志流程
在了解levelDB读取RLog之前,我们需要了解一些基本概念
- block_start_location: 读取的真实起始位置(应该是block size的整数倍)
- initial_offset_: 表示应该从该偏移开始读取数据。需要注意的是,由于levelDB的RLog数据是以32KB为单位存储,所以initial_offset_需要进一步调整为某个block的起始地址,也就是上面提到的block_start_location。确定原则:如果initial_offset_处于trailer的位置,那么block_start_location调整为initial_offset_所在block的下一个block,否则调整为当前block
- backing_store: 是levelDB用来读取block的一个缓存,该缓存重复使用,其定义如下 :
char * const buffer_[KBlockSize] - buffer: backing_store的代理,这个和PosixSequentialFile的read接口有关
- eof: 当前RLog文件是否读到末尾的标志
读日志流程如下:
- 从block_start_location开始读取数据,当读取的字节数大于7B(header size)时,开始进行header解析
- 根据header的解析结果,获取到record的数据长度length
- 如果headerSize + length大于bufferSize并且如果eof为false,那么代表读取出现了错误,文件可能由于某种情况,导致数据出现了腐蚀(比如可能是由于磁盘的corruption)。因为此时RLog文件还没读到结尾,所以一次成功的读,buffer size必然为32KB,又因为headerSize + length最大也就是32KB,所以在这种情况下,不可能出现headerSize+length>bufferSize; 相反地,如果此时eof为true,那么则不认为是一个错误,因为有可能是因为读写同时进行,导致写线程只写入了header以及一部分数据,还没来得及写其他数据,就被读线程读到了,从而导致这种情况。
- 如果需要检查crc32的值,那么根据之前length,获取对应长度的数据,然后进行crc32的校验。
- 将buffer指针的size进行修正,确保buffer中的指针指向了buffer中的下一个record
此处稍微有点难以理解,见下图。在第4步之前buffer中的指针指向R1,在第5步时,其指针指向R2,同时调整buffer size。
- 判断当前读到的block的起始偏移地址是否小于initial_offset_,如果是,则直接跳过本次读取(因为levelDB只会读取比initial_offset_更高偏移的数据。你可能会认为为什么不在最开始的时候判断,笔者最开始也有这样的疑惑,但是最开始是无法获知的,因为一个block中可能有多个record,而其record的起始偏移只能通过不断的读出数据之后才能确定)。如下图,在一个block中,第一次读出的record的起始地址小于initial_offset_,此时该record需要丢弃,而第二次读出的record的起始地址大于initial_offset_,此时才算是有效的读取。
- 返回record的类型,并将其数据通过指针的方式回传。
- 读取到一个record segment之后,判断该record segment是否是一个起始segment,因为record的类型有Full、First、Middle、Last,如果record type的类型是Middle、Last,那么levelDB并不会向前追溯找到其第一个record segment。
- 如果record tpye为Full类型,那么可以直接返回这条record,如果type为First,那么还要继续重复1~7步,知道读取到该record的所有segment,然后返回一整条record。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。