Designing Data-Intensive Applications 中文翻译摘要,第三章存储和查询-part1

总述

        总的来说,数据库只需要做两件事情,当你给他数据的时候,他要把数据存起来,当你随后找他要的时候,它还能正确的把数据吐给你

        第二章我们讨论了数据模型和查询语言,讲述了你向数据库提供是数据的格式以及如何去查询他,这章我们站在数据库的角度上来探讨这个问题.你如何来存储交给你的数据以及当别人需要查询的时候又怎么能找到对应的数据。

        第二章讲了,说明性语言的好处就是我只需要告诉他我要干什么,底层怎么搞我是不用关心的,但是这章为什么180度掉头开始讲存储的细节呢?虽然你确实不会自己去实现一套存储体系,但是你却需要为你的系统选一套存储架构。为了能让存储在你特定的负载状态下运转良好,你必须知道揭开数据库的面纱,了解底层他是如何工作的。

        首先我们先从熟悉的数据库开始,关系数据库和NoSQL数据库,我们会讲解2个存储引擎,基于日志的存储引擎和面向页的存储引擎(*page-oriented* storage engines)

支持你数据库的数据结构

        前面扯了一堆没用的,最后一个结论是我们的数据想要快速查询就需要索引,设置索引需要权衡读写速度,因为索引越多,查询应该是越快,但是写就越慢。相比直接写数据,数据库还需要维护一套索引数据,降低写的速度。所以需要根据具体业务,决定哪些数据或者字段需要加索引。

哈希索引

        k-v存储和编程语言中的字典其实非常像,底层一般使用hashmap实现。假设我们有一个最简单的数据库,每当有写请求的时候,就把他追加到一个文件中。对于这个"数据库", 给他加索引最简单的方法是构建一个在内存当中的hashmap,里面存储每个key对应文件中的偏移量(如图Figure3-1),每当有一条写请求的时候,把数据追加到文件中,然后更新hashmap中对应key的偏移。


        虽然听上去如此简单,但是这是确实可行的,Bitcask数据库默认版本的索引就是这么搞的。Bitcask的读写性能非常高,因为所有key都在内存中,最多只需要一次磁盘读写就能把数据读出来,而且如果内存够大的话,hashmap不止存偏移还存些数据,那可能连一次磁盘读写都不需要结果就返回了

        Bitcask 适合的场景是key少但读写量非常大, 例如某个视频的播放次数,每当人点击就需要+1,但是视频数量总体没有那么大。把所有key放进内存听上去还可以(但我估计现在的视频网站也不太行了, 长视频也就是几十万还好,短视频肯定要上亿了……)



        刚才聊的都是索引,但是其实磁盘也有一个问题,如果一直把数据追加到一个文件,那一旦磁盘写满了怎么办?一个比较好的方法是文件分段,也就是设置一个大小,当数据文件超过这个大小时,就把数据写到一个新的文件上面去。然后我们就可以对这个文件做一个合并(*compaction*)操作了,也就是把一个文件中重复的key扔掉,只保留最新的结果。如图Figure 3-2


        另外我们也可以对多个文件做合并操作,这样能使文件更小,如Figure 3-3。因为这些文件一旦被关闭后就不会再修改了,所以我们可以在后台异步的把合并后的结果写到新的文件中,服务同时依旧可以先用老的文件进行查询操作(这个时候写的操作对应一个打开的新文件,不会是这些超过指定大小的被关闭的文件)。当新的文件生成好后,他们把读请求切换到这些新文件中,然后把老的删掉。


        因为每个文件都有自己的Hashmap索引,所以在查某一个key的时候,先找最新的文件对应的hashmap是否有key,如果有就返回,如果没有就查第二新的文件对应的hashmap,以此类推。因为有合并操作,所以文件的数量不会特别大,查找也就不会去查很多个hashmap了。

其实这还只是想当然,要想让一个想法在真实情况下切实可行,以下问题是你不得不思考和面对的,下面是bitcask在这些问题的处理方式。

    文件格式,使用CSV还是二进制格式,CSV其实不是最好的格式,二进制格式,比如第一个字节存字符串长度,然后存实际内容,会更小,也更简单。

    删除数据,如果是删除操作,需要在文件中添加一条特殊的删除数据(也叫墓碑),在数据文件合并的时候,墓碑数据告诉合并进程忽略前面的数据,直接删这个key

    异常恢复,如果数据库崩了重启,那内存里面的索引就没有了,这个时候最low的方法就是基于所有的数据文件重建索引,一旦文件大了就很慢了,Bitcask把每个文件的hashmap存储一份镜像到磁盘上,这样直接读就行了,就很快。

    数据部分写, 如果一个数据文件写到一半,程序崩了,Bitcask有一个校验码,能够发现这些问题的文件

    并发控制, 因为写是要求严格有序的,所以一般都只有一个写进程来写文件,但是因为文件要么只能追加,要么完全不可修改,所以读是可以多线程的。

