HBase概述
Hbase是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,用于存储海量的结构化或者半结构化,非结构化的数据,底层上的数据是以二进制流的形式存储在 HDFS 上的数据块中的
HBase应用场景
- 写密集型应用,每天写入量巨大,而相对读数量较小的应用,比如微信的历史消息,游戏日志等等
- 不需要复杂查询条件且有快速随机访问的需求。HBase只支持基于rowkey的查询,对于HBase来说,单条记录或者小范围的查询是可以接受的,大范围的查询由于分布式的原因,可能在性能上有点影响,而对于像SQL的join等查询,HBase无法支持。
- 对性能和可靠性要求非常高的应用,由于HBase本身没有单点故障,可用性非常高。数据量较大,而且增长量无法预估的应用,HBase支持在线扩展,即使在一段时间内数据量呈井喷式增长,也可以通过HBase横向扩展来满足功能。
- 结构化和半结构化的数据,基于Hbase动态列,稀疏存的特性。Hbase支持同一列簇下的列动态扩展,无需提前定义好所有的数据列,并且采用稀疏存的方式方式,在列数据为空的情况下不占用存储空间。
Hbase和MySQL比较
HBase | MySQL | 备注 | |
---|---|---|---|
类型 | NoSQL | RDS | |
数据 | 结构化或者半结构化,非结构化的数据 | 结构化数据 | |
分布式 | 数据存储在HDFS,支持分布式存储 | MySQL是单机,本身没有内置的分布式存储,可以通过MySQL复制或者数据分片的技术来达到分布式存储效果 | |
数据一致性 | HBase中每一条数据只会出现在一个Region,它的数据冗余备份不是在region这个层面做的,还是依赖HDFS来做的冗余,HBase支持行级事务,即一个put操作要么成功,要么失败。有RegionServer宕机的时候,Region会被分配到其他的RegionServer上,同时写WAL Log | 数据同步可能存在延迟(主从延迟),导致数据一致性问题 | |
持久化技术 | LSM 树 | redo log | |
查询语言 | NoSQL:查询数据不灵活:不能使用column之间过滤查询 | SQL |
HBase的数据模型
HBase中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此,可以视为一个“四维坐标”,即[行键, 列族, 列限定符, 时间戳]
- 行健(Row Key):表的主键,表中的记录默认按照行健升序排序
- 时间戳(Timestamp):每次数据操作对应的时间戳,可以看作是数据的版本号
- 列族(Column Family):表在水平方向有一个或者多个列族组成,一个列族中可以由任意多个列组成,列族支持动态扩展,无需预先定义列的数量以及类型,所有列均以二进制格式存储,用户需要自行进行类型转换。所有的列族成员的前缀是相同的,例如“c:name”和“c:age”两个列都属于c这个列族。每个列簇的数据都集中存放在一起形成一个存储单元Store。
- 表和区域(Table&Region):当表随着记录数不断增加而变大后,会逐渐分裂成多份,成为区域,一个区域是对表的水平划分,不同的区域会被Master分配给相应的RegionServer进行管理
- 单元格(Cell):表存储数据的单元。由{行健,列(列族:标签),时间戳}唯一确定,其中的数据是没有类型的,以二进制的形式存储。
HBase 的数据概念构成了如下图所示的逻辑视图
行式存储、列式存储、列簇式存储
行式存储:基于行的存储,是将整行数据连续存在一起。在基于行存储的表中,即使只需要读取指定列时,也需要先将对应行的数据读取到内存,然后再过滤目标列,这样会导致过多的磁盘IO、内存和时间开销,所以行式存储比较适用于每次需要访问完整行的场景。
列式存储:基于列的存储,是将列数据连续存储在一起。因为是将相同类型的数据存储在了一起,往往压缩比比较高,从而也会降低磁盘IO、内存和时间开销,所以,列式存储适用于仅在单列或少数列上操作的场景。特别是在大数据时代,数据的列和行都比较多时候,列式存储优势会更加明显。但反过来,列式存储对于获取整行的请求效率就没那么高了,需要多次IO读取多个列的数据,然后再合并返回。
列簇式存储:从概念上来说,列簇式存储介于行式存储和列式存储之间,可以通过不同的设计思路在行式存储和列式存储两者之间相互切换。比如,一张表只设置一个列簇,这个列簇包含所有用户的列。HBase中一个列簇的数据是存储在一起的,因此这种设计模式就等同于行式存储。再比如,一张表设置大量列簇,每个列簇下仅有一列,很显然这种设计模式就等同于列式存储。
HBase 架构设计
HBase 系统架构
HBase由三种类型的服务器以主从模式构成:
- HRegion Server:负责数据的读写服务,用户通过与HRegion server交互来实现对数据的访问。HRegion server 通过 HDFS Clinet 与 HDFS 进行交互。
- HBase HMaster:负责Region的分配及数据库的创建和删除等操作,在空闲时间进行数据的负载均衡,监控 RegionServer,处理 RegionServer 故障转移
- ZooKeeper:负责维护集群的状态(某台服务器是否在线,服务器之间数据的同步操作及master的选举等)。
HDFS的DataNode负责存储所有Region Server所管理的数据,即HBase中的所有数据都是以HDFS文件的形式存储的。
HBase 存储架构
HBase 的数据都存储在 HRegion Server 上,在HRegion Server 上有HRegion、Hlog等等一些概念。
HBase可以存储海量的数据,而且数据是存储在HDFS中,HDFS是分布式的。所以,HBase一张表的数据会分到多台机器上的。那HBase是怎么切割一张表的数据的呢?用的就是RowKey来切分,其实就是表的横向切割。
- HRegion:数据表的一个分片,当数据表大小超过一定阈值就会“水平切分”,分裂为两个HRegion。HRegion是集群负载均衡的基本单位。通常一张表的HRegion会分布在整个集群的多台HRegionServer上,一个HRegionServer上会管理多个HRegion,当然,这些HRegion一般来自不同的数据表。
- Store:
- Mem Store:HBase在写数据的时候,会先写到Mem Store,可以理解成 内存 buffer
- StoreFile:MemStore超过一定阈值,就会将内存中的数据刷写到硬盘上,形成StoreFile
- HFile: HBase中KeyValue数据的存储格式,HFile是Hadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile
- HLogFile,HBase中WAL(Write Ahead Log) 的存储格式,物理上是Hadoop的Sequence File,Sequence File 的Key是 HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和 region名字外,同时还包括sequence number和timestamp,timestamp是” 写入时间”,sequence number的起始值为0,或者是最近一次存入文件系 统sequence numbe.
存储数据结构
HBase的一个列簇(Column Family)本质上就是一棵LSM树(Log-Structured Merge-Tree)。LSM树分为内存部分和磁盘部分。
内存部分是一个维护有序数据集合的数据结构。一般来讲,内存数据结构可以选择平衡二叉树、红黑树、跳跃表(SkipList)等维护有序集的数据结构,这里由于考虑并发性能,HBase选择了表现更优秀的跳跃表。
磁盘部分是由一个个独立的文件组成,每一个文件又是由一个个数据块组成。对于数据存储在磁盘上的数据库系统来说,磁盘寻道以及数据读取都是非常耗时的操作(简称IO耗时)。因此,为了避免不必要的IO耗时,可以在磁盘中存储一些额外的二进制数据,这些数据用来判断对于给定的key是否有可能存储在这个数据块中,这个数据结构称为布隆过滤器(Bloom Filter)。
跳跃表
跳跃表(skiplist)是一种随机化的数据, 跳跃表以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操作都可以在对数期望时间下完成, 并且比起平衡树来说, 跳跃表的实现要简单直观得多。
跳跃表主要由以下部分构成:
- 表头(head):负责维护跳跃表的节点指针。
- 跳跃表节点:保存着元素值,以及多个层。
- 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
- 表尾:全部由 NULL 组成,表示跳跃表的末尾。
LSM树
LSM树(Log-Structured-Merge-Tree)和B+树类似,它们被设计出来都是为了更好地将数据存储到大容量磁盘中。相对于B+树,LSM树拥有更好的随机写性能。
LSM树的结构
LSM树的结构是横跨内存和磁盘的,包含memtable、immutable memtable、SSTable等多个部分。
memtable
顾名思义,memtable是在内存中的数据结构,用以保存最近的一些更新操作,当写数据到memtable中时,会先通过WAL的方式备份到磁盘中,以防数据因为内存掉电而丢失。
预写式日志(Write-ahead logging,缩写 WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先写入log文件中。
memtable可以使用跳跃表或者搜索树等数据结构来组织数据以保持数据的有序性。当memtable达到一定的数据量后,memtable会转化成为immutable memtable,同时会创建一个新的memtable来处理新的数据。
immutable memtable
顾名思义,immutable memtable在内存中是不可修改的数据结构,它是将memtable转变为SSTable的一种中间状态。目的是为了在转存过程中不阻塞写操作。写操作可以由新的memtable处理,而不用因为锁住memtable而等待。
SSTable
SSTable(Sorted String Table)即为有序键值对集合,是LSM树组在磁盘中的数据的结构。如果SSTable比较大的时候,还可以根据键的值建立一个索引来加速SSTable的查询。下图是一个简单的SSTable结构示意:
memtable中的数据最终都会被转化为SSTable并保存在磁盘中,后续还会有相应的SSTable日志合并操作,也是LSM树结构的重点。
最终LSM树的结构可以由下图简单表示:
布隆过滤器
Bloom Filter(布隆过滤器)是一种多哈希函数映射的快速查找算法。它是一种空间高效的概率型数据结构,通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。
布隆过滤器的优势在于,利用很少的空间可以做到精确率较高,空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。为什么不允许删除元素呢:删除意味着需要将对应的 k 个 bits 位置设置为 0,其中有可能是其他元素对应的位。
可以简单理解成下面这张图,元素1,6,9 经过 hash 函数之后存在了数组中,在Bloom Filters 可视化网站 可以看到动态视频。然后判断存不存在只需要判断hash 之后的元素对应的数组位置是不是都等于1
HBase 读写流程
读流程
- Client 请求读取数据时,先转发到 ZK 集群
- 在 ZK 集群中寻找到相对应的 HRegion Server,再找到对应的 HRegion
- HRegion Server 先是查 Region 的 MemStore,如果在 MemStore 中获取到数据,那么就会直接返回
- 否则就是再由 HRegion 找到对应的 Store File,从而查到具体的数据
在整个架构中,HMaster 和 HRegion Server 可以是同一个节点上,可以有多个 HMaster 存在,但是只有一个 HMaster 在活跃。
在 Client 端会进行 rowkey-> HRegion 映射关系的缓存,降低下次寻址的压力。
写流程
- Client 进行发起数据的插入请求,如果 Client 本身存储了关于 Rowkey 和 Region 的映射关系的话,那么就会先查找到具体的对应关系
- 如果没有的话,就会在ZK中进行查找到对应 Region server,然后再转发到具体的 Region 上
HRegionServer先把数据写入到HLog,以防止数据丢失。 - 然后找到对应的HRegion,将数据写入到MemStore,在MemStore中会对Row Key进行排序。如果HLog和MemStore均写入成功,则这条数据写入成功,给Client反馈写入成功
- 等达到MemStore 的刷写时机后,将数据刷写到StoreFile中,形成HFile文件
HBase 使用
RowKey 设计
HBase中row key用来检索表中的记录,支持以下三种方式:
- 通过单个row key访问:即按照某个row key键值进行get操作;
- 通过row key的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描;
- 全表扫描:即直接扫描整张表中所有行记录。
row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
RowKey规则
- rowkey唯一原则:必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
- rowkey长度原则:Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。
- 数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;
MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。 - 目前操作系统是都是64位系统,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性。
- 数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;
- rowkey散列原则:把主键哈希后当成rowkey的头部,这也是避免热点数据、数据倾斜的重要方式
列族的设计
不要在一张表里定义太多的column family。目前Hbase并不能很好的处理超过2~3个column family的表。因为某个column family在flush的时候,它邻近的column family也会因关联效应被触发flush,最终导致系统产生更多的I/O。
热点数据
HBase的表会被划分为1个或多个Region,被托管在RegionServer中。
由下面的图我们可以看出,Region有两个重要的属性:StartKey和EndKey。表示这个Region维护的rowkey的范围,当我们要读写数据时,如果rowkey落在某个start-end key范围内,那么就会定位到目标region并且读写到相关的数据。
默认的情况下,创建一张表是,只有1个region,start-end key没有边界,所有数据都在这个region里装,然而,当数据越来越多,region的size越来越大时,大到一定的阀值,hbase认为再往这个region里塞数据已经不合适了,就会找到一个midKey将region一分为二,成为2个region,这个过程称为分裂(region-split)。而midKey则为这二个region的临界(这个中间值这里不作讨论是如何被选取的)。
此时,我们假设假设rowkey小于midKey则为阴被塞到1区,大于等于midKey则会被塞到2区,如果rowkey还是顺序增大的,那数据就总会往2区里面写数据,而1区现在处于一个被冷落的状态,而且是半满的。2区的数据满了会被再次分裂成2个区,如此不断产生被冷落而且不满的Region,当然,这些region有提供数据查询的功能。
什么是热点和数据倾斜
- 热点:发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。
- 大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求,造成资源浪费。设计良好的数据访问模式以使集群被充分,均衡的利用。
- 数据倾斜:Hbase数据也是存储在少数节点上。这就是数据倾斜。
Go 读写HBase
客户端: https://github.com/tsuna/gohbase
package main
import (
"context"
"io"
"os"
"github.com/sirupsen/logrus"
"github.com/tsuna/gohbase"
"github.com/tsuna/gohbase/hrpc"
)
func init() {
// 以Stdout为输出,代替默认的stderr
logrus.SetOutput(os.Stdout)
// 设置日志等级
logrus.SetLevel(logrus.DebugLevel)
}
type HbaseClient struct {
client gohbase.Client
}
func main() {
zkquorum := ""
client := gohbase.NewClient(
zkquorum,
gohbase.ZookeeperRoot("/hbase"),
)
hbaseClient := HbaseClient{
client: client,
}
logrus.Info(hbaseClient)
}
// PutsByRowKey add RowKey
func (hb *HbaseClient) PutsByRowKey(ctx context.Context, table, rowKey string, values map[string]map[string][]byte) (err error) {
putRequest, err := hrpc.NewPutStr(context.Background(), table, rowKey, values)
if err != nil {
return err
}
_, err = hb.client.Put(putRequest)
if err != nil {
return err
}
return nil
}
// UpdateByRowKey ...
func (hb *HbaseClient) UpdateByRowKey(ctx context.Context, table, rowKey string, values map[string]map[string][]byte) (err error) {
putRequest, err := hrpc.NewPutStr(context.Background(), table, rowKey, values)
if err != nil {
return err
}
_, err = hb.client.Put(putRequest)
if err != nil {
return err
}
return
}
// GetsByRowKey ...
func (hb *HbaseClient) GetsByRowKey(ctx context.Context, table, rowKey string) (*hrpc.Result, error) {
getRequest, err := hrpc.NewGetStr(context.Background(), table, rowKey)
if err != nil {
return nil, err
}
res, err := hb.client.Get(getRequest)
if err != nil {
return nil, err
}
return res, nil
}
// GetsByRowKeyCF ...
func (hb *HbaseClient) GetsByRowKeyCF(ctx context.Context, table, rowKey string, families map[string][]string) (*hrpc.Result, error) {
getRequest, err := hrpc.NewGetStr(context.Background(), table, rowKey, hrpc.Families(families))
if err != nil {
return nil, err
}
res, err := hb.client.Get(getRequest)
if err != nil {
return nil, err
}
return res, nil
}
// DeleteByRowKey ...
func (hb *HbaseClient) DeleteByRowKey(ctx context.Context, table, rowKey string, value map[string]map[string][]byte) (err error) {
delRequest, err := hrpc.NewDelStr(context.Background(), table, rowKey, value)
if err != nil {
return nil
}
_, err = hb.client.Delete(delRequest)
if err != nil {
return nil
}
return
}
// ScanByTable ...
func (hb *HbaseClient) ScanByTable(ctx context.Context, table, startRow, stopRow string) ([]*hrpc.Result, error) {
scanRequest, err := hrpc.NewScanRangeStr(context.Background(), table, startRow, stopRow)
if err != nil {
return nil, err
}
scan := hb.client.Scan(scanRequest)
var res []*hrpc.Result
for {
getRsp, err := scan.Next()
if err == io.EOF || getRsp == nil {
break
}
if err != nil {
return nil, err
}
res = append(res, getRsp)
}
return res, nil
}
文章评论