Hbase是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,用于存储海量的结构化或者半结构化,非结构化的数据,底层上的数据是以二进制流的形式存储在 HDFS 上的数据块中的
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中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此,可以视为一个“四维坐标”,即[行键, 列族, 列限定符, 时间戳]
行式存储:基于行的存储,是将整行数据连续存在一起。在基于行存储的表中,即使只需要读取指定列时,也需要先将对应行的数据读取到内存,然后再过滤目标列,这样会导致过多的磁盘IO、内存和时间开销,所以行式存储比较适用于每次需要访问完整行的场景。
列式存储:基于列的存储,是将列数据连续存储在一起。因为是将相同类型的数据存储在了一起,往往压缩比比较高,从而也会降低磁盘IO、内存和时间开销,所以,列式存储适用于仅在单列或少数列上操作的场景。特别是在大数据时代,数据的列和行都比较多时候,列式存储优势会更加明显。但反过来,列式存储对于获取整行的请求效率就没那么高了,需要多次IO读取多个列的数据,然后再合并返回。
列簇式存储:从概念上来说,列簇式存储介于行式存储和列式存储之间,可以通过不同的设计思路在行式存储和列式存储两者之间相互切换。比如,一张表只设置一个列簇,这个列簇包含所有用户的列。HBase中一个列簇的数据是存储在一起的,因此这种设计模式就等同于行式存储。再比如,一张表设置大量列簇,每个列簇下仅有一列,很显然这种设计模式就等同于列式存储。
HDFS的DataNode负责存储所有Region Server所管理的数据,即HBase中的所有数据都是以HDFS文件的形式存储的。
HBase 的数据都存储在 HRegion Server 上,在HRegion Server 上有HRegion、Hlog等等一些概念。
HBase可以存储海量的数据,而且数据是存储在HDFS中,HDFS是分布式的。所以,HBase一张表的数据会分到多台机器上的。那HBase是怎么切割一张表的数据的呢?用的就是RowKey来切分,其实就是表的横向切割。
HBase的一个列簇(Column Family)本质上就是一棵LSM树(Log-Structured Merge-Tree)。LSM树分为内存部分和磁盘部分。
内存部分是一个维护有序数据集合的数据结构。一般来讲,内存数据结构可以选择平衡二叉树、红黑树、跳跃表(SkipList)等维护有序集的数据结构,这里由于考虑并发性能,HBase选择了表现更优秀的跳跃表。
磁盘部分是由一个个独立的文件组成,每一个文件又是由一个个数据块组成。对于数据存储在磁盘上的数据库系统来说,磁盘寻道以及数据读取都是非常耗时的操作(简称IO耗时)。因此,为了避免不必要的IO耗时,可以在磁盘中存储一些额外的二进制数据,这些数据用来判断对于给定的key是否有可能存储在这个数据块中,这个数据结构称为布隆过滤器(Bloom Filter)。
跳跃表(skiplist)是一种随机化的数据, 跳跃表以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操作都可以在对数期望时间下完成, 并且比起平衡树来说, 跳跃表的实现要简单直观得多。
LSM树(Log-Structured-Merge-Tree)和B+树类似,它们被设计出来都是为了更好地将数据存储到大容量磁盘中。相对于B+树,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
在整个架构中,HMaster 和 HRegion Server 可以是同一个节点上,可以有多个 HMaster 存在,但是只有一个 HMaster 在活跃。
在 Client 端会进行 rowkey-> HRegion 映射关系的缓存,降低下次寻址的压力。
HBase中row key用来检索表中的记录,支持以下三种方式:
row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
不要在一张表里定义太多的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有提供数据查询的功能。
客户端: 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
}
HBase 知识手册
hbase系列-Hbase热点问题、数据倾斜和rowkey的散列设计