《设计数据密集型应用》第三章(1) 存储索引:LSM-tree

本章主要解决的是如何为应用选择一个合适的数据库,使之能够正确地进行数据的存储和检索。不同的数据库的工作方式可能会差异很大,因此我们作为开发者需要对每个数据库的特性了然于胸,才能选择到适合应用的数据库。

本节内容先介绍数据结构和索引的相关知识。

数据库的数据结构

先从一个世界上最简单的数据库来引起话题,这两个bash函数就实现了一个数据库:

#!/bin/bash

db_set () {
    echo "$1,$2" >> database
}

db_get () {
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

通过上面的语句,我们很容易的可以使用这个数据库:

$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
$ db_get 42
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}

$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
$ db_get 42
{"name":"San Francisco","attractions":["Exploratorium"]}

$ cat database
123456,{"name":"London","attractions":["Big Ben","London Eye"]}
42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
42,{"name":"San Francisco","attractions":["Exploratorium"]}

这个数据库用的很爽是不是?但它存在着一个隐藏的问题:当数据量不断增加时,每次查找一条记录的时间会越来越长,时间复杂度为O(n)。那要解决这个问题,就引出了索引的概念。

索引是和存储的数据分离的额外结构,也就是对索引的添加、删除等操作,并不会影响数据库中的数据。索引的目的是为了提升查询性能。但由于索引会带来一些额外的写操作,因此会带来写入性能的降低。那么在实际应用中,如何选择合适的列创建索引,是完全由开发者来决定的。

Hash索引

前面的shell数据库,之所以查询性能会在大数据量下降低,是由于数据库并不知道某个key下的最后一条记录的存储位置。构建一个key和存储位置的关系,便是Hash索引的初衷。Hash索引适用于作为key-value型的数据库的索引结构。

类似CSV格式的key-value日志存储

Hash索引的结构为一个HashMap,索引和数据的key是相同的, 索引的value为数据在文件中的byte offset。在每次写入一条新纪录时,在Hash索引中,更新这条记录对应的byte offset值;当查询一个值时,先在Hash索引中找到key对应的数据文件偏移量offset,然后查找到该位置,读出值即可。

这种方式的实现就是Bitcask,它将所有的keys放在RAM中,values存储在磁盘上,这样只需要最多一次寻址,就可以得到想要查找的值。这种很适合key的数量变化不是很快,同时每个key的值都会频繁变化的情况。

如果key的值都不相同,并且有大量写发生的情况,如果索引在一个文件中,很可能最终会用尽磁盘空间。如何避免这种情况呢?

一个好的解决方法是,分成多个文件存储,每个文件为一个segment。当文件到达特定大小时,将这个segment文件关闭,并写入到一个新的segment中。这样可以压缩每个segment文件,丢弃重复的key取值,只保留最新更新的值,减少每个segment文件的大小。这些动作都是在后台线程中进行的,不会影响正常的数据写入。在压缩完成后,用压缩后的segment文件替换掉压缩前的segment文件即可。以下分别是Segment的压缩,和多个Segments文件的压缩。

Segment的压缩
多个Segments的压缩

每个文件在内存中有自己的Hash索引,因此在查找时,先查找最新的Hash索引,如果key不存在,则继续查找第二新的索引,直到找遍所有的索引。

此外,还有以下的实现细节是需要注意的:

  1. 文件格式:CSV格式并不是最合适进行日志存储的,二进制格式更快且更简单;
  2. 删除记录:如果想要删除一个key时,需要追加一个特殊的删除标记,当在进行segment合并时,处理这个标记,丢弃这个key所有的values;
  3. 故障恢复:当数据库重启时,内存中的Hash索引需要读完所有的segment才能恢复,非常耗时,因此这里Bitcask采用了定期写入索引快照的方式,加速了索引读入内存的速度;
  4. 部分写入记录:数据库崩溃时写了一半的数据记录,Bitcask通过checksum的方式将这些损坏的数据标识为删除;
  5. 并发控制:写入文件是严格有序的,因此只有一个写入线程,但由于数据文件是不可变的,因此读取可以有多个线程并行。

追加日志存储的总结

  • 好处:
  1. 文件块的追加写,要比磁盘的随机写速度快;
  2. 并发和故障恢复更加简单,不用担心有一部分旧的和一部分新的数据;
  3. 合并旧的segments可以避免数据文件碎片化的问题。
  • 坏处:
  1. Hash索引必须在内存中,不能用于海量key值的情况;
  2. 范围查询效率不高,必须在Hash表中逐个查找。

SSTable的概念

SSTable的全称为Sorted String Table,和前面的追加日志型数据库,SSTable要求key-value对是按照key进行排序的,并且在每个合并后的segment文件中每个key只能出现一次。这样带来的好处是:

  1. Segments的合并更加简单和高效,类似于归并排序;如果key在多个segments中出现,使用最近的segment文件中的值。


    多个SSTable segments文件的合并
  2. 不需要在内存中保存所有key的索引,只需要记录一些key的索引,便可以由他们的排序顺序,快速找到要查找的key的位置。


    SSTable的内存索引
  3. 每个读请求扫描的key-value对的一段,可以将这段数据写入磁盘前进行压缩,
    这样每个内存索引中的值指向的就是一个压缩块的起始位置,起到减少I/O带宽的作用。

SSTable的实现

在内存中保存一个有序的结构是比较简单的,有许多树的数据结构可以使用,比如红黑树,AVL树等。那么实现一个SSTable的过程如下:

  1. 当发生写时,将它添加到内存中的树数据结构中,成为memtable;
  2. 当memtable的大小超过某个值时,写入到磁盘成为SSTable文件,同时创建出一个新的memtable实例;
  3. 处理读请求的方式为,首先在memtable中查找key的值,然后在最新的磁盘segment中,然后在第二新的磁盘segment中,以此类推;
  4. 始终在后台进行segments的合并来丢弃被覆盖和删除的值。

为了避免当数据库崩溃时,memtable数据的丢失,将每次写操作记录在磁盘中的一个独立日志文件,以用于故障恢复。

LSM树

LSM树的算法就是上面介绍的SSTable,比如LevelDBRocksDB都是该算法的应用。此外还有Lucene,在ElasticsearchSolr中被广泛采用的全文本搜索引擎,也就是将文本的每个单词作为key,整个文本作为value的类SSTable。

LSM树的性能优化:

  1. 由于查询需要逐个查找每个segments,才知道某个key是否存在,因此使用Bloom Filter来快速判断某个key是否在数据库中存在,而不需要读取磁盘内容。

  2. SSTables压缩和合并的优化方式:

  • size-tierd:HBase,新的和小的SSTable,持续地合并为老的和大的SSTable;
  • leveled:key分裂成为多个更小的SSTable,并且将更老的数据移动到独立的层。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354