持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
本系列主要是《数据密集型应用系统设计》阅读笔记,本文记录日志结构的存储引擎主题的笔记心得。
基本存储
日志结构的DB基本存储是key-value存储,如下代码片段所示:
#!/bin/bash
db_set () {
echo "$1,$2" >> database
}
db_get () {
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}
key-value存储的功能主要是get和put.
- 调用set在db中保存你输入的key和value。
- 调用get查找key的值并返回。
grep "^$1," #database 匹配key
sed -e "s/^$1,//" #替换匹配到的行里面的key,为空字符串,也就是每行值留下value
可以看出,这个的底层的存储格式是一个纯文本,查找的时候使用tail -n 1来查询最后set的key对应的value。
来分析一下这个数据库。
- set性能好,采用追加更新日志文件的方式
- set实现简单,很多复杂问题没考虑(比如病发控制、回收磁盘空间、控制日志大小等)。日志机制非常有用,在很多系统里面都使用到了。
- get的性能取决于文件的大小,文件大的时候则性能差。查找key必须从从头到尾扫描整个数据库文件。
关于grep,sed的工作模式,我在不可错过的文本处理工具:sed和awk有介绍,他们是按照以行为流的方式来处理文本的。
索引
为了高效的查找数据,往往需要一个数据结构来帮忙,称这个数据结构为索引。
索引就像路标,帮定位想要的数据。
哈希索引
对于上面的数据库,是key-value模式,那它的索引应该怎么设计呢? hashmap是一种我们非常熟悉的高效数据结构,用它来作为磁盘文件的索引,将每个key映射到数据文件的特定的字节偏移量,就找到了每个值的位置。如下图所示:
效率
所有的key都可以存在内存中,value的大小可以超过内存大小,只需要一次磁盘寻址,就可以把value加载到内存。
- set虽然有些额外开销,但效率是O(1)
- get的效率从O(n)变为O(1)
日志压缩
对于每个键频繁更新到场景,如果一直追加,则文件慢慢膨胀,如何避免用尽磁盘空间?
- 将日志分解成一定大小的段,当文件达到一定大小事就关闭它,后续更新写入到新的文件段中去。
- 在关闭的段文件上执行压缩,如下图所示
- 段还可以继续合并,如下图所示
段的压缩等操作不会修改原来的文件,在后台任务执行就可以了,而且运行的时候,旧的段文件还可以继续正常读取,当压缩合并完成后,再将读请求切换到新的段文件上,旧的段文件可以安全删除。
如果同时存在多个段,则每个段都自己的内存哈希表,则首先检查最新的段的hashmap,如果key不存在,则检查第二新的,依此类推。
其他考虑
在一个可用的数据库落地之前,还有一些问题不可避免。
- 文件格式 CSV不是日志的最佳格式。更好的方式时使用结构化的数据,比如首先以字节为单位记录value的长度,之后跟上原始的value值。
- 删除记录 删除时前面一直没有提到的一个功能。使用hashmap也不难做到,删除key就好。
只是还要考虑文件的存储。 当需要删除一个记录的时候,需要在数据文件中追加一个特殊的删除记录。但合并日志段时,一旦发现墓碑标记,则丢弃这个删除键的所有值。
- 奔溃恢复 内存中的hashmap如果想从文件中重建是一个效率很低的过程。所以每个段段hash map的快照存储在磁盘上,可以在奔溃恢复的场景下更快的加载到内存。
- 部分写入的记录 数据库随时可能会奔溃,包括将记录追加到日志的过程,使用教checksum可以更方便的发现损坏部分以丢弃。
- 并发控制 写入时以严格的顺序追加日志,所以一般采用一个线程。读数据时读不可变文件,可支持并发。
总结
总的来说,追加文件的,也就是日志结构的存储具有很多的优势:
- 顺序写比随机写快的多
- 段文件的并发和奔溃恢复很简单
- 合并旧段可以避免文件碎片问题。 当然也有一些局限:
- hashmap必须全部放入内存,磁盘上的hashmap表现不够优秀
- 区间查询效率不高。
参考文献
- 《数据密集型应用系统设计》