当前位置:   article > 正文

HBase2.x学习笔记

HBase2.x学习笔记

文章目录

一、HBase 简介

1、HBase 定义

官网:https://hbase.apache.org/

1.1 概述

HBase 是 BigTable 的开源 Java 版本。是建立在 HDFS 之上,提供高可靠性、高性能、列存储、可伸缩、实时读写 NoSql 的数据库系统。它介于 NoSql 和 RDBMS 之间,仅能通过主键(row key)和主键的 range 来检索数据,仅支持单行事务(可通过 hive 支持来实现多表 join 等复杂操作)。

主要用来存储结构化和半结构化的松散数据。Hbase 查询数据功能很简单,不支持 join 等复杂操作,不支持复杂的事务(行级的事务) Hbase 中支持的数据类型:byte[] 与 hadoop 一样,Hbase 目标主要依靠横向扩展,通过不断增加廉价的商用服务器,来增加计算和存储能力。HBase 中的表一般有这样的特点:

  • 大:一个表可以有上十亿行,上百万列
  • 面向列:面向列(族)的存储和权限控制,列(族)独立检索。
  • 稀疏:对于为空(null)的列,并不占用存储空间,因此,表可以设计的非常稀疏

1.2 HBase 与 Hadoop 的关系

HDFS

  • 为分布式存储提供文件系统
  • 针对存储大尺寸的文件进行优化,不需要对 HDFS 上的文件进行随机读写
  • 直接使用文件
  • 数据模型不灵活
  • 使用文件系统和处理框架
  • 优化一次写入,多次读取的方式

HBase

  • 提供表状的面向列的数据存储
  • 针对表状数据的随机读写进行优化
  • 使用 key-value 操作数据
  • 提供灵活的数据模型
  • 使用表状存储,支持 MapReduce,依赖 HDFS
  • 优化了多次读,以及多次写

1.3 RDBMS 与 HBase 的对比

关系型数据库

结构

  • 数据库以表的形式存在
  • 支持 FAT、NTFS、EXT、文件系统
  • 使用 Commit log 存储日志
  • 参考系统是坐标系统
  • 使用主键(PK)
  • 支持分区
  • 使用行、列、单元格

功能

  • 支持向上扩展
  • 使用 SQL 查询
  • 面向行,即每一行都是一个连续单元
  • 数据总量依赖于服务器配置
  • 具有 ACID 支持
  • 适合结构化数据
  • 传统关系型数据库一般都是中心化的
  • 支持事务
  • 支持 Join

HBase

结构

  • 数据库以 region 的形式存在
  • 支持 HDFS 文件系统
  • 使用 WAL(Write-Ahead Logs)存储日志
  • 参考系统是 Zookeeper
  • 使用行键(row key)
  • 支持分片
  • 使用行、列、列族和单元格

功能

  • 支持向外扩展
  • 使用 API 和 MapReduce 来访问 HBase 表数据
  • 面向列,即每一列都是一个连续的单元
  • 数据总量不依赖具体某台机器,而取决于机器数量
  • HBase 不支持 ACID(Atomicity、Consistency、Isolation、Durability)
  • 适合结构化数据和非结构化数据
  • 一般都是分布式的
  • HBase 不支持事务
  • 不支持 Join

1.4 HBase 特征简要

  • 海量存储

Hbase 适合存储 PB 级别的海量数据,在 PB 级别的数据以及采用廉价 PC 存储的情况下,能在几十到百毫秒内返回数据。这与 Hbase 的极易扩展性息息相关。正式因为 Hbase 良好的扩展性,才为海量数据的存储提供了便利。

  • 列式存储

这里的列式存储其实说的是列族存储,Hbase 是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定

  • 极易扩展

Hbase 的扩展性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)。 通过横向添加 RegionSever 的机器,进行水平扩展,提升 Hbase 上层的处理能力,提升 Hbsae 服务更多 Region 的能力。 备注:RegionServer 的作用是管理 region、承接业务的访问,这个后面会详细的介绍通过横向添加 Datanode 的机器,进行存储层扩容,提升 Hbase 的数据存储能力和提升后端存储的读写能力

  • 高并发

由于目前大部分使用 Hbase 的架构,都是采用的廉价 PC,因此单个 IO 的延迟其实并不小,一般在几十到上百 ms 之间。这里说的高并发,主要是在并发的情况下,Hbase 的单个 IO 延迟下降并不多。能获得高并发、低延迟的服务

  • 稀疏

稀疏主要是针对 Hbase 列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的

2、HBase 数据模型

HBase 的设计理念依据 Google 的 BigTable 论文,论文中对于数据模型的首句介绍:Bigtable 是一个稀疏的、分布式的、持久的多维排序 map。之后对于映射的解释如下:该映射由行键、列键和时间戳索引;映射中的每个值都是一个未解释的字节数组。最终 HBase 关于数据模型和 BigTable 的对应关系如下:HBase 使用与 Bigtable 非常相似的数据模型。用户将数据行存储在带标签的表中。数据行具有可排序的键和任意数量的列。该表存储稀疏,因此如果用户喜欢,同一表中的行可以具有疯狂变化的列

最终理解 HBase 数据模型的关键在于稀疏、分布式、多维、排序的映射。其中映射 map指代非关系型数据库的 key-Value 结构

2.1 HBase 逻辑结构

HBase 可以用于存储多种结构的数据,以 JSON 为例,存储的数据原貌为

{
  "row_key1": {
    "personal_info": {
      "name": "zhangsan",
      "city": " 北 京 ",
      "phone": "131********"
    },
    "office_info": {
      "tel": "010-1111111",
      "address": "atguigu"
    }
  },
  "row_key11": {
    "personal_info": {
      "city": " 上 海 ",
      "phone": "132********"
    },
    "office_info": {
      "tel": "010-1111111"
    }
  },
  "row_key2":{
  ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

2.2 HBase 物理存储结构

物理存储结构即为数据映射关系,而在概念视图的空单元格,底层实际根本不存储

2.3 HBase的表数据模型

  • Name Space

命名空间,类似于关系型数据库的 database 概念,每个命名空间下有多个表。HBase 两个自带的命名空间,分别是 hbase 和 default,hbase 中存放的是 HBase 内置的表,default表是用户默认使用的命名空间

  • Table

类似于关系型数据库的表概念。不同的是,HBase 定义表时只需要声明列族即可,不需要声明具体的列。因为数据存储时稀疏的,所有往 HBase 写入数据时,字段可以动态、按需指定。因此,和关系型数据库相比,HBase 能够轻松应对字段变更的场景

  • 行键 Row Key

HBase 表中的每行数据都由一个 RowKey 和多个 Column(列)组成,数据是按照 RowKey 的字典顺序存储的,并且查询数据时只能根据 RowKey 进行检索,所以 RowKey 的设计十分重要。与nosql数据库一样,row key是用来检索记录的主键。访问hbase table中的行,只有三种方式:

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

Row Key 行键可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),在hbase内部,row key保存为字节数组。Hbase会对表中的数据按照rowkey排序(字典顺序)。存储时,数据按照Row key的字典序(byte order)排序存储。设计key时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)。

注意: 字典序对int排序的结果是 1,10,100,11,12,13,14,15,16,17,18,19,2,20,21 … 。要保持整形的自然序,行键必须用0作左填充。行的一次读写是原子操作 (不论一次读写多少列)。这个设计决策能够使用户很容易的理解程序在对同一个行进行并发更新操作时的行为。

  • 列族 Column Family

HBase表中的每个列,都归属于某个列族。列族是表的schema的一部分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history , courses:math 都属于 courses 这个列族。

访问控制、磁盘和内存的使用统计都是在列族层面进行的。 列族越多,在取一行数据时所要参与IO、搜寻的文件就越多,所以,如果没有必要,不要设置太多的列族。

  • 列 Column

HBase 中的每个列都由 **Column Family(列族)**和 **Column Qualifier(列限定符)**进行限定,例如 info:name,info:age。建表时,只需指明列族,而列限定符无需预先定义

  • 时间戳 Timestamp

HBase中通过row和columns确定的为一个存贮单元称为cell。每个 cell都保存着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64位整型。时间戳可以由hbase(在数据写入时自动 )赋值,此时时间戳是精确到毫秒的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳。每个 cell中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,hbase提供了两种数据版本回收方式:

  1. 保存数据的最后n个版本
  2. 保存最近一段时间内的版本(设置数据的生命周期TTL)

用户可以针对每个列族进行设置

  • 单元 Cell

{rowkey, column Family:column Qualifier, timestamp} 唯一确定的单元。cell 中的数据全部是字节码形式存贮

  • 版本号 VersionNum

数据的版本号,每条数据可以有多个版本号,默认值为系统时间戳,类型为Long

3、HBase 基本架构

3.1 Master

实现类为 HMaster,负责监控集群中所有的 RegionServer 实例。主要作用如下:

  • 管理元数据表格 hbase:meta,接收用户对表格创建修改删除的命令并执行
  • 监控 region 是否需要进行负载均衡,故障转移和 region 的拆分;通过启动多个后台线程监控实现上述功能:
    • LoadBalancer 负载均衡器:周期性监控 region 分布在 regionServer 上面是否均衡,由参数 hbase.balancer.period 控制周期时间,默认 5 分钟。
    • CatalogJanitor 元数据管理器:定期检查和清理 hbase:meta 中的数据。meta 表内容在进阶中介绍
    • MasterProcWAL master 预写日志处理器:把 master 需要执行的任务记录到预写日志 WAL 中,如果 master 宕机,让 backupMaster读取日志继续干

