HBase深度历险 | 京东物流技术团队

发布时间 2023-12-25 14:34:01作者: 京东云开发者

简介

HBase 的全称是 Hadoop Database,是一个分布式的,可扩展,面向列簇的数据库,是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。本文会像剥洋葱一样,层层剥开她的心,直到一丝不挂。

 

特点

首先我们看一下hbase有哪些特点:

•高性能

基于LSM树的数据结构设计,保证了顺序写,并且通过布隆过滤器,compaction等内部优化手段来优化读性能,使得hbase具有很高的读写性能。

•高可靠

hbase在数据写入之前,会先写入WAL预写日志,用来防止机器宕机时,内存中数据的丢失问题。

•易扩展

底层依赖hdfs,当磁盘空间不足时,可以直接水平扩展。

•稀疏性

稀疏性是 HBase 中的一个突出的特点,在其他数据库中,对于空值的处理一般都会填充 null,对于成百上千万列的表来说,通常会存在大量的空值,如果使用填充 null 的策略,势必会造成大量空间的浪费。而对于 HBase 空值不需要任何填充,因此稀疏性是 HBase 的列可以无限扩展的一个重要的条件。

•列簇存储

使用了列簇存储,可以使用户自由选择哪些列来放在同一个列簇中

•多版本

hbase支持数据多版本的保存,通过时间戳来排序。用户可以根据需要选择最新版本或者某个历史版本。

 

什么是列簇存储

前边提到了hbase是按照列簇存储来存储数据的,我们先来对比一下行式存储,列式存储以及列簇存储的区别。

行式存储

传统的关系型数据库都是按行存储的,也就是每行数据都是存储在一起的。

列式存储

数据按列存储,就是每列数据是存储在一起的。按列存储有好处呢?

1.可以节约存储空间,只存储有用的内容,按行存储需要为null值进行处理,而按列存储就不需要了。
2.每一列的类型是一致的,在数据落到磁盘时,可以获得更好的数据压缩效率。
3.大多数情况下,我们再查询的时候是不需要查询出整张表的所有列的,只会用到部分列,但是因为按行存储的方式,每次都必须先将所有字段查询出来,在过滤出我们需要的字段。但是按列存储就不一样了,就像是吃自助餐,按需查询,需要什么就只查询什么,很明显也是可以减少磁盘IO。
列簇存储

那什么是列簇存储呢?按列存储是每个列单独存储在一起,这种方式对于字段很多的表来说,如果一次查询设计的列数量太多的话,势必会造成大量的磁盘IO,从而影响查询性能。而列簇存储的意思就是,一个列簇下边可以存储多个列,每个列簇存储在一个文件里,这样就能减少一些磁盘IO,从而提升查询性能。

每个列簇的数据存储在一起,每个列簇可以自由选择储存哪些列。所以列簇存储实际上是给用户提供了一种自由选择的权利,如果所有的列都放到一个列簇里,那实际就相当于按行存储一样,每次查询需要把所有的列全部都查出来,而如果每个列单独存一个列簇的话,就像按列存储一样了。

在当前体系中不建议设置太多列簇,后面也会提到,是因为MemStore在进行flush时候的最小单元不是MemStore,而是整个region,因此设置太多的列簇会是flush十分耗能,但是这种架构为 HBase 将来演变成 HTAP(Hybrid Transactional and Analytical Processing)系统提供了最核心的基础。

 

架构原理

HBase架构整体上分为3个部分Zookeepper集群,HMaster以及Region server。架构图看上去还挺复杂的,但是最核心的是region server,后边会从region server -> region -> store -> hfile-> data block,一点点的拨开它的外衣。

 

 

1.Zookeeper

 

 

Zookeepper是一个分布式的无中心的元数据存储服务,用来探测和记录HBase集群中服务器的状态信息,hmaster和region server通过定时发送心跳和zk维持关系。

Zookeepper中存储了hbase:meta 表,这张表维护了整个集群所有的 Region 信息,client首先会访问zk,查询hbase:meta 表信息,为了查询性能会将hbase:meta 表信息缓存在客户端。

2.HMaster

 

HMaster在hbase中是属于一个leader的角色,不参与具体表数据的管理,只负责宏观把控,主要工作内容有两方面,一个是管理Region server,另一个就执行一些高危操作,比如执行DDL(建表,删除表等)。

HBase Master的功能:

