关于hbase:HBase原理HBase读取流程

41次阅读

共计 6946 个字符,预计需要花费 18 分钟才能阅读完成。

和写流程相比,HBase 读数据的流程更加简单。次要基于两个方面的起因:一是因为 HBase 一次范畴查问可能会波及多个 Region、多块缓存甚至多个数据存储文件;二是因为 HBase 中更新操作以及删除操作的实现都很简略,更新操作并没有更新原有数据,而是应用工夫戳属性实现了多版本;删除操作也并没有真正删除原有数据,只是插入了一条标记为 ”deleted” 标签的数据,而真正的数据删除产生在零碎异步执行 Major Compact 的时候。很显然,这种实现思路大大简化了数据更新、删除流程,然而对于数据读取来说却意味着套上了层层桎梏:读取过程须要依据版本进行过滤,对曾经标记删除的数据也要进行过滤。

本节系统地将 HBase 读取流程的各个环节串起来进行解读。读流程从头到尾能够分为如下 4 个步骤:Client-Server 读取交互逻辑,Server 端 Scan 框架体系,过滤淘汰不合乎查问条件的 HFile,从 HFile 中读取待查找 Key。其中 Client-Server 交互逻辑次要介绍 HBase 客户端在整个 scan 申请的过程中是如何与服务器端进行交互的,了解这点对于应用 HBase Scan API 进行数据读取十分重要。理解 Server 端 Scan 框架体系,从宏观上介绍 HBase RegionServer 如何逐渐解决一次 scan 申请。接下来的大节会对 scan 流程中的外围步骤进行更加深刻的剖析。

Client-Server 读取交互逻辑

Client-Server 通用交互逻辑在之前介绍写入流程的时候曾经做过解读:Client 首先会从 ZooKeeper 中获取元数据 hbase:meta 表所在的 RegionServer,而后依据待读写 rowkey 发送申请到元数据所在 RegionServer,获取数据所在的指标 RegionServer 和 Region(并将这部分元数据信息缓存到本地),最初将申请进行封装发送到指标 RegionServer 进行解决。

在通用交互逻辑的根底上,数据读取过程中 Client 与 Server 的交互有很多须要关注的点。从 API 的角度看,HBase 数据读取能够分为 get 和 scan 两类,get 申请通常依据给定 rowkey 查找一行记录,scan 申请通常依据给定的 startkey 和 stopkey 查找多行满足条件的记录。但从技术实现的角度来看,get 申请也是一种 scan 申请(最简略的 scan 申请,scan 的条数为 1)。从这个角度讲,所有读取操作都能够认为是一次 scan 操作。

HBase Client 端与 Server 端的 scan 操作并没有设计为一次 RPC 申请,这是因为一次大规模的 scan 操作很有可能就是一次全表扫描,扫描后果十分之大,通过一次 RPC 将大量扫描后果返回客户端会带来至多两个十分重大的结果:

•大量数据传输会导致集群网络带宽等系统资源短时间被大量占用,重大影响集群中其余业务。

•客户端很可能因为内存无奈缓存这些数据而导致客户端 OOM。

实际上 HBase 会依据设置条件将一次大的 scan 操作拆分为多个 RPC 申请,每个 RPC 申请称为一次 next 申请,每次只返回规定数量的后果。上面是一段 scan 的客户端示例代码:

public static void scan(){
    HTable table=... ;
    Scan scan=new Scan();
    scan.withStartRow(startRow)
        // 设置检索起始 row
        .withStopRow(stopRow)
        // 设置检索完结 row
        .setFamilyMap (Map<byte[],Set<byte[]>familyMap>)
        // 设置检索的列簇和对应列簇下的列汇合
        .setTimeRange(minStamp,maxStamp)
        // 设置检索 TimeRange
        .setMaxVersions(maxVersions)
        // 设置检索的最大版本号
        .setFilter(filter)
        // 设置检索过滤器
    scan.setMaxResultSize(10000);
    scan.setCacheing(500);
    scan.setBatch(100);
    ResultScanner rs=table.getScanner(scan);
    for (Result r : rs){for (KeyValue kv : r.raw()){......}
    }
} 

