阅读这篇文章,但愿你首先已经对Leveldb有了必定的了解,并预先知晓下列概念:html
本文不是一篇专一于源代码解析的文章,也不是一篇Leveldb的介绍文。咱们更但愿探讨的是对于通常的单机数据存储引擎存在哪些问题,Leveldb做为一个经典实现,是采用什么策略并如何解决这些问题的。
Leveldb的解决方案是出于什么考虑,如何高效实现的,作出了哪些权衡以及如何组织代码和工程。你能够先从如下几篇文章对Leveldb有一个基本了解。git
Leveldb的实现原理github
首先提出几个问题:数组
首先,Leveldb所处理的每条记录都是一条键值对,因为它基于sequence number提供快照读,准确来讲应该是键,序列号,值三元组,因为用户通常关心最新的数据,能够简化为键值对。缓存
Leveldb对持久化的保证是基于操做日志的,一条写操做只有落盘到操做日志中以后(暂时先这么理解,实际上这里有所出入,后面在优化部分会讲到)才会在内存中生效,才能被读取到。这就保证了对于已经能见到的操做,一定能够从操做日志中恢复。
它对一致性的保障能够认为是顺序一致性(这里的一致性不是数据库理论的一致性,不强调从安全状态到另外一个安全状态,而是指从各个视图看事件发生的顺序是一致的,因为使用了write batch 竞争锁,实际上写入是串行化的,但同时并发的写操做的顺序取决于线程抢占锁的顺序)。
在这里咱们能够稍微脱离leveldb的实现讨论一下一致性,能不能实现线性一致性呢?若是咱们不支持追加操做的情形下,写是幂等的,若是确保版本号是按照操做开始时间严格递增分配的,即便并发读写也是能够的,这样作还有一个问题,就是如何支持快照读,那就必须保留每个写记录,但它们是乱序的,进行查找将是困难的,咱们能够经过设置同步点,两个同步点之间的是写缓冲,快照读只有在写缓冲中须要遍历查找,在写缓冲被刷入以前重排序记录,刷入的时机是任意小于当前同步点版本号的写操做执行完毕。上述所描述的只可能适合于对热点key的大量并发写。上面所讨论的接近编程语言的内存模型,能够参考JMM内存模型或者C++内存模型。安全
Leveldb对写操做的要求是持久化到操做日志中,其所应对的数据量也超出了内存范围,或者说其存储内容的存储主体仍是在磁盘上,只不过基于最近写的数据每每会被大量访问的假设在内存中存储了较新的数据。leveldb的核心作法就是保存了多个版本的数据以让写入操做不须要在磁盘中查找键的位置,将随机写改成顺序写,将这一部分代价某种程度上转嫁给读时在0层SSTable上的查找。那么它的读性能受到影响了吗?我的认为它的读性能稍显不足主要是受制于LSM的检索方式而非因为多版本共存的问题,固然写的便利也是基于这样的组织方式。数据结构
上面这几段主要是我的的一些想法,可能有些混乱,剩余的几个问题将在下面的部分再详细解答。架构
leveldb的实现大体上能够分红如下几层结构:
首先让咱们考虑设计一款相似于Leveldb的存储产品,那么面临的主要问题主要是如下几项:
在内存中存放的数据主要包含当前数据库的元信息、memtable、ImmutableMemtable,前者显然是必要的,后二者存放的都是最新更新的数据。那么为何须要有ImmutableMemtable呢。这是为了在持久化到磁盘上的同时保持对外服务可用,若是没有这样一个机制,那么咱们要么须要持久化两次,并在第一次持久化的中途记录增量日志,第二次应用上去,这是CMS垃圾回收器的作法,可是显然十分复杂;还有一种选择是咱们预留必定的空间,直接将要持久化的memtable拷贝一份,这样作显然会浪费大量可用内存,对于一个数据库来讲,这是灾难性的。
那么元信息具体应该包含哪些信息呢?
上面列出了一些比较重要的元信息,可能还有遗漏
memtable的键包含三个部分:
键的比较器首先按照递增顺序比较user key,而后安装递减顺序比较sequence number,这两个足以惟一肯定一条记录了。把user key放到前面的缘由是,这样对同一个user key的操做就能够按照sequence number顺序连续存放了,不一样的user key是互不相干的,所以把它们的操做放在一块儿也没有什么意义。用户所传入的是LookupKey,它也是由User Key和Sequence Number组合而成的,其格式为:
| Size (int32变长)| User key (string) | sequence number (7 bytes) | value type (1 byte) |
这里的Size是user key长度+8,也就是整个字符串长度了。value type是kValueTypeForSeek,它等于kTypeValue。因为LookupKey的size是变长存储的,所以它使用kstart_记录了user key string的起始地址,不然将不能正确的获取size和user key。
memtable自己存储同一键的多个版本的数据,这一点从刚刚指出的键的格式也能够看出。这里为何不直接在写的时候直接将原有值替换并使用用户键做为查找键呢?毕竟在memtable中add和update都须要先进行查找。我的认为除了须要支持快照读也没有别的解释了,虽然这样作会使得较老的记录没有被compact而较新的记录已经compact了的奇怪现象发生,但并不影响数据库的读写,在性能上也没有损害。那么快照读为什么是必要的呢?这个问题我目前也没有很好的回答,读者能够自行思考。
memtable的追加操做主要是将键值对进行编码操做并最后委托给跳表处理,代码很简单,就放上来吧。
// KV entry字符串有下面4部分链接而成 // key_size : varint32 of internal_key.size() // key bytes : char[internal_key.size()] // value_size : varint32 of value.size() // value bytes : char[value.size()] size_t key_size = key.size(); size_t val_size = value.size(); size_t internal_key_size = key_size + 8; const size_t encoded_len = VarintLength(internal_key_size) + internal_key_size + VarintLength(val_size) + val_size; char* buf = arena_.Allocate(encoded_len); char* p = EncodeVarint32(buf, internal_key_size); memcpy(p, key.data(), key_size); p += key_size; EncodeFixed64(p, (s << 8) | type); p += 8; p = EncodeVarint32(p, val_size); memcpy(p, value.data(), val_size); assert((p + val_size) - buf == encoded_len); table_.Insert(buf);
有关跳表能够参考下列文章:
根据传入的LookupKey获得在memtable中存储的key,而后调用Skip list::Iterator的Seek函数查找。Seek直接调用Skip list的FindGreaterOrEqual(key)接口,返回大于等于key的Iterator。而后取出user key判断时候和传入的user key相同,若是相同则取出value,若是记录的Value Type为kTypeDeletion,返回Status::NotFound(Slice())。本质上依然委托跳表处理。
Leveldb本身实现了基于引用计数的垃圾回收和一个简单的内存池Arena,其实现预先分配大内存块,划分为不一样对齐的内存空间,其机制乏善可陈,在这里就很少言,放张图吧。
Arena主要提供了两个申请函数:其中一个直接分配内存,另外一个能够申请对齐的内存空间。Arena没有直接调用delete/free函数,而是由Arena的析构函数统一释放全部的内存。应该说这是和leveldb特定的应用场景相关的,好比一个memtable使用一个Arena,当memtable被释放时,由Arena统一释放其内存。
另外就是对于许多类好比memtable、table、cahe等leveldb都加上了引用计数,其实现也很是简单,就是在对象中加入数据域refs,这也很是好理解。好比在迭代的过程当中,已经进入下一个block中了,上一个block理应能够释放了,但它有可能被传递出去提供某些查询服务使用,在其计数不为0时不容许释放,同理对于immutable_memtable,当它持久化完毕时,若是还在为用户提供读服务,也不能释放。不得不说Leveldb的工程层次很清楚,几乎没有循环引用的问题。
对于一个db,大体须要存储下列文件
单个SSTable文件的组织以下图所示:
大体分为几个部分:
全部类型的block格式是一致的,主要包含下面几部分:
其中type指的是采用哪一种压缩方式,当前主要是snappy压缩,接下来主要讲讲block data部分的组织:
snappy是前缀压缩的,为了兼顾查找效率,在构建Block时,每隔几个key就直接存储一个重启点key。Block在结尾记录全部重启点的偏移,能够二分查找指定的key。Value直接存储在key的后面,无压缩。
普通的kv对存储结构以下:
整体的Block Data以下:
整体来看Block可分为k/v存储区和后面的重启点存储区两部分,后面主要是重启点的位置和个数。Block的大小是根据参数固定的,当不能存放下一条记录时多余的空间将会闲置。
SSTable在代码上主要有负责读相关的Table、Block和对应的Iterator实现;在写上主要是BlockBuilder和TableBuilder。能够看出来这也是个典型的二层委托结构了,上面的层次将操做委托给下面层次的类执行,本身管控住progress的信息,控制当前的下层实体。这里咱们主要关心Table和Block中应该存放哪些信息以支持它们的操做。
先讲讲简单的Block,毫无疑问除了数据(char*+size)自己之外就是重启点了,重启点但是查询的利器啊,直接的思路是解析重启点部分红一个vector等,实际上Leveldb不是这样作的,只是保留了一个指向重启点部分的指针,至于为何咱们在查询一节里再详谈。
再说说Table,
首先,咱们考虑在内存中构建一个连续的内存区域表明一个block的内容,它又能够分为两部分:1. 数据的写入 2. 数据写入完毕后附加信息的添加。 先考虑追加一条记录,咱们须要知道哪些东西?
在肯定这些须要的信息后,追加的过程就是查找和维护这些信息以及单纯的memcpy了。
第二步,让咱们考虑在数据写入完毕以后须要为block添加其余信息的过程:
如今,咱们能够把这么一段char[]的数据转换成Slice表达的block了。接下来,让咱们考虑如何批量的把数据写入单个SSTable文件中。这一样分为三个步骤:1. 追加数据 2. 附加信息 3. Flush到文件。 咱们依次考虑。
追加数据须要作哪些:
实际上向文件写入是以Block为单位的,当咱们完成一个Block时,在将它写入文件时须要作什么呢?
最后,当数据所有添加完毕,该SSTable文件今后将不可变动,这一步须要执行的是:
SSTable的遍历主要委托给一个two level iterator处理,咱们只须要弄清楚它的Next操做就能明白其工做原理。所谓的two level,指的是索引一层,数据一层。在拿到一个SSTable文件的时候,咱们先解析它的Index block部分,而后根据当前的index初始化data block层的iterator。接下来咱们主要关注Next的过程。
分为两种情形:
固然,二级迭代器还作了许多的其余工做,好比容许你传入block function,但这和咱们讨论的主线无关,这里就不过多陈述了。
SSTable的查询也委托给iter处理,其主要过程就是对key的定位,也是主要分为三部分:
不管是index block仍是data block,它们的iter实现是一致的,其查找都遵循如下过程:
这里最绝妙的是两点
咱们都知道磁盘的读写是十分耗时的,索引的手段大量减小了磁盘读的必要。固然,还有许多加速的手段好比过滤器和缓存,咱们将在最后一节详细解释。
这里咱们主要关注db的元信息,也即Manifest文件。
首先,Manifest中应该包含哪些信息呢?
首先是使用的coparator名、log编号、前一个log编号、下一个文件编号、上一个序列号。这些都是日志、sstable文件使用到的重要信息,这些字段不必定必然存在。其次是compact点,可能有多个,写入格式为{kCompactPointer, level, internal key}。其后是删除文件,可能有多个,格式为{kDeletedFile, level, file number}。最后是新文件,可能有多个,格式为{kNewFile, level, file number, file size, min key, max key}。对于版本间变更它是新加的文件集合,对于MANIFEST快照是该版本包含的全部sstable文件集合。下面给出一张Manifest示意结构图。
Leveldb在写入每一个字段以前,都会先写入一个varint型数字来标记后面的字段类型。在读取时,先读取此字段,根据类型解析后面的信息。
在代码中元信息这一部分主要是Version类和VersionSet类。LeveDB用 Version 表示一个版本的元信息,Version中主要包括一个FileMetaData指针的二维数组,分层记录了全部的SST文件信息。 FileMetaData 数据结构用来维护一个文件的元信息,包括文件大小,文件编号,最大最小值,引用计数等,其中引用计数记录了被不一样的Version引用的个数,保证被引用中的文件不会被删除。除此以外,Version中还记录了触发Compaction相关的状态信息,这些信息会在读写请求或Compaction过程当中被更新。在CompactMemTable和BackgroundCompaction过程当中会致使新文件的产生和旧文件的删除。每当这个时候都会有一个新的对应的Version生成,并插入VersionSet链表头部。
VersionSet是一个Version构成的双向链表,这些Version按时间顺序前后产生,记录了当时的元信息,链表头指向当前最新的Version,同时维护了每一个Version的引用计数,被引用中的Version不会被删除,其对应的SST文件也所以得以保留,经过这种方式,使得LevelDB能够在一个稳定的快照视图上访问文件。VersionSet中除了Version的双向链表外还会记录一些如LogNumber,Sequence,下一个SST文件编号的状态信息。
这里咱们主要探讨二个问题:
描述一次变动的是VersionEdit类,而最为直接的持久化和apply它的办法就是
首先,咱们看看VersionEdit包含哪些内容:
std::string comparator_; uint64_t log_number_; uint64_t prev_log_number_; uint64_t next_file_number_; SequenceNumber last_sequence_; bool has_comparator_; bool has_log_number_; bool has_prev_log_number_; bool has_next_file_number_; bool has_last_sequence_; std::vector< std::pair<int, InternalKey> > compact_pointers_; DeletedFileSet deleted_files_; std::vector< std::pair<int, FileMetaData> > new_files_;
对比上文Manifest的结构,咱们不难发现:Manifest文件记录的是一组VersionEdit值,在Manifest中的一次增量内容称做一个Block。
Manifest Block := N * VersionEdit
能够看出恢复元信息的过程也变成了依次应用VersionEdit的过程,这个过程当中有大量的中间Version产生,但这些并非咱们所须要的。LevelDB引入VersionSet::Builder来避免这种中间变量,方法是先将全部的VersoinEdit内容整理到VersionBuilder中,而后一次应用产生最终的Version,这种实现上的优化以下图所示:
Compaction过程会形成文件的增长和删除,这就须要生成新的Version,上面提到的Compaction对象包含本次Compaction所对应的VersionEdit,Compaction结束后这个VersionEdit会被用来构造新的VersionSet中的Version。同时为了数据安全,这个VersionEdit会被Append写入到Manifest中。在库重启时,会首先尝试从Manifest中恢复出当前的元信息状态,过程以下:
数据写入Memtable以前,会首先顺序写入Log文件,以免数据丢失。LevelDB实例启动时会从Log文件中恢复Memtable内容。因此咱们对Log的需求是:
LevelDB首先将每条写入数据序列化为一个Record,单个Log文件中包含多个Record。同时,Log文件又划分为固定大小的Block单位。对于一个log文件,LevelDB会把它切割成以32K为单位的物理Block(能够作Block Cache),并保证Block的开始位置必定是一个新的Record。这种安排使得发生数据错误时,最多只需丢弃一个Block大小的内容。显而易见地,不一样的Record可能共存于一个Block,同时,一个Record也可能横跨几个Block。
Block := Record * N Record := Header + Content Header := Checksum + Length + Type Type := Full or First or Midder or Last
Log文件划分为固定长度的Block,每一个Block中包含多个Record;Record的前56个字节为Record头,包括32位checksum用作校验,16位存储Record实际内容数据的长度,8位的Type能够是Full、First、Middle或Last中的一种,表示该Record是否完整的在当前的Block中,若是不是则经过Type指明其先后的Block中是否有当前Record的前驱后继。
Db恢复的步骤:
读的过程能够分为两步:查找对应key+读取对应值,主要问题在第一步。前面咱们在SSTable章节中已经详细解释了对于单个SSTable文件如何快速定位key,在MemTable章节解释了如何在内存中快速定位key;咱们先大体列出查找的流程:
那么咱们接下来的问题是对于第0层以及接下来若干层,如何快速定位key到某个SSTable文件?
对于Level > 1的层级,因为每一个SSTable没有交叠,在version中又包含了每一个SSTable的key range,你可使用二分查找快速找到你处于哪两个点之间,再判断这两个点是否属于同一个SSTable,就能够快速知道是否在这一层存在以及存在于哪一个SSTable。
对于0层的,看来只能遍历了,因此咱们须要控制0层文件的数目。
完成插入操做包含两个具体步骤:
log文件内是key无序的,而Memtable中是key有序的。对于删除操做,基本方式与插入操做相同的,区别是,插入操做插入的是Key:Value 值,而删除操做插入的是“Key:删除标记”,由后台Compaction程序执行真正的垃圾回收操做。
其中的具体步骤能够参阅操做日志管理和memtable详解这两部分。
在解释Leveldb的log compaction过程以前咱们先回顾几个关于如何作compaction的重要问题:
先回答第一个问题:,LevelDB之因此须要Compaction是有如下几方面缘由:
咱们接下来将主要围绕这些问题给出Leveldb的答案。
这里咱们主要谈谈二,何时判断,如何判断到达了这个临界状态?
首先了解Leveldb的两种Compaction:
在MakeRoomForWrite函数中:
说明下为何会有第4点:由于每进行一次minor compaction,level 0层文件个数可能超过事先定义的值,因此会又进行一次major compcation。而此次major compaction,imm_是空的,因此才会有第4条判断。
上文的MakeRoomForWrite主要针对Minor compaction,能够看出其判断的依据主要就是有没有足够的空间执行下一次写入操做;这里咱们将主要关注major compaction,也就是文件的合并,其执行主要是在后台的清理线程。
major compaction的触发方式主要有三种:
既然要判断这几个条件,就要维护相关信息,咱们看看Leveldb为它们维护了哪些信息。
首先,介绍下列事实
不一样level之间,可能存在Key值相同的记录,可是记录的Seq不一样。 最新的数据存放在较低的level中,其对应的seq也必定比level+1中的记录的seq要大。 所以当出现相同Key值的记录时,只须要记录第一条记录,后面的均可以丢弃。 level 0中也可能存在Key值相同的数据,但其Seq也不一样。数据越新,其对应的Seq越大。 且level 0中的记录是按照user_key递增,seq递减的方式存储的,相同user_key对应的记录被汇集在一块儿按照Seq递减的方式存放的。 在更高层的Compaction时,只须要处理第一条出现的user_key相同的记录便可,后面的相同user_key的记录均可以丢弃。 删除记录的操做也会在此时完成,删除数据的记录会被直接丢弃,而不会被写入到更高level的文件。
接下来,咱们分别对几种触发方式详细介绍其机制:
这几个触发条件并不是无的放矢,单个文件过大的容量会吸引大量的查询而且这些查询的速度因为其容量均会减慢,考虑极端状况,只有一个SSTable,那么查询最快也得经历其全部重启点的二分查找。容量越大,可以装入内存的table就更少,须要发生文件读的可能性就越大。对每一层次来讲,上面的理由依然成立,一层的容量过大,要么是文件数不少,要么是单个文件的容量过大,后者已经分析过了,前者会致使二分变慢,并且新数据和老数据没有区分度,不能对于这一假设(新的数据每每被更频繁地访问)作优化,并且对于同一key,其记录数变多,重启点能覆盖的key变少,即便单个文件内的查找也变得低效。
某个文件频繁地被查找,可能出于几种情形:1. 它包含了太多的热点key最新的记录,也就是说它的查找大部分命中了。2. 它的key range 和一些长期木有更新而又被常常访问的key重合了,这种就是出现大量未命中的查找。我的认为compaction主要改善的是后者,这也是为何布隆过滤器使得seek compaction无足轻重,由于判断一个SSTable是否含有对应key所须要的IO资源变少了,但若是你命中了,该读的仍是得读,布隆并不能改善啥,因此我的认为主要为了改善第二点。
上面两段就是Leveldb对于compaction的IO消耗与单次comapct收益权衡以后给出的答案。
首先,咱们来说讲minor compaction,它的目的是把immutable_memtable写入0层的SSTable文件中。咱们已经只读如何遍历一个memtable了,也知道如何经过逐条添加构建一个SSTable了,更清楚了SSTable如何持久化到文件中。对上述步骤不明白的,请参阅上文memtable和sstable章节,因此minor compaction的过程不是理所固然的吗?
这里,主要仍是强调两点:
接下来,咱们主要解析major compaction。
除level0外,每一个level内的SSTable之间不会有key的重叠:也就是说,某一个key只会出如今该level(level > 0)内的某个SSTable中。可是某个key可能出如今多个不一样level的SSTable中。所以,大部分情形下,Compaction应该是发生在不一样的level之间的SSTable之间。
对level K的某个SSTable S1,Level K+1中可以与它进行Compaction的SSTable必须知足条件:与S1存在key范围的重合。
如上图所示,对于SSTable X,其key范围为hello ~ world,在level K+1中,SSTable M的key范围为 mine ~ yours,与SSTable X存在key范围的重合,同时SSTable N也是这样。所以,对于 SSTable X,其Compaction的对象是Level K+1的SSTable M和SSTable N。
最后,考虑特殊情形——level0 的情况。Level 0的SSTable之间也会存在key范围的重合,所以进行Compaction的时候,不只须要在level 1寻找可Compaction的SSTable,同时也要在level 0寻找,以下图示:
先从触发点开始考虑,咱们就先从简单的状况——也就是compact单个文件开始讲起。先假设咱们须要compact Level K层的某个文件,首先咱们要作的就是首先找到参与compaction的全部文件,而后遍历这些文件中的全部记录,选取里面有效且最新的记录写入到新的SSTable文件。最后用新生成的SSTable文件替换掉原来的Level K + 1层的文件。
这样咱们就面临一个生死攸关的问题了:当处理一条记录的时候,如何判断要不要将它写入新文件中呢?答案是当有比它更新的同一key的记录就抛弃它,那么如何找到这个更新的记录呢?
最简单的作法:因为Level k 比Level k+1新,Level k+1又不会出现key 重合,咱们很天然地能够获得一个重新到旧的遍历顺序,只要去新写入的SSTable中查询便可。但这样每次写入都须要一次查询,依然太慢了。咱们能不能先按key序遍历,在同一key内部再按seq递减序遍历,这样只要保留每一个key区间的第一个。Leveldb就是这么作的,可是如何实现呢?
Leveldb使用了一个merging iterator,它统筹控制每一个SSTable的iterator,并在它们中选取一个前进,而后跳过全部同一key的记录。这样处理一条记录所需的查找代价从查询新SSTable文件的全部内容变成了询问几个SSTable对应iter的当前游标,不可谓不妙啊,使人惊叹的作法!下图是一个简单的流程示意:
关于iterator的详细参考能够阅读下列文章:
下一步,咱们把它扩展到一层文件的compaction:对于大多数层,因为文件之间的key range没有交叠,因此你彻底能够迭代进行上面的操做,分别对每个文件合并。实际上major compaction是按key range来的,它每次会compact一个level中的一个范围内的SSTable,而后将这个key范围更新,下次就compact下一范围,控制每一层参与一次compact的SSTable数量。
接下来,咱们考虑Level 0 的情形,因为咱们必须保证0层的总比1层新,假设0层原本有两个同一key的记录,较新的那个被合并到1层以后,查询时在0层能查到较老的那个,bug出现了!因此咱们不得不找出本层全部和当前所要合并的文件有重叠的文件加入合并集合来解决。
而后,咱们来说讲删除的情形。Leveldb中的删除是一个特殊记录,它不会致使数据当即被删除,而是查询到删除记录后将会忽略更老的记录。真正的删除过程是发生在Compaction中的,这里咱们又得问一个问题了:那么删除记录须要写入到上一层吗?须要的,不然在上上层的记录就有可能被查到,只有最上层的删除记录会真正被删除,因此删除是逐步逐层地进行的,一层一层删去过去的记录。
咱们考虑major compaction对服务可用性和性能的影响:在生成新SSTable期间,旧的SSTable依然可用。因为SSTable本就是不可写的,因此对写服务不会形成任何不可用,对于读服务,依然能够在老的SSTable上进行。新的SSTable写到的是一个临时文件,当写入完毕后会进行重命名操做,可是注意对于旧文件,必须查询它在内存中有没有对应的table以及该table的引用计数。只有当没有读服务在该文件上,才能删除该文件。因此,综上compaction对服务可用性没有什么影响。
最后,咱们还须要生成一次compact点,进行一次version edit并写入Manifest文件,最终使当前version更新到新版本。这个过程在元信息管理中已经讲述过了,就再也不赘述了。
Leveldb采用write batch来优化并发写,对每个写操做,先经过传入的键值对构造一个WriteBatch对象,这玩意里面其实就是一个字符串,多个并发写的write batch最后会被合并成一个。这一段的代码确实精妙,请参阅下列文章。
对于Leveldb这种主要基于磁盘存储的引擎,cache优化是很是天然的想法。levelDb中引入了两个不一样的Cache:Table Cache和Block Cache。其中Block Cache是配置可选的。cache主要仍是做用在读过程当中,详细状况你们请参阅下列文章:
源代码实现解析:
如何做用在读操做流程中的:
先了解布隆过滤器的原理和概念:
对实现感兴趣的盆友,能够继续看这篇文章
增长过滤器就须要在写入SSTable的时候向过滤器添加本身写入的键,这一点能够回头看SSTable写入过程。过滤器的做用在Compaction一章中也说了,主要为了改善当发现目标key在某个SSTable的key range内,但事实上未命中时,减小IO消耗,因此你们也知道解析过滤器部分应该用在哪儿了吧。