总结

  • 监控 RegionServer
  • 处理 RegionServer 故障转移
  • 处理元数据的变更
  • 处理 region 的分配或移除
  • 在空闲时间进行数据的负载均衡
  • 通过 Zookeeper 发布自己的位置给客户端

3.2 Region Server

Region Server 实现类为 HRegionServer,主要作用如下:

  • 负责存储 HBase 的实际数据
  • 处理分配给它的 Region
  • 刷新缓存到 HDFS
  • 维护 HLog
  • 执行压缩
  • 负责处理 Region 分片

几个组件如下

  • Write-Ahead logs

HBase 的修改记录,当对 HBase 读写数据的时候,数据不是直接写进磁盘,它会在内存中保留一段时间(时间以及数据量阈值可以设定)。但把数据保存在内存中可能有更高的概率引起数据丢失,为了解决这个问题,数据会先写在一个叫做 Write-Ahead logfile 的文件中,然后再写入内存中。所以在系统出现故障的时候,数据可以通过这个日志文件重建

  • HFile

这是在磁盘上保存原始数据的实际的物理文件,是实际的存储文件

  • Store

HFile 存储在 Store 中,一个 Store 对应 HBase 表中的一个列族。

  • MemStore

顾名思义,就是内存存储,位于内存中,用来保存当前的数据操作,所以当数据保存在 WAL 中之后,RegsionServer 会在内存中存储键值对

  • Region

Hbase 表的分片,HBase 表会根据 RowKey 值被切分成不同的 region 存储在 RegionServer 中,在一个 RegionServer 中可以有多个不同的 region

3.3 Zookeeper

HBase 通过 Zookeeper 来做 master 的高可用、记录 RegionServer 的部署信息、并且存储有 meta 表的位置信息。

HBase 对于数据的读写操作时直接访问 Zookeeper 的,在 2.3 版本推出 Master Registry模式,客户端可以直接访问 master。使用此功能,会加大对 master 的压力,减轻对 Zookeeper的压力。

3.4 HDFS

HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 HBase 提供高容错的支持

二、HBase 快速入门

1、HBase 安装部署

1.1 前置环境与下载

# HBase启动需要有zookeeper和hadoop,这个可以参考之前的文章
# 三台服务器开启zookeeper
bin/zkServer.sh start
# 开启hadoop
sbin/start-dfs.sh
sbin/start-yarn.sh

# 然后下载hbase
# https://archive.apache.org/dist/hbase/
wget https://archive.apache.org/dist/hbase/2.4.11/hbase-2.4.11-bin.tar.gz
tar -zxvf hbase-2.4.11-bin.tar.gz -C /opt/module/
mv /opt/module/hbase-2.4.11 /opt/module/hbase
# 配置环境变量
sudo vim /etc/profile.d/my_env.sh
# 添加
#HBASE_HOME
export HBASE_HOME=/opt/module/hbase
export PATH=$PATH:$HBASE_HOME/bin
# 使用 source 让配置的环境变量生效
source /etc/profile.d/my_env.sh

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

1.2 HBase 的配置文件

cd /opt/module/hbase/conf
# hbase-env.sh 修改内容
vim hbase-env.sh
# hbase-env.sh 修改内容,可以添加到最后,默认是依赖自己,即开箱即用
export HBASE_MANAGES_ZK=false

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

修改hbase-site.xml ,vim hbase-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
    <property>
        <name>hbase.zookeeper.quorum</name>
        <value>hadoop102,hadoop103,hadoop104</value>
        <description>The directory shared by RegionServers.</description>
    </property>
    <!--  <property>-->
    <!--  <name>hbase.zookeeper.property.dataDir</name>-->
    <!--  <value>/export/zookeeper</value>-->
    <!--  <description> 记得修改 ZK 的配置文件 -->
    <!--  ZK 的信息不能保存到临时文件夹-->
    <!--  </description>-->
    <!--  </property>-->
    <property>
        <name>hbase.rootdir</name>
        <value>hdfs://hadoop102:8020/hbase</value>
        <description>The directory shared by RegionServers.</description>
    </property>
    <property>
        <name>hbase.cluster.distributed</name>
        <value>true</value>
    </property>
</configuration>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

然后修改regionservers,类似hadoop的workers

hadoop102
hadoop103
hadoop104
  • 1
  • 2
  • 3

最后解决 HBase 和 Hadoop 的 log4j 兼容性问题,修改 HBase 的 jar 包,使用 Hadoop 的 jar 包

mv /opt/module/hbase/lib/client-facing-thirdparty/slf4j-reload4j-1.7.33.jar /opt/module/hbase/lib/client-facing-thirdparty/slf4j-reload4j-1.7.33.jar.bak
  • 1

1.3 分发与启动

# 分发hbase配置文件
xsync /opt/module/hbase/
# HBase 服务的启动
bin/hbase-daemon.sh start master
bin/hbase-daemon.sh start regionserver
# 群启
bin/start-hbase.sh
bin/stop-hbase.sh
# 启动成功后,可以通过“host:port”的方式来访问 HBase 管理页面
# http://hadoop102:16010

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

1.4 高可用(可选,推荐)

在 HBase 中 HMaster 负责监控 HRegionServer 的生命周期,均衡 RegionServer 的负载, 如果HMaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以HBase 支持对 HMaster 的高可用配置

# 关闭 HBase 集群(如果没有开启则跳过此步)
bin/stop-hbase.sh
# 在 conf 目录下创建 backup-masters 文件
touch conf/backup-masters
# 在 backup-masters 文件中配置高可用 HMaster 节点,注意可以写多个
echo hadoop103 > conf/backup-masters
# 将整个 conf 目录 scp 到其他节点
xsync conf
# 重启 hbase,打开页面测试查看
bin/start-hbase.sh
bin/stop-hbase.sh

# 宕机机器开启,但也只会变成backup,注意,如果要停止的话需要进入master那台机器停止,因为backup没有集群信息
bin/hbase-daemon.sh start master

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2、HBase Shell 基本操作

2.1 基本操作

# 进入 HBase 客户端命令行
bin/hbase shell

# 查看帮助命令
# 能够展示 HBase 中所有能使用的命令,主要使用的命令有 namespace 命令空间相关,DDL 创建修改表格,DML 写入读取数据
hbase:001:0> help
# namespace
# 创建命名空间
hbase:002:0> help 'create_namespace'
# 创建命名空间 bigdata
hbase:003:0> create_namespace 'bigdata'
# 查看所有的命名空间
hbase:004:0> list_namespace

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2.2 DDL

# DDL
# 创建表
# 在 bigdata 命名空间中创建表格 student,两个列族。info 列族数据维护的版本数为 5 个,如果不写默认版本数为 1
hbase:005:0> create 'bigdata:student', {NAME => 'info', VERSIONS => 5}, {NAME => 'msg'}
# 如果创建表格只有一个列族,没有列族属性,可以简写
# 如果不写命名空间,使用默认的命名空间 default
hbase:009:0> create 'student1','info'
# 查看表
# 查看表有两个命令:list 和 describe
hbase:013:0> list
# describe:查看一个表的详情
hbase:014:0> describe 'student1'
# 修改表
# 表名创建时写的所有和列族相关的信息,都可以后续通过 alter 修改,包括增加删除列族
# 增加列族和修改信息都使用覆盖的方法
hbase:015:0> alter 'student1', {NAME => 'f1', VERSIONS => 3}
# 删除信息使用特殊的语法
hbase:015:0> alter 'student1', NAME => 'f1', METHOD => 'delete'
hbase:016:0> alter 'student1', 'delete' => 'f1'
# 删除表
# shell 中删除表格,需要先将表格状态设置为不可用
hbase:017:0> disable 'student1'
hbase:018:0> drop 'student1'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

2.3 DML

# DML
# 写入数据
# 在 HBase 中如果想要写入数据,只能添加结构中最底层的 cell。可以手动写入时间戳指定 cell 的版本,推荐不写默认使用当前的系统时间。
hbase:019:0> put 'bigdata:student','1001','info:name','zhangsan'
hbase:020:0> put 'bigdata:student','1001','info:name','lisi'
hbase:021:0> put 'bigdata:student','1001','info:age','18'
# 如果重复写入相同 rowKey,相同列的数据,会写入多个版本进行覆盖

# 读取数据
# 读取数据的方法有两个:get 和 scan
# get 最大范围是一行数据,也可以进行列的过滤,读取数据的结果为多行 cell
hbase:022:0> get 'bigdata:student','1001'
hbase:023:0> get 'bigdata:student','1001' , {COLUMN => 'info:name'}
# 也可以修改读取 cell 的版本数,默认读取一个。最多能够读取当前列族设置的维护版本数
hbase:024:0>get 'bigdata:student','1001' , {COLUMN => 'info:name', VERSIONS => 6}
# scan 是扫描数据,能够读取多行数据,不建议扫描过多的数据,推荐使用 startRow 和stopRow 来控制读取的数据,默认范围左闭右开
hbase:025:0> scan 'bigdata:student',{STARTROW => '1001',STOPROW => '1002'}
# 实际开发中使用 shell 的机会不多,所有丰富的使用方法到 API 中介绍

