log structured storage

http://blog.notdot.net/2009/12/Damn-Cool-Algorithms-Log-structured-storage 

一般来讲,如果你在设计一个存储系统,例如文件系统或者数据库神马的,你的一个主要关注点应该是怎么把数据存在磁盘上。你不得不去操心怎么为数据开辟空间,以什么样的方式存储索引信息。你还得琢磨当你要给一个对象追加内容时会发生什么(例如给一个文件追加数据),你还需要考虑碎片,当你删除旧东西,然后在这个旧东西的空间上又写了新内容后,很容易出现碎片。上面讲的这些都是比较复杂的问题,它们的解决方案总是很低效,而且容易引入bug。

Log structured storage(基于log结构的存储)这个技术能够处理这些issue。这个存储结构是在1980年提出的,不过最近它好像经常被选为database的存储结构。最初,它的应用场景是文件系统,但是由于一些缺点它没能被广泛的采纳。但是,对于database engine来说,这些都不是问题,而且在更方便的存储管理之外,Log Structured Storage还给database engine带来了额外的好处。

从名字可以看出,log structure storage的基础结构就是log,也就是一个只可以在尾部追加的data entry序列。只要你想写入新的data,你只需简单的把data追加到log的尾部即可,不用去disk上找空间来存储。对data的索引也用log实现: Metadata的更新也是附加到log尾部。这样看起来好像比较低效,但是基于磁盘的索引结构(例如BTree)一般都是很扁平的,所以每次写入时我们需要更新的index node实际上是很少的。

我们来看一个简单的例子,最初我们有一个log file,其中包括一个data item(Foo),和一个指向data的index node(A):



到现在一切都很顺利,假设我们要加入第二个data item,我们在log的尾部追加新的item (Bar), 然后更新index entry(A -> A’),最后把更新过的版本(A’)也追加到log尾部:



这时,最初的index entry (A)仍然在logfile中,但是它已经没用了: 替代它的是A‘,它同时指向Foo和Bar。当有某个东东需要读文件系统时,它首先找到index的root node,然后像使用其他基于disk的索引一样使用root node。 查找index root要保证速度。最朴素的方法就是直接去找log的最后一个block,因为每次写操作完成后,最后的一个block一定是index root。然而,这并不对,因为有可能在你想当然的去最后一个block读index root时,另一个进程进行了一次写操作。我们可以通过在log的开头添加一个block来避免这个issue,这个新block包含一个指针,指向当前的root。只要我们更新了logfile,就对第一个entry进行重写,来保证它指向的是最新的root node。为了简洁,本文的图里没有标出这个block。 下面,我们来看看当我们更新一个element的时候会发生什么。例如我们修改Foo:



首先,我们在log的尾部写入Foo的更新版本(Foo’)。然后,我们再更新index nodes(在这个例子中,只有A’),并且把更新后的index nodes也写到log的尾部。和之前一样,Foo的老版本仍然在log中,但是最新的index已经不会指向它了。

你可能会认为这个系统根本就不靠谱。如果这些没用的旧数据就一直这么放着,我们将会用光所有的存储。在文件系统中,这个问题的解决方案是把磁盘当作一个环状buffer,用光了之后,便循环到头部覆盖那些旧数据。当这一情况发生时,那些仍然有效的数据也被重新追加到log尾部,就好像新写入的数据一样。 在一个常规的文件系统中,这就是本文开头提到的一个短板。一旦disk满了,文件系统将会在garbage collection上消耗太多的时间。实际上当你的disk 容量已经到了80%的时候,文件系统就已经开始慢慢的不能用了。

如果把log structured storage用在database engine上,上面说的就不算个问题了。我们可以在native file system上实现这个技术。我们把database拆分成若干个定长的chunk,那么每当我们需要申请空间时,我们可以选择一个chunk,把它内部的有效数据重写到log尾部(并更新index node),然后将这个chunk删除。上面的例子中,第一段空间看起来有些稀疏,那么我们就对它下手吧:



具体的操作就是把Bar这份数据copy到log的尾部,并且跟上更新的index nodes(A”’)。完成之后,第一段log整个就没用了,可以删除了。

这个方案有几个优点。首先,不会强制要求从最老的chunk开始删除: 如果我们发现中间有一段非常稀疏,我们可以选择对它进行垃圾收集。对于数据库来说,这是非常有用的,因为数据库中的数据特征是有一部分长期不变,另一部分被反复重写。我们不希望把时间浪费在重写那些长期不变的数据的操作上。同时,对于何时去做垃圾收集也是很灵活的:可以等到某个chunk已经几乎全部失效的时候再做垃圾收集,这样的话可以减少后续的操作。

对于database而言,好处还不止这些。为了保持transactional consistency,数据库经常使用“write ahead log”技术,缩写为WAL。当database要把一个transaction保存在disk上时,它首先要把所有的写入WAL,然后把它们flush到disk上,最后更新真正的database文件。这样做可以很方便的从crash中恢复。那么,如果我们采用log structured storage,那么Write Ahead Log本身就是database file,所以我们只需写一次数据。在crash恢复的场景中,我们只需要简单的打开database,从最后保存的index header开始,线性向前搜索并修复那些丢失的index update。

利用上面讲到的恢复机制的优势,我们还能够进一步优化写操作。相对于典型的每次写操作都更新index node的方式,我们可以把写操作cache在内存中,然后周期性的写到disk上。上面提到的恢复机制用来进行灾难恢复,当然,我们需要提供一种方式来区分完成的会话与未完成的会话。

采用这种方法,备份也会变得更加easy:我们可以不停地以chunk为单位,以增量的方式备份数据库。

最后一个主要优点是和并发与会话相关的。为了提供transactional consistency,大多数database使用了复杂的锁系统来控制在何时通过何种步骤更新数据。根据一致性要求的等级,可能需要读取操作去获得锁来保证在读的时候data没有被修改,写操作也需要加锁,这会导致严重的性能下降。

我们可以用Multiversion Concurrency Control(MVCC)来化解这种issue。只要有读操作发生,首先要查找root index node,把这个node作为会话的remainder。因为在log structured storage中,已经存在的data是绝对不会被修改的,所以当前的读操作就拥有了database在它读取之前的一个snapshot: 也就是说,与此同时没有任何一个并发操作可以影响到它读取的数据,这样,我们实现了lock-free的读操作。

对于写操作,可以使用Optimistic concurrency。在典型的read-modify-write操作中,首先执行上面提到过的读操作。然后,要写如改变后的data,需要拿到write lock。然后通过查找index,检查index指向的data和我们上次读到的是否一样,可以很快的确定data有没有被修改过。如果是一样的,那么说明这段data没有被修改,我们直接修改即可。如果不一样了,那就说明conflicting transaction发生了,这时回滚一下,重新回到读操作即可。

转载于:https://my.oschina.net/heatonn1/blog/189861

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值