共计 9948 个字符,预计需要花费 25 分钟才能阅读完成。
HBase 采纳 LSM 树架构,天生实用于写多读少的利用场景。在实在生产线环境中,也正是因为 HBase 集群杰出的写入能力,能力反对当下很多数据激增的业务。须要阐明的是,HBase 服务端并没有提供 update、delete 接口,HBase 中对数据的更新、删除操作在服务器端也认为是写入操作,不同的是,更新操作会写入一个最新版本数据,删除操作会写入一条标记为 deleted 的 KV 数据。所以 HBase 中更新、删除操作的流程与写入流程完全一致。当然,HBase 数据写入的整个流程随着版本的迭代在一直优化,但总体流程变动不大。
写入流程的三个阶段
从整体架构的视角来看,写入流程能够概括为三个阶段。
1)客户端解决阶段:客户端将用户的写入申请进行预处理,并依据集群元数据定位写入数据所在的 RegionServer,将申请发送给对应的 RegionServer。
2)Region 写入阶段:RegionServer 接管到写入申请之后将数据解析进去,首先写入 WAL,再写入对应 Region 列簇的 MemStore。
3)MemStore Flush 阶段:当 Region 中 MemStore 容量超过肯定阈值,零碎会异步执行 f lush 操作,将内存中的数据写入文件,造成 HFile。
用户写入申请在实现 Region MemStore 的写入之后就会返回胜利。MemStoreFlush 是一个异步执行的过程。
1. 客户端解决阶段
HBase 客户端解决写入申请的外围流程基本上能够概括为三步。
步骤 1:用户提交 put 申请后,HBase 客户端会将写入的数据增加到本地缓冲区中,合乎肯定条件就会通过 AsyncProcess 异步批量提交。HBase 默认设置 autoflush=true,示意 put 申请间接会提交给服务器进行解决;用户能够设置 autoflush=false,这样,put 申请会首先放到本地缓冲区,等到本地缓冲区大小超过肯定阈值(默认为 2M,能够通过配置文件配置)之后才会提交。很显然,后者应用批量提交申请,能够极大地晋升写入吞吐量,然而因为没有爱护机制,如果客户端解体,会导致局部曾经提交的数据失落。
步骤 2:在提交之前,HBase 会在元数据表 hbase:meta 中依据 rowkey 找到它们归属的 RegionServer,这个定位的过程是通过 HConnection 的 locateRegion 办法实现的。如果是批量申请,还会把这些 rowkey 依照 HRegionLocation 分组,不同分组的申请意味着发送到不同的 RegionServer,因而每个分组对应一次 RPC 申请。
Client 与 ZooKeeper、RegionServer 的交互过程如图所示。
•客户端依据写入的表以及 rowkey 在元数据缓存中查找,如果可能查找出该 rowkey 所在的 RegionServer 以及 Region,就能够间接发送写入申请(携带 Region 信息)到指标 RegionServer。
•如果客户端缓存中没有查到对应的 rowkey 信息,须要首先到 ZooKeeper 上 /hbase-root/meta-region-server 节点查找 HBase 元数据表所在的 RegionServer。向 hbase:meta 所在的 RegionServer 发送查问申请,在元数据表中查找 rowkey 所在的 RegionServer 以及 Region 信息。客户端接管到返回后果之后会将后果缓存到本地,以备下次应用。
•客户端依据 rowkey 相干元数据信息将写入申请发送给指标 RegionServer,Region Server 接管到申请之后会解析出具体的 Region 信息,查到对应的 Region 对象,并将数据写入指标 Region 的 MemStore 中。
步骤 3:HBase 会为每个 HRegionLocation 结构一个近程 RPC 申请 MultiServerCallable,并通过 rpcCallerFactory. newCaller()执行调用。将申请通过 Protobuf 序列化后发送给对应的 RegionServer。
2. Region 写入阶段
服务器端 RegionServer 接管到客户端的写入申请后,首先会反序列化为 put 对象,而后执行各种查看操作,比方查看 Region 是否是只读、MemStore 大小是否超过 blockingMemstoreSize 等。查看实现之后,执行一系列外围操作
Region 写入流程
1)Acquire locks:HBase 中应用行锁保障对同一行数据的更新都是互斥操作,用以保障更新的原子性,要么更新胜利,要么更新失败。
2)Update LATEST_TIMESTAMP timestamps:更新所有待写入(更新)KeyValue 的工夫戳为以后零碎工夫。
3)Build WAL edit:HBase 应用 WAL 机制保证数据可靠性,即首先写日志再写缓存,即便产生宕机,也能够通过复原 HLog 还原出原始数据。该步骤就是在内存中构建 WALEdit 对象,为了保障 Region 级别事务的写入原子性,一次写入操作中所有 KeyValue 会构建成一条 WALEdit 记录。
4)Append WALEdit To WAL:将步骤 3 中结构在内存中的 WALEdit 记录程序写入 HLog 中,此时不须要执行 sync 操作。以后版本的 HBase 应用了 disruptor 实现了高效的生产者消费者队列,来实现 WAL 的追加写入操作。
5)Write back to MemStore:写入 WAL 之后再将数据写入 MemStore。
6)Release row locks:开释行锁。
7)Sync wal:HLog 真正 sync 到 HDFS,在开释行锁之后执行 sync 操作是为了尽量减少持锁工夫,晋升写性能。如果 sync 失败,执行回滚操作将 MemStore 中曾经写入的数据移除。
8)完结写事务:此时该线程的更新操作才会对其余读申请可见,更新才理论失效。
3. MemStore Flush 阶段
随着数据的一直写入,MemStore 中存储的数据会越来越多,零碎为了将应用的内存放弃在一个正当的程度,会将 MemStore 中的数据写入文件造成 HFile。f lush 阶段是 HBase 的十分外围的阶段,实践上须要重点关注三个问题:
•MemStore Flush 的触发机会。即在哪些状况下 HBase 会触发 f lush 操作。
•MemStore Flush 的整体流程。
•HFile 的构建流程。HFile 构建是 MemStore Flush 整体流程中最重要的一个局部,这部分内容会波及 HFile 文件格式的构建、布隆过滤器的构建、HFile 索引的构建以及相干元数据的构建等。
Region 写入流程
数据写入 Region 的流程能够形象为两步:追加写入 HLog,随机写入 MemStore。
1. 追加写入 HLog
HLog 保障胜利写入 MemStore 中的数据不会因为过程异样退出或者机器宕机而失落,但实际上并不齐全如此,HBase 定义了多个 HLog 长久化等级,使得用户在数据高牢靠和写入性能之间进行衡量。
(1)HLog 长久化等级
HBase 能够通过设置 HLog 的长久化等级决定是否开启 HLog 机制以及 HLog 的落盘形式。HLog 的长久化等级分为如下五个等级。
• SKIP_WAL:只写缓存,不写 HLog 日志。因为只写内存,因而这种形式能够极大地晋升写入性能,然而数据有失落的危险。在理论利用过程中并不倡议设置此等级,除非确认不要求数据的可靠性。
• SYNC_WAL:同步将数据写入日志文件中,须要留神的是,数据只是被写入文件系统中,并没有真正落盘。HDFS Flush 策略详见 HADOOP-6313。
• FSYNC_WAL:同步将数据写入日志文件并强制落盘。这是最严格的日志写入等级,能够保证数据不会失落,然而性能绝对比拟差。
• USER_DEFAULT:如果用户没有指定长久化等级,默认 HBase 应用 SYNC_WAL 等级长久化数据。
用户能够通过客户端设置 HLog 长久化等级,代码如下:
put.setDurability(Durability.SYNC_WAL);
(2)HLog 写入模型
在 HBase 的演进过程中,HLog 的写入模型几经改良,写入吞吐量失去极大晋升。之前的版本中,HLog 写入都须要通过三个阶段:首先将数据写入本地缓存,而后将本地缓存写入文件系统,最初执行 sync 操作同步到磁盘。
很显然,三个阶段是能够流水线工作的,基于这样的构想,写入模型天然就想到“生产者 - 消费者”队列实现。然而之前版本中,生产者之间、消费者之间以及生产者与消费者之间的线程同步都是由 HBase 零碎实现,应用了大量的锁,在写入并发量十分大的状况下会频繁呈现恶性抢占锁的问题,写入性能较差。
以后版本中,HBase 应用 LMAX Disruptor 框架实现了无锁有界队列操作。基于 Disruptor 的 HLog 写入模型如图所示。
Hlog 写入模型
图中最左侧局部是 Region 解决 HLog 写入的两个前后操作:append 和 sync。当调用 append 后,WALEdit 和 HLogKey 会被封装成 FSWALEntry 类,进而再封装成 Ring BufferTruck 类放入 Disruptor 无锁有界队列中。当调用 sync 后,会生成一个 SyncFuture,再封装成 RingBufferTruck 类放入同一个队列中,而后工作线程会被阻塞,期待 notify()来唤醒。
图最右侧局部是消费者线程,在 Disruptor 框架中有且仅有一个消费者线程工作。这个框架会从 Disruptor 队列中顺次取出 RingBufferTruck 对象,而后依据如下选项来操作:
•如果 RingBufferTruck 对象中封装的是 FSWALEntry,就会执行文件 append 操作,将记录追加写入 HDFS 文件中。须要留神的是,此时数据有可能并没有理论落盘,而只是写入到文件缓存。
•如果 RingBufferTruck 对象是 SyncFuture,会调用线程池的线程异步地批量刷盘,刷盘胜利之后唤醒工作线程实现 HLog 的 sync 操作。
2. 随机写入 MemStore
KeyValue 写入 Region 分为两步:首先追加写入 HLog,再写入 MemStore。MemStore 应用数据结构 ConcurrentSkipListMap 来理论存储 KeyValue,长处是可能十分敌对地反对大规模并发写入,同时跳跃表自身是有序存储的,这有利于数据有序落盘,并且有利于晋升 MemStore 中的 KeyValue 查找性能。
KeyValue 写入 MemStore 并不会每次都随机在堆上创立一个内存对象,而后再放到 ConcurrentSkipListMap 中,这会带来十分重大的内存碎片,进而可能频繁触发 Full GC。HBase 应用 MemStore-Local Allocation Buffer(MSLAB)机制事后申请一个大的(2M)的 Chunk 内存,写入的 KeyValue 会进行一次封装,程序拷贝这个 Chunk 中,这样,MemStore 中的数据从内存 f lush 到硬盘的时候,JVM 内存留下来的就不再是小的无奈应用的内存碎片,而是大的可用的内存片段。
基于这样的设计思路,MemStore 的写入流程能够表述为以下 3 步。
1)查看以后可用的 Chunk 是否写满,如果写满,从新申请一个 2M 的 Chunk。
2)将以后 KeyValue 在内存中从新构建,在可用 Chunk 的指定 offset 处申请内存创立一个新的 KeyValue 对象。
3)将新创建的 KeyValue 对象写入 ConcurrentSkipListMap 中。
MemStore Flush
1. 触发条件
HBase 会在以下几种状况下触发 flush 操作。
•MemStore 级别限度:当 Region 中任意一个 MemStore 的大小达到了下限(hbase.hregion.memstore.flush.size,默认 128MB),会触发 MemStore 刷新。
•Region 级别限度:当 Region 中所有 MemStore 的大小总和达到了下限(hbase.hregion. memstore.block.multiplier *hbase.hregion.memstore.flush.size),会触发 MemStore 刷新。
•RegionServer 级别限度:当 RegionServer 中 MemStore 的大小总和超过低水位阈值 hbase.regionserver.global.memstore.size.lower.limit*hbase.regionserver.global.memstore.size,RegionServer 开始强制执行 flush,先 flush MemStore 最大的 Region,再 flush 次大的,顺次执行。如果此时写入吞吐量仍然很高,导致总 MemStore 大小超过高水位阈值 hbase.regionserver.global.memstore.size,RegionServer 会阻塞更新并强制执行 flush,直至总 MemStore 大小降落到低水位阈值。
•当一个 RegionServer 中 HLog 数量达到下限(可通过参数 hbase.regionserver.maxlogs 配置)时,零碎会选取最早的 HLog 对应的一个或多个 Region 进行 f lush。
•HBase 定期刷新 MemStore:默认周期为 1 小时,确保 MemStore 不会长时间没有长久化。为防止所有的 MemStore 在同一时间都进行 flush 而导致的问题,定期的 f lush 操作有肯定工夫的随机延时。
•手动执行 flush:用户能够通过 shell 命令 f lush ‘tablename’ 或者 f lush’regionname’ 别离对一个表或者一个 Region 进行 f lush。
2. 执行流程
为了缩小 flush 过程对读写的影响,HBase 采纳了相似于两阶段提交的形式,将整个 flush 过程分为三个阶段。
1)prepare 阶段:遍历以后 Region 中的所有 MemStore,将 MemStore 中以后数据集 CellSkipListSet(外部实现采纳 ConcurrentSkipListMap)做一个快照 snapshot,而后再新建一个 CellSkipListSet 接管新的数据写入。prepare 阶段须要增加 updateLock 对写申请阻塞,完结之后会开释该锁。因为此阶段没有任何费时操作,因而持锁工夫很短。
2)flush 阶段:遍历所有 MemStore,将 prepare 阶段生成的 snapshot 长久化为临时文件,临时文件会对立放到目录.tmp 下。这个过程因为波及磁盘 IO 操作,因而绝对比拟耗时。
3)commit 阶段:遍历所有的 MemStore,将 flush 阶段生成的临时文件移到指定的 ColumnFamily 目录下,针对 HFile 生成对应的 storefile 和 Reader,把 storefile 增加到 Store 的 storef iles 列表中,最初再清空 prepare 阶段生成的 snapshot。
3. 生成 HFile
HBase 执行 f lush 操作之后将内存中的数据依照特定格局写成 HFile 文件,本大节将会顺次介绍 HFile 文件中各个 Block 的构建流程。
(1)HFile 构造
本书第 5 章对 HBase 中数据文件 HFile 的格局进行了具体阐明,HFile 构造参见下图。HFile 顺次由 Scanned Block、Non-scanned Block、Load-on-open 以及 Trailer 四个局部组成。
• Scanned Block:这部分次要存储实在的 KV 数据,包含 Data Block、LeafIndex Block 和 Bloom Block。
• Non-scanned Block:这部分次要存储 Meta Block,这种 Block 大多数状况下能够不必关怀。
• Load-on-open:次要存储 HFile 元数据信息,包含索引根节点、布隆过滤器元数据等,在 RegionServer 关上 HFile 就会加载到内存,作为查问的入口。
• Trailer:存储 Load-on-open 和 Scanned Block 在 HFile 文件中的偏移量、文件大小(未压缩)、压缩算法、存储 KV 个数以及 HFile 版本等根本信息。Trailer 局部的大小是固定的。
MemStore 中 KV 在 f lush 成 HFile 时首先构建 Scanned Block 局部,即 KV 写进来之后先构建 Data Block 并顺次写入文件,在造成 Data Block 的过程中也会顺次构建造成 Leaf index Block、Bloom Block 并顺次写入文件。一旦 MemStore 中所有 KV 都写入实现,Scanned Block 局部就构建实现。
Non-scanned Block、Load-on-open 以及 Trailer 这三局部是在所有 KV 数据实现写入后再追加写入的。
(2)构建 ”Scanned Block” 局部
下图所示为 MemStore 中 KV 数据写入 HFile 的根本流程,可分为以下 4 个步骤。
KV 数据写入 HFile 流程图
1)MemStore 执行 flush,首先新建一个 Scanner,这个 Scanner 从存储 KV 数据的 CellSkipListSet 中顺次从小到大读出每个 cell(KeyValue)。这里必须留神读取的程序性,读取的程序性保障了 HFile 文件中数据存储的程序性,同时读取的程序性是保障 HFile 索引构建以及布隆过滤器 Meta Block 构建的前提。
2)appendGeneralBloomFilter:在内存中应用布隆过滤器算法构建 BloomBlock,下文也称为 Bloom Chunk。
3)appendDeleteFamilyBloomFilter:针对标记为 ”DeleteFamily” 或者 ”DeleteFamilyVersion” 的 cell,在内存中应用布隆过滤器算法构建 BloomBlock,根本流程和 appendGeneralBloomFilter 雷同。
4)(HFile.Writer)writer.append:将 cell 写入 Data Block 中,这是 HFile 文件构建的外围。
(3)构建 Bloom Block
图为 Bloom Block 构建示意图,理论实现中应用 chunk 示意 Block 概念,两者等价。
构建 Bloom Block
布隆过滤器内存中保护了多个称为 chunk 的数据结构,一个 chunk 次要由两个元素组成:
•一块间断的内存区域,次要存储一个特定长度的数组。默认数组中所有位都为 0,对于 row 类型的布隆过滤器,cell 进来之后会对其 rowkey 执行 hash 映射,将其映射到位数组的某一位,该位的值批改为 1。
•firstkey,第一个写入该 chunk 的 cell 的 rowkey,用来构建 Bloom IndexBlock。
cell 写进来之后,首先判断以后 chunk 是否曾经写满,写满的规范是这个 chunk 包容的 cell 个数是否超过阈值。如果超过阈值,就会从新申请一个新的 chunk,并将以后 chunk 放入 ready chunks 汇合中。如果没有写满,则依据布隆过滤器算法应用多个 hash 函数别离对 cell 的 rowkey 进行映射,并将相应的位数组地位为 1。
(4)构建 Data Block
一个 cell 在内存中生成对应的布隆过滤器信息之后就会写入 Data Block,写入过程分为两步。
1)Encoding KeyValue:应用特定的编码对 cell 进行编码解决,HBase 中次要的编码器有 DiffKeyDeltaEncoder、FastDiffDeltaEncoder 以及 PrefixKeyDeltaEncoder 等。编码的基本思路是,依据上一个 KeyValue 和以后 KeyValue 比拟之后取 delta,开展讲就是 rowkey、column family 以及 column 别离进行比拟而后取 delta。如果前后两个 KeyValue 的 rowkey 雷同,以后 rowkey 就能够应用特定的一个 f lag 标记,不须要再残缺地存储整个 rowkey。这样,在某些场景下能够极大地缩小存储空间。
2)将编码后的 KeyValue 写入 DataOutputStream。
随着 cell 的一直写入,以后 Data Block 会因为大小超过阈值(默认 64KB)而写满。写满后 Data Block 会将 DataOutputStream 的数据 f lush 到文件,该 Data Block 此时实现落盘。
(5)构建 Leaf Index Block
Data Block 实现落盘之后会立即在内存中构建一个 Leaf Index Entry 对象,并将该对象退出到以后 Leaf Index Block。Leaf Index Entry 对象有三个重要的字段。
• firstKey:落盘 Data Block 的第一个 key。用来作为索引节点的理论内容,在索引树执行索引查找的时候应用。
• blockOffset:落盘 Data Block 在 HFile 文件中的偏移量。用于索引指标确定后疾速定位指标 Data Block。
• blockDataSize:落盘 Data Block 的大小。用于定位到 Data Block 之后的数据加载。
Leaf Index Entry 的构建如图所示。
同样,Leaf Index Block 会随着 Leaf Index Entry 的一直写入缓缓变大,一旦大小超过阈值(默认 64KB),就须要 f lush 到文件执行落盘。须要留神的是,LeafIndex Block 落盘是追加写入文件的,所以就会造成 HFile 中 Data Block、LeafIndex Block 穿插呈现的状况。
和 Data Block 落盘流程一样,Leaf Index Block 落盘之后还须要再往上构建 RootIndex Entry 并写入 Root Index Block,造成索引树的根节点。然而根节点并没有追加写入 ”Scanned block” 局部,而是在最初写入 ”Load-on-open” 局部。
能够看出,HFile 文件中索引树的构建是由低向上倒退的,学生成 Data Block,再生成 Leaf Index Block,最初生成 Root Index Block。而检索 rowkey 时刚好相同,先在 Root Index Block 中查问定位到某个 Leaf Index Block,再在 Leaf IndexBlock 中二分查找定位到某个 Data Block,最初将 Data Block 加载到内存进行遍历查找。
(6)构建 Bloom Block Index
实现 Data Block 落盘还有一件十分重要的事件:查看是否有曾经写满的 BloomBlock。如果有,将该 Bloom Block 追加写入文件,在内存中构建一个 BloomIndex Entry 并写入 Bloom Index Block。
整个流程与 Data Block 落盘后构建 Leaf Index Entry 并写入 Leaf Index Block 的流程齐全一样。在此不再赘述。
根本流程总结:flush 阶段生成 HFile 和 Compaction 阶段生成 HFile 的流程完全相同,不同的是,flush 读取的是 MemStore 中的 KeyValue 写成 HFile,而 Compaction 读取的是多个 HFile 中的 KeyValue 写成一个大的 HFile,KeyValue 起源不同。KeyValue 数据生成 HFile,首先会构建 Bloom Block 以及 Data Block,一旦写满一个 Data Block 就会将其落盘同时结构一个 Leaf Index Entry,写入 LeafIndex Block,直至 Leaf Index Block 写满落盘。实际上,每写入一个 KeyValue 就会动静地去构建 ”Scanned Block” 局部,等所有的 KeyValue 都写入实现之后再动态地构建 ”Non-scanned Block” 局部、”Load on open” 局部以及 ”Trailer” 局部。
- MemStore Flush 对业务的影响
在实际过程中,flush 操作的不同触发形式对用户申请影响的水平不尽相同。失常状况下,大部分 MemStore Flush 操作都不会对业务读写产生太大影响。比方零碎定期刷新 MemStore、手动执行 flush 操作、触发 MemStore 级别限度、触发 HLog 数量限度以及触发 Region 级别限度等,这几种场景只会阻塞对应 Region 上的写申请,且阻塞工夫较短。
然而,一旦触发 RegionServer 级别限度导致 f lush,就会对用户申请产生较大的影响。在这种状况下,零碎会阻塞所有落在该 RegionServer 上的写入操作,直至 MemStore 中数据量升高到配置阈值内。
文章基于《HBase 原理与实际》一书