HBase 最全存储原理(一)

问题导读:
1、存储模式有哪些?
2、Hbase为什么要使用列族?
3、hbase数据压缩方式有哪些?
4、HBase架构如何理解?



存储模式

常见的关系型数据库大都使用行式存储,例如 mysql、Oracle ,但 HBase 不一样,它使用的是列式存储!

行式存储与列式存储

什么是行式存储,什么是列式存储?假如有这样一张表



那么行式存储是将一行数据存储在一起



列式存储是将一列数据存储在一起


底层存储结构如下,行式存储是存储玩一行,接着存储下一行,而列式存储是把一列数据存储在一起,由于列数据不存在一列存完了的概念,所以列数据与列数据之间不是紧挨着的,而是相互分离的。





优缺点

行式存储
优点

  • 随机读效率高
  • 优秀的事务支持


缺点

  • 需要维护大量索引,存储成本较高
  • 不能线性扩展


列式存储
优点

  • 当列中有数据重复时,会进行压缩,存储成本较低
  • 当查询多列数据时,由于不同的列是分开存储的,可以并行查询,提供查询效率


缺点

  • 事务支持的不好


应用场景

行式存储

  • 表与表之间有关联,需要连表查询
  • 需要联机事务处理
  • 查询需求大于存储需求


列式存储

  • 经常只访问某几列的数据时
  • 对数据压缩和线性扩展有要求的时
  • 存储需求大于读取需求时


总结,行式存储  适合OLTP(联机事务处理)场景,列式存储适合 OLAP(联机事务分析)场景

说明
上面对行式存储与列式存储的介绍是从比较宽泛的角度出发的,并不是说 HBase 是列式存储,它的内部设计就跟上面的完全一样。行式存储与列式存储更多的是两种存储思想。具体的实现,不同的产品有自己的实现方式。
HBase 的列族式存储
HBase 更像是列族式数据库而不是列式数据库

列族
列族在 HBase 官方术语中叫 Columns Family ,也有人它为列簇
什么是列族,其实在我们日常工作中经常使用过列族,比如我们有一张职工档案表,如图


其中 基本信息、工作信息就是列族,而姓名、性别、年龄则是归属在基本信息这个列族下的列。
注意,日常工作中我们会将一张表的列设计成多层,比如上面的工档案表,基本信息可能还会分为个人基本信息和家庭基本信息,但是 HBase 只支持两层结构

HBase 的表由多个列族构造,每个列族又可以有若干个列,列族是 HBase 表的组成部分,而列不是,因此,创建表的时候,可以不声明列,但是不能不声明列族!

列族需要在创建表时就定义好,并且修改的不能太频繁,数量也不能太多,理论上是限制在几十个,但是实际中可能会更少。列族的名称必须是由可打印的字符组成,这个是与其他的值或名字的命名规范显著不同的地方。

Hbase 为什么要使用列族
一行数据有若干列组成,若干列又构成一个列族(column family),这不仅有助于构建数据的语义边界或者局部边界,还有助于给他们设置某些特性(如压缩),或者指示他们存贮在内存中,一个列族的所有列存贮在同一个底层的存储文件中。
列族设置
Hbase 的访问控制、磁盘和内存的使用统计都是在列族层面进行的,所以设计列族时一般是把相关的列或者是需要经常访问的列放在一起,组成列族,这样后期访问数据时会快速一些!

HBase 的列
HBase 的列属于某一个列族,列名使用列族名作为前缀,列可以再创建表时声明,也可以再表创建好后动态添加!
不同于列族需要先创建才能使用,列则灵活的多,可以在插入数据的时候指定

HBase 的表

Table = RowKey + Family + column + Timestamp + Value
HBase 的表由多个列族(Family) 构造,每个列族又可以有若干个列(column),HBase 的表每条数据都有一个行键(RowKey),行键是该条数据在表中是唯一标示,类似于关系型数据库中的主键,列中的每一个数据还有一个时间戳/版本号(Timestamp),HBase 支持多版本,当你开启多版本支持后,列中的数据发送变化时,旧数据会被保留,新旧数据通过一个时间戳作为区分,因此也可以理解为 Timestamp 是该数据的版本号!
除了存储历史数据,HBase 也是支持对列数据进行查询操作的

HBase 的存储结构
之前把 HBase 归类为列式存储,那是从外在功能表现层面分类的,如果从内部存储层面来分类的话, HBase 应该归类为 key value 型数据库
(Table,RowKey,Family,column,Timestamp) -> Value

在存储时,每个值的key由 表名、列族名、列名、行键和版本号组成

HBase 列的数据版本
HBase 列的数据支持多个版本,默认是3个
还是以职工档案表为例子


