欢迎投稿

今日深度:

HBase学习,

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列为MillerMaterial列为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开发自定义拆分算法

更多内容可以阅读这篇文章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 shelllist命令看不到)中(最新版称为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里面,因此必须加锁避免线程访问发生冲突。由于可能有多个写请求同时存在,因此写请求获取的是updatesLockreadLock,而snapshot同一时间只有一个,因此获取的是updatesLockwriteLock

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的库。建表时指定,否则会被分配default namespace。

  • 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块中是否包含目标记录
  1. 读写请求一般会先访问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维护一个严格单调递增的事务号:

  • 当写入事务(一组PUTDELETE命令)开始时,它将检索下一个最高的事务编号。这称为WriteNumber。每个新建的KeyValue都会包括这个WriteNumber,又称为Memstore timestamp注意他和KeyValue的timestamp属性不同
  • 当读取事务(一次SCANGET)启动时,它将检索上次提交的事务的事务编号。这称为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维护一个严格单调递增的事务号。

  • 当写入事务(一组PUTDELETE命令)开始时,它将检索下一个最高的事务编号。这称为WriteNumber
  • 当读取事务(一次SCANGET)启动时,它将检索上次提交的事务的事务编号。这称为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 使用场景

在一般情况下,我们使用GetScan命令,加上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相关事件
9.6.1.2 应用
  • 权限验证
    可以在preGetprePost中执行权限验证。
  • 外键
    利用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表的GetScan操作中返回用户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;
	}
}
  1. 将谢谢处理器和依赖一起打包为.jar文件
  2. 上传该jar文件到HDFS
  3. 用我们之前提到过的一种方式来加载该协处理器
  4. 写一个GetScan测试程序来验证

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);
}
  1. 对以上.proto文件执行protoc命令来生成java代码Sum.javasrc目录:
$ mkdir src
$ protoc --java_out=src ./sum.proto
  1. Endpoint协处理器代码编写
    继承刚才生成的类,并实现CoprocessorCoprocessorService接口的方法:继承刚才生成的类,并实现CoprocessorCoprocessorService接口的方法:
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);
    }
}
  1. 客户端调用代码:
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();
}
  1. 加载Endpoint协处理器
  2. 执行上述客户端代码,进行测试

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时就用行列模式,其他时候用行模式即可。
  1. Hedged Reads(对冲读)
    Hadoop 2.4.0 引入的一项HDFS特性。
  • 普通的每个HDFS读请求都对应一个线程
  • 对冲读开启后,如果读取未返回,则客户端会针对相同数据的不同Block副本生成第二个读请求。
  • 使用先返回的任何一个,并丢弃另一个读请求。
  • 可通过hedgedReadOpshedgeReadOpsWin 指标评估开启对冲读的效果
  • 在追求最大化吞吐量时,开启对冲读可能导致性能下降

12.1.4 delete

删除指定数据

12.2 二级索引

12.2.1 基本思路


如上图,单独建立一个HBase表,存F:C1列到RowKey的索引。

那么,当要查找满足F:C1=C11F:C2列数据,就可以去索引表找到F:C1=C11对应的RowKey,再回原表查找该行的F:C2数据。

12.2.2 协处理器的实现方案

RegionObserverprePut在每次写入主表数据时,写一条到索引表,即可建立二级索引。

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
      foo0004
      
      a-foo0003
      b-foo0001
      c-foo0003
      c-foo0004
      d-foo0002
      
    • Hash算法
      用固定的hash算法,对每个key求前缀。查询时,也用这个算法。
    • 逆置Key
      将固定长度或范围的前N个字符逆序。但会牺牲排序性
    • 业务必须用时间序列或连续递增数字时,可以在开头加如type这类的前缀使得分布均匀。
  • 定位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使用总结

www.htsjk.Com true http://www.htsjk.com/hbase/24634.html NewsArticle HBase学习, HBase学习 0x01 摘要 本文是一篇HBase学习综述,将会介绍HBase的特点、对比其他数据存储技术、、架构、存储、数据结构、使用、过滤器等。 未完成 0x02 HBase基础概念 2.1 HBase是...
相关文章
    暂无相关文章
评论暂时关闭