书籍介绍:https://book.douban.com/subject/30115996/
初识HBase
列式存储与行式存储
列式存储是相对于传统关系型数据库的行式存储来说的
- 两者的区别就是如何组织表
- 行存储是将各行放入连续的物理位置,很像传统的记录和文件系统
- 列存储法是将数据按照列存储到数据库中,与行存储类似
列式存储的主要优点之一:
- 大幅降低系统的I/O,尤其是在海量数据查询时
下图是两种存储方法的图形化解释:
什么时候使用HBase?
HBase 的存储基于
Hadoop
,Hadoop
实现了一个分布式文件系统(HDFS
)
HDFS
有高容错性特点:
- 被设计用来部署在低廉的硬件上,而且它提供高吞吐量以访问应用程序的数据,适合那些有着超大数据集的应用程序
HBase
采用Key/Value
的存储方式,这意味着,即使随着数据量增大,也几乎不会导致查询的性能下降
HBase
是一个列式数据库,当你的表字段很多的时候:
- 可以把其中几个字段放在集群的一部分机器上,而另外几个字段放到另外一部分机器上,充分分散了负载压力
如此复杂的存储结构和分布式的存储方式带来的代价是:
- 只是存储少量数据,它也不会很快
HBase
并不快,只是当数据量很大的时候它慢的不明显不适合使用 HBase 的场景:
- 主要需求是数据分析,比如做报表
- 单表数据量不超过千万
适合使用 HBase 的场景:
- 单表数据量超千万,而且并发还挺高
- 数据分析需求较弱,或者不需要那么灵活或者实时
部署架构
HBase 有两种服务器:
- Master 服务器和
RegionServer
服务器,一般一个 HBase 集群有一个 Master 服务器和多个RegionServer
服务器- Master 服务器负责维护表结构信息
实际的数据都存储在
RegionServer
服务器上:
RegionServer
是直接负责存储数据的服务器RegionServer
保存的表数据直接存储在 Hadoop 的HDFS
上HBase 有一点很特殊:
- 客户端获取数据由客户端直连
RegionServer
的,当 Master 挂掉之后依然可以查询数据,只是丧失了表管理相关的能力RegionServer 非常依赖
ZooKeeper
服务,没有 ZooKeeper 就没有 HBaseZooKeeper 管理了 HBase 所有
RegionServer
的信息,包括具体的数据段存放在哪个 RegionServer 上客户端每次与 HBase 连接,其实都是先与
ZooKeeper
通信:
- 查询出哪个
RegionServer
需要连接,然后再连接 RegionServer
Region
Region 是一段数据的集合,HBase 中的表一般拥有一个到多个
Region
Region 特性:
- Region 不能跨服务器,一个 RegionServer 上有一个或者多个 Region
- 数据量小的时候,一个 Region 足以存储所有数据,但是,当数据量大的时候,HBase 会拆分 Region
- 当 HBase 在进行负载均衡的时候,也有可能会从一台
RegionServer
上把 Region 移动到另一台 RegionServer 上- Region 是基于 HDFS 的,它的所有数据存取操作都是调用了
HDFS
的客户端接口来实现的
RegionServer
RegionServer 就是存放 Region 的容器,就是服务器上的一个服务
- 一般来说,一个服务器只会安装一个 RegionServer 服务
当客户端从 ZooKeeper 获取
RegionServer
的地址后,会直接从 RegionServer 获取数据
Master
客户端从 ZooKeeper 获取了
RegionServer
的地址后,会直接从 RegionServer 获取数据
- 不光是获取数据,包括插入、删除等所有的数据操作都是直接操作
RegionServer
,而不需要经过 MasterMaster 只负责各种协调工作,比如建表、删表、移动 Region、合并等操作
- 它们的共性就是需要跨 RegionServer,这些操作由哪个 RegionServer 来执行都不合适
- HBase 就将这些操作放到了Master上了
这种结构的好处是大大降低了集群对 Master 的依赖,而 Master 节点一般只有一个到两个,一旦宕机:
- 如果集群对 Master 的依赖度很大,那么就会产生单点故障问题
在 HBase 中,即使 Master 宕机了,集群依然可以正常地运行,依然可以存储和删除数据
存储架构
最基本的存储单位是列,一个列或者多个列形成一行
传统数据库是严格的行列对齐,比如这行有三个列 a、b、c,下一行肯定也有三个列 a、b、c
- 而在
HBase
中,这一行有三个列 a、b、c,下一个行也许是有 4 个列 a、e、f、g在
HBase
中,行跟行的列可以完全不一样,这个行的数据跟另外一个行的数据也可以存储在不同的机器上:
- 甚至同一行内的列也可以存储在完全不同的机器上
每个行都拥有唯一的行键(
row key
)来标定这个行的唯一性
- 每个列都有多个版本,多个版本的值存储在单元格(
cell
)中,若干个列又可以被归类为一个列族
行键(rowkey)
rowkey 是由用户指定的一串不重复的字符串,
rowkey
会直接决定这个 row 的存储位置HBase 中无法根据某个 column 来排序,系统永远是根据 rowkey 来排序的(根据字典排序)
rowkey
就是决定 row 存储顺序的唯一凭证如果插入 HBase 的时候,不小心用了之前已经存在的 rowkey,这会把之前存在的那个 row 更新掉
之前已经存在的值会被放到这个单元格的历史记录里面,并不会丢掉,只是你需要带上版本参数才可以找到这个值
- 一个列上可以存储多个版本的单元格(
cell
),单元格就是数据存储的最小单元一个 Region 就是多个行(Row)的集合,在 Region 中行的排序按照行键(
rowkey
)字典排序
列族(column family)
若干列可以组成列族(
column family
),建表的时候有几个列族是一开始就定好的
- 表的很多属性,比如过期时间、数据块缓存以及是否压缩等都是定义在列族上,而不是定义在表上或者列上
同一个表里的不同列族可以有完全不同的属性配置,但是同一个列族内的所有列都会有相同的属性
因为他们都在一个列族里面,而属性都是定义在列族上的
一个没有列族的表是没有意义的,因为列必须依赖列族而存在
列名称的规范是列族:列名,比如
brother:age、brother:name
列族存在的意义:
- HBase 会把相同列族的列尽量放在同一台机器上,如果想让某几个列被放到一起,你就给他们定义相同的列族
一个表要设置多少个列族合适?
官方的建议是:越少越好(一般来说一个就够用了),因为 HBase 并不希望大家指定太多的列族
列族太多会极大程度地降低数据库性能,此外,列族定得太多,容易出 BUG
单元格(cell)
一个列上可以存储多个版本的值,多个版本的值被存储在多个单元格里面
- 多个版本之间用版本号(
Version
)来区分,唯一确定一条结果的表达式是:
行键:列族:列:版本号(rowkey:column family:column:version)
版本号是可以省略的,如果不写版本号,
HBase
默认返回最后一个版本的数据
让HBase跑起来
基本架构
HBase 中有一个 Master 用来管理元数据:
- 就像
Hadoop
中的 namenodeRegionServer 是用来存储数据的:
- 相当于
Hadoop
中的 datanodeZooKeeper 负责维护 HBase 的所有节点,如果
ZooKeeper
宕掉了,一个节点都连不上生产环境下的完全部署模式是基于 HDFS 的,使用 HDFS 来存储数据
- 但是在单机模式下 HBase 可以直接使用普通文件系统来存储数据
在使用中就算把 Master 关掉了,依旧可以从 HBase 中读取数据和写入数据,只是不能建表或者修改表
- 因为客户端读取数据的时候只是跟 ZooKeeper 和
RegionServer
交互,所以,ZooKeeper 甚至比 Master 还重要
数据块编码
数据块编码主要是针对 Key/Value 中的
Key
进行编码,减少Key
存储所占用的空间
- 因为很多 Key 的前缀都是重复的
可以看到这么多行的 Key 其实有很大一部分的字符是重复的
如果只存储递进值,就可以避免存储重复的前缀,这就是前缀编码(Prefix)
前缀编码(Prefix)
如果使用前缀编码作为数据块编码方式,那么它只会存储第一个
Key
的完整字符串后面的 key 只存储跟第一个 key 的差异字符
快速差异编码(Fast Diff)
快速差异编码(
Fast Diff
)借鉴了 Diff 编码的思路,也考虑到了差异编码速度慢的致命缺陷
Fast Diff
的实现比Diff
更快,也是比较推荐的算法由于还是有很多计算过程存在,所以快速差异算法的速度依然属于比较慢的
压缩器
压缩器的作用是可以把
HBase
的数据按压缩的格式存储,这样可以更节省磁盘空间
- 是可选的,不过推荐大家还是安装
Snappy
压缩器,这是 HBase 官方目前排名比较高的压缩器
基本操作
新建表(create)
新建表需要注意的几点:
- HBase 的表都是由列族(
Column Family
)组成的- 没有列族的表是没有意义的
- 列并不是依附于表上,而是依附于列族上
# 新建一个表 'test',包含了一个列族 'cf'
# HBase 新建表时,至少需要一个列族
create 'test', 'cf'
查看数据库表(list)
hbase(main):010:0> list
TABLE
test
test1
test3
3 row(s)
Took 0.0048 seconds
=> ["test", "test1", "test3"]
查看表属性(describe)
hbase(main):018:0> describe "test"
删除表(drop)
在删除 HBase 表之前的时候,必须先执行停用(disable)命令
- 因为可能有很多客户端现在正好连着,而且也有可能
HBase
正在做合并或者分裂操作如果你这时删除了表,会造成无法恢复的错误
HBase
也不会让你直接就删除表,而是需要先做一个 disable 操作,意思是把这个表停用掉,并且下线
hbase(main):019:0> disable "test"
Took 0.8052 seconds
hbase(main):020:0> drop "test"
Took 0.4512 seconds
hbase(main):021:0>
修改表(alter)
可以使用
alter
命令对表进行修改,修改时无需禁用表
- 但是强烈建议在生产环境下执行这个命令之前,最好先停用(disable)这个表
因为对列族的所有操作都会同步到 所有拥有这个表的
RegionServer
上
- 当有很多客户端都在连着的时候,直接新增一个列族对性能的影响较大(还有可能出现意外的问题)
# 修改多个属性
alter 't1', 'f1', {NAME => 'f2', IN_MEMORY => true}, {NAME => 'f3', VERSIONS => 5}
# 新增列族
alter 't1', 'cf2'
# 删除列族
alter 't1', NAME => 'f1', METHOD => 'delete'
插入(put)
HBase
中行的每一个列都存储在不同的位置,插入数据时必须指定要存储在哪个单元格而单元格需要根据表、行、列这几个维度来定位
- 因此插入数据的时候必须指定把数据插入到哪个表的哪个列族的哪个行的哪个列
hbase(main):024:0> put 'test1','row1','cf:name','jack'
Took 0.0838 seconds
hbase(main):025:0> scan 'test1'
ROW COLUMN+CELL
row1 column=cf:name, timestamp=1543161899520, value=jack
1 row(s)
Took 0.0301 seconds
向 test 表插入一个单元格,这个单元格的 rowkey 为 row1,该单元格的列族为 cf,该单元格的列名为 name,数据值为 jack
插入成功后,使用
scan
命令查看表中数据,可以看到表中有一条记录,ROW 列显示的就是rowkey
COLUMN+CELL 显示的就是这个记录的:
- 具体列族、列、时间戳(timestamp)、值(value)信息
- 时间戳:
- 每一个单元格都可以存储多个版本(version)的值
- HBase 的单元格并没有 version 这个属性
- 它用 timestamp 来存储该条记录的时间戳,这个时间戳就用来当版本号使用
- 这个 timestamp 虽然说是时间的标定,其实你可以输入任意的数字,比如 1、2、3 都可以存储进去
- 列族和列的标识:
- HBase 并没有专门的一个列族的栏来显示列族这个属性,它总是把列族和列用
列族:列
的组合方式来一起显示- 无论是
put
存储还是scan
的查询使用的列定义,都是列族:列
的格式
获取单条数据(get)
get 只能查询一个单元格的记录,在表的数据很大的时候,get 查询的速度远远高于
scan
get 'test','row7',{COLUMN=>'cf:name',VERSIONS=>5}
COLUMN CELL
cf:name timestamp=3, value=wangwu
cf:name timestamp=2, value=lisi
cf:name timestamp=1, value=zhangsan
查询多条数据(scan)
Scan 是最常用的查询表数据的命令,这个命令相当于传统数据库的
select
在 HBase 中用起始行(STARTROW)和结束行(ENDROW)来限制显示记录的条数
STARTROW 和 ENDROW 都是可选的参数,可以不输入
如果 ENDROW 不输入的话,就从 STARTROW 开始一直显示下去直到表的结尾
如果 STARTROW 不输入的话,就从表头一直显示到 ENDROW 为止
scan 'test',{STARTROW=>'row3'}
ROW COLUMN+CELL
row3 column=cf:name, timestamp=1471112677398, value=alex
row4 column=cf:name, timestamp=1471112686290, value=jim
scan 'test',{ENDROW=>'row4'}
ROW COLUMN+CELL
row2 column=cf:name, timestamp=2222222222222, value=billy
row3 column=cf:name, timestamp=1471112677398, value=alex
删除单元格数据(delete)
HBase 删除记录并不是真的删除了数据,而是放置了一个墓碑标记(
tombstone marker
)
把这个版本连同之前的版本都标记为不可见了
这是为了性能着想,这样 HBase 就可以定期去清理这些已经被删除的记录,而不用每次都进行删除操作
定期 的时间点是在 HBase 做自动合并(
compaction
):
HBase 整理存储文件时的一个操作,会把多个文件块合并成一个文件
这样删除操作对于
HBase
的性能影响被降到了最低,就算在很高的并发负载下大量删除记录也不怕了在记录被真正删除之前还是可以查询到的
- 只需要在 scan 命令后跟上
RAW=>true
参数和适当的 VERSIONS 参数:
- 就可以看到被打上墓碑标记(
tombstone marker
)的记录- 跟上 RAW 就是查询到表的所有未经过过滤的原始记录
# 删除某一单元格数据
delete 'test','row4','cf:name'
# 根据版本删除数据(删除这个版本之前的所有版本)
delete't1','r1','c1',ts
删除整行数据(deleteall)
如果一个行有很多列,用 delete 来删除记录会把人累死,可以
deleteall
命令来删除整行记录
# 只需要明确到 rowkey 即可
deleteall 'test','row3'
内部探险
数据模型
Namespace(表命名空间):将多个表分到一个组进行统一管理
Table
(表):
- 一个表由一个或者多个列族组成
- 数据属性比如:
- 超时时间(
TTL
),压缩算法(COMPRESSION
)等,都在列族的定义中定义- 定义完列族后表是空的,只有添加了行,表才有数据
Row(行):
- 一个行包含了多个列,这些列通过列族来分类
- 行中的数据所属列族只能从该表所定义的列族中选取
- 由于 HBase 是一个列式数据库,所以一个行中的数据可以分布在不同的服务器上
Column Family(列族):
列族是多个列的集合,HBase 会尽量把同一个列族的列放到同一个服务器上
- 这样可以提高存取性能,并且可以批量管理有关联的一堆列
所有的数据属性都是定义在列族上
在
HBase
中,建表定义的不是列,而是列族Column Qualifier(列):
- 多个列组成一个行,列族和列经常用
Column Family: Column Qualifier
来一起表示
- 列是可以随意定义的,一个行中的列不限名字、不限数量。
Cell(单元格):
一个列中可以存储多个版本的数据,而每个版本就称为一个单元格(Cell)
- 所以在 HBase 中的单元格跟传统关系型数据库的单元格概念不一样
HBase 中的数据细粒度比传统数据结构更细一级,同一个位置的数据还细分成多个版本
Timestamp(时间戳/版本号):
- 既可以把它称为是时间戳,也可以称为是版本号,因为它是用来标定同一个列中多个单元格的版本号的
- 不指定版本号的时候,系统会自动采用当前的时间戳来作为版本号
- 当手动定义了一个数字来当作版本号的时候,这个
Timestamp
就真的是只有版本号的意义了
HBase是否支持表关联?
官方给出的答案是干脆的,那就是不支持
如果想实现数据之间的关联,就必须自己去实现了,这是挑选
NoSQL
数据库必须付出的代价
HBase 是否支持 ACID?
ACID 是事务正确执行的保证,HBase 部分支持 了
ACID
表命名空间有什么用?
表命名空间主要是用来对表分组,那么对表分组有什么用?
命名空间可以填补 HBase 无法在一个实例上分库的缺憾
- 通过命名空间我们可以像关系型数据库一样将表分组,对于不同的组进行不同的环境设定,比如配额管理、安全管理等
HBase 中有两个保留表空间是预先定义好的:
hbase
:系统表空间,用于组织 HBase 内部表default
:那些没有定义表空间的表都被自动分配到这个表空间下
架构回顾
一个
HBase
集群由一个 Master(也可以把两个 Master 做成 HighAvailable)和多个 RegionServer 组成:
- Master:负责启动的时候分配
Region
到具体的 RegionServer,执行各种管理操作
- 比如 Region 的分割和合并
- HBase 中的 Master 的角色功能比其他类型集群弱很多,
HBase
的 Master 很特别
- 因为数据的读取和写入都跟它没什么关系,它挂了业务系统照样运行
- 当然 Master 也不能宕机太久,有很多必要的操作
- 比如创建表、修改列族配置,以及更重要的分割和合并都需要它的操作
- RegionServer:
- RegionServer 上有一个或者多个 Region
- 我们读写的数据就存储在 Region 上
- 如果你的 HBase 是基于 HDFS 的(单机 HBase 可基于本地磁盘)
- 那么 Region 所有数据存取操作都是调用了
HDFS
的客户端接口来实现的- Region:
- 表的一部分数据,HBase 是一个会自动分片的数据库
- 一个 Region 就相当于关系型数据库中分区表的一个分区,或者
MongoDB
的一个分片- HDFS:
- HBase 并不直接跟服务器的硬盘交互,而是跟 HDFS 交互,所以 HDFS 是真正承载数据的载体
- ZooKeeper:
- ZooKeeper 在 HBase 中的比 Master 更重要,把 Master 关掉业务系统照样跑,能读能写
- 但是把 ZooKeeper 关掉,就不能读取数据了
- 因为读取数据所需要的元数据表
hbase:meata
的位置存储在 ZooKeeper 上
RegionServer 内部架构
一个 RegionServer 包含有:
- 一个 WAL:预写日志,WAL 是 Write-Ahead Log 的缩写,就是:
- 预先写入:
- 当操作到达 Region 的时候,HBase 先把操作写到 WAL 里面去
- HBase 会把数据放到基于内存实现的 Memstore 里
- 等数据达到一定的数量时才刷写(
flush
)到最终存储的 HFile 内
- 而如果在这个过程中服务器宕机或者断电了,那么数据就丢失了
- WAL 是一个保险机制,数据在写到 Memstore 之前
- 先被写到 WAL 了,这样当故障恢复的时候依旧可以从 WAL 中恢复数据
- 多个 Region:
- Region 相当于一个数据分片,每一个 Region 都有起始 rowkey 和结束
rowkey
,代表了它所存储的 row 范围
Region 内部架构
每一个 Region 内都包含有多个 Store 实例,一个 Store 对应一个列族的数据
- 如果一个表有两个列族,那么在一个 Region 里面就有两个 Store
- Store 内部有
MemStore
和 HFile 这两个组成部分
预写日志(WAL)
预写日志(Write-ahead log,WAL):
- 就是设计来解决宕机之后的操作恢复问题的,数据到达 Region 的时候是先写入 WAL
- 然后再被加载到
Memstore
,就算 Region 的机器宕掉了,由于 WAL 的数据是存储在 HDFS 上的,所以数据并不会丢失如果你的系统对性能要求极高、对数据一致性要求不高,并且系统的性能瓶颈出现在 WAL 上的时候
- 可以考虑使用异步写入
WAL
,否则,使用默认的配置即可。
WAL 滚动
WAL 是一个环状的滚动日志结构,因为这种结构写入效果最高,而且可以保证空间不会持续变大
Store 内部结构
在 Store 中有两个重要组成部分:
- MemStore:每个 Store 中有一个 MemStore 实例,数据写入 WAL 之后就会被放入 MemStore
- MemStore 是内存的存储对象,只有当 MemStore 满了的时候才会将数据刷写(flush)到 HFile 中
- HFile:在 Store 中有多个 HFile,当 MemStore 满了之后 HBase 就会在 HDFS 上生成一个新的 HFile
- 然后把 MemStore 中的内容写到这个 HFile 中
- HFile 直接跟 HDFS 打交道,它是数据的存储实体
WAL 是存储在 HDFS 上的,Memstore 是存储在内存中的,HFile 又是存储在 HDFS 上的
- 数据是先写入 WAL,再被放入 Memstore,最后被持久化到 HFile 中
数据在进入
HFile
之前已经被存储到 HDFS 一次了,为什么还需要被放入 Memstore?
- 这是因为 HDFS 上的文件只能创建、追加、删除,但是不能修改
对于一个数据库来说,按顺序地存放数据是非常重要的,这是性能的保障,所以不能按照数据到来的顺序来写入硬盘
- 可以使用内存先把数据整理成顺序存放,然后再一起写入硬盘,这就是 Memstore 存在的意义
虽然
Memstore
是存储在内存中的,HFile 和 WAL 是存储在 HDFS 上的
但由于数据在写入 Memstore 之前,要先被写入 WAL
所以增加 Memstore 的大小并不能加速写入速度
Memstore 存在的意义是维持数据按照
rowkey
顺序排列,而不是做一个缓存
MemStore
设计 MemStore 的原因有以下几点:
- 由于 HDFS 上的文件不可修改,为了让数据顺序存储从而提高读取效率
- HBase 使用了
LSM
树结构来存储数据,数据会先在 Memstore 中整理成 LSM 树,最后再刷写到 HFile 上- 优化数据的存储,比如一个数据添加后就马上删除了
- 这样在刷写的时候就可以直接不把这个数据写到 HDFS 上
不过不要想当然地认为读取也是先读取 Memstore 再读取磁盘
读取的时候是有专门的缓存叫 BlockCache
- 这个 BlockCache 如果开启了,就是先读
BlockCache
,读不到才是读 HFile+Memstore
HFile(StoreFile)
HFile 是数据存储的实际载体,我们创建的所有表、列等数据都存储在 HFile 里面
HFile 是由一个一个的块组成的
- 在
HBase
中一个块的大小默认为 64KB,由列族上的 BLOCKSIZE 属性定义这些块区分了不同的角色:
- Data:数据块
- 每个 HFile 有多个 Data 块,我们存储在 HBase 表中的数据就在这里,Data 块其实是可选的
- 但是几乎很难看到不包含 Data 块的 HFile
- Meta:元数据块。
- Meta 块是可选的,Meta 块只有在文件关闭的时候才会写入
- Meta 块存储了该 HFile 文件的元数据信息
- 在 v2 之前布隆过滤器(Bloom Filter)的信息直接放在 Meta 里面存储,v2 之后分离出来单独存储
- FileInfo:
- 文件信息,其实也是一种数据存储块
- FileInfo 是 HFile 的必要组成部分,是必选的,它只有在文件关闭的时候写入,存储的是这个文件的信息
- 比如最后一个 Key(LastKey),平均的 Key 长度(AvgKeyLen)等
- DataIndex:
- 存储 Data 块索引信息的块文件
- 索引的信息其实也就是 Data 块的偏移值(offset),DataIndex 也是可选的,有 Data 块才有 DataIndex
- MetaIndex:
- 存储 Meta 块索引信息的块文件
- MetaIndex 块也是可选的,有 Meta 块才有 MetaIndex
- Trailer:必选的,它存储了 FileInfo、DataIndex、MetaIndex 块的偏移值
在物理存储上我们管
MemStore
刷写而成的文件叫 HFile,StoreFile 就是 HFile 的抽象类而已
Data 数据块
Data 数据块的第一位存储的是块的类型,后面存储的是多个 KeyValue 键值对
- 也就是单元格(
Cell
)的实现类,Cell 是一个接口,KeyValue 是它的实现类
KeyValue 类
一个 KeyValue 类里面最后一个部分是存储数据的 Value,而前面的部分都是存储跟该单元格相关的元数据信息
如果你存储的 value 很小,那么这个单元格的绝大部分空间就都是
rowkey、column family、column
等的元数据
- 所以大家的列族和列的名字如果很长,大部分的空间就都被拿来存储这些数据了
不过如果采用适当的压缩算法就可以极大地节省存储列族、列等信息的空间了
- 所以在实际的使用中,可以通过指定压缩算法来压缩这些元数据
不过压缩和解压必然带来性能损耗,所以使用压缩也需要根据实际情况来取舍
- 如果你的数据主要是归档数据,不太要求读写性能,那么压缩算法就比较适合你
增删查改的真正面目
HBase 是一个可以随机读写的数据库,而它所基于的持久化层 HDFS 却是要么新增,要么整个删除,不能修改的系统
那 HBase 怎么实现我们的增删查改的?
真实的情况是这样的:HBase 几乎总是在做新增操作。
- 当你新增一个单元格的时候,HBase 在 HDFS 上新增一条数据
- 当你修改一个单元格的时候,HBase 在 HDFS 又新增一条数据,只是版本号比之前那个大(或者你自己定义)
- 当你删除一个单元格的时候,HBase 还是新增一条数据
- 只是这条数据没有 value,类型为 DELETE,这条数据叫墓碑标记(
Tombstone
)由于数据库在使用过程中积累了很多增删查改操作,数据的连续性和顺序性必然会被破坏
- 为了提升性能,HBase 每间隔一段时间都会进行一次合并(
Compaction
),合并的对象为 HFile 文件合并分为
minor compaction
和major compaction
- 在 HBase 进行 major compaction 的时候,它会把多个 HFile 合并成 1 个 HFile
- 在这个过程中,一旦检测到有被打上墓碑标记的记录,在合并的过程中就忽略这条记录
- 这样在新产生的 HFile 中,就没有这条记录了,自然也就相当于被真正地删除了
数据结构总结
HBase 数据的内部结构大体如下:
- 一个 RegionServer 包含多个 Region,划分规则是:
- 一个表的一段键值在一个
RegionServer
上会产生一个 Region- 不过当某一行的数据量太大了(要非常大),HBase 也会把这个 Region 根据列族切分到不同的机器上去
- 一个 Region 包含多个
Store
,划分规则是:
- 一个列族分为一个
Store
,如果一个表只有一个列族
- 那么这个表在这个机器上的每一个 Region 里面都只有一个 Store
- 一个 Store 里面只有一个 Memstore
- 一个 Store 里面有多个 HFile,每次 Memstore 的刷写(flush)就产生一个新的 HFile 出来
KeyValue 的写入和读出
一个 KeyValue 被持久化到
HDFS
的过程的如下:
- WAL:数据被发出之后第一时间被写入 WAL,由于 WAL 是基于 HDFS 来实现的
- 所以也可以说现在单元格就已经被持久化了,但是 WAL 只是一个暂存的日志,它是不区分
Store
的
- 这些数据是不能被直接读取和使用
- Memstore:数据随后会立即被放入
Memstore
中进行整理,Memstore 会负责按照 LSM 树的结构来存放数据
- 这个过程就像我们在打牌的时候,抓牌之后在手上对牌进行整理的过程
- HFile:最后,当 Memstore 太大了达到尺寸上的阀值,或者达到了刷写时间间隔阀值的时候
- HBaes 会把这个
Memstore
的内容刷写到 HDFS 系统上,称为一个存储在硬盘上的 HFile 文件- 至此,我们可以称为数据真正地被持久化到硬盘上,就算宕机,断电,数据也不会丢失了
读出
由于有 MemStore(基于内存)和 HFile(基于HDFS)这两个机制
你一定会立马想到先读取 MemStore,如果找不到,再去
HFile
中查询。这是显而易见的机制
- 可惜 HBase 在处理读取的时候并不是这样的
实际的读取顺序是先从
BlockCache
中找数据,找不到了再去Memstore
和 HFile 中查询数据墓碑标记和数据不在一个地方,读取数据的时候怎么知道这个数据要删除呢?
如果这个数据比它的墓碑标记更早被读到,那在这个时间点真是不知道这个数据会被删除
- 只有当扫描器接着往下读,读到墓碑标记的时候才知道这个数据是被标记为删除的,不需要返回给用户
所以 HBase 的 Scan 操作在取到所需要的所有行键对应的信息之后还会继续扫描下去
- 直到被扫描的数据大于给出的限定条件为止,这样它才能知道哪些数据应该被返回给用户,而哪些应该被舍弃
所以你增加过滤条件也无法减少 Scan 遍历的行数
- 只有缩小 STARTROW 和 ENDROW 之间的行键范围才可以明显地加快扫描的速度
在 Scan 扫描的时候 store 会创建 StoreScanner 实例,StoreScanner 会把 MemStore 和 HFile 结合起来扫描
- 所以具体从 MemStore 还是 HFile 中读取数据,外部的调用者都不需要知道具体的细节
当 StoreScanner 打开的时候,会先定位到起始行键(STARTROW)上,然后开始往下扫描
其中红色块部分都是属于指定 row 的数据
- Scan 要把所有符合条件的
StoreScanner
都扫描过一遍之后才会返回数据给用户
Region的定位
Region 的查找,早期的设计(0.96.0)之前是被称为三层查询架构:
Region:查找的数据所在的 Region
.META.:
- 是一张元数据表,它存储了所有 Region 的简要信息,
.META.
表中的一行记录就是一个 Region
- 该行记录了该 Region 的起始行、结束行和该 Region 的连接信息
- 这样客户端就可以通过这个来判断需要的数据在哪个
Region
上-ROOT-:
- 是一张存储
.META.
表的表,.META.
可以有很多张
- 而
-ROOT-
就是存储了.META.
表在什么 Region 上的信息(.META.
表也是一张普通的表,也在 Region 上)通过两层的扩展最多可以支持约 171 亿个 Region
-ROOT-
表记录在 ZooKeeper 上,路径为:/hbase/root-region-server
Client 查找数据的流程从宏观角度来看是这样的:
- 用户通过查找 zk(ZooKeeper)的
/hbase/root-regionserver
节点来知道-ROOT-
表在什么 RegionServer 上- 访问
-ROOT-
表,看需要的数据在哪个.META.
表上,这个.META.
表在什么 RegionServer 上- 访问
.META.
表来看要查询的行键在什么 Region 范围里面- 连接具体的数据所在的 RegionServer,这回就真的开始用
Scan
来遍历 row 了
从 0.96 版本之后这个三层查询架构被改成了二层查询架构,
-ROOT-
表被去掉了
- 同时 zk 中的
/hbase/root-region-server
也被去掉了- 直接把
.META.
表所在的 RegionServer 信息存储到了 zk 中的/hbase/meta-region-server
再后来引入了 namespace,
.META.
表被修改成了hbase:meta
新版 Region 查找流程:
- 客户端先通过 ZooKeeper 的
/hbase/meta-region-server
节点查询到哪台 RegionServer 上有hbase:meta
表- 客户端连接含有
hbase:meta
表的 RegionServer,hbase:meta
表存储了所有 Region 的行键范围信息
- 通过这个表就可以查询出要存取的 rowkey 属于哪个 Region 的范围里面
- 以及这个 Region 又是属于哪个
RegionServer
- 获取这些信息后,客户端就可以直连其中一台拥有要存取的 rowkey 的 RegionServer,并直接对其操作
- 客户端会把 meta 信息缓存起来,下次操作就不需要进行以上加载
hbase:meta
的步骤了