第一次插入插入了姓名、年龄、2个字段,在底层,他们是2个key value 键值对,并且每个键都有一个时间戳/版本号(Timestamp)
第二次补充了工资字段,并对应一个新的时间戳 t3
第三次更新了年龄字段,并对应新时间戳 t4
这条数据虽然有三个版本,但是它们都对应同一个 RowKey
现在再来看这条数据,姓名只有一个版本 t1 -> 张三,年龄有两个版本 t2 -> 22、t4 -> 23 ,工资只有一个版本 t3 -> 1000, 当查询数据时,默认展示列最新版本的数据,也就是说,我们查询 RowKey =1 时,结果为 张三 23 1000

数据存储原型
HBase 是通过如下的数据结构维护数据的
SortedMap<
    RowKey,List<
        SortedMap<
            Column,List<value,Timestamp>
        >
    >
>


第一个 SortedMap 是对 RowKey 进行排序,第二个 SortedMap 是对 Column 进行排序
ps:之前不是说 HBase 底层采用 key value 键值对的结构存储吗,现在怎么又变成来了这种数据结构?
其实这只是 HBase 的数据在不同抽象层次上的表现形式,就向任何数据在硬盘上最终都变现为连续或不连续的0、1一样,所以既可以说 HBase 是 key value 型的数据结构,也可以说他是这种带层级的数据结构!

HBase 表
HBase 建表语句

create '命名空间:表名',
{
NAME => '列族名1',
VERSIONS => '版本数量',
EVICT_BLOCKS_ON_CLOSE => 'false',
NEW_VERSION_BEHAVIOR => 'false',
KEEP_DELETED_CELLS => 'FALSE',
CACHE_DATA_ON_WRITE => 'false',
DATA_BLOCK_ENCODING => 'NONE',
TTL => '生存时间,单位秒,FOREVER 表示永不过期',
MIN_VERSIONS => '0',
REPLICATION_SCOPE => '复制范围',
BLOOMFILTER => 'ROW',
CACHE_INDEX_ON_WRITE => 'false',
IN_MEMORY => 'false',
CACHE_BLOOMS_ON_WRITE => 'false',
PREFETCH_BLOCKS_ON_OPEN => 'false',
COMPRESSION => '数据压缩算法,hbase 中常用的为 SNAPPY',
BLOCKCACHE => 'true',
BLOCKSIZE => '65536'
},
{
NAME => '列族名2',
VERSIONS => '1',
EVICT_BLOCKS_ON_CLOSE => 'false',
NEW_VERSION_BEHAVIOR => 'false',
KEEP_DELETED_CELLS => 'FALSE',
CACHE_DATA_ON_WRITE => 'false',
DATA_BLOCK_ENCODING => 'NONE',
TTL => 'FOREVER',
MIN_VERSIONS => '0',
REPLICATION_SCOPE => '0',
BLOOMFILTER => 'ROW',
CACHE_INDEX_ON_WRITE => 'false',
IN_MEMORY => 'false',
CACHE_BLOOMS_ON_WRITE => 'false',
PREFETCH_BLOCKS_ON_OPEN => 'false',
COMPRESSION => 'NONE',
BLOCKCACHE => 'true',
BLOCKSIZE => '65536'
}


也可以使用以下简洁的语句创建表
create '命名空间:表名','列簇1','列簇2'

这样创建出来的表,列簇各属性的默认值如下

{
NAME => '字段名(没有默认值)',
VERSIONS => '1',
EVICT_BLOCKS_ON_CLOSE => 'false',
NEW_VERSION_BEHAVIOR => 'false',
KEEP_DELETED_CELLS => 'FALSE',
CACHE_DATA_ON_WRITE => 'false',
DATA_BLOCK_ENCODING => 'NONE',
TTL => 'FOREVER',
MIN_VERSIONS => '0',
REPLICATION_SCOPE => '0',
BLOOMFILTER => 'ROW',
CACHE_INDEX_ON_WRITE => 'false',
IN_MEMORY => 'false',
CACHE_BLOOMS_ON_WRITE => 'false',
PREFETCH_BLOCKS_ON_OPEN => 'false',
COMPRESSION => 'NONE',
BLOCKCACHE => 'true',
BLOCKSIZE => '65536'
}

hbase 数据压缩
在建表时,可以给列族设置 COMPRESSION 属性以开启该列族的数据压缩(数据压缩是以列族为单位的)。这是一种用 CPU 时间换 IO速度和存储空间的方式。

COMPRESSION 属性有以下枚举值

'NONE' : 不开启数据压缩
'GZ' : 即 GZIP ,压缩率比 Snappy、LZO 高,对应的,压缩解压时占用的 CPU 资源也更多。推荐用于不经常访问的冷数据
'LZO' : 与 GZIP 相比压缩率虽然不高,但使用的 CPU 资源更少,推荐用于经常访问的热数据,在2011 年 Google 推出 Snappy 之前,LZO 是默认推荐的设置
'SNAPPY' :Snappy 具有与 LZO 相似的质量,但表现得更好,推荐用于经常访问的热数据。是目前默认推荐的设置
'LZ4' : 与 LZO 相比,LZ4 的压缩率和 LZO 的压缩率相差不多,但是 LZ4 的解压 / 压缩速度更快

