Vite存储层设计详解之blockDB文件系统设计



  • blockDB用来存储交易(Transaction/AccountBlock)、快照(SnapshotBlock)数据,这两种数据类型在一个快照生成时都是一个最终确认的状态,写入后无需修改,因此非常适合通过快速追加写入的方式进行高效率的存储。blockDB的存储就是按照追加写入优化的思路进行设计的,通过ringBuffer将需要顺序写入的数据依次写入一个队列,然后异步地批量写入文件。

    1.写入数据的提交

    一个SnapshotBlock和关联的AccountBlock列表会作为一个整体提交到存储层进行存储,存储层依次遍历AccountBlock和他们关联的SnapshotBlock,对相关数据结构进行序列化,然后写入存储。

    2.ringBuffer

    序列化后的数据并不会直接写入文件,而是写入一个称为ringBuffer的内存缓冲区,这个缓冲区由连续的10M的子数据段(Segment)构成,每个数据段有对应的递增序列号fileId。对于每个Block,首先会写入该Block序列化后的字节长度值,后面才是真正的数据内容。因为Block和段不是一一对应,就会存在一个Block的数据需要跨段的问题,为了能够定位和存储一个Block数据段,通过[fileId, offset]二元组定位一个Block的起始位置,其中offset为该Block在该段中的offset。

    为了便于提高效率,减少开辟和回收缓存区的开销,这里会将这些连续段拼接成一个称为ringBuffer的虚拟环,新数据追加到环的末尾(Tail),旧数据从队头(Head)弹出,追加和弹出的操作都是通过移动段下标的方式来完成的,队头和队尾之间的为待flush的已使用段,其他部分的段为可以被覆盖的空闲段。

    已使用段的数据被用作写缓存,同时也可以做读缓存,空闲段的数据如果之前曾经写入过有效数据,则也会被当做读缓存,因此整个ringBuffer都可以被用来做读缓存。整个ringBuffer相当于是一个最近写入数据的read-write buffer。

    在短期写入ring buffer的数据超过flush速度时,会导致数据超过ring buffer现有容量,ring buffer会自动扩容,待数据逐步写入文件列表后,ring buffer会自动收缩到初始容量。

    3.文件列表、随机读取及账本同步

    blockDB使用文件大小固定的小文件列表来存储block数据,每个文件对应上面ringBuffer中的一个段,文件名既是前面ringBuffer中的标记fileId。通过定期的flush操作,ringBuffer中的已使用段会被依次写入文件系统,已flush的段会成为空闲段。

    随机读取一个block操作首先会通过blockDB索引获取[fileId, offset]二元组,然后根据fileId尝试在ringBuffer中定位段,如果失败,在通过fileId打开对应的小文件,并seek到offset位置,从该位置的开头读取数据大小后,就可以连续读取该Block对应的数据块,这时候可能会涉及到要跨文件读取fileId+1的下一个小文件。因为采用了小文件来存储,seek操作相比大文件来说会快很多,并且对于系统的页缓存也比较友好。

    小文件列表在顺序写入和批量顺序读取上有很好的性能,这个特性在"账本同步"的场景中非常有用。

    4.数据回滚

    blockDB只支持将数据从最新状态删到某个历史状态,不允许删除中间一段历史数据,即数据是一个连续片段,不允许有数据空洞。

    回滚数据分为预删除和删除两个阶段。在预删除阶段,先在ring buffer中删除对应数据,然后标记需要删除到的目标位置,标记完后这段数据已不可读,但并未被真正删除。在下次"异步批量Flush"时,开启删除阶段,这时会真正删除文件列表中的数据。

    5.数据压缩

    目前使用snappy算法做数据压缩,对每个block进行压缩。


Log in to reply