# 删除数据
# 删除数据的方法有两个:delete 和 deleteall
# delete 表示删除一个版本的数据,即为 1 个 cell,不填写版本默认删除最新的一个版本
hbase:026:0> delete 'bigdata:student','1001','info:name'
# deleteall 表示删除所有版本的数据,即为当前行当前列的多个 cell。(执行命令会标记数据为要删除,不会直接将数据彻底删除,删除数据只在特定时期清理磁盘时进行)
hbase:027:0> deleteall 'bigdata:student','1001','info:name'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

2.4 其他

# 显示服务器状态
status 'node01'
# whoami显示 HBase 当前用户
whoami
# 显示当前所有的表
list
# 统计指定表的记录数
count 'user'
# 展示表结构信息
describe 'user'
# 检查表是否存在,适用于表量特别多的情况
exists 'user'
# 检查表是否启用或禁用:is_enabled、is_disabled
is_enabled 'user'
# truncate清空表
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

三、HBase API

代码较为底层,一般二次开发jar包引入使用

1、环境准备与连接

1.1 环境准备

新建项目后在 pom.xml 中添加依赖,注意:会报错 javax.el 包不存在,是一个测试用的依赖,不影响使用

<dependencies>
    <dependency>
        <groupId>org.apache.hbase</groupId>
        <artifactId>hbase-server</artifactId>
        <version>2.4.11</version>
        <exclusions>
            <exclusion>
                <groupId>org.glassfish</groupId>
                <artifactId>javax.el</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>javax.el</artifactId>
        <version>3.0.1-b06</version>
    </dependency>
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

1.2 单线程连接

HBase 的客户端连接由 ConnectionFactory 类来创建,用户使用完成之后需要手动关闭连接。同时连接是一个重量级的,推荐一个进程使用一个连接,对 HBase 的命令通过连接中的两个属性 Admin 和 Table 来实现