其中,for (Result r : rs)语句理论等价于 Result r=rs.next()。每执行一次 next()操作,客户端先会从本地缓存中查看是否有数据,如果有就间接返回给用户,如果没有就发动一次 RPC 申请到服务器端获取,获取胜利之后缓存到本地。

单次 RPC 申请的数据条数由参数 caching 设定,默认为 Integer.MAX_VALUE。每次 RPC 申请获取的数据都会缓存到客户端,该值如果设置过大,可能会因为一次获取到的数据量太大导致服务器端 / 客户端内存 OOM;而如果设置太小会导致一次大 scan 进行太屡次 RPC,网络老本高。

对于很多非凡业务有可能一张表中设置了大量(几万甚至几十万)的列,这样一行数据的数据量就会十分大,为了避免返回一行数据但数据量很大的状况,客户端能够通过 setBatch 办法设置一次 RPC 申请的数据列数量。

另外,客户端还能够通过 setMaxResultSize 办法设置每次 RPC 申请返回的数据量大小(不是数据条数),默认是 2G。

Server 端 Scan 框架体系

从宏观视角来看,一次 scan 可能会同时扫描一张表的多个 Region,对于这种扫描,客户端会依据 hbase:meta 元数据将扫描的起始区间 [startKey, stopKey) 进行切分,切分成多个相互独立的查问子区间,每个子区间对应一个 Region。比方以后表有 3 个 Region,Region 的起始区间别离为:[“a”, “c”),[“c”, “e”),[“e”,”g”),客户端设置 scan 的扫描区间为[“b”, “f”)。因为扫描区间显著逾越了多个 Region,须要进行切分,依照 Region 区间切分后的子区间为[“b”, “c”),[“c”,”e”),[“e”, “f “)。

HBase 中每个 Region 都是一个独立的存储引擎,因而客户端能够将每个子区间申请别离发送给对应的 Region 进行解决。下文会聚焦于单个 Region 解决 scan 申请的外围流程。

RegionServer 接管到客户端的 get/scan 申请之后做了两件事件:首先构建 scanneriterator 体系;而后执行 next 函数获取 KeyValue,并对其进行条件过滤。

1. 构建 Scanner Iterator 体系

Scanner 的外围体系包含三层 Scanner:RegionScanner,StoreScanner,MemStoreScanner 和 StoreFileScanner。三者是层级的关系:

•一个 RegionScanner 由多个 StoreScanner 形成。一张表由多少个列簇组成,就有多少个 StoreScanner,每个 StoreScanner 负责对应 Store 的数据查找。

•一个 StoreScanner 由 MemStoreScanner 和 StoreFileScanner 形成。每个 Store 的数据由内存中的 MemStore 和磁盘上的 StoreFile 文件组成。绝对应的,StoreScanner 会为以后该 Store 中每个 HFile 结构一个 StoreFileScanner,用于理论执行对应文件的检索。同时,会为对应 MemStore 结构一个 MemStoreScanner,用于执行该 Store 中 MemStore 的数据检索。

须要留神的是,RegionScanner 以及 StoreScanner 并不负责理论查找操作,它们更多地承当组织调度工作,负责 KeyValue 最终查找操作的是 StoreFileScanner 和 MemStoreScanner。三层 Scanner 体系能够用图示意。


Scanner 的三层体系

结构好三层 Scanner 体系之后筹备工作并没有实现,接下来还须要几个十分外围的关键步骤,如图所示。


Scanner 工作流程

1)过滤淘汰局部不满足查问条件的 Scanner。StoreScanner 为每一个 HFile 结构一个对应的 StoreFileScanner,须要留神的事实是,并不是每一个 HFile 都蕴含用户想要查找的 KeyValue,相同,能够通过一些查问条件过滤掉很多必定不存在待查找 KeyValue 的 HFile。次要过滤策略有:Time Range 过滤、Rowkey Range 过滤以及布隆过滤器,下图中 StoreFile3 查看未通过而被过滤淘汰。