第一眼看上去,只许追加的数据文件好像很浪费空间,直接在原地改就好了,为什么要追加写呢?有以下几个好处

1. 无论是追加还是合并都是顺序写磁盘,这可比随机写不知道快多少,尤其是机械磁盘,没准顺序写1000字节比随机写1字节快.但是如果是SSD,后面在讨论

2. 并发和异常恢复更容易,你不用担心某个数据更新到一半程序崩了,然后文件里面是一半新数据,一半老数据.

3.避免文件碎片化,比如原来数据10字节,新的数据5字节,有5个字节就空在那了。

但是hash table的索引还是有很多缺陷的

1. 索引必须在内存中,如果key特别特别多就废了,虽说讲道理可以把hashmap存到磁盘里面,但是目前没什么方法能够让一个磁盘里面的hashmap性能变得可用。

2. 范围查询不支持,例如你想查key在 kitty00000到kitty99999之间的数据,就废了

SSTables 和 LSM-Trees

        在Figure3.3中,每一个数据文件都是连续的kv值,他们之间是按照写入的顺序排列的,除了针对同一个key,后面的值需要覆盖前面,数据之间的顺序并不重要。

        现在我们做一点改变,要求文件中的数据是按照key排序的,第一眼看上去,这会影响顺序写的特性,但是这块我们后面再说。我们管这个叫排序字符表(*Sorted String Table*),简称SSTable,由于有合并进程,所以我们要求每一个key只在合并的文件中出现一次。SSTable相比原始的Hash Table有以下有点

1. 文件合并特别简单,就是一个简单的归并排序。这里面有一个小问题,如果多个文件都包含一个key,归并排序时候应该选哪个值?因为每个文件块包含一段时间内的所有写入的数据,所以直接选时间最新的value,把其他的值扔掉就可以了。

2. 当查找指定key时,不需要给每一个key都在内存中建一个索引, 例如Figure 3.5, 假设查找的key是handiwork,但是却没有存handiwork的索引,不用担心,我们存了handbaghandsome 的偏移值.所以我们只要跳到handbag的偏移,然后挨个扫描每条数据,知道handsome为止,如果有就返回,如果没有就说明数据库里没有这个key。虽然我们还要存些key对应的偏移,但是不用存所有的了,比如每隔几KB存一个key对应的偏移,这样一次遍历几KB的数据依旧很快。 但是内存占用小很多

3. 因为每次请求的时候都是要扫描一块数据文件,所以可以干脆把这段数据存成一个一个数据块然后进行压缩,这样不仅空间节省磁盘空间,读写的IO带宽也会减小。

构建维护SSTable

        前面想的挺好,但是一开始怎么能让每个文件按照key排序呢?这么搞

        虽然在磁盘上维护一个排序的数据结构也可以(比如B树),但是在内存里面搞就很简单了,比如平衡二叉树,红黑树。所以我们的存储引擎可以这么设计

1. 在内存维护一个平衡排序树,比如红黑树,当写数据的时候,把数插入到树中。这种内存树也叫memtable

2. 当memtable比较大了以后,比如说几M,把他写到一个磁盘上去。因为内存中数据已经排好序了,所以这步很快,还是顺序写。这个文件块就成了数据库中最新的数据文件。当文件写好后,清理memtable,写请求重新写到新的memtable去。

3. 当有一个读请求时,先查内存的memtable有没有,然后查最新的文件块,然后查次新的,以此类推

4. 定期离线把数据块做合并,删除过期和用户删除的数据

        这个流程大多数时候都挺好,除了一个问题,一旦数据库跑着跑着崩溃了,那内存中的数据就丢了,为了避免这个,往往在每次写的时候再写一份日志文件,就跟最开始的追加一样,他不需要排序,只是当数据库重启时候能够恢复数据.

