时间:2023-08-26|浏览:187
1. 写入数据的提交 一个快照块(SnapshotBlock)和关联的账户块(AccountBlock)列表对相关的数据结构进行序列化,并写入存储。
2. ringBuffer 序列化后的数据不会直接写入文件,而是先写入一个名为ringBuffer的内存缓冲区。这个缓冲区由一系列连续的10M子数据段(Segment)构成,每个数据段都有一个递增的序列号fileId。对于每个块(Block),首先会写入该块序列化后的字节长度,然后再是实际的数据内容。因为块和段不是一一对应的,所以会存在一个块的数据需要跨越多个段的情况。为了能够定位和存储一个块的数据段,可以通过[fileId, offset]二元组来确定块的起始位置,其中offset表示该块在段中的偏移量。
为了提高效率并减少开辟和回收缓存区的开销,这里将这些连续段拼接成一个虚拟环,称为ringBuffer。新数据追加到环的末尾(Tail),旧数据从队头(Head)弹出。追加和弹出操作通过移动段下标的方式完成。队头和队尾之间是待写入文件的已使用段,其他部分是可以被覆盖的空闲段。
已使用段的数据既可以被用作写缓存,又可以做读缓存。空闲段的数据如果之前写入过有效数据,也可以被当做读缓存。因此,整个ringBuffer都可以作为读缓存。整个ringBuffer就相当于一个最近写入数据的读写缓冲区。
当短期写入ringBuffer的数据超过flush速度时,会导致数据超过ringBuffer现有容量。此时,ringBuffer会自动扩容。待数据逐步写入文件列表后,ringBuffer会自动收缩到初始容量。
3. 文件列表、随机读取和账本同步 blockDB使用固定大小的小文件列表来存储块数据。每个文件对应ringBuffer中的一个段,文件名即为ringBuffer中的fileId。通过定期的flush操作,ringBuffer中的已使用段会依次写入文件系统,已flush的段会变为空闲段。
进行随机读取操作时,首先通过blockDB索引获取[fileId, offset]二元组,然后根据fileId在ringBuffer中定位段。如果定位失败,则通过fileId打开对应的小文件,并进行seek到offset位置。从该位置的开头读取数据大小后,就可以连续读取该块对应的数据块。可能需要跨越多个文件读取fileId+1的下一个小文件。由于采用小文件存储,相较于大文件,seek操作速度更快,也对系统的页缓存更友好。
小文件列表在顺序写入和批量顺序读取上具有良好的性能,这个特性在"账本同步"场景中非常有用。
4. 数据回滚 blockDB仅支持从最新状态回滚数据到某个历史状态,不允许删除中间的历史数据,即数据是连续的片段,不允许存在数据空洞。
数据回滚分为预删除和删除两个阶段。在预删除阶段,先在ringBuffer中删除相应的数据,然后标记要回滚到的目标位置。标记完毕后,这段数据变得不可读,但并未真正删除。下次进行"异步批量Flush"操作时,会进入删除阶段,此时会真正删除文件列表中的数据。
5. 数据压缩 目前使用snappy算法对每个块进行数据压缩。