从零开始开发一个单机存储引擎

2018-04-01  本文已影响0人  陈非的技术随想

从零开始开发一个单机存储引擎

1.VDL Logstore概述

如何设计存储引擎,使得读写接口的性能足够高,如何保证在机器宕机时,存储引擎能够将已存储的数据恢复到一个一致性状态。如何测试存储引擎的正确性?本文将着重介绍一下VDL系统的日志存储引擎--Logstore的架构设计与核心流程实现,及为了保证Logstore的正确性,我们做了哪些工作;为了进一步提高Logstore的读写性能,我们又做了哪些工作。希望通过这篇文章,给大家介绍一下设计和开发一个存储引擎的『前世今生』。

1.1 Logstore提供的功能

VDL中有两种日志形态,一种是raft日志(以下称为raft log),由raft算法产生和使用,另一种是用户形态的Log(以下称为user log),由用户产生和使用。Logstore作为VDL日志存储引擎,同时存储着VDL的raft log 和user log。Logstore在设计中,将两种Log形态组合成一个Log Entry。只是通过不同的头部信息来区分。Logstore需要同时提供两种不同形态的Log操作接口,主要有以下几类:

2.Logstore的架构设计

2.1系统架构

Logstore由数据文件和索引文件组成,同时Logstore还会在内存中缓存最新的一段Log Entry,用于Raft lib能够快速地从内存中读取到最近Raft log,同时用户也能够快速读取到最新存储到Logstore中的user log。Logstore的组成如下图所示:


image.png

segment由一条一条的raft log entry组成,raft log的data部分存放的是user log。每个segment文件对应一个index文件,index file由index entry组成,index 文件中的索引项纪录了对应raft log的位置和大小等信息。示意图如下所示:


image.png

3. Logstore的核心流程实现

3.1 读数据流程

Logstore读数据分为两种情况:

Read in MemCache,MemCache的元数据记录了缓存的Log范围信息,当读取范围刚好落在MemCache内时,则Logstore直接从MemCache中读取Log并返回。
Read in Segment,当上层读取的Log范围未完全落在MemCache中时,则会从segment文件中读取。Logstore记录了每个segment的Log范围元数据信息,先通过segment范围元数据信息,定位到读取的开始segment,然后在通过索引来定位具体的文件偏移。例如,读取raft index 为10010-10019这段范围的raft log,segment范围如下图所示:


image.png

根据segment的Log范围元数据信息,我们可以知道此次读取范围开始位置和结束位置都在segment_2中,由于Raft log entry的长度是不固定的,如何定位读取开始位置和结束位置的文件偏移呢?这时候就需要用到索引项,在Logstore中每个Log entry对应的索引项大小是固定的,索引项纪录了该raft log entry在segment文件内的文件偏移。segment_2对应的index文件第一个索引项纪录的是raft index为10001的raft log entry索引项,所以需要在index文件中超找raft log index范围是:10010-10019,就非常简单了。直接读取index 文件的第10到第19范围的索引项,然后根据索引项内的文件偏移到segment上读取raft log。大概的流程如下图所示:

image.png

3.2 写数据流程

raft算法要求写入的raft log必须强制落盘后,才能返回成功。通过将log entry批量异步写入segment文件,并调用sync_file_range函数强制刷盘。为了提升写入segment性能,segment文件创建时就预分配了512MB的磁盘空间,这种预分配文件空间的方式有助于提升写性能。将索引信息写入index文件是异步写完后就返回。同步写segment,异步写index的方式降低了raft log写耗时,但又不影响raft算法的正确性。因为raft算法是以segment中的数据作为参考标准的。

Logstore写入流程如下图所示:

image.png

3.3 数据恢复流程

Logstore必须要考虑到在VDL系统异常退出时,存储的数据有可能出现不一致。例如在Logstore写数据过程中,机器突然宕机。这时候就有可能只写入了部分数据,在设计Logstore时就必须考虑到如何支持数据恢复操作,保证写入Logstore的数据的一致性。

在Logstore中,只有最后一个segment文件可能出现数据不一致的可能。因为Logstore在写满一个segment文件后,会创建一个新的segment文件。在创建新的segment文件之前,Logstore通过sync系统调用让最后的segment对应的index文件内容强制刷盘,并且最后一个segment文件写入本身就是同步写。通过这种机制保证了只有最后一个segment写入的数据存在部分写的可能。而在这之前的segment文件和index文件内容都是完整的。

有了上面的保证,数据恢复我们只需要考虑最后一个segment及其index文件中的数据是否完整。Logstore通过一个标识文件来标识系统是否正常退出,如果文件存在且里面的标记为正常退出,Logstore就走正常启动流程,否则,转入数据恢复流程,Logstore数据恢复流程,主要操作如下图所示:


image.png

4.Logstore的测试

为保证Logstore的正确性,我们对Logstore对外提供的接口函数及内部调用的核心函数都做了单元测试,通过gitlab+jenkins持续集成的方式,保证每次提交都会触发脚本将所有的单元测试重新运行一次,如果新增代码或改动代码,导致单元测试失败,我们可以立刻发现。通过这种持续集成的方式,我们可以保证每次代码提交的质量。

仅仅有单元测试还是不够的,因为我们无法预测Logstore某个接口函数异常,对整个VDL系统造成什么影响。所以,我们还对Logstore进行了异常测试,通过一个自研工具FIU,对Logstore中特定的函数注入各种异常条件,测试Logstore的在异常情况下,对系统的影响。我们在Logstore相关代码中插入固定的异常代码,然后通过FIU来触发相应的异常点。这样就可以让Logstore走入指定的异常逻辑代码。异常注入测试主要分为两类:

有了这类异常测试,我们可以提前去模拟线上有可能出现的异常场景,并修复可能存在的未知缺陷。保证VDL上线后更加稳定、可靠。并且添加异常各类异常测试用例是一个持续的过程,伴随着VDL系统开发和演进的全过程。

5.Logstore的性能优化

为保证Logstore具有高性能的读写,在设计阶段就考虑到了。比如通过文件空间预分配来提升写性能,通过mmap方式读日志数据,提升读性能。在代码开发完成后,结合go pprof和火焰图来定位Logstore的性能开销较大的系统调用或代码段,并做相应优化。性能优化方面的工作,比较有意义的几点,可以分享一下:

6.总结

本文介绍了Logstore在设计、开发、测试和性能优化等方面,我们所做的工作。希望能够给读者在设计和开发分布式存储系统时,提供一定的参考思路。在后续演进中,我们希望结合业务场景,对数据做冷热分离,进一步降低生产系统的成本。到时候有新的心得体会,我们继续给大家分享。

上一篇 下一篇

猜你喜欢

热点阅读