2)每个 Scanner seek 到 startKey。这个步骤在每个 HFile 文件中(或 MemStore)中 seek 扫描起始点 startKey。如果 HFile 中没有找到 starkKey,则 seek 下一个 KeyValue 地址。HFile 中具体的 seek 过程比较复杂。

3)KeyValueScanner 合并构建最小堆。将该 Store 中的所有 StoreFileScanner 和 MemStoreScanner 合并造成一个 heap(最小堆),所谓 heap 实际上是一个优先级队列。在队列中,依照 Scanner 排序规定将 Scanner seek 失去的 KeyValue 由小到大进行排序。最小堆治理 Scanner 能够保障取出来的 KeyValue 都是最小的,这样顺次一直地 pop 就能够由小到大获取指标 KeyValue 汇合,保障有序性。

2. 执行 next 函数获取 KeyValue 并对其进行条件过滤
通过 Scanner 体系的构建,KeyValue 此时曾经能够由小到大顺次通过 KeyValueScanner 取得,但这些 KeyValue 是否满足用户设定的 TimeRange 条件、版本号条件以及 Filter 条件还须要进一步的查看。查看规定如下:

1)查看该 KeyValue 的 KeyType 是否是 Deleted/DeletedColumn/DeleteFamily 等,如果是,则间接疏忽该列所有其余版本,跳到下列(列簇)。

2)查看该 KeyValue 的 Timestamp 是否在用户设定的 Timestamp Range 范畴,如果不在该范畴,疏忽。

3)查看该 KeyValue 是否满足用户设置的各种 filter 过滤器,如果不满足,疏忽。

4)查看该 KeyValue 是否满足用户查问中设定的版本数,比方用户只查问最新版本,则疏忽该列的其余版本;反之,如果用户查问所有版本,则还须要查问该 cell 的其余版本。

过滤淘汰不合乎查问条件的 HFile

过滤 StoreFile 产生在图中第 3 步,过滤伎俩次要有三种:依据 KeyRange 过滤,依据 TimeRange 过滤,依据布隆过滤器进行过滤。

1)依据 KeyRange 过滤:因为 StoreFile 中所有 KeyValue 数据都是有序排列的,所以如果待检索 row 范畴 [startrow,stoprow] 与文件起始 key 范畴 [f irstkey,lastkey] 没有交加,比方 stoprow < f irstkey 或者 startrow > lastkey,就能够过滤掉该 StoreFile。

2)依据 TimeRange 过滤:StoreFile 中元数据有一个对于该 File 的 TimeRange 属性[miniTimestamp, maxTimestamp],如果待检索的 TimeRange 与该文件工夫范畴没有交加,就能够过滤掉该 StoreFile;另外,如果该文件所有数据曾经过期,也能够过滤淘汰。

3)依据布隆过滤器进行过滤:零碎依据待检索的 rowkey 获取对应的 Bloom Block 并加载到内存(通常状况下,热点 Bloom Block 会常驻内存的),再用 hash 函数看待检索 rowkey 进行 hash,依据 hash 后的后果在布隆过滤器数据中进行寻址,即可确定待检索 rowkey 是否肯定不存在于该 HFile。

从 HFile 中读取待查找 Key

在一个 HFile 文件中 seek 待查找的 Key,该过程能够合成为 4 步操作,如图所示。

HFile 读取待查 Key 流程

  1. 依据 HFile 索引树定位指标 Block

HRegionServer 关上 HFile 时会将所有 HFile 的 Trailer 局部和 Load-on-open 局部加载到内存,Load-on-open 局部有个十分重要的 Block——Root Index Block,即索引树的根节点。

一个 Index Entry,由 BlockKey、Block Offset、BlockDataSize 三个字段组成。

BlockKey 是整个 Block 的第一个 rowkey,如 Root Index Block 中 ”a”, “m”, “o”,”u” 都为 BlockKey。Block Offset 示意该索引节点指向的 Block 在 HFile 的偏移量。

HFile 索引树索引在数据量不大的时候只有最下面一层,随着数据量增大开始决裂为多层,最多三层。

一次查问的索引过程,根本流程能够示意为:

1)用户输出 rowkey 为 ’fb’,在 Root Index Block 中通过二分查找定位到 ’fb’ 在 ’a’ 和 ’m’ 之间,因而须要拜访索引 ’a’ 指向的两头节点。因为 Root IndexBlock 常驻内存,所以这个过程很快。

2)将索引 ’a’ 指向的两头节点索引块加载到内存,而后通过二分查找定位到 fb 在 index ‘d’ 和 ’h’ 之间,接下来拜访索引 ’d’ 指向的叶子节点。

3)同理,将索引 ’d’ 指向的两头节点索引块加载到内存,通过二分查找定位找到 fb 在 index ‘f’ 和 ’g’ 之间,最初须要拜访索引 ’f’ 指向的 Data Block 节点。

4)将索引 ’f’ 指向的 Data Block 加载到内存,通过遍历的形式找到对应 KeyValue。

上述流程中,Intermediate Index Block、Leaf Index Block 以及 Data Block 都须要加载到内存,所以一次查问的 IO 失常为 3 次。然而实际上 HBase 为 Block 提供了缓存机制,能够将频繁应用的 Block 缓存在内存中,以便进一步放慢理论读取过程。

2. BlockCache 中检索指标 Block

从 BlockCache 中定位待查 Block 都非常简单。Block 缓存到 BlockCache 之后会构建一个 Map,Map 的 Key 是 BlockKey,Value 是 Block 在内存中的地址。其中 BlockKey 由两局部形成——HFile 名称以及 Block 在 HFile 中的偏移量。BlockKey 很显然是全局惟一的。依据 BlockKey 能够获取该 Block 在 BlockCache 中内存地位,而后间接加载出该 Block 对象。如果在 BlockCache 中没有找到待查 Block,就须要在 HDFS 文件中查找。

3. HDFS 文件中检索指标 Block

上文说到依据文件索引提供的 Block Offset 以及 Block DataSize 这两个元素能够在 HDFS 上读取到对应的 Data Block 内容。这个阶段 HBase 会下发命令给 HDFS,HDFS 执行真正的 Data Block 查找工作,如图所示。

HDFS 文件检索 Block

整个流程波及 4 个组件:HBase、NameNode、DataNode 以及磁盘。其中 HBase 模块做的事件上文曾经做过了阐明,须要特地阐明的是 FSDataInputStream 这个输出流,HBase 会在加载 HFile 的时候为每个 HFile 新建一个从 HDFS 读取数据的输出流——FSDataInputStream,之后所有对该 HFile 的读取操作都会应用这个文件级别的 InputStream 进行操作。

应用 FSDataInputStream 读取 HFile 中的数据块,命令下发到 HDFS,首先会分割 NameNode 组件。NameNode 组件会做两件事件:

•找到属于这个 HFile 的所有 HDFSBlock 列表,确认待查找数据在哪个 HDFSBlock 上。家喻户晓,HDFS 会将一个给定文件切分为多个大小等于 128M 的 Data Block,NameNode 上会存储数据文件与这些 HDFSBlock 的对应关系。

•确认定位到的 HDFSBlock 在哪些 DataNode 上,抉择一个最优 DataNode 返回给客户端。HDFS 将文件切分成多个 HDFSBlock 之后,采取肯定的策略依照三正本准则将其散布在集群的不同节点,实现数据的高牢靠存储。HDFSBlock 与 DataNode 的对应关系存储在 NameNode。

NameNode 告知 HBase 能够去特定 DataNode 上拜访特定 HDFSBlock,之后,HBase 会再分割对应 DataNode。DataNode 首先找到指定 HDFSBlock,seek 到指定偏移量,并从磁盘读出指定大小的数据返回。

DataNode 读取数据实际上是向磁盘发送读取指令,磁盘接管到读取指令之后会挪动磁头到给定地位,读取出残缺的 64K 数据返回。

4. 从 Block 中读取待查找 KeyValue
HFile Block 由 KeyValue(由小到大顺次存储)形成,但这些 KeyValue 并不是固定长度的,只能遍历扫描查找。

文章基于《HBase 原理与实际》一书

正文完
 0