•监控所有RegionServer的状态,在集群处于数据恢复或者动态调整负载时,分配Region到某一个Region Server中
•提供DDL相关的API, 新建(create),删除(delete)和更新(update)表结构

 

3.Region server

Region server是整个habse架构最为核心的模块,负责实际数据的读写,当访问数据时, 客户端与HBase的Region server直接通信。

HBase的表根据Row Key的区域分成多个Region, 一个Region包含这这个区域内所有数据。而Region server负责管理多个Region, 负责在这个Region server上的所有region的读写操作,一个Region server最多可以管理1000个region。

每个 Region server都把自己的数据存储在HDFS中,如果一个服务器既是Region server又是HDFS的Datanode. 那么这个Region server的数据会在把其中一个副本存储在本地的HDFS中, 加速访问速度。

但是, 如果是一个新迁移来的Region server, 这个region server的数据并没有本地副本. 直到HBase运行compaction, 才会把一个副本迁移到本地的Datanode上面。

 

3.1 Region server架构图

 

 

RegionServer 主要用来响应用户的 IO 请求,是 HBase 中最核心的模块,由 WAL(HLog)、BlockCache 以及多个 Region 构成。

 

3.2 HLog(WAL)

HLog其实就是WAL (Write-Ahead-Log) 预写日志,就是在写入memstore之前,会先写到HLog,给数据来个备份,HLog是保存在磁盘上的,从而保证高可靠性。HLog是region server级别的,也就是说整个region server共用一个HLog。

HLog在hbase中有两个作用:

•当region server宕机的时候,还在内存中未进行写入磁盘的数据不会丢失,仍然可以通过 HLog 日志恢复。
•用于实现 HBase 集群间主从复制,通过回放主集群推送过来的 HLog 日志实现主从复制。

HLog 的日志文件存放在 HDFS 中,hbase 集群默认会在 hdfs 上创建 hbase 文件夹,在该文件夹下有一个 WAL 目录,其中存放着所有相关的 HLog,HLog 并不会永久存在,在整个 HBase 总 HLog 会经历如下过程:

1.发生写入操作的时候会先构建 HLog。
2.因为 HLog 会不断追加,所以整个文件会越来越大,因此需要支持滚动日志文件存储,所以 HBase 后台每间隔一段时间(默认一小时)会产生一个新的 HLog 文件,历史 HLog 标记为历史文件。
3.一旦数据进入到磁盘,形成 HFile 后,HLog 中的数据就没有存在必要了,因为 HFile 存储在 HDFS 中,HDFS 文件系统保障了其可靠性,因此当该 HLog 中的数据都落地成磁盘后,该 HLog 会变为失效状态,对应的操作是将该文件从 WAL 移动到 oldWAl 目录,此时文件依旧存在,并未进行删除。
4.hbase 有一个后台进程,默认每间隔一分钟会对失效日志文件进行判断,如果没有任何引用操作,那么此时的文件会被彻底的从物理删除。

 

3.3 BlockCache

HBase会将从HFile查询出的数据缓存到BlockCache,以便后续可以直接从内存读取,减少磁盘IO。

BlockCache是region级别的,每个region只有一个BlockCache。

HBase的数据仅仅独立存在于MemStore和HFile中,BlockCache中缓存的只是HFile中的部分热点数据。

 

3.4 region

每个region由一个或多个store组成,region是集群负载均衡的基本单位,是MemStore进行flush的基本单元。整个region其实就是一个LSM树,memstore对应C0树,存储在内存中,作为写缓存,HFile对应Cn树,存储在磁盘上。通过保证顺序写,在牺牲部分读性能的前提下,来大幅度提升写入性能,并且通过布隆过滤器和compaction等内部的优化来弥补读性能,从而达到读写性能都很高。

 

3.4.1 store

每个store由MemStore和多个StoreFile组成,StoreFile底层实际是HFile,StoreFile是hbase对HFile的封装。每个列簇存储在一个store,所以有几个列簇,就会有几个store,列簇太多就会有过多的MemStore,就会占用过多的内存,并且在进行flush的时候,也会造成更大的耗能。

3.4.1.1 MemStore

MemStore是hbase的写缓存,在写入请求时,当写入HLog成功之后,变先写入MemStore,并且会按照rowkey字典序进行排序,当达到一定阈值的时候,才会flush数据到hdfs形成hfile文件。hbase的MemStore是采用了跳表这一数据结构,在这里就不过多介绍了。