public class HBaseConnect {
    public static void main(String[] args) throws IOException {
        // 1. 创建配置对象
        Configuration conf = new Configuration();
        // 2. 添加配置参数
        conf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104");
        // 3. 创建 hbase 的连接
        // 默认使用同步连接
        Connection connection =
                ConnectionFactory.createConnection(conf);
        // 可以使用异步连接
        // 主要影响后续的 DML 操作
        CompletableFuture<AsyncConnection> asyncConnection =
                ConnectionFactory.createAsyncConnection(conf);
        // 4. 使用连接
        System.out.println(connection);
        // 5. 关闭连接
        connection.close();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

1.3 多线程创建连接(推荐)

使用类单例模式,确保使用一个连接,可以同时用于多个线程

public class HBaseConnect {
    // 设置静态属性 hbase 连接
    public static Connection connection = null;

    static {
        // 创建 hbase 的连接
        try {
            // 使用配置文件的方法
            connection = ConnectionFactory.createConnection();
        } catch (IOException e) {
            System.out.println("连接获取失败");
            e.printStackTrace();
        }
    }

    /**
     * 连接关闭方法,用于进程关闭时调用
     *
     * @throws IOException
     */
    public static void closeConnection() throws IOException {
        if (connection != null) {
            connection.close();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

在 resources 文件夹中创建配置文件 hbase-site.xml,添加以下内容

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
    <property>
        <name>hbase.zookeeper.quorum</name>
        <value>hadoop102,hadoop103,hadoop104</value>
    </property>
</configuration>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2、DDL

创建 HBaseDDL 类,添加静态方法即可作为工具类

public class HBaseDDL {
    // 添加静态属性 connection 指向单例连接
    public static Connection connection = HBaseConnect.connection;
}
  • 1
  • 2
  • 3
  • 4

2.1 创建命名空间

/**
 * 创建命名空间
 *
 * @param namespace 命名空间名称
 */
public static void createNamespace(String namespace) throws
        IOException {
    // 1. 获取 admin
    // 此处的异常先不要抛出 等待方法写完 再统一进行处理
    // admin 的连接是轻量级的 不是线程安全的 不推荐池化或者缓存这个连接
    Admin admin = connection.getAdmin();
    // 2. 调用方法创建命名空间
    // 代码相对 shell 更加底层 所以 shell 能够实现的功能 代码一定能实现
    // 所以需要填写完整的命名空间描述
    // 2.1 创建命令空间描述建造者 => 设计师
    NamespaceDescriptor.Builder builder =
            NamespaceDescriptor.create(namespace);
    // 2.2 给命令空间添加需求
    builder.addConfiguration("user", "atguigu");
    // 2.3 使用 builder 构造出对应的添加完参数的对象 完成创建
    // 创建命名空间出现的问题 都属于本方法自身的问题 不应该抛出
    try {
        admin.createNamespace(builder.build());
    } catch (IOException e) {
        System.out.println("命令空间已经存在");
        e.printStackTrace();
    }
    // 3. 关闭 admin
    admin.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

2.2 判断表格是否存在

/**
 * 判断表格是否存在
 * @param namespace 命名空间名称
 * @param tableName 表格名称
 * @return ture 表示存在
 */
public static boolean isTableExists(String namespace,String
        tableName) throws IOException {
    // 1. 获取 admin
    Admin admin = connection.getAdmin();
    // 2. 使用方法判断表格是否存在
    boolean b = false;
    try {
        b = admin.tableExists(TableName.valueOf(namespace, tableName));
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3. 关闭 admin
    admin.close();
    // 3. 返回结果
    return b;
    // 后面的代码不能生效
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

2.3 创建表

/**
 * 创建表格
 * @param namespace 命名空间名称
 * @param tableName 表格名称
 * @param columnFamilies 列族名称 可以有多个
 */
public static void createTable(String namespace , String
        tableName , String... columnFamilies) throws IOException {
    // 判断是否有至少一个列族
    if (columnFamilies.length == 0){
        System.out.println("创建表格至少有一个列族");
        return;
    }
    // 判断表格是否存在
    if (isTableExists(namespace,tableName)){
        System.out.println("表格已经存在");
        return;
    }
    // 1.获取 admin
    Admin admin = connection.getAdmin();
    // 2. 调用方法创建表格
    // 2.1 创建表格描述的建造者
    TableDescriptorBuilder tableDescriptorBuilder =
            TableDescriptorBuilder.newBuilder(TableName.valueOf(namespace, tableName));
    // 2.2 添加参数
    for (String columnFamily : columnFamilies) {
        // 2.3 创建列族描述的建造者
        ColumnFamilyDescriptorBuilder
                columnFamilyDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily));
        // 2.4 对应当前的列族添加参数
        // 添加版本参数
        columnFamilyDescriptorBuilder.setMaxVersions(5);
        // 2.5 创建添加完参数的列族描述
        tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptorBuilder.build());
    }
    // 2.6 创建对应的表格描述
    try {
        admin.createTable(tableDescriptorBuilder.build());
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3. 关闭 admin
    admin.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

2.4 修改表

/**
 * 修改表格中一个列族的版本
 *
 * @param namespace    命名空间名称
 * @param tableName    表格名称
 * @param columnFamily 列族名称
 * @param version      版本
 */
public static void modifyTable(String namespace, String
        tableName, String columnFamily, int version) throws IOException {
    // 判断表格是否存在
    if (!isTableExists(namespace, tableName)) {
        System.out.println("表格不存在无法修改");
        return;
    }
    // 1. 获取 admin
    Admin admin = connection.getAdmin();
    try {
        // 2. 调用方法修改表格
        // 2.0 获取之前的表格描述
        TableDescriptor descriptor =
                admin.getDescriptor(TableName.valueOf(namespace, tableName));
        // 2.1 创建一个表格描述建造者
        // 如果使用填写 tableName 的方法 相当于创建了一个新的表格描述建造者没有之前的信息
        // 如果想要修改之前的信息 必须调用方法填写一个旧的表格描述
        TableDescriptorBuilder tableDescriptorBuilder =
                TableDescriptorBuilder.newBuilder(descriptor);
        // 2.2 对应建造者进行表格数据的修改
        ColumnFamilyDescriptor columnFamily1 =
                descriptor.getColumnFamily(Bytes.toBytes(columnFamily));
        // 创建列族描述建造者
        // 需要填写旧的列族描述
        ColumnFamilyDescriptorBuilder
                columnFamilyDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(columnFamily1);
        // 修改对应的版本
        columnFamilyDescriptorBuilder.setMaxVersions(version);
        // 此处修改的时候 如果填写的新创建 那么别的参数会初始化

        tableDescriptorBuilder.modifyColumnFamily(columnFamilyDescriptorBuilder.build());
        admin.modifyTable(tableDescriptorBuilder.build());
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3. 关闭 admin
    admin.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

2.5 删除表

/**
 * 删除表格
 * @param namespace 命名空间名称
 * @param tableName 表格名称
 * @return true 表示删除成功
 */
public static boolean deleteTable(String namespace ,String tableName) throws IOException {
    // 1. 判断表格是否存在
    if (!isTableExists(namespace,tableName)){
        System.out.println("表格不存在 无法删除");
        return false;
    }
    // 2. 获取 admin
    Admin admin = connection.getAdmin();
    // 3. 调用相关的方法删除表格
    try {
        // HBase 删除表格之前 一定要先标记表格为不可以
        TableName tableName1 = TableName.valueOf(namespace, tableName);
        admin.disableTable(tableName1);
        admin.deleteTable(tableName1);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 4. 关闭 admin
    admin.close();
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3、DML

创建类 HBaseDML

public class HBaseDML {
    // 添加静态属性 connection 指向单例连接
    public static Connection connection = HBaseConnect.connection;
}

  • 1
  • 2
  • 3
  • 4
  • 5

3.1 插入数据

/**
 * 插入数据
 * @param namespace 命名空间名称
 * @param tableName 表格名称
 * @param rowKey 主键
 * @param columnFamily 列族名称
 * @param columnName 列名
 * @param value 值
 */
public static void putCell(String namespace,String
        tableName,String rowKey, String columnFamily,String
                                   columnName,String value) throws IOException {
    // 1. 获取 table
    Table table = connection.getTable(TableName.valueOf(namespace, tableName));
    // 2. 调用相关方法插入数据
    // 2.1 创建 put 对象
    Put put = new Put(Bytes.toBytes(rowKey));
    // 2.2. 给 put 对象添加数据
    put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(columnName), Bytes.toBytes(value));
    // 2.3 将对象写入对应的方法
    try {
        table.put(put);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3. 关闭 table
    table.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

3.2 读取数据

/**
 * 读取数据 读取对应的一行中的某一列
 *
 * @param namespace 命名空间名称
 * @param tableName 表格名称
 * @param rowKey 主键
 * @param columnFamily 列族名称
 * @param columnName 列名
 */
public static void getCells(String namespace, String tableName,
                            String rowKey, String columnFamily, String columnName) throws
        IOException {
    // 1. 获取 table
    Table table =connection.getTable(TableName.valueOf(namespace, tableName));
    // 2. 创建 get 对象
    Get get = new Get(Bytes.toBytes(rowKey));
    // 如果直接调用 get 方法读取数据 此时读一整行数据
    // 如果想读取某一列的数据 需要添加对应的参数
    get.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(columnName));
    // 设置读取数据的版本
    get.readAllVersions();
    try {
        // 读取数据 得到 result 对象
        Result result = table.get(get);
        // 处理数据
        Cell[] cells = result.rawCells();
        // 测试方法: 直接把读取的数据打印到控制台
        // 如果是实际开发 需要再额外写方法 对应处理数据
        for (Cell cell : cells) {
            // cell 存储数据比较底层
            String value = new String(CellUtil.cloneValue(cell));
            System.out.println(value);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 关闭 table
    table.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

3.3 扫描数据

/**
 * 扫描数据
 *
 * @param namespace 命名空间
 * @param tableName 表格名称
 * @param startRow 开始的 row 包含的
 * @param stopRow 结束的 row 不包含
 */
public static void scanRows(String namespace, String tableName,
                            String startRow, String stopRow) throws IOException {
    // 1. 获取 table
    Table table = connection.getTable(TableName.valueOf(namespace, tableName));
    // 2. 创建 scan 对象
    Scan scan = new Scan();
    // 如果此时直接调用 会直接扫描整张表
    // 添加参数 来控制扫描的数据
    // 默认包含
    scan.withStartRow(Bytes.toBytes(startRow));
    // 默认不包含
    scan.withStopRow(Bytes.toBytes(stopRow));
    try {
        // 读取多行数据 获得 scanner
        ResultScanner scanner = table.getScanner(scan);
        // result 来记录一行数据 cell 数组
        // ResultScanner 来记录多行数据 result 的数组
        for (Result result : scanner) {
            Cell[] cells = result.rawCells();
            for (Cell cell : cells) {
                System.out.print (new String(CellUtil.cloneRow(cell)) + "-" + new
                        String(CellUtil.cloneFamily(cell)) + "-" + new
                        String(CellUtil.cloneQualifier(cell)) + "-" + new
                        String(CellUtil.cloneValue(cell)) + "\t");
            }
            System.out.println();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3. 关闭 table
    table.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

3.4 带过滤扫描

/**
 * 带过滤的扫描
 *
 * @param namespace 命名空间
 * @param tableName 表格名称
 * @param startRow 开始 row
 * @param stopRow 结束 row
 * @param columnFamily 列族名称
 * @param columnName 列名
 * @param value value 值
 * @throws IOException
 */
public static void filterScan(String namespace, String tableName,
                              String startRow, String stopRow, String columnFamily, String
                                      columnName, String value) throws IOException {
    // 1. 获取 table
    Table table = connection.getTable(TableName.valueOf(namespace, tableName));
    // 2. 创建 scan 对象
    Scan scan = new Scan();
    // 如果此时直接调用 会直接扫描整张表
    // 添加参数 来控制扫描的数据
    // 默认包含
    scan.withStartRow(Bytes.toBytes(startRow));
    // 默认不包含
    scan.withStopRow(Bytes.toBytes(stopRow));
    // 可以添加多个过滤
    FilterList filterList = new FilterList();
    // 创建过滤器
    // (1) 结果只保留当前列的数据
    ColumnValueFilter columnValueFilter = new ColumnValueFilter(
            // 列族名称
            Bytes.toBytes(columnFamily),
            // 列名
            Bytes.toBytes(columnName),
            // 比较关系
            CompareOperator.EQUAL,
            // 值
            Bytes.toBytes(value)
    );
    // (2) 结果保留整行数据
    // 结果同时会保留没有当前列的数据
    SingleColumnValueFilter singleColumnValueFilter = new
            SingleColumnValueFilter(
            // 列族名称
            Bytes.toBytes(columnFamily),
            // 列名
            Bytes.toBytes(columnName),
            // 比较关系
            CompareOperator.EQUAL,
            // 值
            Bytes.toBytes(value)
    );
    // 本身可以添加多个过滤器
    filterList.addFilter(singleColumnValueFilter);
    // 添加过滤
    scan.setFilter(filterList);
    try {
        // 读取多行数据 获得 scanner
        ResultScanner scanner = table.getScanner(scan);
        // result 来记录一行数据 cell 数组
        // ResultScanner 来记录多行数据 result 的数组
        for (Result result : scanner) {
            Cell[] cells = result.rawCells();
            for (Cell cell : cells) {
                System.out.print(new
                        String(CellUtil.cloneRow(cell)) + "-" + new
                        String(CellUtil.cloneFamily(cell)) + "-" + new
                        String(CellUtil.cloneQualifier(cell)) + "-" + new
                        String(CellUtil.cloneValue(cell)) + "\t");
            }
            System.out.println();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3. 关闭 table
    table.close();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

3.5 删除数据

/**
 * 删除 column 数据
 *
 * @param nameSpace
 * @param tableName
 * @param rowKey
 * @param family
 * @param column
 * @throws IOException
 */
public static void deleteColumn(String nameSpace, String tableName,
                                String rowKey, String family, String column) throws IOException {
    // 1.获取 table
    Table table = connection.getTable(TableName.valueOf(nameSpace, tableName));
    // 2.创建 Delete 对象
    Delete delete = new Delete(Bytes.toBytes(rowKey));
    // 3.添加删除信息
    // 3.1 删除单个版本
    delete.addColumn(Bytes.toBytes(family),Bytes.toBytes(column));
    // 3.2 删除所有版本
    delete.addColumns(Bytes.toBytes(family), Bytes.toBytes(column));
    // 3.3 删除列族
    // delete.addFamily(Bytes.toBytes(family));
    // 3.删除数据
    table.delete(delete);
    // 5.关闭资源
    table.close();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

四、HBase 原理分析

官方文档:https://hbase.apache.org/2.4/book.html

1、架构分析

1.1 Master 架构

Meta 表格介绍:(警告:不要去改这个表),全称 hbase:meta,只是在 list 命令中被过滤掉了,本质上和 HBase 的其他表格一样。

RowKey:

  • ([table],[region start key],[region id]) 即 表名,region 起始位置和 regionID

列:

  • info:regioninfo 为 region 信息,存储一个 HRegionInfo 对象
  • info:server 当前 region 所处的 RegionServer 信息,包含端口号
  • info:serverstartcode 当前 region 被分到 RegionServer 的起始时间

如果一个表处于切分的过程中,即 region 切分,还会多出两列 info:splitA 和 info:splitB,存储值也是 HRegionInfo 对象,拆分结束后,删除这两列。

注意:在客户端对元数据进行操作的时候才会连接 master,如果对数据进行读写,直接连接zookeeper 读取目录/hbase/meta-region-server 节点信息,会记录 meta 表格的位置。直接读取即可,不需要访问 master,这样可以减轻 master 的压力,相当于 master 专注 meta 表的写操作,客户端可直接读取 meta 表。在 HBase 的 2.3 版本更新了一种新模式:Master Registry。客户端可以访问 master 来读取meta 表信息。加大了 master 的压力,减轻了 zookeeper 的压力。HMaster仅仅维护者table和HRegion的元数据信息,负载很低

总结:

  • 为Region server分配region
  • 负责region server的负载均衡
  • 发现失效的region server并重新分配其上的region
  • HDFS上的垃圾文件回收
  • 处理schema更新请求

1.2 RegionServer 架构

  • HRegion server维护HMaster分配给它的region,处理对这些region的IO请求
  • HRegion server负责切分在运行过程中变得过大的region 从图中可以看到,Client访问HBase上数据的过程并不需要HMaster参与(寻址访问Zookeeper和HRegion server,数据读写访问HRegione server)

一些组件介绍

  • MemStore

写缓存,由于 HFile 中的数据要求是有序的,所以数据是先存储在 MemStore 中,排好序后,等到达刷写时机才会刷写到 HFile,每次刷写都会形成一个新的 HFile,写入到对应的文件夹 store 中。

一个 HRegion 由多个 Store 组成,每个 Store 包含一个列族的所有数据 Store 包括位于内存的 Memstore 和位于硬盘的 StoreFile

  • WAL

由于数据要经 MemStore 排序后才能刷写到 HFile,但把数据保存在内存中会有很高的概率导致数据丢失,为了解决这个问题,数据会先写在一个叫做 Write-Ahead logfile 的文件中,然后再写入 MemStore 中。所以在系统出现故障的时候,数据可以通过这个日志文件重建

每个Region Server维护一个Hlog,而不是每个Region一个。这样不同region(来自不同table)的日志会混在一起,这样做的目的是不断追加单个文件相对于同时写多个文件而言,可以减少磁盘寻址次数,因此可以提高对table的写性能。带来的麻烦是,如果一台region server下线,为了恢复其上的region,需要将region server上的log进行拆分,然后分发到其它region server上进行恢复

  • BlockCache

读缓存,每次查询出的数据会缓存在 BlockCache 中,方便下次查询

2、HRegion管理和HMaster工作机制

2.1 HRegion管理

  • HRegion分配

任何时刻,一个HRegion只能分配给一个HRegion Server。HMaster记录了当前有哪些可用的HRegion Server。以及当前哪些HRegion分配给了哪些HRegion Server,哪些HRegion还没有分配。当需要分配的新的HRegion,并且有一个HRegion Server上有可用空间时,HMaster就给这个HRegion Server发送一个装载请求,把HRegion分配给这个HRegion Server。HRegion Server得到请求后,就开始对此HRegion提供服务。

  • HRegion Server上线

HMaster使用zookeeper来跟踪HRegion Server状态。当某个HRegion Server启动时,会首先在zookeeper上的server目录下建立代表自己的znode。由于HMaster订阅了server目录上的变更消息,当server目录下的文件出现新增或删除操作时,HMaster可以得到来自zookeeper的实时通知。因此一旦HRegion Server上线,HMaster能马上得到消息。

  • HRegion Server下线

当HRegion Server下线时,它和zookeeper的会话断开,zookeeper而自动释放代表这台server的文件上的独占锁。HMaster就可以确定:

  1. HRegion Server和zookeeper之间的网络断开了。
  2. HRegion Server挂了。

无论哪种情况,HRegion Server都无法继续为它的HRegion提供服务了,此时HMaster会删除server目录下代表这台HRegion Server的znode数据,并将这台HRegion Server的HRegion分配给其它还活着的节点

2.2 HMaster工作机制

  • master上线

master启动进行以下步骤:

  1. 从zookeeper上获取唯一一个代表active master的锁,用来阻止其它HMaster成为master。
  2. 扫描zookeeper上的server父节点,获得当前可用的HRegion Server列表。
  3. 和每个HRegion Server通信,获得当前已分配的HRegion和HRegion Server的对应关系。
  4. 扫描.META.region的集合,计算得到当前还未分配的HRegion,将他们放入待分配HRegion列表。
  • master下线

由于HMaster只维护表和region的元数据,而不参与表数据IO的过程,HMaster下线仅导致所有元数据的修改被冻结(无法创建删除表,无法修改表的schema,无法进行HRegion的负载均衡,无法处理HRegion 上下线,无法进行HRegion的合并,唯一例外的是HRegion的split可以正常进行,因为只有HRegion Server参与),表的数据读写还可以正常进行。因此HMaster下线短时间内对整个HBase集群没有影响

从上线过程可以看到,HMaster保存的信息全是可以冗余信息(都可以从系统其它地方收集到或者计算出来)因此,一般HBase集群中总是有一个HMaster在提供服务,还有一个以上的‘HMaster’在等待时机抢占它的位置

3、写流程

写流程顺序正如 API 编写顺序,首先创建 HBase 的重量级连接

  • 首先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server;
  • 访问对应的 Region Server,获取 hbase:meta 表,将其缓存到连接中,作为连接属性 MetaCache,由于 Meta 表格具有一定的数据量,导致了创建连接比较慢;之后使用创建的连接获取 Table,这是一个轻量级的连接,只有在第一次创建的时候会检查表格是否存在访问 RegionServer,之后在获取 Table 时不会访问 RegionServer;
  • 调用Table的put方法写入数据,此时还需要解析RowKey,对照缓存的MetaCache,查看具体写入的位置有哪个 RegionServer;
  • 将数据顺序写入(追加)到 WAL,此处写入是直接落盘的,并设置专门的线程控制 WAL 预写日志的滚动(类似 Flume);根据写入命令的 RowKey 和 ColumnFamily 查看具体写入到哪个 MemStory,并且在 MemStory 中排序;
  • 向客户端发送 ack;
  • 等达到 MemStore 的刷写时机后,将数据刷写到对应的 story 中

HBase使用MemStore和StoreFile存储对表的更新。 数据在更新时首先写入Log(WAL log)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时,系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了。 当系统出现意外时,可能导致内存(MemStore)中的数据丢失,此时使用Log(WAL log)来恢复checkpoint之后的数据。

StoreFile是只读的,一旦创建后就不可以再修改。因此HBase的更新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(minor_compact, major_compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行split,等分为两个StoreFile。

由于对表的更新是不断追加的,compact时,需要访问Store中全部的 StoreFile和MemStore,将他们按row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,合并的过程还是比较快

4、MemStore Flush

MemStore 刷写由多个线程控制,条件互相独立,主要的刷写规则是控制刷写文件的大小,在每一个刷写线程中都会进行监控

刷写规则一

当某个 memstroe 的大小达到了 hbase.hregion.memstore.flush.size(默认值 128M),其所在 region 的所有 memstore 都会刷写。当 memstore 的大小达到了

  • hbase.hregion.memstore.flush.size(默认值 128M)
  • hbase.hregion.memstore.block.multiplier(默认值 4)

时,会刷写同时阻止继续往该 memstore 写数据(由于线程监控是周期性的,所有有可能面对数据洪峰,尽管可能性比较小)

刷写规则二

HRegionServer 中的属性 MemStoreFlusher 内部线程 FlushHandler 控制。标准为**LOWER_MARK(低水位线)**和 HIGH_MARK(高水位线),意义在于避免写缓存使用过多的内存造成 OOM

当 region server 中 memstore 的总大小达到低水位线:

  • java_heapsize
  • hbase.regionserver.global.memstore.size(默认值 0.4)
  • hbase.regionserver.global.memstore.size.lower.limit(默认值 0.95,强制刷新之前区域服务器中所有内存的最大大小。默认为hbase.regionserver.global. memory .size(0.95)的95%。如果此值为100%,则在由于内存存储限制而阻塞更新时,会发生最小可能的刷新)

region 会按照其所有 memstore 的大小顺序(由大到小)依次进行刷写。直到 region server中所有 memstore 的总大小减小到上述值以下

当 region server 中 memstore 的总大小达到高水位线:

  • java_heapsize
  • hbase.regionserver.global.memstore.size(默认值 0.4)

时,会同时阻止继续往所有的 memstore 写数据

刷写规则三

为了避免数据过长时间处于内存之中,到达自动刷写的时间,也会触发 memstoreflush。由 HRegionServer 的属性 PeriodicMemStoreFlusher 控制进行,由于重要性比较低,5min才会执行一次。自动刷新的时间间隔由该属性进行配置hbase.regionserver.optionalcacheflushinterval(默认1 小时)

刷写规则四

当 WAL 文件的数量超过 hbase.regionserver.max.logs,region 会按照时间顺序依次进行刷写,直到 WAL 文件数量减小到 hbase.regionserver.max.log 以下(该属性名已经废弃,现无需手动设置,最大值为 32)

5、读流程

5.1 HFile 结构

HFile 是存储在 HDFS 上面每一个 store 文件夹下实际存储数据的文件。里面存储多种内容。包括数据本身(keyValue 键值对)、元数据记录、文件信息、数据索引、元数据索引和一个固定长度的尾部信息(记录文件的修改情况)。

键值对按照块大小(默认 64K)保存在文件中,数据索引按照块创建,块越多,索引越大。每一个 HFile 还会维护一个布隆过滤器(就像是一个很大的地图,文件中每有一种 key,就在对应的位置标记,读取时可以大致判断要 get 的 key 是否存在 HFile 中)。KeyValue 内容如下:

rowlength -----------→ key 的长度
row -----------------→ key 的值
columnfamilylength --→ 列族长度
columnfamily --------→ 列族
columnqualifier -----→ 列名
timestamp -----------→ 时间戳(默认系统时间)
keytype -------------→ Put
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

由于 HFile 存储经过序列化,所以无法直接查看。可以通过 HBase 提供的命令来查看存储在 HDFS 上面的 HFile 元数据内容

bin/hbase hfile -m -f /hbase/data/命名空间/表名/regionID/列族/HFile名
  • 1

5.2 读流程

创建连接同写流程。

  • 创建 Table 对象发送 get 请求
  • 优先访问 Block Cache,查找是否之前读取过,并且可以读取 HFile 的索引信息和布隆过滤器
  • 不管读缓存中是否已经有数据了(可能已经过期了),都需要再次读取写缓存和store 中的文件
  • 最终将所有读取到的数据合并版本,按照 get 的要求返回即可

5.3 合并读取数据优化

每次读取数据都需要读取三个位置,最后进行版本的合并。效率会非常低,所有系统需要对此优化

  • HFile 带有索引文件,读取对应 RowKey 数据会比较快
  • Block Cache 会缓存之前读取的内容和元数据信息,如果 HFile 没有发生变化(记录在 HFile 尾信息中),则不需要再次读取
  • 使用布隆过滤器能够快速过滤当前 HFile 不存在需要读取的 RowKey,从而避免读取文件。(布隆过滤器使用 HASH 算法,不是绝对准确的,出错会造成多扫描一个文件,对读取数据结果没有影响)

6、StoreFile Compaction

由于 memstore 每次刷写都会生成一个新的 HFile,文件过多读取不方便,所以会进行文件的合并,清理掉过期和删除的数据,会进行 StoreFile Compaction。

Compaction 分为两种,分别是 Minor Compaction 和** Major Compaction**。Minor Compaction会将临近的若干个较小的 HFile 合并成一个较大的 HFile,并清理掉部分过期和删除的数据,有系统使用一组参数自动控制,Major Compaction 会将一个 Store 下的所有的 HFile 合并成一个大 HFile,并且会清理掉所有过期和删除的数据,由参数 hbase.hregion.majorcompaction控制,默认 7 天。

Minor Compaction 控制机制,参与到小合并的文件需要通过参数计算得到,有效的参数有 5 个

  • hbase.hstore.compaction.ratio(默认 1.2F)合并文件选择算法中使用的比率
  • hbase.hstore.compaction.min(默认 3) 为 Minor Compaction 的最少文件个数
  • hbase.hstore.compaction.max(默认 10) 为 Minor Compaction 最大文件个数
  • hbase.hstore.compaction.min.size(默认 128M)为单个 Hfile 文件大小最小值,小于这个数会被合并
  • hbase.hstore.compaction.max.size(默认 Long.MAX_VALUE)为单个 Hfile 文件大小最大值,高于这个数不会被合并

小合并机制为拉取整个 store 中的所有文件,做成一个集合。之后按照从旧到新的顺序遍历。判断条件为:

  • 过小合并,过大不合并
  • 文件大小 / hbase.hstore.compaction.ratio < (剩余文件大小和) 则参与压缩。所有把比值设置过大,如 10 会最终合并为 1 个特别大的文件,相反设置为 0.4,会最终产生 4 个 storeFile。不建议修改默认值
  • 满足压缩条件的文件个数达不到个数要求(3 <= count <= 10)则不压缩

7、Region Split

Region 切分分为两种,创建表格时候的预分区即自定义分区,同时系统默认还会启动一个切分规则,避免单个 Region 中的数据量太大

7.1 预分区(自定义分区)

每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能

# 手动设定预分区
create 'staff1','info', SPLITS => ['1000','2000','3000','4000']
# 生成 16 进制序列预分区
create 'staff2','info',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}

# 按照文件中设置的规则预分区
# 创建 splits.txt 文件内容如下
aaaa
bbbb
cccc
dddd
# 然后执行
create 'staff3', 'info',SPLITS_FILE => 'splits.txt'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

最后一种方法是使用java API,不常用也不推荐

public class HBaseConnect {
        public static void main(String[] args) throws IOException {
            // 1.获取配置类
            Configuration conf = HBaseConfiguration.create();
            // 2.给配置类添加配置

            conf.set("hbase.zookeeper.quorum","hadoop102,hadoop103,hadoop104"
            );
            // 3.获取连接
            Connection connection =
                    ConnectionFactory.createConnection(conf);
            // 3.获取 admin
            Admin admin = connection.getAdmin();
            // 5.获取 descriptor 的 builder
            TableDescriptorBuilder builder =
                    TableDescriptorBuilder.newBuilder(TableName.valueOf("bigdata",
                            "staff4"));
            // 6. 添加列族

            builder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(
                    Bytes.toBytes("info")).build());
            // 7.创建对应的切分
            byte[][] splits = new byte[3][];
            splits[0] = Bytes.toBytes("aaa");
            splits[1] = Bytes.toBytes("bbb");
            splits[2] = Bytes.toBytes("ccc");
            // 8.创建表
            admin.createTable(builder.build(),splits);
            // 9.关闭资源
            admin.close();
            connection.close();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

7.2 系统拆分

Region 的拆分是由 HRegionServer 完成的,在操作之前需要通过 ZK 汇报 master,修改对应的 Meta 表信息添加两列 info:splitA 和 info:splitB 信息。之后需要操作 HDFS 上面对应的文件,按照拆分后的 Region 范围进行标记区分,实际操作为创建文件引用,不会挪动数据。刚完成拆分的时候,两个 Region 都由原先的RegionServer 管理。之后汇报给 Master,由Master将修改后的信息写入到Meta表中。等待下一次触发负载均衡机制,才会修改Region的管理服务者,而数据要等到下一次压缩时,才会实际进行移动

不管是否使用预分区,系统都会默认启动一套 Region 拆分规则。不同版本的拆分规则有差别。系统拆分策略的父类为 RegionSplitPolicy。

  • 0.94 版本之前 => ConstantSizeRegionSplitPolicy

    当 1 个 region 中 的 某 个 Store 下 所 有 StoreFile 的 总 大 小 超 过
    hbase.hregion.max.filesize (10G),该 Region 就会进行拆分

  • 0.94 版本之后,2.0 版本之前 => IncreasingToUpperBoundRegionSplitPolicy

    当 1 个 region 中 的 某 个 Store 下 所 有 StoreFile 的 总 大 小 超 过Min(initialSize*R^3 ,hbase.hregion.max.filesize"),该 Region 就会进行拆分。其中 initialSize 的默认值为 2*hbase.hregion.memstore.flush.size,R 为当前 Region Server 中属于该 Table 的Region 个数(0.94 版本之后),具体的切分策略为:

    • 第一次 split:1^3 * 256 = 256MB
    • 第二次 split:2^3 * 256 = 2048MB
    • 第三次 split:3^3 * 256 = 6912MB
    • 第四次 split:4^3 * 256 = 16384MB > 10GB,因此取较小的值 10GB
    • 后面每次 split 的 size 都是 10GB 了。
  • 2.0 版本之后 => SteppingSplitPolicy

    Hbase 2.0 引入了新的 split 策略:如果当前 RegionServer 上该表只有一个 Region,按照 2 * hbase.hregion.memstore.flush.size 分裂,否则按照 hbase.hregion.max.filesize 分裂

五、HBase 优化

1、RowKey 设计

一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于哪个一个预分区的区间内,设计 rowkey的主要目的 ,就是让数据均匀的分布于所有的 region中,在一定程度上防止数据倾斜。接下来我们就谈一谈 rowkey 常用的设计方案

  • 生成随机数、hash、散列值
  • 时间戳反转
  • 字符串拼接

现在有一个需求,源数据如下

user         data                    pay
zhangsan     2022-01-05 09:08:00     100
  • 1
  • 2
  • 统计张三在 2021 年 12 月份消费的总金额
  • 统计所有人在 2021 年 12 月份消费的总金额

1.1 实现需求 1

为了能够统计张三在 2021 年 12 月份消费的总金额,我们需要用 scan 命令能够得到张三在这个月消费的所有记录,之后在进行累加即可。Scan 需要填写 startRow 和 stopRow:

scan : startRow -> ^A^Azhangsan2021-12 
       endRow -> ^A^Azhangsan2021-12.
  • 1
  • 2
  • 避免扫描数据混乱,解决字段长度不一致的问题,可以使用相同阿斯卡码值的符号进行填充,框架底层填充使用的是阿斯卡码值为 1 的^A
  • 最后的日期结尾处需要使用阿斯卡码略大于’-'的值

最终得到 rowKey 的设计为

# 注意 rowkey 相同的数据会视为相同数据覆盖掉之前的版本
rowKey: userdate(yyyy-MM-dd HH:mm:SS)
  • 1
  • 2

1.2 实现需求 2

问题提出:按照需要 1 的 rowKey 设计,会发现对于需求 2,完全没有办法写 rowKey 的扫描范围。此处能够看出 hbase 设计 rowKey 使用的特点为:**适用性强 泛用性差 能够完美实现一个需求 但是不能同时完美实现多个需要。**如果想要同时完成两个需求,需要对 rowKey 出现字段的顺序进行调整。调整的原则为:可枚举的放在前面。其中时间是可以枚举的,用户名称无法枚举,所以必须把时间放在前面。

# 最终满足 2 个需求的设计,可以穷举的写在前面即可
rowKey 设计格式 => date(yyyy-MM)^A^Auserdate(-dd hh:mm:ss ms)
#(1)统计张三在 2021 年 12 月份消费的总金额
scan: startRow => 2021-12^A^Azhangsan
      stopRow => 2021-12^A^Azhangsan.
#(2)统计所有人在 2021 年 12 月份消费的总金额
scan: startRow => 2021-12
      stopRow => 2021-12.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

1.3 添加预分区优化

预分区的分区号同样需要遵守 rowKey 的 scan 原则。所有必须添加在 rowKey 的最前面,前缀为最简单的数字。同时使用 hash 算法将用户名和月份拼接决定分区号。(单独使用用户名会造成单一用户所有数据存储在一个分区)

# 添加预分区优化
startKey stopKey
001
001      002
002      003
...
119      120
# 分区号=> hash(user+date(MM)) % 120
# 分区号填充 如果得到 1 => 001
rowKey 设计格式 => 分区号 date(yyyy-MM)^A^Auserdate(-dd hh:mm:ss ms)

# 缺点:实现需求 2 的时候,由于每个分区都有 12 月份的数据,需要扫描 120 个分区
# 解决方法:提前将分区号和月份进行对应
# 提前将月份和分区号对应一下
#000 到 009 分区 存储的都是 1 月份数据
#010 到 019 分区 存储的都是 2 月份数据
#...
#110 到 119 分区 存储的都是 12 月份数据


#是 9 月份的数据
分区号=> hash(user+date(MM)) % 10 + 80
分区号填充 如果得到 85 => 085
#得到 12 月份所有人的数据,扫描 10 次
scan: startRow => 1102021-12
stopRow => 1102021-12.
...
startRow => 1122021-12
stopRow => 1122021-12.
..
startRow => 1192021-12
stopRow => 1192021-12.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

2、参数优化

2.1 Zookeeper 会话超时时间

进入hbase-site.xml修改

  • 属性:zookeeper.session.timeout
  • 解释:默认值为 90000 毫秒(90s)。当某个 RegionServer 挂掉,90s 之后 Master 才能察觉到。可适当减小此值,尽可能快地检测 regionserver 故障,可调整至 20-30s
# 看你能有都能忍耐超时,同时可以调整重试时间和重试次数
hbase.client.pause(默认值 100ms)
hbase.client.retries.number(默认 15 次)
  • 1
  • 2
  • 3

2.2 设置 RPC 监听数量

  • 属性:hbase.regionserver.handler.count
  • 解释:默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。

2.3 手动控制 Major Compaction

  • 属性:hbase.hregion.majorcompaction
  • 解释:默认值:604800000 秒(7 天), Major Compaction 的周期,若关闭自动 Major Compaction,可将其设为 0。如果关闭一定记得自己手动合并,因为大合并非常有意义

2.4 优化 HStore 文件大小

  • 属性:hbase.hregion.max.filesize
  • 解释:默认值 10737418240(10GB),如果需要运行 HBase 的 MR 任务,可以减小此值,因为一个 region 对应一个 map 任务,如果单个 region 过大,会导致 map 任务执行时间过长。该值的意思就是,如果 HFile 的大小达到这个数值,则这个 region 会被切分为两个 Hfile

2.5 优化 HBase 客户端缓存

  • 属性:hbase.client.write.buffer
  • 解释:默认值 2097152bytes(2M)用于指定 HBase 客户端缓存,增大该值可以减少 RPC调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少 RPC 次数的目的。

2.6 指定 scan.next 扫描 HBase 所获取的行数

  • 属性:hbase.client.scanner.caching
  • 解释:用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大

2.7 BlockCache 占用 RegionServer 堆内存的比例

  • 属性:hfile.block.cache.size
  • 解释:默认 0.4,读请求比较多的情况下,可适当调大

2.8 MemStore 占用 RegionServer 堆内存的比例

  • 属性:hbase.regionserver.global.memstore.size
  • 解释:默认 0.4,写请求较多的情况下,可适当调大

Lars Hofhansl(拉斯·霍夫汉斯)推荐 Region 设置 20G,刷写大小设置 128M,其它默认

3、JVM 调优

JVM 调优的思路有两部分:一是内存设置,二是垃圾回收器设置。垃圾回收的修改是使用并发垃圾回收,默认 PO+PS 是并行垃圾回收,会有大量的暂停。理由是 HBsae 大量使用内存用于存储数据,容易遭遇数据洪峰造成 OOM,同时写缓存的数据是不能垃圾回收的,主要回收的就是读缓存,而读缓存垃圾回收不影响性能,所以最终设置的效果可以总结为:防患于未然,早洗早轻松

# 设置使用 CMS 收集器(并发)
-XX:+UseConcMarkSweepGC

# 保持新生代尽量小,同时尽早开启 GC,例如
# 在内存占用到 70%的时候开启 GC
-XX:CMSInitiatingOccupancyFraction=70
# 指定使用 70%,不让 JVM 动态调整
-XX:+UseCMSInitiatingOccupancyOnly
# 新生代内存设置为 512m
-Xmn512m
# 并行执行新生代垃圾回收
-XX:+UseParNewGC
# 设 置 scanner 扫 描 结 果 占 用 内 存 大 小 , 在 hbase-site.xml 中,设置
# hbase.client.scanner.max.result.size(默认值为 2M)为 eden 空间的 1/8(大概在 64M)
#  设置多个与 max.result.size * handler.count 相乘的结果小于 Survivor Space(新生代经过垃圾回收之后存活的对象)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4、HBase 使用经验法则

  • Region 大小控制 10-50G
  • cell 大小不超过 10M(性能对应小于 100K 的值有优化),如果使用 mob(Medium-sized Objects 一种特殊用法)则不超过 50M
  • 1 张表有 1 到 3 个列族,不要设计太多。最好就 1 个,如果使用多个尽量保证不会同时读取多个列族
  • 1 到 2 个列族的表格,设计 50-100 个 Region
  • 列族名称要尽量短,不要去模仿 RDBMS(关系型数据库)具有准确的名称和描述
  • 如果 RowKey 设计时间在最前面,会导致有大量的旧数据存储在不活跃的 Region中,使用的时候,仅仅会操作少数的活动 Region,此时建议增加更多的 Region 个数
  • 如果只有一个列族用于写入数据,分配内存资源的时候可以做出调整,即写缓存不会占用太多的内存

六、整合 Phoenix

1、Phoenix 简介

1.1 Phoenix 定义

Phoenix 是 HBase 的开源 SQL 皮肤。可以使用标准 JDBC API 代替 HBase 客户端 API来创建表,插入数据和查询 HBase 数据。

1.2 为什么使用 Phoenix

官方给的解释为:在 Client 和 HBase 之间放一个 Phoenix 中间层不会减慢速度,因为用户编写的数据处理代码和 Phoenix 编写的没有区别(更不用说你写的垃圾的多),不仅如此Phoenix 对于用户输入的 SQL 同样会有大量的优化手段(就像 hive 自带 sql 优化器一样)。

Phoenix 在 5.0 版本默认提供有两种客户端使用(瘦客户端和胖客户端),在 5.1.2 版本安装包中删除了瘦客户端,本文也不再使用瘦客户端。而胖客户端和用户自己写 HBase 的API 代码读取数据之后进行数据处理是完全一样的

2、Phoenix快速入门

官网:https://phoenix.apache.org/

2.1 Phoenix安装部署

wget http://archive.apache.org/dist/phoenix/phoenix-5.1.2/phoenix-hbase-2.4-5.1.2-bin.tar.gz

tar -zxvf phoenix-hbase-2.4-5.1.2-bin.tar.gz -C /opt/module/
mv phoenix-hbase-2.4-5.1.2-bin/ phoenix

# 复制 server 包并拷贝到各个节点的 hbase/lib
cd /opt/module/phoenix/
cp phoenix-server-hbase-2.4-5.1.2.jar /opt/module/hbase/lib/
xsync /opt/module/hbase/lib/phoenix-server-hbase-2.4-5.1.2.jar

# 配置环境变量
vim /etc/profile.d/my_env.sh
#phoenix
export PHOENIX_HOME=/opt/module/phoenix
export PHOENIX_CLASSPATH=$PHOENIX_HOME
export PATH=$PATH:$PHOENIX_HOME/bin
# 刷新环境变量
source /etc/profile.d/my_env.sh

# 重启 HBase
stop-hbase.sh
start-hbase.sh

# 连接 Phoenix
/opt/module/phoenix/bin/sqlline.py hadoop102,hadoop103,hadoop104:2181

# 错误解决
# 出现下面错误的原因是之前使用过 phoenix,建议删除之前的记录
# 警告: Failed to load history
# java.lang.IllegalArgumentException: Bad history file syntax! The 
# history file `/home/atguigu/.sqlline/history` may be an older 
# history: please remove it or use a different history file.

# 解决方法:在/home/atguigu 目录下删除.sqlline 文件夹
rm -rf .sqlline/

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

2.2 Phoenix Shell 操作

table

建议查看官网:https://phoenix.apache.org/language/index.html

# 显示所有表
!table 或 !tables


# 创建表
# 直接指定单个列作为 RowKey
CREATE TABLE IF NOT EXISTS student(
id VARCHAR primary key,
name VARCHAR,
age BIGINT,
addr VARCHAR);

# 在 phoenix 中,表名等会自动转换为大写,若要小写,使用双引号,如"us_population"。
# 指定多个列的联合作为 RowKey
CREATE TABLE IF NOT EXISTS student1 (
id VARCHAR NOT NULL,
name VARCHAR NOT NULL,
age BIGINT,
addr VARCHAR
CONSTRAINT my_pk PRIMARY KEY (id, name));
# 注:Phoenix 中建表,会在 HBase 中创建一张对应的表。为了减少数据对磁盘空间的占,Phoenix 默认会对 HBase 中的列名做编码处理。
# 具体规则可参考官网链接:https://phoenix.apache.org/columnencoding.html,若不想对列名编码,可在建表语句末尾加上 COLUMN_ENCODED_BYTES = 0;

# 插入数据
upsert into student values('1001','zhangsan', 10, 'beijing');
# 查询记录
select * from student;
select * from student where id='1001';
# 删除记录
delete from student where id='1001';
# 删除表
drop table student;
# 退出命令行
!quit

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

表的映射

默认情况下,HBase 中已存在的表,通过 Phoenix 是不可见的。如果要在 Phoenix 中操作 HBase 中已存在的表,可以在 Phoenix 中进行表的映射。映射方式有两种:视图映射和表映射

# 启动
/opt/module/hbase/bin/hbase shell
# 创建 HBase 表 test
create 'test','info1','info2'
# 视图映射
# Phoenix 创建的视图是只读的,所以只能用来做查询,无法通过视图对数据进行修改等操作
create view "test"(id varchar primary key,"info1"."name" varchar, "info2"."address" varchar);
# 删除视图
drop view "test";

# 表映射
# 在 Pheonix 创建表去映射 HBase 中已经存在的表,是可以修改删除 HBase 中已经存在的数据的
# 删除 Phoenix 中的表,那么 HBase 中被映射的表也会被删除
# 注:进行表映射时,不能使用列名编码,需将 column_encoded_bytes 设为 0
create table "test"(id varchar primary key,"info1"."name" varchar, "info2"."address" varchar) column_encoded_bytes=0;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

数字类型说明

HBase 中的数字,底层存储为补码,而 Phoenix 中的数字,底层存储为在补码的基础上,将符号位反转。故当在 Phoenix 中建表去映射 HBase 中已存在的表,当 HBase 中有数字类型的字段时,会出现解析错误的现象

# Hbase 演示
create 'test_number','info'
put 'test_number','1001','info:number',Bytes.toBytes(1000)
scan 'test_number',{COLUMNS => 'info:number:toLong'}
# phoenix 演示:
create view "test_number"(id varchar primary key,"info"."number" bigint);
select * from "test_number";

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

解决方法:

(1)Phoenix 种提供了 unsigned_int,unsigned_long 等无符号类型,其对数字的编码解码方式和 HBase 是相同的,如果无需考虑负数,那在 Phoenix 中建表时采用无符号类型是最合适的选择

drop view "test_number";
create view "test_number"(id varchar primary key,"info"."number" unsigned_long);
select * from "test_number";
  • 1
  • 2
  • 3

(2)如需考虑负数的情况,则可通过 Phoenix 自定义函数,将数字类型的最高位,即符号位反转即可,自定义函数可参考如下链接:https://phoenix.apache.org/udf.html

2.3 Phoenix JDBC 操作

此处演示一个标准的 JDBC 连接操作,实际开发中会直接使用别的框架内嵌的 Phoenix 连接,胖客户端加入maven依赖

<dependencies>
   <dependency>
     <groupId>org.apache.phoenix</groupId>
     <artifactId>phoenix-client-hbase-2.4</artifactId>
     <version>5.1.2</version>
   </dependency>
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编写代码

public class PhoenixClient {
    public static void main(String[] args) throws SQLException {
        // 标准的 JDBC 代码
        // 1.添加链接
        String url = "jdbc:phoenix:hadoop102,hadoop103,hadoop104:2181";
        // 2. 创建配置
        // 没有需要添加的必要配置 因为 Phoenix 没有账号密码
        Properties properties = new Properties();
        // 3. 获取连接
        Connection connection = DriverManager.getConnection(url,properties);
        // 5.编译 SQL 语句
        PreparedStatement preparedStatement = connection.prepareStatement("select * from student");
        // 6.执行语句
        ResultSet resultSet = preparedStatement.executeQuery();
        // 7.输出结果
        while (resultSet.next()){
            System.out.println(resultSet.getString(1) + ":" +
                    resultSet.getString(2) + ":" + resultSet.getString(3));
        }
        // 8.关闭资源
        connection.close();
        // 由于 Phoenix 框架内部需要获取一个 HBase 连接,所以会延迟关闭
        // 不影响后续的代码执行
        System.out.println("hello");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

3、Phoenix 二级索引

3.1 二级索引配置文件

添加如下配置到 HBase 的 HRegionserver 节点的 hbase-site.xml

<!-- phoenix regionserver 配置参数-->
<property>
  <name>hbase.regionserver.wal.codec</name>
  <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
</property>
  • 1
  • 2
  • 3
  • 4
  • 5

3.2 全局索引(global index)

Global Index 是默认的索引格式,创建全局索引时,会在 HBase 中建立一张新表。也就是说索引数据和数据表是存放在不同的表中的,因此全局索引适用于多读少写的业务场景。

写数据的时候会消耗大量开销,因为索引表也要更新,而索引表是分布在不同的数据节点上的,跨节点的数据传输带来了较大的性能消耗。在读数据的时候 Phoenix 会选择索引表来降低查询消耗的时间

# 创建单个字段的全局索引
CREATE INDEX my_index ON my_table (my_col);
#例如
create index my_index on student1(age);
#删除索引
DROP INDEX my_index ON my_table
drop index my_index on student1;

# 查看二级索引是否有效,可以使用 explainPlan 执行计划,有二级索引之后会变成范围扫描
explain select id,name from student1 where age = 10;
# 如果想查询的字段不是索引字段的话索引表不会被使用,也就是说不会带来查询速度的提升
explain select 
id,name,addr from student1 where age = 10;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.3 包含索引(covered index)

# 创建携带其他字段的全局索引(本质还是全局索引)
CREATE INDEX my_index ON my_table (v1) INCLUDE (v2);
# 先删除之前的索引
drop index my_index on student1;
# 创建包含索引
create index my_index on student1(age) include (addr);
# 之后使用执行计划查看效果,使用explain

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.4 本地索引(local index)

Local Index 适用于写操作频繁的场景。索引数据和数据表的数据是存放在同一张表中(且是同一个 Region),避免了在写操作的时候往不同服务器的索引表中写索引带来的额外开销

# my_column 可以是多个
CREATE LOCAL INDEX my_index ON my_table (my_column);
# 本地索引会将所有的信息存在一个影子列族中,虽然读取的时候也是范围扫描,但是没有全局索引快,优点在于不用写多个表了
#删除之前的索引
drop index my_index on student1;
#创建本地索引
CREATE LOCAL INDEX my_index ON student1 (age,addr);
#使用执行计划
explain select id,name,addr from student1 where age = 10;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

七、Hive集成

1、集成使用

1.1 使用场景

如果大量的数据已经存放在 HBase 上面,需要对已经存在的数据进行数据分析处理,那么 Phoenix 并不适合做特别复杂的 SQL 处理,此时可以使用 hive 映射 HBase 的表格,之后写 HQL 进行分析处理。

1.2 配置

在 hive-site.xml 中添加 zookeeper 的属性

<property>
  <name>hive.zookeeper.quorum</name>
  <value>hadoop102,hadoop103,hadoop104</value>
</property>
<property>
  <name>hive.zookeeper.client.port</name>
  <value>2181</value>
</property>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2、案例举例

2.1 案例一

目标:建立 Hive 表,关联 HBase 表,插入数据到 Hive 表的同时能够影响 HBase 表。

# 在 Hive 中创建表同时关联 HBase
CREATE TABLE hive_hbase_emp_table(
 empno int,
 ename string,
 job string,
 mgr int,
 hiredate string,
 sal double,
 comm double,
 deptno int
)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,info:ename,info:job,info:mgr,info:hiredate,info:sal,info:comm,info:deptno")
TBLPROPERTIES ("hbase.table.name" = "hbase_emp_table");
# 提示:完成之后,可以分别进入 Hive 和 HBase 查看,都生成了对应的表

# (2)在 Hive 中创建临时中间表,用于 load 文件中的数据
# 提示:不能将数据直接 load 进 Hive 所关联 HBase 的那张表中
CREATE TABLE emp(
 empno int,
 ename string,
 job string,
 mgr int,
 hiredate string,
 sal double,
 comm double,
 deptno int
)
row format delimited fields terminated by '\t';
# (3)向 Hive 中间表中 load 数据
load data local inpath '/opt/software/emp.txt' into table emp;
# (4)通过 insert 命令将中间表中的数据导入到 Hive 关联 Hbase 的那张表中
insert into table hive_hbase_emp_table select * from emp;
# 查看 Hive 以及关联的 HBase 表中是否已经成功的同步插入了数据
hive> select * from hive_hbase_emp_table;
Hbase> scan 'hbase_emp_table'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

2.2 案例二

目标:在 HBase 中已经存储了某一张表 hbase_emp_table,然后在 Hive 中创建一个外部表来关联 HBase 中的 hbase_emp_table 这张表,使之可以借助 Hive 来分析 HBase 这张表中的数据。

# 在 Hive 中创建外部表
CREATE EXTERNAL TABLE relevance_hbase_emp(
 empno int,
 ename string,
 job string,
 mgr int,
 hiredate string,
 sal double,
 deptno int
)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,info:ename,info:job,info:mgr,info:hiredate,info:sal,info:comm,info:deptno") 
TBLPROPERTIES ("hbase.table.name" = "hbase_emp_table");


# 关联后就可以使用 Hive 函数进行一些分析操作了
hive (default)> select deptno,avg(sal) monery from relevance_hbase_emp group by deptno;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/437194
推荐阅读
相关标签
  

闽ICP备14008679号