性能优化

        可以看到,当key不存在时,数据库的性能会比较差,因为他需要先查内存,然后遍历所有数据文件,每个文件还要扫描一段磁盘数据。这就比较慢了,所以我们可以用Bloom Filter来优化这件事情。Bloom Filter细节就不讲了,效果就是首先他很小,其次当他说这个key不存在是,那就一定不存在,这样针对不存在的key一大部分就会很快返回了。

        另外在何时合并数据文件已经合并的顺序上也有很多的优化方法,最常用的方法是size-tiered 和 leveled. LevelDB 和 RocksDB用leveled合并法, HBase 用 size-tiered,Cassandra两个都支持。实话实说,细节看不懂,不过可以试试看这个

B树

        B树是当下最常用的索引结构,他基本上是所有关系数据库的索引实现,非关系数据库也有很多用它的。根SSTable一样,B树也是按照key排序,这样kv查询和范围查询时候都可以很快搞定,但是他俩相同的地方也就这么多了。B树有一个全然不同的设计哲学

        之前的索引把数据库拆分为若干个段数据块(segment),每段数据块顺序写,大小大约是几M或更大,但是B树却不同,他把数据库打散成一个个小块(block/page),每个小块一般是4K,一次读写一个小块。这种设计更贴近底层的硬件结构,因为物理磁盘实际上也是分成小块的,或者说按页的。

    每页数据有一个唯一的地址,这样数据块就可以有一个像指针一样的东西,指向另一页数据,只不过这个指针是在磁盘上。我们可以用这个指针构建一棵树,例如Figure 3.6


        有一个数据块是根节点,无论你要查哪个key,都从这个根节点开始找,每个节点有若干的key,指针指向对应的孩子节点,每个孩子节点负责特定范围的key

        假设我们要找key是251,从根节点找到200和300之间的指针,然后找到200到300之间的子树,再然后以此类推,找到250到270之间的子树,最后找到251的值。对于一个节点来说,他的孩子节点个数称作branching factor,例如Figure3.6的branching factor是6,这个数字依赖于存储指针的空间和范围大小,不过一般来说都是几百。

        如果要更新一个key的值,首先找到对应的节点,然后把value改掉,再把节点的值写回磁盘,搞定,所有指针全都还有效。如果是要加一个key,如果这个节点还有足够空间,那就直接加进去就行,如果没有,就需要节点分裂。把一个节点拆成2个节点然后放进去。


B树的可靠性

简单来说B树在更新的时候需要写一个数据页到磁盘上,这个操作假设是不会改变他在磁盘上的位置的,也就是所有的指针都还有效。与此同时,有些操作需要去更新多个数据页(多个节点),比如当节点分裂的时候,需要把一个节点数据分拆到两个节点,也就是两页数据中,还要改父节点的指针。一旦这个过程挂了,就会出现数据不一致的问题,比如有一个孩子节点没有父节点指向他,这就导致所以不完整或者出错。

为了解决这个问题,提高可靠性,通常会加一个数据结构,加*write-ahead log*(WAL),在每次B树需要修改之前,把操作写进这个日志中,一旦程序崩溃,可以根据WAL来恢复数据。

另外一个需要注意的地方就是多线程同时修改,这个简单的解决方法是加一个锁,这在之前是不需要的,因为之前都是后台异步将多个日志文件合并,完成后切换新的索引。不涉及多线程的问题

B树的优化

B树的优化有无数种,简单讲几种

1. 原始版本修改B树节点时是在原始位置修改,再加一个WAL保证可靠性,还有一种是不在原始位置修改,而是创建一个新的节点,然后让父节点去更新指针,这样一致性就很好

2. 节点不用存整个key而是他的缩写,比如我们根据父节点能知道这个节点的范围是100000-10005,那如果你的key如果是100001,100002,100003,100004,就不如1,2,3,4,5省地方,但其实他们表达的东西是一样的。

3. 从概念上说,每一页数据可以放在磁盘上的任意位置,但是如果你查一个大范围的时候,就需要用B树去做很多次磁盘寻道操作。很多实现B树时尽可能让叶子节点在磁盘上顺序排列,这样在查范围的时候就好像是顺序读了,磁盘寻道很快。但是其实这是很难做到的,因为当你插入、删除数据时候,就很难维护这个有序的排列了。与之相反,LSM-tree在这方面就是一把好手

4. 给叶子节点加一个指针,指向他的下一个邻居节点,就是B+树,这样你在查一个范围的时候就不用每次从根节点找了。而是顺着这个邻居指针一直往后找。

5. B树一些分化后的产物,比如fractal trees,借鉴了上面日志结构的查询以减少磁盘寻道(没看懂)

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

推荐阅读更多精彩内容