MemStore的作用:

•写缓存:攒批数据,批量落盘,减少磁盘IO,提升性能
•排序:维持数据按照 RowKey 的字典序排列

MemStore在读写时都起到了很大的作用,最大的耗能操作时在flush操作,下边我们详细介绍一下。

Memstore Flush触发条件

我们已经知道,当MemStore的大小达到一定的阈值时,就会flush数据到HDFS上,形成hfile文件,但是需要注意的是,MemStore进行flush的最小操作单元不是MemStore,而是整个HRegion。也就是说,有一个MemStore需要进行flush,整个HRegion都会受影响,所以一个HRegion如果有过多的Memstore,每次flush的开销必然会很大,所以每个表不应该设置过多的列簇。下边介绍一下触发flush操作的具体条件:

1.当一个Region中某个MemStore的大小达到hbase.hregion.memstore.flush.size(默认值128MB)时,会触发该Region中所有MemStore Flush,不会阻塞写操作。
2.当一个Region中所有MemStore的大小总和达到hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size(默认值2 * 128 = 256MB)时,会触发该Region中所有MemStore Flush,期间阻塞该Region的写操作。
3.当一个Region准备下线时的MemStore大小总和达到hbase.hregion.preclose.flush.size(默认值5MB)时,会触发该Region中所有MemStore Flush,然后Region才能关闭。
4.当一个RegionServer中所有MemStore的大小总和达到hbase.regionserver.global.memstore.size * HBASE_HEAPSIZE(默认值0.4 * 堆空间大小)时,会从MemStore最大的Region开始,触发该RegionServer中所有Region的Flush,并阻塞整个RegionServer的写操作。直到MemStore大小回落到上一个参数值的hbase.regionserver.global.memstore.size.lower.limit(默认值0.95)倍,才解除阻塞。
5.当一个RegionServer中的WAL(即HLog)数量达到hbase.regionserver.maxlogs(默认值32)时,HBase就选取最早的一个WAL对应的那些Region进行MemStore Flush,期间也会阻塞对应Region的写操作。
6.RegionServer会定期Flush MemStore,周期为hbase.regionserver.optionalcacheflushinterval(默认值1小时)。为了避免所有Region同时Flush,定期刷新会有随机的延时。
7.用户可以通过执行flush [table]flush [region]命令来手动Flush一张表或一个Region的MemStore。

 

3.4.1.2 HFile

 

 

