😊 如果您觉得这篇文章有用 ✔️ 的话,请给博主一个一键三连 🚀🚀🚀 吧 (点赞 🧡、关注 💛、收藏 💚)!!!您的支持 💖💖💖 将激励 🔥 博主输出更多优质内容!!!
HBase 中的列和列族
- 1.HBase 的数据模型
- 1.1 HBase 逻辑结构
- 1.2 HBase 物理存储结构
- 2.HBase 与关系型数据库的对比
- 3.HBase 是怎样存储数据的
- 3.1 宏观架构
- 3.2 RegionServer
- 3.3 Region
- 3.4 WAL
- 3.4.1 如何启用 WAL
- 3.4.2 异步写入 WAL
- 3.4.3 WAL 滚动
- 3.4.4 WAL 归档和删除
- 3.5 Store
1.HBase 的数据模型
在逻辑上,HBase 的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从 HBase 的底层物理存储结构(K-V)来看,HBase 更像是一个 multi-dimensional map
。
1.1 HBase 逻辑结构
先从一个逻辑结构模型图开始看起:
Table
(表):一个表由一个或者多个列族构成。数据的属性。比如:name
、age
、TTL
(超时时间)等等都在列族里边定义。定义完列族的表是个空表,只有添加了数据行以后,表才有数据。Column Family
(列族):在 HBase 里,可以将多个列组合成一个列族。建表的时候不用创建列,因为列是可增减变化的,非常灵活。唯一需要确定的就是列族,也就是说 一个表有几个列族是一开始就定好的。此外表的很多属性,比如数据过期时间、数据块缓存以及是否使用压缩等都是定义在列族上的,而不是定义在表上或者列上。这一点与以往的关系型数据库有很大的差别。列族存在的意义是:HBase 会把相同列族的列尽量放在同一台机器上,所以说想把某几个列放在一台服务器上,只需要给他们定义相同的列族。Row
(行):一个行包含多个列,这些列通过列族来分类。行中的数据所属的列族从该表所定义的列族中选取,不能选择这个表中不存在的列族。由于 HBase 是一个面向列存储的数据库,所以一个行中的数据可以分布在不同的服务器上。RowKey
(行键):RowKey 和 MySQL 数据库的主键比起来简单很多,RowKey 必须要有,如果用户不指定的话,会有默认的。RowKey 完全由是用户指定的一串不重复的字符串,另外,RowKey 按照字典序排序。一个 RowKey 对应的是一行数据!!!Region
:Region 就是一段数据的集合。之前提到过高表的概念,把高表进行水平切分,假设分成两部分,那么这就形成了两个 Region。注意一下 Region 的几个特性:- Region 不能跨服务器,一个 RegionServer 可以有多个 Region。
- 数据量小的时候,一个 Region 可以存储所有的数据;但是当数据量大的时候,HBase 会拆分 Region。
- 当 HBase 在进行负载均衡的时候,也有可能从一台 RegionServer 上把 Region 移动到另一服务器的 RegionServer 上。
- Region 是基于 HDFS 的,它的所有数据存取操作都是调用 HDFS 客户端完成的。
RegionServer
:RegionServer 就是存放 Region 的容器,直观上说就是服务器上的一个服务。负责管理维护 Region,详细情况后边再说!
1.2 HBase 物理存储结构
以上是一个基本的逻辑结构,底层的物理存储结构才是重中之重的内容,看下图,并且将尝试换个角度解释上边的几个概念:
具体来说:
NameSpace
:命名空间,类似于关系型数据库的 DatabBase 概念,每个命名空间下有多个表。HBase 有两个自带的命名空间,分别是hbase
和default
,hbase
中存放的是 HBase 内置的表,default
表是用户默认使用的命名空间。Row
:HBase 表中的每行数据都由一个 RowKey 和多个 Column(列)组成,数据是按照 RowKey 的字典顺序存储的,并且查询数据时只能根据 RowKey 进行检索,所以 RowKey 的设计十分重要。Column
:列,HBase 中的每个列都由Column Family
(列族)和Column Qualifier
(列限定符)进行限定,例如info: name
,info: age
。建表时,只需指明列族,而列限定符无需预先定义。TimeStamp
:时间戳,用于标识数据的不同版本(version
),每条数据写入时,如果不指定时间戳,系统会自动为其加上该字段,其值为写入 HBase 的时间。Cell
:单元格,由{RowKey, Column Family: Column Qualifier, TimeStamp}
唯一确定的单元。Cell 中的数据是没有类型的,全部是字节码形式存贮。
2.HBase 与关系型数据库的对比
传统关系型数据库的表结构图如下:
其中每行都是不可分割的,也正是体现了数据库 第一范式 的原子性,也就是说三个列必须在一起,而且要被存储在同一台服务器上,甚至是同一个文件里面。
HBase 的表架构如图所示:
HBase 的每一个行都是离散的,因为列族的存在,所以一个行里不同的列甚至被分配到了不同的服务器上。行的概念被减弱到了一个抽象的存在。在实体上,把多个列定义为一个行的关键词 RowKey,也就是行这个概念在 HBase 中的唯一体验。
HBase 的存储语句中必须精确的写出要将数据存放到哪个单元格,单元格由 表 : 列族 : 行 : 列 来唯一确定。用人话说就是要写清楚数据要被存储在哪个表的哪个列族的哪个行的哪个列。如果一行有 10 10 10 列,那么存储一行的数据就需要写明 10 10 10 行的语句。
3.HBase 是怎样存储数据的
如果上边所有的概念还是不甚清楚,那么接下来的架构深入探索将会让你有一个更透彻的理解。
HBase 是一个数据库,那么数据肯定是以某种实体形式存储在硬盘上的,先来看一下 HBase 是怎样存储数据的。使用 “显微镜” 逐步放大 HBase 的架构,从最宏观的 Master 和 RegionServer 结构,一直到最小的单元格 Cell。一边使用图示直观的感受,一边配上文字解释说明!!!
3.1 宏观架构
宏观架构图示如下:
从这张图上可以看到是一个 HBase 集群由一个 Master(也可配置成多个,HA)和多个 RegionServer 组成,之后再详细介绍 RegionServer 的架构。上面的图示说明了 HBase 的服务器角色构成,下边给出具体的介绍:
Master
:负责启动的时候分配 Region 到具体的 RegionServer,执行各种管理操作,比如 Region 的分割与合并。在 HBase 中,Master 的角色地位比其他类型的集群弱很多。数据的读写操作与它没有关系,它挂了之后,集群照样运行。具体的原因后边后详细介绍。但是 Master 也不能宕机太久,有很多必要的操作,比如创建表、修改列族配置,以及更重要的分割与合并都需要它的操作。RegionServer
:RegionServer 就是一台机器,在它上边有多个 Region。我们读写的数据就存储在 Region 中。Region
:它是表拆分出来的一部分,HBase 是一个会自动切片的数据库。当数据库过高时,就会进行拆分。HDFS
:HBase 的数据存储是基于 HDFS 的,它是真正承载数据的载体。Zookeeper
:在本集群中负责存储hbase:meata
的位置存储信息,客户端在写数据时需要先读取元数据信息。
3.2 RegionServer
在宏观架构图的最后一个 RegionServer 中可以看到 ,它的内部是多个 Region 的集合:
现在我们放大一下这个 RegionServer 的内部架构:
从这幅图中我们可以看到一个 RegionServer 包含一下几个部分,详细说一说:
- 一个 WAL:WAL 是
Write-Ahead Log
的缩写,翻译为 预写入日志。从名字大概也能猜出它的作用,当操作到达 Region 的时候,HBase 先把操作写入到 WAL 中,然后把数据放入到基于内存实现的 MemStore 中,等到一定的时机再把数据刷写形成 HFile 文件,存储到 HDFS 上。WAL 是一个保险机制,数据在写到 MemStore 之前被先写到 WAL 中,这样如果在刷写过程中出现事故,可以从 WAL 恢复数据。 - 多个 Region:Region 已经多次提到了,它就是数据库的一部分,每一个 Region 都有起始的 RowKey 和结束的 RowKey,代表了它存储的 Row 的范围。
3.3 Region
我们再放大Region的内部结构:
从图中可以看的出来,一个 Region 包含
- 多个 Store:一个 Region 有多个 Store,其实一个 Store 就是对应一个列族的数据,如图就有三个列族。从最后一个 Store 中我们又可以看出,Store 是由
MemStore
和HFile
组成的,后边会有详细说明。
3.4 WAL
预写入日志 就是设计来解决宕机之后的操作恢复问题的,WAL 是保存在 HDFS 上的持久化文件。数据到达 Region 的时候,先写入 WAL,然后被加载到 MemStore 中。这样就算 Region 宕机了,操作没来得及执行持久化,也可以再重启的时候从 WAL 加载操作并执行。
3.4.1 如何启用 WAL
WAL 是默认开启的,也可以手动关闭它,这样增删改操作会快一点。但是这样做牺牲的是数据的安全性,所以不建议关闭。
关闭方法:
Mutation.setDurability(Durability.SKIP_WAL)
3.4.2 异步写入 WAL
如果不惜通过关闭 WAL 来提高性能的话,还可以考虑一下折中的方案:异步写入 WAL。
正常情况下,客户端提交的 put
、delete
、append
操作来到 Region 的时候,先调用 HDFS 的客户端写到 WAL 中。哪怕只有一个改动,也会调用 HDFS 的接口来写入数据。可以想象到,这种方式尽可能的保证了数据的安全性,代价是频繁的消耗资源。
如果不想关闭 WAL,又不想每次都耗费那么大的资源,每次改动都调用 HDFS 客户端,可以选择异步的方式写入 WAL:
Mutation.setDurability(Durability.ASYNC_WAL)
这样设定以后,Region 会等到条件满足的时候才将操作写到 WAL。这里的条件指的是隔多久,写一次,默认的时间间隔是 1 1 1 s。
如果异步写入数据的时候出错了怎么办呢?比如客户端的操作现在在 Region 内存中,由于时间间隔未到 1 1 1 s,操作还没来得及写入到 WAL,Region 挂了(邪门不?就差那么一丢丢不到 1 1 1 s)。出错了是没有任何事务可以保证的。
3.4.3 WAL 滚动
之前学习过 MapReduce 的 shuffle
机制,所以猜得到 WAL 是一个唤醒的滚动日志数据结构,因为这种结构不会导致占用的空间持续变大,而且写入效率也最高。
通过 WAL 日志切换,这样可以避免产生单独的过大的 WAL 日志文件,这样可以方便后续的日志清理(可以将过期日志文件直接删除)。另外如果需要使用日志进行恢复时,也可以同时解析多个小的日志文件,缩短恢复所需时间。
WAL 的检查间隔由 hbase.regionserver.logroll.period
定义,默认值是一个小时。检查的内容是把当前 WAL 中的操作跟实际持久化到 HDFS 上的操作做比较,看哪些操作已经被持久化了,如果已经被持久化了,该 WAL 就会被移动到 HDFS 上的 .oldlogs
文件夹下。
一个 WAL 实例包含多个 WAL 文件。WAL 文件的最大数量可以手动通过参数配置。
其它的触发滚动的条件是:
- WAL 的大小超过了一定的阈值。
- WAL 文件所在的 HDFS 文件块快要满了。
3.4.4 WAL 归档和删除
归档:WAL 创建出来的文件都会放在 /hbase/.log
下,在 WAL 文件被定为归档时,文件会被移动到 /hbase/.oldlogs
下。
删除:判断此 WAL 文件是否不再需要,是否没有被其他引用指向这个 WAL 文件。
会引用此文件的服务:
- TTL 进程:该进程会保证 WAL 文件一直存活,直到达到
hbase.master.logcleaner.ttl
定义的超时时间(默认 10 10 10 分钟)为止。 - 备份机制(
replication
):如果你开启了 HBase 的备份机制,那么 HBase 要保证备份集群已经完全不需要这个 WAL 文件了,才会删除这个 WAL 文件。这里提到的replication
不是文件的备份数,而是 0.90 0.90 0.90 版本加入的特性,这个特性用于把一个集群的数据实时备份到另外一个集群。如果你的手头就一个集群,可以不用考虑这个因素。
只有当该 WAL 文件没有被以上两种情况引用的时候,才会被系统彻底清除掉。
3.5 Store
解释完了 WAL,放大一下 Store 的内部架构:
Store 有两个重要的部分:
MemStore
:每个 Store 都有一个 MemStore 实例。数据写入到 WAL 之后就会被放入 MemStore 中。MemStore 是内存的存储对象,只有到达一定的时机才会被刷写到 HFile 中去。什么时机呢?后边详细说明,这篇笔记主要记录架构方面的内容。HFile
:在 Store 中有多个 HFile,每次刷写都会形成一个 HFile 文件落盘在 HDFS 上。HFile 直接跟 HDFS 打交道,它是数据存储的实体。
这里提出一点疑问:
客户端的操作到达 Region 时,先将数据写到 WAL 中,而 WAL 是存储在 HDFS 上的。所以就相当于数据已经持久化了,那么为什么还要从 WAL 加载到 MemStore 中,再刷写形成 HFile 存到 HDFS 上呢?
简单的说就是:数据进入 HFile 之前就已经被持久化了,为什么还要放入 MemStore?
这是因为 HDFS 支持文件的创建、追加、删除,但是不能修改。对于一个数据库来说,数据的顺序是非常重要的。第一次 WAL 的持久化是为了保证数据的安全性,无序的。再读取到 MemStore 中,是为了排序后存储。所以 MemStore 的意义在于维持数据按照 RowKey 的字典序排列,而不是做一个缓存提高写入效率。
补一张图,对比着来看,关于 MemStore 刷写: