HBase学习,
HBase学习
0x01 摘要
本文是一篇HBase学习综述,将会介绍HBase的特点、对比其他数据存储技术、、架构、存储、数据结构、使用、过滤器等。
未完成
0x02 HBase基础概念
2.1 HBase是什么
-
起源
HBase源于Google 2005年的论文Bigtable。由Powerset公司在2007年发布第一个版本,2008年成为Apache Hadoop子项目,2010年单独升级为Apache顶级项目。 -
设计目标
HBase的设计目标就是为了那些巨大的表,如数十亿行、数百万列。 -
一句话概括
HBase是一个开源的、分布式的、版本化、列式存储的非关系型数据库。
2.2 HBase相对于RDMBS能解决什么问题
| 扩展性 | 表设计 | 负载均衡 | failover | 事务 | 适用数据量 | |
|---|---|---|---|---|---|---|
| RDBMS | 差 | 灵活性较弱 | 差 | 同步实现 | 支持 | 万级 |
| HBase | 强 | 十亿级行,百万级列;动态列,每行列可不同。且引入列族和数据多版本概念。 | 强 | 各组件都支持HA | MVCC, Produce LOCK;行级事务 | 亿级 |
2.3 HBase特点
- 可扩展,表自动分片,且支持自动Failover
- 高效地、强一致性地读写海量数据,CP
- MapReduce可读写HBase
- JavaAPI, Thrift / REST API
- 依赖Block cache和布隆过滤器提供实时查询
- 服务端侧的过滤器实现谓词下推,加速查询
- 可通过JMX监控HBase各指标
- 面向列,列式存储,且列可以按需动态增加
- 稀疏。空Cell不占空间
- 数据多版本
- 数据类型单一,都是字符串,无类型(要用类型可用Phoenix实现)
2.4 HBase与Hadoop
作为曾经Hadoop项目的子项目,HBase还是与Hadoop生态关系密切。HBase底层存储用了HDFS,并可直接用MapReduce操作HBase
2.5 HBase与CAP
2.5.1 CAP简介
CAP定理指出,分布式系统可以保持以下三个特征中的两个:
- Consistency,一致性
请求所有节点都看到相同的数据 - Availability,可用性
每个请求都能及时收到响应,无论成功还是失败。 - Partition tolerance,分区容忍
即使其他组件无法使用,系统也会继续运行。
2.5.2 HBase的取舍-CP
HBase选择的是一致性和分区容忍即CP。
这篇文章给出了为什么分区容忍重要的原因[http://codahale.com/you-cant-sacrifice-partition-tolerance/。
已经有测试证明 HBase面对网络分区情况时的正确性。
2.6 HBase使用场景
- 持久化存储大量数据(TB、PB)
- 对扩展伸缩性有要求
- 需要良好的随机读写性能
- 简单的业务KV查询
- 能够同时处理结构化和非结构化的数据
2.7 行/列存储
2.7.1 简介
HBase是基于列存储的。本节对比下行列两种存储格式。
从上图可以看到,行列存储最大的不同就是表的组织方式不同。
2.7.2 数据压缩
列式存储,意味着该列数据往往类型相同,可以采用某种压缩算法进行统一压缩存储。
比如下面这个例子,用字典表的方式压缩存储字符串:
查询Customers列为Miller且Material列为Regrigerator的流程如下:
2.7.3 行列存储对比
| 行 | 列 | |
|---|---|---|
| 优点 | 1.便于按行查询数据,OLTP往往是此场景 2.便于行级插入、删除、修改 3.易保证行级一致性 |
1.便于按列使用数据,如对列分组、排序、聚合等,OLAP很多是这样 2.列数据同类型,便于压缩 3.表设计灵活,易扩展列 |
| 缺点 | 1.当只需查询某几个列时,还是会读整行数据 2.扩展列代价往往较高 |
1.不便于按行使用数据 2.很难保证行级一致性 |
| 优化思想 | 读取过程尽量减少不需要的数据 | 提高读写效率 |
| 优化措施 | 1.设计表时尽量减少冗余列 2.内存中累积写入到阈值再批量写入 |
1.多线程方式并行读取不同列文件 2.行级一致性,可通过加入RDBMS中回滚机制、校验码等 3.内存中累积写入到阈值再批量写入 |
| 应用场景 | OLTP | OLAP |
0x03 HBase架构
3.1 Client
Client有访问Hbase的接口,会去meta表查询目标region所在位置(此信息放入缓存)并连接对应RegionServer。当master rebanlance会region死亡会重新查找。
3.2 Zookeeper
3.3 HMaster
3.4 HRegionServer
3.4.1 主要职责
3.4.2 HRegionServer组件
- 一个RegionServer上存在多个Region和一个Hlog实例。
- HLog的就是WAL(Write-Ahead-Log),相当于RDBMS中的redoLog,写数据时会先写一份到HLog。可以配置
MultiWAL,多Region时使用多个管道来并行写入多个WAL流。一个RS共用一个Hlogd的原因是减少磁盘IO开销,减少磁盘寻道时间。 - Region属于某个表水平拆分的结果(初始一个Region),每个表的Region分部到多个RegionServer。
- Region上按列族划分为多个Store
- 每个Store有一个MemStore,当有读写请求时先请求MemStore
- 每个Store又有多个StoreFile
- HFiles是数据的实际存储格式,他是二进制文件。StoreFile对HFile进行了封装。HBase的数据在底层文件中时以KeyValue键值对的形式存储的,HBase没有数据类型,HFile中存储的是字节,这些字节按字典序排列。
- BlockCache
3.5 HDFS
为HBase提供最终的底层数据存储服务,多副本保证高可用性 .
- HBase表的HDFS目录结构如下
/hbase
/data
/<Namespace> (集群里的Namespaces)
/<Table> (该集群的Tables)
/<Region> (该table的Regions)
/<ColumnFamily> (该Region的列族)
/<StoreFile> (该列族的StoreFiles)
- HLog的HDFS目录结构如下
/hbase
/WALs
/<RegionServer> (RegionServers)
/<WAL> (WAL files for the RegionServer)
3.6 Region
3.6.1 概述
- 一个RegionServer上存在多个Region和一个Hlog实例。
- Region属于某个表水平拆分的结果(初始一个Region),每个表的Region分部到多个RegionServer。
- Region上按列族划分为多个Store
- 每个Store有一个MemStore,当有读写请求时先请求MemStore。MemStore内部是根据
RowKey,Column,Version排序 - 每个Store又有多个StoreFile
3.6.2 Region分配
- HMaster使用
AssignmentManager,他会通过.META.表检测Region分配的合法性,当发现不合法(如RegionServer挂掉)时,调用LoadBalancerFactory来重分配Region到其他RS。 - 分配完成并被RS打开后,需要更新
.META.表。
3.6.3 Region Merge(合并)
3.6.3.1 手动合并
注意,这里说的是Region级别的合并。
该过程对Client来说是异步的。
该过程是Master和RegionServer共同参与,步骤如下:
3.6.3.2 自动合并
这里指的是StoreFile级别的合并。
合并根据许多因素,可能有益于形同表现也有可能是负面影响。
当MemStore不断flush到磁盘,StoreFile会越来越多。HBase Compact过程,就是RegionServer定期将多个小StoreFile合并为大StoreFile,也就是LSM小树合并为大树。这个操作的目的是增加读的性能,否则搜索时要读取多个文件。
HBase中合并有两种:
- Minor Compact
- 合并过程:
仅会挑选少量小的、临近的StoreFile进行合并。 - 输出
每个Store合并完成的输出是少量较大StoreFile。
- 合并过程:
- Major Compact
- 合并过程
合并一个Region上的所有HFile,此时会删除那些无效的数据:- 有更新时,老的数据就无效了,最新的那个<key, value>就被保留
- 被删除的数据,将墓碑<key,del>和旧的<key,value>都删掉
- TTL过期的数据,合并时干掉不再写入合并后的StoreFile
- 通过maxVersion制定了最大版本的数据,超出的旧版本数据会在合并时被清理掉不再写入合并后的StoreFile
- 输出
每个Store合并完成的输出是一个较大的StoreFile。
- 合并过程
3.6.3.3 合并算法
默认Major Compact 7天执行一次,可能会导致异常开销,影响系统表现,所以可以进行手动调优。
当前版本(1.2.0版)采用的合并策略为ExploringCompactionPolicy,挑选最佳的一系列StoreFiles,可以减少合并带来的消耗。
3.6.3.4 合并/scan并发问题
当scan查询时遇到合并正在进行,解决此问题方案点这里
3.6.4 Region Split(拆分)
3.6.4.1 自动Split
注意:Split过程是RegionServer进行的,没有Master参与。
上图中,绿色箭头为客户端操作;红色箭头为Master和RegionServer操作:
3.6.5.2 手动Split
在以下情况可以采用预分区(预Split)方式提高效率:
rowkey按时间递增(或类似算法),导致最近的数据全部读写请求都累积到最新的Region中,造成数据热点。- 扩容多个RS节点后,可以手动拆分Region,以均衡负载
- 在
BulkLoad大批数据前,可提前拆分Region以避免后期因频繁拆分造成的负载
3.6.5.3 Split算法
一般来说,手动拆分是弥补rowkey设计的不足。我们拆分region的方式必须依赖数据的特征:
- 字母/数字类rowkey
可按范围划分。比如A-Z的26个字母开头的rowkey,可按[A, D]…[U,Z]这样的方式水平拆分Region。 - 自定义算法
HBase中的RegionSplitter工具可根据特点,传入算法、Region数、列族等,自定义拆分:- HexStringSplit
假设RowKey都是十六进制字符串来进行拆分 - UniformSplit
假设RowKey都是随机字节数组来进行拆分 - DecimalStringSplit
假设RowKey都是00000000到99999999范围内的十进制字符串 - 可使用SplitAlgorithm开发自定义拆分算法
- HexStringSplit
更多内容可以阅读这篇文章Apache HBase Region Splitting and Merging
3.6.5 Region状态
HBase的HMaster负责为每个Region维护了状态并存在META表,持久化到Zookeeper。
- OFFLINE: Region离线且未打开
- OPENING: Region正在打开过程中
- OPEN: Region已经打开,且RegionServer已经通知了Master
- FAILED_OPEN: RegionServer打开该Region失败
- CLOSING: Region正在被关闭过程中
- CLOSED: Region已经关闭,且RegionServer已经通知了Master
- FAILED_CLOSE: RegionServer关闭该Region失败
- SPLITTING: RegionServer通知了Master该Region正在切分(Split)
- SPLIT: RegionServer通知了Master该Region已经结束切分(Split)
- SPLITTING_NEW: 该Region是由正在进行的切分(Split)创建
- MERGING: RegionServer通知了Master该Region和另一个Region正在被合并
- MERGED: RegionServer通知了Master该Region已经被合并完成
- MERGING_NEW: 该Region正在被两个Region的合并所创建
上图颜色含义如下:
- 棕色:离线状态。是一个特殊的瞬间状态。
- 绿色:在线状态,此时Region可以正常提供服务接受请求
- 浅蓝色:瞬态
- 红色:失败状态,需要引起运维人员会系统注意,手动干预
- 黄色:Region切分/合并后的引起的终止状态
- 灰色:由切分/合并而来的Region的初始状态
具体状态转移说明如下:
3.6.6 负载均衡
由Master的LoadBalancer线程周期性的在各个RegionServer间移动region维护负载均衡。
3.6.7 Failover
请点击这里
3.6.8 Region不能过多的原因
3.6.9 Region-RegionServer Locality(本地化)
RegionServer本地性是通过HDFS Block副本实现。
当某个RS故障后,其他的RS也许会因为Region恢复而被Master分配非本地的Region的StoreFiles文件(其实就是之前挂掉的RS节点上的StoreFiles的HDFS副本)。但随着新数据写入该Region,或是该表被合并、StoreFiles重写等之后,这些数据又变得相对来说本地化了。
3.7 Region元数据-META表
3.7.1 概述
Region元数据存于.META.表(没错,也是一张HBase表,只是HBase shell的list命令看不到)中(最新版称为hbase:meta表)。该表保存了所有region的详细信息
.META.表的位置信息存在ZK中。
3.7.2 表结构
- Key
([table],[region start key],[region id]) - Values
info:regioninfo
info:server (包含该Region的RegionServer之server:port)
info:serverstartcode (包含该Region的RegionServer启动时间)
3.8 MemStore
3.8.1 概述
一个Store有一个MemStore,保存数据修改。当flush后,当前MemStore就被清理了。
注意,MemStorez中的数据按 RowKey 字典升序排序。
3.8.2 Flush
注意:Memstore Flush最小单位是Region,而不是单个MemStore。
当Flush发生时,当前MemStore实例会被移动到一个snapshot中,然后被清理掉。在此期间,新来的写操作会被新的MemStore和刚才提到的备份snapshot接受,直到flush工作成功完成后,snapshot才会被废弃。
MemStore Flush时机如下:
- Region级别
Region内的其中一个MemStore大小达到阈值(hbase.hregion.memstore.flush.size),该Region所有MemStore一起发生Flush,输入磁盘。 - RegionServer级别
当RS内的全部MemStore使用总量达到了阈值(hbase.regionserver.global.memstore.upperLimit),那么会一起按Region的MemStore用量降序排列flush,直到降低到阈值(hbase.regionserver.global.memstore.lowerLimit.)以下。 - HLog-WAL文件
当region server的WAL的log数量达到hbase.regionserver.max.logs,该server上多个region的MemStore会被刷写到磁盘(按照时间顺序),以降低WAL的大小。
3.8.3 Snapshot
MemStore Flush时,为了避免对读请求的影响,MemStore会对当前内存数据kvset创建snapshot,并清空kvset的内容。
读请求在查询KeyValue的时候也会同时查询snapshot,这样就不会受到太大影响。但是要注意,写请求是把数据写入到kvset里面,因此必须加锁避免线程访问发生冲突。由于可能有多个写请求同时存在,因此写请求获取的是updatesLock的readLock,而snapshot同一时间只有一个,因此获取的是updatesLock的writeLock。
3.8.4 写入
数据修改操作先写入MemStore,在该内存为有序状态。
3.8.5 读取
先查MemStore,查不到再去查StoreFile。
Scan具体读取步骤如下:
3.9 Storefile
3.9.1 概述
一个Store有>=0个SotreFiles(HFiles)。
StoreFiles由块(Block)组成。块大小( BlockSize)是基于每个列族配置的。压缩是以块为单位。
StoreFile对HFile进行了轻度封装。HFile是在HDFS中存储数据的文件格式。它包含一个多层索引,允许HBase在不必读取整个文件的情况下查找数据。这些索引的大小是块大小(默认为64KB),key大小和存储数据量的一个重要因素。
注意,HFile中的数据按 RowKey 字典升序排序。
3.9.2 HFile格式
HFile格式基于BigTable论文中的SSTable。
3.9.3 HFile块
HFile块不同于HDFS块,HFile块的默认大小是64KB,而Hadoop块的默认大小为64MB。顺序读多的情况下使用较大HFile块,随机访问多的时候可使用较小HFile块。
HBase同一RS上的所有Region共用一份读缓存。当读取磁盘上某一条数据时,HBase会将整个HFile block读到cache中。此后,当client请求临近的数据时可直接访问缓存,响应更快,也就是说,HBase鼓励将那些相似的,会被一起查找的数据存放在一起。
注意,当我们在做全表scan时,为了不刷走读缓存中的热数据,记得关闭读缓存的功能(因为HFile放入LRUCache后,不用的将被清理)
更多关于HFile BlockCache资料请查看HBase BlockCache 101
3.10 BlockCache
HBase提供两种不同的BlockCache实现,来缓存从HDFS读取的数据:
- 堆内的LRUBlockCache。
默认占HBase用的Java堆大小的40%
数据、META表(永远开启缓存)、HFile、key、布隆过滤器等大量使用LRUBlockCache。
默认情况下,对所有用户表都启用了块缓存,也就是说任何读操作都将加载LRU缓存。 - 通常在堆外的BucketCache
一般可用LRUBlockCache保存HFile 索引文件和BloomFilter,其他数据放在BucketCache
3.11 KeyValue
KeyValue是HBase的最核心内容。他主要由keylength, valuelength, key, value 四部分组成。
3.11.1 key
key由包括了rowkey、列族、列、时间戳、keytype(put, detele等操作)等信息
3.11.2 例子
一个put操作如下:
Put #1: rowkey=row1, cf:attr1=value1
他的key组成如下:
rowlength -----------→ 4
row -----------------→ row1
columnfamilylength --→ 2
columnfamily --------→ cf
columnqualifier -----→ attr1
timestamp -----------→ server time of Put
keytype -------------→ Put
3.11.3 小结
所以我们在设计列族、列、rowkey的时候,要尽量简短,不然会大大增加KeyValue大小。
0x04 HBase数据模型
4.1 逻辑模型
4.1.1 逻辑视图与稀疏性
上表是HBase逻辑视图,其中空白的区域并不会占用空间。这也就是为什么成为HBase是稀疏表的原因。
4.1.2 HBase数据模型基本概念
-
Namespace
类似RDBMS的库。建表时指定,否则会被分配defaultnamespace。 -
Table
类似RDBMS的表 -
RowKey
- 是Byte Array(字节数组),是表中每条记录的“主键”,即唯一标识某行的元素,方便快速查找,RowKey的设计非常重要。
- MemStore和HFile中按RowKey的字典升序排列。
- 且RowKey符合最左匹配原则。如设计RowKey为
uid+phone+name,那么可以匹配一下内容:
但是无法用RowKey支持以下搜索:
-
Column Family
即列族,拥有一个名称(string),包含一个或者多个列,物理上存在一起。比如,列courses:history 和 courses:math都是 列族 courses的成员.冒号(:)是列族的分隔符。建表时就必须要确定有几个列族。每个 -
Column
即列,属于某个columnfamily,familyName:columnName。列可动态添加 -
Version Number
即版本号,类型为Long,默认值是系统时间戳,可由用户自定义。相同行、列的cell按版本号倒序排列。多个相同version的写,只会采用最后一个。 -
Value(Cell)
{row, column, version} 组一个cell即单元格,其内容是byte数组。 -
Region
表水平拆分为多个Region,是HBase集群分布数据的最小单位。
4.2 物理模型
-
HBase表的同一个region放在一个目录里
-
一个region下的不同列族放在不同目录
4.3 有序性
每行的数据按rowkey->列族->列名->timestamp(版本号)逆序排列,也就是说最新版本数据在最前面。
4.4 ACID
请查看事务章节。
0x05 HBase 容错
5.1 RegionServer
- Region Failover:发现失效的Region,就到正常的RegionServer上恢复该Region
- RegionSever Failover:由HMaster对其上的region进行迁移
具体来说
5.2 Master Failover
Zookeeper选举机制选出一个新的Leader Master。但要注意在没有Master存活时:
- 数据读写仍照常进行,因为读写操作是通过
.META.表进行。 - 无master过程中,region切分、负载均衡等无法进行(因为master负责)
5.3 Zookeeper容错
Zookeeper是一个可靠地分布式服务
5.4 HDFS
HDFS是一个可靠地分布式服务
0x06 HBase数据流程
6.1 Region定位流程
| 块索引 | 布隆过滤器 | |
|---|---|---|
| 功能 | 快速定位记录在HFile中可能的块 | 快速判断HFile块中是否包含目标记录 |
- 读写请求一般会先访问MemStore
6.2 写流程
6.3 读流程
6.4 删除
6.5 TTL过期
过期数据不会创建墓碑,只是在Major Compact的时候被清理掉,不再写入合并后的数据
0x07 HBase数据结构
7.1 概述
7.2 LSM树简介
7.3 LSM树在HBase的应用
HFile格式基于Bigtable论文中SSTable。
7.3.1 写入
7.3.2 读取
7.3.3 读取优化
- 布隆过滤器。
可快速得到是否数据不在该集合,但不能100%肯定数据在这个集合,即所谓假阳性。 - 合并
合并后,就不用再遍历繁多的小树了,直接找大树
7.3.4 删除
添加<key, del>标记,称为墓碑。
在Major Compact中被删除的数据和此墓碑标记才会被真正删除。
7.3.5 合并
HBase Compact过程,就是RegionServer定期将多个小StoreFile合并为大StoreFile,也就是LSM小树合并为大树。这个操作的目的是增加读的性能,否则搜索时要读取多个文件。
HBase中合并有两种:
- Minor Compact
仅合并少量的小HFile - Major Compact
合并一个Region上的所有HFile,此时会删除那些无效的数据(更新时,老的数据就无效了,最新的那个<key, value>就被保留;被删除的数据,将墓碑<key,del>和旧的<key,value>都删掉)。很多小树会合并为一棵大树,大大提升度性能。
0x08 HBase事务
8.1 ACID
HBase和RDBMS类似,也提供了事务的概念,只不过HBase的事务是行级事务,可以保证行级数据的ACID性质。
8.1.1 A-原子性
- 针对同一行(就算是跨列)的所有修改操作具有原子性,所有put操作要么全成功要么全失败。
- HBase有类似CAS的操作:
boolean checkAndPut(byte[] row, byte[] family, byte[] qualifier,
byte[] value, Put put) throws IOException;
8.1.2 C/I-一致性/隔离性
- Get一致性
查询得到的所有行都是某个时间点的完整行。 - Scan一致性
以上的时间不是cell中的时间戳,而是事务提交时间。
当构建StoreFileScanner后,会自动关联一个MultiVersionConcurrencyControl Read Point,他是当前的MemStore版本,scan操作只能读到这个点之前的数据。ReadPoint之后的更改会被过滤掉,不能被搜索到。
这类事务隔离保证在RDBMS中称为读提交(RC)
8.1.3 V-可见性
当没有使用writeBuffer时,客户端提交修改请求并收到成功响应时,该修改立即对其他客户端可见。原因是行级事务。
8.1.4 D-持久性
所有可见数据也是持久化的数据。也就是说,每次读请求不会返回没有持久化到磁盘的数据(注意,这里指hflush而不是fsync到磁盘)。
而那些返回成功的操作,就已经是持久化了;返回失败的,当然就不会持久化。
8.1.5 可调性
HBase默认要求上述性质,但可根据实际场景调整,比如修改持久性为定时刷盘。
关于ACID更多内容,请参阅HBase-acid-semantics和ACID in HBase
8.2 事务
8.2.1 简介
HBase支持单行ACID性质,但在HBASE-3584新增了对多操作事务支持,还在HBASE-5229新增了对跨行事务的支持。HBase所有事务都是串行提交的。
HBase采用了一种MVCCMVCC思想,每个RegionServer维护一个严格单调递增的事务号:
- 当写入事务(一组
PUT或DELETE命令)开始时,它将检索下一个最高的事务编号。这称为WriteNumber。每个新建的KeyValue都会包括这个WriteNumber,又称为Memstore timestamp,注意他和KeyValue的timestamp属性不同。 - 当读取事务(一次
SCAN或GET)启动时,它将检索上次提交的事务的事务编号。这称为ReadPoint。
因为HBase事务不能跨Region,所以这些MVCC信息就分别保存在RegionServer内存中。
写事务流程:
读事务流程:
8.2.2 锁机制
为了实现事务特性,HBase采用了各种并发控制策略,包括各种锁机制、MVCC机制等,但没有实现混合的读写事务。
HBase提供了两种同步锁机制:
- 一种是基于CountDownLatch实现的互斥锁,保证了行级数据的原子性;
- 另一种是基于ReentrantReadWriteLock实现的读写锁,该锁可以给临界资源加上read-lock或者write-lock。其中read-lock允许并发的读取操作,而write-lock是完全的互斥操作。HBase利用读写锁实现了Store级别、Region级别的数据一致性
8.3 隔离性+锁实现
8.3.1 写写并发
8.3.3 批量写写并发
写入前统一获取所有行的行锁,获取到才进行操作。执行写操作完成后统一释放所有行锁,避免死锁。
8.3.3 读写并发
读写并发采用MVCC思想,每个RegionServer维护一个严格单调递增的事务号。
- 当写入事务(一组
PUT或DELETE命令)开始时,它将检索下一个最高的事务编号。这称为WriteNumber。 - 当读取事务(一次
SCAN或GET)启动时,它将检索上次提交的事务的事务编号。这称为ReadPoint。
8.4 scan和合并
当scan时遇到合并正在进行,HBase处理方案如下:
跟踪scanner使用的最早的ReadPoint,不返回Memstore timestamp大于该ReadPoint的那些KeyValue。
该Memstore timestamp删除的时机就是当它比最早的那个scanner还早时。
8.4 第三方实现
通过集成Tephra,Phoenix可以支持ACID特性。Tephra也是Apache的一个项目,是事务管理器,它在像HBase这样的分布式数据存储上提供全局一致事务。HBase本身在行层次和区层次上支持强一致性,Tephra额外提供交叉区、交叉表的一致性来支持可扩展性、一致性。
0x09 HBase协处理器
9.1 简介
协处理器可让我们在RegionServer服务端运行用户代码,实现类似RDBMS的触发器、存储过程等功能。
9.2 风险
- 运行在协处理器上的代码能直接访问数据,所以存在数据损坏、中间人攻击或其他恶意数据访问的风险。
- 当前没有资源隔离机制,所以一个初衷良好的协处理器可能实际上会影响集群性能和稳定性。
9.3 使用场景
在一般情况下,我们使用Get或Scan命令,加上Filter,从HBase获取数据然后进行计算。这样的场景在小数据规模(如几千行)和若干列时性能表现尚好。然而当行数扩大到十亿行、百万列时,网络传输如此庞大的数据会使得网络很快成为瓶颈,而客户端也必须拥有强大的性能、足够的内存来处理计算这些海量数据。
在上述海量数据场景,协处理器可能发挥巨大作用:用户可将计算逻辑代码放到协处理器中在RegionServer上运行,甚至可以和目标数据在相同节点。计算完成后,再返回结果给客户端。
9.4 类比
9.4.1 触发器和存储过程
- Observer协处理器
它类似RDBMS的触发器,可以在指定事件(如Get或Put)发生前后执行用户代码。 - Endpoint协处理器
它类似RDBMS的存储过程,也就是说可以在RegionServer上执行数据计算任务。
9.4.2 MR任务
MR任务思想就是将计算过程放到数据节点,提高效率。思想和协处理器相同。
9.4.3 AOP
将协处理看做通过拦截请求然后运行某些自定义代码来应用advice,然后将请求传递到其最终目标(甚至更改目标)。
9.4.4 过滤器
过滤器也是将计算逻辑移到RS上,但设计目标不太相同。
9.5 协处理器的实现
9.5.1 概览
9.6 协处理器分类
9.6.1 Observer
9.6.1.1 简介
-
类似RDBMS的触发器,可以在指定事件(如Get或Put)发生前(preGet)后(postGet)执行用户代码。
-
具体执行调用过程由HBase管理,对用户透明。
-
一般来说Observer协处理器又分为以下几种:
- RegionObserver
可观察Region级别的如Get等各类操作事件 - RegionServerObserver
可观察RegionServer级别的如开启、停止、合并、提交、回滚等事件 - MasterObserver
可观察Master的如表创建/删除、schema修改等事件 - WalObserver
可观察WAL相关事件
- RegionObserver
9.6.1.2 应用
- 权限验证
可以在preGet或prePost中执行权限验证。 - 外键
利用prePut,在插入某个表前插入一条记录到另一张表 - 二级索引
详见HBase Secondary Indexing
9.6.2 Endpoint
9.6.2.1 简介
- 可在数据位置执行计算。
- 具体执行调用过程必须继承通过客户端实现
CoprocessorService接口的方法,显示进行代码调用实现。 - Endpoint通过
protobuf实现
9.6.2.2 应用
- 在一个拥有数百个Region的表上求均值或求和
9.7 加载方法
9.7.1 静态加载(系统级全局协处理器)
- 加载
- 卸载
9.7.2 动态加载(表级协处理器)
该种方式加载的协处理器只能对加载了的表有效。加载协处理器时,表必须离线。
动态加载,需要先将包含协处理器和所有依赖打包成jar,比如coprocessor.jar,放在了HDFS的某个位置(也可放在每个RegionServer的本地磁盘,但是显然很麻烦)。
然后加载方式有以下三种:
-
HBase Shell
-
Java API
TableName tableName = TableName.valueOf("users");
Path path = new Path("hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar");
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();
admin.disableTable(tableName);
HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);
HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet");
columnFamily1.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet");
columnFamily2.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily2);
hTableDescriptor.addCoprocessor(RegionObserverExample.class.getCanonicalName(), path,
Coprocessor.PRIORITY_USER, null);
admin.modifyTable(tableName, hTableDescriptor);
admin.enableTable(tableName);
-
动态卸载
-
HBase Shell
-
Java API
TableName tableName = TableName.valueOf("users"); String path = "hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar"; Configuration conf = HBaseConfiguration.create(); Connection connection = ConnectionFactory.createConnection(conf); Admin admin = connection.getAdmin(); admin.disableTable(tableName); HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName); // columnFamily2.removeCoprocessor() HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet"); columnFamily1.setMaxVersions(3); hTableDescriptor.addFamily(columnFamily1); HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet"); columnFamily2.setMaxVersions(3); hTableDescriptor.addFamily(columnFamily2); admin.modifyTable(tableName, hTableDescriptor); admin.enableTable(tableName); -
9.8 代码例子
9.8.1 背景
官方文档例子。
一个users表,拥有两个列族personalDet(用户详情) 和 salaryDet(薪水详情)
9.8.2 Observer例子
该协处理器能阻止在对users表的Get或Scan操作中返回用户admin的详情信息:
public class RegionObserverExample implements RegionObserver {
private static final byte[] ADMIN = Bytes.toBytes("admin");
private static final byte[] COLUMN_FAMILY = Bytes.toBytes("details");
private static final byte[] COLUMN = Bytes.toBytes("Admin_det");
private static final byte[] VALUE = Bytes.toBytes("You can't see Admin details");
@Override
public void preGetOp(final ObserverContext<RegionCoprocessorEnvironment> e, final Get get, final List<Cell> results)
throws IOException {
if (Bytes.equals(get.getRow(),ADMIN)) {
Cell c = CellUtil.createCell(get.getRow(),COLUMN_FAMILY, COLUMN,
System.currentTimeMillis(), (byte)4, VALUE);
results.add(c);
e.bypass();
}
}
@Override
public RegionScanner preScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> e, final Scan scan,
final RegionScanner s) throws IOException {
// 使用filter从scan中排除ADMIN结果
// 这样的缺点是会覆盖原有的其他filter
Filter filter = new RowFilter(CompareOp.NOT_EQUAL, new BinaryComparator(ADMIN));
scan.setFilter(filter);
return s;
}
@Override
public boolean postScannerNext(final ObserverContext<RegionCoprocessorEnvironment> e, final InternalScanner s,
final List<Result> results, final int limit, final boolean hasMore) throws IOException {
Result result = null;
Iterator<Result> iterator = results.iterator();
while (iterator.hasNext()) {
result = iterator.next();
if (Bytes.equals(result.getRow(), ADMIN)) {
// 也可以通过postScanner方式从结果中移除ADMIN
iterator.remove();
break;
}
}
return hasMore;
}
}
- 将谢谢处理器和依赖一起打包为
.jar文件 - 上传该jar文件到HDFS
- 用我们之前提到过的一种方式来加载该协处理器
- 写一个
Get、Scan测试程序来验证
9.8.3 Endpoint例子
该例子实现一个Endpoint协处理器来计算所有职员的薪水之和:
option java_package = "org.myname.hbase.coprocessor.autogenerated";
option java_outer_classname = "Sum";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED;
message SumRequest {
required string family = 1;
required string column = 2;
}
message SumResponse {
required int64 sum = 1 [default = 0];
}
service SumService {
rpc getSum(SumRequest)
returns (SumResponse);
}
- 对以上
.proto文件执行protoc命令来生成java代码Sum.java到src目录:
$ mkdir src
$ protoc --java_out=src ./sum.proto
- Endpoint协处理器代码编写
继承刚才生成的类,并实现Coprocessor和CoprocessorService接口的方法:继承刚才生成的类,并实现Coprocessor和CoprocessorService接口的方法:
public class SumEndPoint extends Sum.SumService implements Coprocessor, CoprocessorService {
private RegionCoprocessorEnvironment env;
@Override
public Service getService() {
return this;
}
@Override
public void start(CoprocessorEnvironment env) throws IOException {
if (env instanceof RegionCoprocessorEnvironment) {
this.env = (RegionCoprocessorEnvironment)env;
} else {
throw new CoprocessorException("Must be loaded on a table region!");
}
}
@Override
public void stop(CoprocessorEnvironment env) throws IOException {
// do nothing
}
@Override
public void getSum(RpcController controller, Sum.SumRequest request, RpcCallback<Sum.SumResponse> done) {
Scan scan = new Scan();
// 列族
scan.addFamily(Bytes.toBytes(request.getFamily()));
// 列
scan.addColumn(Bytes.toBytes(request.getFamily()), Bytes.toBytes(request.getColumn()));
Sum.SumResponse response = null;
InternalScanner scanner = null;
try {
scanner = env.getRegion().getScanner(scan);
List<Cell> results = new ArrayList<>();
boolean hasMore = false;
long sum = 0L;
do {
hasMore = scanner.next(results);
// 按cell(rowkey/列/timestamp)遍历结果
for (Cell cell : results) {
// 累加结果
sum = sum + Bytes.toLong(CellUtil.cloneValue(cell));
}
results.clear();
} while (hasMore);
// 构建带结果的相应
response = Sum.SumResponse.newBuilder().setSum(sum).build();
} catch (IOException ioe) {
ResponseConverter.setControllerException(controller, ioe);
} finally {
if (scanner != null) {
try {
// 用完记得关闭scanner
scanner.close();
} catch (IOException ignored) {}
}
}
// 返回结果
done.run(response);
}
}
- 客户端调用代码:
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
TableName tableName = TableName.valueOf("users");
Table table = connection.getTable(tableName);
// 构建 对salaryDet列族 gross列 求和 的rpc请求
final Sum.SumRequest request = Sum.SumRequest.newBuilder().setFamily("salaryDet").setColumn("gross").build();
try {
// 调用Entpoint协处理器方法,得到结果
Map<byte[], Long> results = table.coprocessorService(
Sum.SumService.class,
null, /* start key */
null, /* end key */
// 回调方法
new Batch.Call<Sum.SumService, Long>() {
@Override
public Long call(Sum.SumService aggregate) throws IOException {
BlockingRpcCallback<Sum.SumResponse> rpcCallback = new BlockingRpcCallback<>();
// 得到结果
aggregate.getSum(null, request, rpcCallback);
Sum.SumResponse response = rpcCallback.get();
return response.hasSum() ? response.getSum() : 0L;
}
}
);
// 遍历打印结果
for (Long sum : results.values()) {
System.out.println("Sum = " + sum);
}
} catch (ServiceException e) {
e.printStackTrace();
} catch (Throwable e) {
e.printStackTrace();
}
- 加载
Endpoint协处理器 - 执行上述客户端代码,进行测试
0x10 HBase线程模型
0x11 HBase网络模型
0x12 HBase实践
12.1 API
12.1.1 scan
按指定条件获取范围数据
-
scan时会综合StoreFile和MemStore scanner扫描结果。当构建scanner时,会关联一个
MultiVersionConcurrencyControl Read Point,只能读到这个点之前的数据。ReadPoint之后的更改会被过滤掉,不能被搜索到。 -
注意点
12.1.2 put
放入一行数据.
注意点:
- 该过程会先把put放入本地put缓存writeBuffer,达到阈值后再提交到服务器。
- 批量导入
批量导入是最有效率的HBase数据导入方式。 - 海量数据写入前预拆分Region,避免后序自动Split过程阻塞数据导入
- 当对安全性要求没那么高时可以使用WAL异步刷新写入方式。甚至在某些场景下可以禁用WAL
12.1.3 get
传入rowkey得到最新version数据或指定maxversion得到指定版本数据
- 注意点
1.Bloom Filter(以下简称BF)- 有助于减小读取时间。
- HBase中实现了一个轻量级的内存BF结构,可以使得Get操作时从磁盘只读取可能包含目标Row的StoreFile。
- BF本身存储在每个HFile的元数据中,永远不需要更新。当因为Region部署到RegionServer而打开HFile时,BF将加载到内存中。
- 默认开启行级BF,可根据数据特征修改如 行+列级
- 衡量BF开启后影响是否为证明,可以看RS的
blockCacheHitRatio(BlockCache命中率)指标是否增大,增大代表正面影响。 - 需要在删除时重建,因此不适合具有大量删除的场景。
- BF分为行模式和行-列模式,在大量列级PUT时就用行列模式,其他时候用行模式即可。
- Hedged Reads(对冲读)
Hadoop 2.4.0 引入的一项HDFS特性。
- 普通的每个HDFS读请求都对应一个线程
- 对冲读开启后,如果读取未返回,则客户端会针对相同数据的不同Block副本生成第二个读请求。
- 使用先返回的任何一个,并丢弃另一个读请求。
- 可通过
hedgedReadOps和hedgeReadOpsWin指标评估开启对冲读的效果 - 在追求最大化吞吐量时,开启对冲读可能导致性能下降
12.1.4 delete
删除指定数据
12.2 二级索引
12.2.1 基本思路
如上图,单独建立一个HBase表,存F:C1列到RowKey的索引。
那么,当要查找满足F:C1=C11的F:C2列数据,就可以去索引表找到F:C1=C11对应的RowKey,再回原表查找该行的F:C2数据。
12.2.2 协处理器的实现方案
用RegionObserver的prePut在每次写入主表数据时,写一条到索引表,即可建立二级索引。
12.3 join
- HBase本身不支持join。
- 可以自己写程序比如MR实现。
- Phoenix里面有join函数,但是性能很差,稍不注意会把集群打挂。最好不要用hbase系来做join,这种还是用hive来搞比较好。
12.4 Schema设计
更多例子可以看http://hbase.apache.org/book.html#schema.casestudies
12.4.1 列族
- 列族数量越少越好
HBase程序目前不能很好的支持超过2-3个列族。而且当前版本HBase的flush和合并操作都是以Region为最小单位,也就是说列族之间会互相影响(比如大负载列族需要flush时,小负载列族必须进行不必要的flush操作导致IO)。 - 列族多的坏处
当一个表存在多个列族,且基数差距很大时,如A_CF100万行,B_CF10亿行。此时因为HBase按Region水平拆分,会导致A因列族B的数据量庞大而随之被拆分到很多的region,导致访问A列族就需要大量scan操作,效率变低。 - 总的来说最好是设计一个列族就够了,因为一般查询请求也只访问某个列族。
- 列族名不宜过长
列族名尽量简短甚至不需自描述,因为每个KeyValue都会包含列族名,总空间会因为列族名更长而更大,是全局影响。 - BlockSize
每个列族可配BlockSize(默认64KB)。当cell较大时需加大此配置。且该值和StoreFile的索引文件大小成反比。 - 内存列族
可在内存中定义列族,数据还是会被持久化道磁盘,但这类列族在BlockCachez中拥有最高优先级。
12.4.2 Region
- 一个Region的大小一般不超过50GB。
- 一个有1或2个列族的表最佳Region总数为50-100个
12.4.3 RowKey设计
- 避免设计连续RowKey导致数据热点,导致过载而请求响应过慢或无法响应,甚至影响热点Region所在RS的其他Region。常用措施如下:
- Salting-加盐
按期望放置的RS数量设计若干随机前缀,在每个RowKey前随机添加,以将新数据均匀分散到集群中,负载均衡。
Salting可以增加写的吞吐量,但会降低读效率,因为有随机前缀:foo0001 foo0002 foo0003 foo0004a-foo0003 b-foo0001 c-foo0003 c-foo0004 d-foo0002 - Hash算法
用固定的hash算法,对每个key求前缀。查询时,也用这个算法。 - 逆置Key
将固定长度或范围的前N个字符逆序。但会牺牲排序性 - 业务必须用时间序列或连续递增数字时,可以在开头加如type这类的前缀使得分布均匀。
- Salting-加盐
- 定位cell时,需要表名、RowKey、列族、列名和时间戳。而且StoreFile(HFile)有索引,cell过大会导致索引过大(使用时会放入内存)。所以需要设计schema时:
- 列族名尽量简短,甚至只用一个字符;
- 列名尽量简短:
- RowKey保证唯一性,长度可读、简短,尽量使用数字。
- 版本号采用倒序的时间戳,这样可以快速返回最新版本数据
- 同一行不同列族可以拥有同样的RowKey
- RowKey具有不可变性
12.4.4 Versions
指定一个RowKey数据的最大保存的版本个数,默认为3。越少越好,减小开销。
如果版本过多可能导致compact时OOM。
如果非要使用很多版本,那最好考虑使用不同的行进行数据分离。
12.4.5 压缩
详见http://hbase.apache.org/book.html#compression
注意,压缩技术虽然能减小在数据存到磁盘的大小,但在内存中或网络传输时会膨胀。也就是说,不要妄图通过压缩来掩盖过大的RowKey/列族名/列名的负面影响。
0x13 HBase对比其他技术
13.1 对比HDFS/MR
13.2 对比Cassandra
13.3 对比Kudu
0x14 HBase FAQ
0xFF 参考文档
Apache HBase
数据结构-常用树总结
HBase写请求分析
HBase Scan 中 setCaching setMaxResultSize setBatch 解惑
HBase二级索引实现方案
传统的行存储和(HBase)列存储的区别
大数据存取的选择:行存储还是列存储?
处理海量数据:列式存储综述(存储篇)
Hbase行级事务模型
HBase使用总结