通常,您需要在较小的尺寸和更快的压缩/解压缩之间权衡选择,大多数情况下,默认情况下启用 Snappy 是一个不错的选择。

HBase 数据存储的目录结构
我们搭建 HBase 数据库的是时候需要指定数据存储的路径

hbase-site.xml
<property>
    <name>hbase.rootdir</name>
    <value>/hbase</value>
</property>

上面的 /hbase 就是数据存储的目录,hbase 会在该目录下创建一系列目录,它们主要有

/.tmp : 当对表做创建或删除操作的时候,会将表移动到 /.tmp 目录下做操作。它存储的是临时需要修改的数据结构。
/WALs : 预写日志目录,它里面是被 Hlog 实例管理的 WAL 文件
/archive : 存储表的归档与快照。hbase 在分割或合并完成后,会将 Hfile 文件 移到到该目录中,然后将原来的 Hfile 删除掉,这个操作是由 master 上的一个定时任务定期去处理
/corrupt : 存储的是损坏的日志文件,一般为空,如果不为空,说明 HBase 出了一些问题
/data : HBase 数据存储的核心目录,用于存储系统表和用户表的数据
/hbase.id :  存储着 hbase 启动之后,该实例在集群中的唯一标示
/hbase.version :  集群文件格式的版本信息,即 HFile 文件的版本信息
/oldWALs  : 当 /WALs 目录中的 log 文件被持久化到存储文件中后,该日志文件会被移动到该目录中,并等待删除

说完了顶层目录,在来说一下 /data 目录,假如我们创建了一张名叫 test 表,存储在默认的 defaut 命名空间中,则 目录结构如下






HBase 架构


如上图,HBase  主要由这几块组成

region server :负责 HBase 数据的存储和管理
zookeeper : 存储 meta 表
master : 管理 region server  和 zookeeper 中的 meta 表
client : 通过 zookeeper 中的 meta 表,找到需要访问的 region server 的地址和端口号

HBase 元信息表
HBase 元信息表被称为 Mete Table ,它本身也是一张 HBase 表,所以它也有 Row Key 、列族等。Mete Table 中存储着系统中所有的 region 信息。region 是 HBase 中一个很重要的概念,它是存储用户数据的最基本单元,在之后的章节中会详细介绍。
Mete Table 存储在 ZooKeeper 上,客户端需要先访问 ZooKeeper 上Mete Table 找到数据所在的  region  地址后,在访问  region 。
meta 表结构如下



meta 表只有一个列簇 — info,它有4个列

regionInfo :当前 region 的 startKey 与 endKey,name 等信息
server:region 所在服务器及端口
serverStartCode:region server 的开始时间
seqnumDuringOpen:

值得说明的是 Row key ,它的格式为 TableName,StartKey,Timestamp.EncodedName
TableName:表名称
StartKey:表示当前 表 的 region 中存储的第一个 rowkey。如果这个地方为空的话,表明这是 table 的第一个 region。并且如果一个 region 中 StartKey 和 EndKey 都为空的话,表明这个 table 只有一个 region;
Timestamp:Region 创建的时间戳;
EncodedName:TableName,StartKey,Timestamp 字符串的 MD5 Hex 值。
当 region 发送变化时(region挂掉、region 分隔、region 合并等)HBase 的 master 服务会更新 Mete Table 。
Mete Table 相当于 hbase 表的顶级索引。


HBase 中的 LSM 存储思想

LSM
日志结构合并树 (Log-Structured Merge-Tree , LSM),并不属于某一个具体的数据结构,一般是由两个以上的数据结构组成的,并且每一个数据结构对应不同的存储介质。

举例说明,假设有这样一种 LSM ,它有两种数据结构 c0 、c1,其中 c0 存储在内存介质中,c1 存储在磁盘介质中。当有数据插入时,先暂存在 c0 中以获得更好的存储性能,当 c0 中的数据达到预设阈值时,再将数据全部持久化到c1。

这种两层的 LSM 一般都是一层数据结构存储在内存中,另一层数据结构存储在硬盘中。当然 LSM 不一定非要设计成两层,也可以设计成三层,比如 c0、c1、c2 分别存储在内存、硬盘、硬盘中,其中 c2 是对 c1 的聚合归档。
LSM 更多是一种数据结构的设计思想, 可以根据具体情况自由调整。


HBase 中的 LSM 存储实现

HBase 采用三层 LSM 存储结构


第一层 : 内存和日志
为了提高随机写性能,当用户数据到达 region server 时,不会直接写入磁盘中,而是先写入日志和内存中,写入日志中是防止内存断电等情况造成数据丢失。

第二层 : 硬盘
当内存中的数据量达到阈值,异步线程将数据刷写到硬盘上,形成 storeFile 文件

第三层 : 硬盘
随着不断的刷新,硬盘中的小文件越来越多,这不利于管理和随机查询,在合适的时机时,启动线程对小文件执行合并操作形成一个大文件。 

 
  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值