HFile是HBase存储数据的文件组织形式,参考BigTable的SSTable和Hadoop的TFile实现。下图是HFile扥物理结构示意图,如图所示,HFile会被切分为多个大小相等的block块,HFile内部结构还是比较复杂的,有兴趣的同学可以看下(http://hbasefly.com/2016/03/25/hbase-hfile/),本文我们主要看下存储实际数据的data block。

3.4.1.2.1 DataBlock

DataBlock是HBase中数据存储的最小单元。DataBlock中主要存储用户的KeyValue数据(KeyValue后面一般会跟一个timestamp,图中未标出),而KeyValue结构是HBase存储的核心,每个数据都是以KeyValue结构在HBase中进行存储。KeyValue结构磁盘中可以表示为:

 

每个KeyValue都由4个部分构成,分别为key length,value length,key和value。其中key length和value length是两个固定长度的数值,而key是一个复杂的结构,首先是rowkey的长度,接着是rowkey,然后是ColumnFamily的长度,再是ColumnFamily,之后是ColumnQualifier,最后是时间戳和KeyType(keytype有四种类型,分别是Put、Delete、 DeleteColumn和DeleteFamily),value就没有那么复杂,就是一串纯粹的二进制数据。

3.4.1.2.2 基本概念

从上图可以看出一些长度是固定的值,所以把key做一些简化,然后重点看下这些内容的具体含义。Key由RowKey(行键) + ColumnFamily(列族)+ Column Qualifier(列修饰符)+ TimeStamp(时间戳--版本)+ KeyType(类型)组成,而Value就是实际上的值。

 

Rowkey:每行数据的主键,是hbase的唯一的查询条件,因此Rowkey的设计至关重要。
Column family:列簇,每个列簇的数据存储在一起,每个列簇用户可以自由选择存储哪些列,同一个列簇的所有成员具有相同的列簇前缀,通常将同一类型的列存放在一个列簇下,但是要注意不要设置过多的列簇,后边会讲到。
Qualifier:列,就是具体的列,以列簇作为前缀,格式为 Column family:Qualifier。
Timestamp:时间戳,插入单元格时的时间戳,默认作为单元格的版本号。不同版本的数据按照时间戳倒序排序,即最新的数据排在最前面。当数据需要更新的时候,不会向mysql一样,直接对源数据就行更新,而是会新增一条数据,新数据的时间戳更大,因此会排在前面。可以根据时间戳来设置TTL,以及保存的版本个数。
Type:数据类型,用来区别是不是delete的数据,删除数据和更新数据一样,也不会直接进行数据的物理删除,也会新插入一条数据,只是这条数据的type会被标记为delete,在查询是默认会进行过滤。只有在执行Major Compaction操作时,才会进行delete数据的清理。
Cell:单元格,在 HBase 中,值作为一个单元保存在单元格中。要定位一个单元,需要满足 “rowkey + column family + qualifier + timestamp+KeyType” 五个要素。每个 cell 保存着同一份数据的多个版本。cell 中没有数据类型,完全是字节存储。

 

4.读写流程

在介绍读写流程之前,先介绍一下Meta table,为什么要先介绍它,因为它是读写操作的必经之路,是rowkey的引路人。

Meta table

Meta table存储了所有的region信息,Meta table储存在zk中,客户端在第一次读写访问或者region失效时,都会先访问zk,根据rowkey从Meta table获取region server的信息,然后客户端再去访问对应的region server,进行对应的读写操作。

 

写流程

 

1.Client 先访问 zookeeper,获取Meta table表位于哪个 Region Server。
2.访问对应的 Region Server,获取Meta table表,查询出目标数据位于哪个 Region Server 中的哪个 Region 中。并将该 table 的 Region 信息以及 meta 表的位置信息缓存在客户端的 meta cache,方便下次访问。
3.客户端直接与目标 Region Server 进行通信。
4.将数据顺序写入(追加)到 HLog(WAL),防止机器宕机而丢失内存中的数据。
5.将数据写入对应的 MemStore,数据会在 MemStore 进行排序。
6.向客户端发送 ack。
7.等达到 MemStore 的触发条件后,将数据flush到 HFile。

 

读流程

 

1.Client 先访问 zookeeper,获取Meta table表位于哪个 Region Server。
2.访问对应的 Region Server,获取Meta table表,查询出目标数据位于哪个 Region Server 中的哪个 Region 中。并将该 table 的 Region 信息以及 meta 表的位置信息缓存在客户端的 meta cache,方便下次访问。
3.客户端直接与目标 Region Server 进行通信。
4.分别在内存中还未落盘的MemStore和已经落入磁盘的HFile中(对于HFile数据优先从缓存在内存中的BlockCache读取,读缓存没有再从HFile中加载)读取数据,并将查到的所有数据进行合并。注意hbase的数据仅仅独立的存在于内存和磁盘中,就是说MemStore和HFile中的数据一定是全量的数据,而BlockCache (读缓存)只是HFile缓存在内存中的部分热点数据,BlockCache的作用是在读取HFile的数据时,可以直接从内存中读取,减少磁盘IO次数。
5.将从文件 HFile 中查询到的数据块 (Block,HFile 数据存储单元,默认大小为 64KB) 缓存到 Block Cache。
6.将合并后的最终结果,然后返回时间最新的数据返回给客户端。

 

读流程详解

hbase的写操作十分方便,更新实际上只是新加了一条最新时间戳的数据,删除数据也只是添加了一个delete的标记,只有再Major Compaction的时候才会进行物理删除。而因为hbase中同一个rowkey是保存了多版本的数据,以及不同的keytype的数据,所以同一个rowkey会对应多条数据,因此这里不像我们想的那样,如果rowkey命中BlockCache或者MemStore就会直接返回,远没有想象中的简单,因此也不存在先从BlockCache读还是先从MemStore中读的概念,这种说法本身就是不对的。

在读取数据的时候,内存MemStore和磁盘HFile中的数据都要读取,会创建两种类型的Scanner(StoreFileScanner和MemstoreScanner),分别用来探查HFile和MemStore中的数据,然后会根据用户选择的时间范围以及rowkey的范围过滤掉一些Scanner,最后对于HFile的数据会先查找对应的Block,查找Block时会优先从Blockcache中查找,找不到再从HFile中加载。而MemstoreScanner会从Memstore中查询数据,最后将内存和磁盘中的数据进行合并,返回最新的数据给到客户端。简化的流程如下:

1.构建Scanner

每个StoreScanner会为当前该Store中每个HFile构造一个StoreFileScanner,用于实际执行对应文件的检索。同时会为对应Memstore构造一个MemstoreScanner,用于执行该Store中Memstore的数据检索。

1.过滤Scanner

根据Time Range以及RowKey Range对StoreFileScanner以及MemstoreScanner进行过滤,淘汰肯定不存在待检索结果的Scanner。

1.Seek rowkey

所有StoreFileScanner开始做准备工作,在负责的HFile中定位到满足条件的起始Row。Seek过程(此处略过Lazy Seek优化)也是一个很核心的步骤,它主要包含下面三步:

•定位Block Offset:在Blockcache中读取该HFile的索引树结构,根据索引树检索对应RowKey所在的Block Offset和Block Size
•Load Block:根据BlockOffset首先在BlockCache中查找Data Block,如果不在缓存,再在HFile中加载
•Seek Key:在Data Block内部通过二分查找的方式定位具体的RowKey。

 

6.HBase Compaction

HBase 的 MemStore 在达到触发条件的时候,会把MemStore中的数据flush到HDFS上,每次都会形成一个新的HFile文件,所以随着时间的不断累积,同一个 Store 下的 HFile 会越来越多,进而会降低 HBase 查询性能,主要体现在查询数据的 IO 次数增加。为了优化查询性能,HBase 会合并小的 HFile 以减少文件数量,这种合并 HFile 的操作称为 Compaction。

Minor Compaction:会将邻近的若干个 HFile 合并,在合并过程中会清理 TTL 的数据,但不会清理被删除的数据。Minor Compaction 消耗的资源较少,通过少量的 IO 减少文件的个数,提升读取操作的性能,适合高频率地跑。
Major Compaction:会将一个 Store 下的所有 HFile 进行合并,并且会清理掉过期的和被删除的数据,即在 Major Compaction 会删除全部需要删除的数据。一般情况下,Major Compaction 时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此,生产环境下通常关闭自动触发 Major Compaction 功能,改为手动在业务低峰期触发。

 

HBase与传统关系型数据库的对比

•数据类型:

没有数据类型,都是字节数组(有一个工具类Bytes,将java对象序列化为字节数组),而传统的关系型数据库有丰富的数据类型。

•数据操作:

HBase只有很基本的插入、查询、删除等操作,表和表之间是没有关系的,而传统数据库一般都会有很多的函数,并且表之间有关联的特点。

•存储模式:

Hbase基于列簇存储,而传统关系型数据库是基于行存储。

•数据更新:

habse中对于数据update情况,不会向传统数据库一样,直接修改这条记录,而是新插入了一条状态为delete的数据,这么设计的原因是为了保证顺序写,提高数据IO效率,进而提高性能。并且旧数据也不会马上删除,会在compaction时才会进行删除。

•时间版本:

Hbase数据写入cell时,还会附带时间戳,默认为数据写入时RegionServer的时间,但是也可以指定一个不同的时间。数据可以有多个版本,并且可以设置TTL,以及保留的版本个数。

•索引:mysql支持多种类型的索引,而hbase只有rowkey
•易扩展性

搞过mysql分库分表的同学一定体会过它的魅力,十分的麻烦,但是hbase可以动态水平扩展,十分便捷

Rowkey的设计

访问方式

HBase访问只有3种方式

1.通过单个 rowkey 访问
2.通过 rowkey 的 range
3.全表扫描

 

RowKey设计原则

•长度原则

RowKey是一个二进制码流,可以是任意字符串,最大长度为64kb,实际应用中一般为10-100byte,以byte[]形式保存,一般设计成定长。建议越短越好,不要超过16个字节

•唯一原则

必须在设计上保证RowKey的唯一性。由于在HBase中数据存储是Key-Value形式,若向HBase中同一张表插入相同RowKey的数据,则原先存在的数据会被新的数据覆盖。

•排序原则

HBase的RowKey是按照ASCII有序排序的

•散列原则

RowKey应均匀的分布在各个HBase节点上,防止热点数据造成数据倾斜。

 

避免数据热点的方法

•Reversing

顾名思义,直接反转的意思。比如用户id,手机号等,rowkey的头部不具有随机性,但是尾部具有良好的随机性,那这时我们可以将rowkey直接翻转过来。Reversing可以有效的使RowKey随机分布,但是牺牲了RowKey的有序性。利于Get操作,但不利于Scan操作,因为数据在原RowKey上的自然顺序已经被打乱。

•Salting

Salting(加盐)的原理是在原RowKey的前面添加固定长度的随机数,从而保障数据在所有Regions间的负载均衡。

•Hashing

Hashing和加盐是类似的,只是Hashing要求前缀不能是随机的,需要使用一些hash算法,这样客户端可以重构出Hashing后的rowkey。

推荐方案:

一般情况,可以选择业务主键的后3位取余hbase分区数的余数作为加盐的前缀,然后在拼接上业务主键作为rowkey。

 

HBase的缺点

•数据分析能力弱:数据分析是HBase的弱项,比如聚合运算、多维度复杂查询、多表关联查询等。所以,我们一般在HBase之上架设Phoenix或Spark等组件,增强HBase数据分析处理的能力。
•原生不支持二级索引:默认HBase只对rowkey做了单列索引,因此正常情况下对非rowkey列做查询比较慢。所以,我们一般会选择一个HBase二级索引解决方案,目前比较成熟的解决方案是Phoenix,此外还可以选择Elasticsearch/Solr等搜索引擎自己设计实现。
•原生不支持SQL:SQL查询也是HBase的一个弱项,好在这块可以通过引入Phoenix解决,Phoenix是专为HBase设计的SQL层。
•HBase原生不支持全局跨行事务,只支持单行事务模型。同样,可以使用Phoenix提供的全局事务模型组件来弥补HBase的这个缺陷。
•故障恢复时间长

 

注意点总结

1.设计表结构的时候,一定要注意列簇的个数不要设置的过多,不要超过3个,最好就只有一个
2.rowkey的设计一定要注意加盐或hash处理,避免出现热点数据的问题
3.Major Compaction默认开启需要关闭,因为十分耗费资源,影响写入,可以在业务低峰期手动执行
4.MemStore的flush的触发条件,一定要关注Region Server中所有Memstore是否达到阈值这个条件,因为这个会影响整个Region Server的写入
5.BlockCache一定不要理解偏了,hbase中的数据,要么还在写缓存MemStore中未写入磁盘,要么就是已经写入磁盘的数据,这两部分一定包含了全量的数据,BlockCache只是HFile缓存在内存中的部分热点数据。

 

Q&A

1. 常说HBase数据读取要读Memstore、HFile和Blockcache,为什么上面Scanner只有StoreFileScanner和MemstoreScanner两种?没有BlockcacheScanner?

HBase中数据仅仅独立地存在于Memstore和StoreFile中,Blockcache中的数据只是StoreFile中的部分数据(热点数据),即所有存在于Blockcache的数据必然存在于StoreFile中。因此MemstoreScanner和StoreFileScanner就可以覆盖到所有数据。实际读取时StoreFileScanner通过索引定位到待查找key所在的block之后,首先检查该block是否存在于Blockcache中,如果存在直接取出,如果不存在再到对应的StoreFile中读取。

2. 数据更新操作先将数据写入Memstore,再落盘。落盘之后需不需要更新Blockcache中对应的kv?如果不更新,会不会读到脏数据?

如果理清楚了第一个问题,相信很容易得出这个答案:不需要更新Blockcache中对应的kv,而且不会读到脏数据。数据写入Memstore落盘会形成新的文件,和Blockcache里面的数据是相互独立的,以多版本的方式存在。

 

参考资料:

https://my.oschina.net/u/4511602/blog/4916109

https://www.jianshu.com/p/f911cb9e42de

https://www.jianshu.com/p/0e178bce8f63

http://hbasefly.com/2016/12/21/hbase-getorscan/

http://hbasefly.com/2017/06/11/hbase-scan-2/

https://zhuanlan.zhihu.com/p/145551967

https://blog.csdn.net/u012151684/article/details/109040581

https://xie.infoq.cn/article/76f8caba743f8be2beb81441d

https://zhuanlan.zhihu.com/p/159052841?utm_id=0

http://hbasefly.com/2016/04/03/hbase_hfile_index/

http://hbasefly.com/2016/03/25/hbase-hfile/

作者:京东物流 于建飞

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源