HBase分布式数据库基础
第一章 HBase 简介
1.1 HBase 定义
Apache HBase™ 是以 hdfs 为数据存储的,一种分布式、可扩展的 NoSQL 数据库。
在大数据场景中,除了直接以文件形式保存的数据外,还有大量结构化和半结构化的数据,这些数据通常需要支持更新操作,比如随机插入和删除,这使得分布式文件系统HDFS很难满足要求。为了方便用户存取海量结构化和半结构化数据,HBase应运而生。
HBase是构建在分布式文件系统HDFS之上的、支持随机插入和删除的列簇式存储系统,它可被简单理解为一个具有持久化能力的分布式多维有序映射表。
最终理解 HBase 数据模型的关键在于稀疏、分布式、多维、排序的映射。其中映射 map指代非关系型数据库的 key-Value 结构。
HBase特点
- 极好的扩展性
- 弱化ACID需求
- 良好的容错性
HBase 的设计理念依据 Google 的 BigTable 论文,论文中对于数据模型的首句介绍。
Bigtable 是一个稀疏的、分布式的、持久的多维排序 map。
之后对于映射的解释如下:
该映射由行键、列键和时间戳索引;映射中的每个值都是一个未解释的字节数组。
最终 HBase 关于数据模型和 BigTable 的对应关系如下:
HBase 使用与 Bigtable 非常相似的数据模型。用户将数据行存储在带标签的表中。数据行具有可排序的键和任意数量的列。该表存储稀疏,因此如果用户喜欢,同一表中的行可以具有疯狂变化的列。
1.2 HBase 数据模型
逻辑上,HBase 的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从 HBase 的底层物理存储结构(K-V)来看,HBase 更像是一个 multi-dimensional map。
1.2.1 HBase 逻辑结构
存储数据稀疏,数据存储多维,不同的行具有不同的列。
数据存储整体有序,按照RowKey的字典序排列,RowKey为Byte数组
HBase表由行和列组成,每个行由行键(row key)来标识,列划分为若干列族,一个列族中可以包含任意多个列,同一个列族里面的数据存储在一个文件中(不同的列族里的数据存储在不同的文件中)。
当这个文件达到一定大小后,会进行分裂形成多个region。当一个行键在不同的列族中都有相应的列值的话,不同列族中的文件都会存储这个行键的值。
也就是说,一行可能包含多个列族,一个列族有多个列,对某一行而言,某列族文件中只存储了这一行键在列族中有值的那些列(列族可能有上百个列),没有不会存储(不存null,不占空间)。
1.2.2 HBase 物理存储结构
逻辑结构转成物理结构
物理存储结构即为数据映射关系,而在概念视图的空单元格,底层实际根本不存储。
当在t4时间put(插入)row_key1的phone数据时,原来t3的并不会马上被覆盖。当查询row_key1的phone时会返回时间戳最大的t4那一个数据(最新的)。
从保存格式上可以看出,每行数据中不同版本的cell value会重复保存rowkey,colum family和colum qualifier,因此,为了节省存储空间,这几个字段值在保证业务易理解的前提下,应尽可能短。
HBase是列式存储引擎吗?
列式存储格式是指以列为单位存储数据的存储格式,相比于传统的行式存储格式,它具有压缩比高,读取IO少(此处指可避免无意义的IO)等优点,目前被广泛应用于各种存储引擎中。
对于HBase而言,它并不是一个列式存储引擎,而是列簇式存储引擎,即同一列簇中的数据会单独存储,但列簇内数据是行式存储的。为了将HBase改造成列式存储引擎,进一步提高读写性能,Cloudera公司开源了分布式数据库Kudu。
1.2.3 数据模型
1)NameSpace
命名空间,类似于关系型数据库的database 概念,每个命名空间下有多个表。HBase有两个自带的命名空间,分别是 hbase 和 default。
- hbase 中存放的是 HBase 内置的表
- default 表是用户建表时未指定namespace的表都创建在此
命名空间的结构:
- Table:表,所有的表都是命名空间的成员,即表必属于某个命名空间,如果没有指定,则在 default 默认的命名空间中。
- RegionServer group:一个命名空间包含了默认的 RegionServer Group。
- Permission:权限,命名空间能够让我们来定义访问控制列表 ACL(Access Control List)。例如,创建表,读取表,删除,更新等等操作。
- Quota:限额,可以强制一个命名空间可包含的 region 的数量。
2)Table
类似于关系型数据库的表概念。不同的是,HBase 定义表时只需要声明列族即可,不需要声明具体的列。这意味着,往 HBase 写入数据时,字段可以动态、按需指定。因此,和关系型数据库相比,HBase 能够轻松应对字段变更的场景。
3)Row
HBase 表中的每行数据都由一个 RowKey 和多个 Column(列)组成,数据是按照 RowKey的字典顺序存储的,并且查询数据时只能根据 RowKey 进行检索,所以 RowKey 的设计十分重要。
与 nosql 数据库们一样,RowKey 是用来检索记录的主键。访问 HBASE table 中的行,只有三种方式:
- 通过单个 RowKey 访问
- 通过 RowKey 的 range(正则)
- 全表扫描
RowKey 没有数据类型,可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),在 HBASE 内部,RowKey 保存为字节数组(byte[])。存储时,数据按照 RowKey 的字典序(byte order)排序存储。设计 RowKey 时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
4)ColumnFamily
列族:HBASE 表中的每个列,都归属于某个列族。列族是表的 schema 的一部分(而列不是),必须在使用表之前定义,但Column Family可包含无数个动态列。
列名都以列族作为前缀。例如 courses:history,courses:math都属于 courses 这个列族。
Column Family是访问控制的基本单位,同一个Column Family上的数据会在物理上存储在一个文件中。
HBase 中的每个列都由 Column Family(列族)和 Column Qualifier(列限定符)进行限定,例如 info:name,info:age。
建表时,只需指明列族,而列限定符无需预先定义。
5)Cell
由{rowkey, column Family:column Qualifier, time Stamp} 唯一确定的单元。cell 中的数据是没有类型的,全部是字节码形式存贮。
cell 中的数据是没有类型的,全部是字节码形式存贮。 关键字:无类型、字节码
6)Time Stamp (时间戳)
用于标识数据的不同版本(version),每条数据写入时,如果不指定时间戳,系统会自动为其加上该字段,其值为写入 HBase 的时间。
每个 cell 都保存着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64 位整型(long)。
时间戳可以由 HBASE(在数据写入时自动 )赋值,此时时间戳是精确到毫秒 的当前系统时间。
时间戳也可以由客户显式赋值。如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳。
每个 cell 中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。 为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,HBASE 提供 了两种数据版本回收方式。一是保存数据的最后 n 个版本,二是保存最近一段 时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。
在HBase中,timestamp是一个很重要的概念。它记录着往HBase进行增删改操作的时间(系统自动赋值),它的值越大,说明是这个操作就越新,通常我们从HBase得到的只是那个最新操作的结果,但是之前的操作(时间戳小的)会保留直到达到一定的版本数或者设定时间。
1.3 HBase 系统架构
Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等几个组件组成,HBase依赖于ZooKeeper和HDFS。
HBase采用了经典的master/slave架构,如上图所示,与HDFS不同的是,它的master与slave不直接互联,而是通过引入ZooKeeper让两类服务解耦,这使得HBase master变得完全无状态,进而避免了master宕机导致整个集群不可用。
Zookeeper
HBase 通过 Zookeeper 来做 master 的高可用、记录 RegionServer 的部署信息、并且存储有 meta 表的位置信息。
HBase 对于数据的读写操作时直接访问 Zookeeper 的,在 2.3 版本推出 Master Registry模式,客户端可以直接访问 master。使用此功能,会加大对 master 的压力,减轻对 Zookeeper的压力。
ZooKeeper内部存储这有关HBase的重要元信息和状态信息,担任着Master与RegionServer之间的服务协调角色,具体职责如下:
- 保证任何适合,集群中只有一个master;
- 存储所有Region的寻址入口;(DML的请求通过ZK分发到HRegionServer不通过HMaster,HMaster是处理DDL的请求。HMaster宕机不会影响客户端的读写请求;但是取法进行create 'stu4','info'的DDL操作。当原有的Meta元数据信息改变时也无法维护。)
- 实时监控Region Server的上线和下线信息,并实时通报给Master;
- 存储HBase的schema和table元数据。
Hmaster
Hmaster 是所有 Region Server 的管理者,其实现类为 HMaster。
HMaster可以存在多个,主HMaster由ZooKeeper动态选举产生,当主HMaster出现故障后,系统可由ZooKeeper动态选举出新的HMaster接管。HMaster本身是无状态的(所有状态信息均保存在zookeeper中),主HMaster挂掉后,不会影响正常的读写服务。HMaster主要有以下职责:
- 对于表的操作:create, delete, alter
- 监控 RegionServer,为 RegionServer 分配 Region(维护整个集群的负载均衡,在空闲时间进行数据的负载均衡 )
- 维护集群的元数据信息,处理 region 的分配或转移(发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上
- 当 RegionSever 失效的时候,协调对应 Hlog 的拆分
通过启动多个后台线程监控实现上述功能:
①LoadBalancer 负载均衡器
周期性监控 region 分布在 regionServer 上面是否均衡,由参数 hbase.balancer.period 控制周期时间,默认 5 分钟。
②CatalogJanitor 元数据管理器
定期检查和清理 hbase:meta 中的数据。meta 表内容在进阶中介绍。
③MasterProcWAL master 预写日志处理器
把 master 需要执行的任务记录到预写日志 WAL 中,如果 master 宕机,让 backupMaster读取日志继续干。
分布式系统中的无状态理解?
无状态服务(stateless service)对单次请求的处理,不依赖其他请求,也就是说,处理一次请求所需的全部信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身不存储任何信息
有状态服务(stateful service)则相反,它会在自身保存一些数据,先后的请求是有关联的
HregionServer
Region Server 为 Region 的管理者,直接对接用户的读写请求,是真正的“干活”的节点。其实现类为 HRegionServer,主要作用如下:
- 管理 master 为其分配的 Region,处理来自客户端的读写请求(get, put, delete)
- 负责和底层 HDFS 的交互(存储数据到 HDFS)
- 负责 Region 变大以后的拆分(拆分合并 region 的实际执行者,有 master 监控,有 regionServer 执行)
- 负责 Storefile 的合并工作 ,刷新缓存到HDFS,维护Hlog
HDFS
为 Hbase 提供最终的底层数据存储服务,同时为HBase 提供高可用(Hlog 存储在HDFS)的支持,具体功能概括如下:
- 提供元数据和表数据的底层分布式存储服务
- 保证的高可靠和高可用性 (数据多副本)
Client
Client提供HBase访问接口,与RegionServer交互读写数据,并维护cache加快对HBase的访问速度。
1. 4 Master架构
Meta表
描述HBase表的表,元数据表。有了 Region 标识符,就可以唯一标识每个 Region。为了定位每个 Region 所在的位置,可以构建一张映射表。
映射表的每个条目包含两项内容,一项是 Region 标识符,另一项是 服务器标识。
这个条目就表示 Region 和 Region 服务器之间的对应关系,从而就可以使用户知道某个 Region 存储在哪个 Region 服务器中。
这个映射表包含了关于 Region 的元数据,因此也被称为“元数据表”,又名“Meta表”。使用 scan 命令可查看 Meta 表的结构,如图所示
Meta 表中的每一行记录了一个 Region 的信息。RowKey 包含表名、起始行键和时间戳信息,中间用逗号隔开,第一个 Region 的起始行键为空。时间戳之后用.隔开的为分区名称的编码字符串,该信息是由前面的表名、起始行键和时间戳进行字符串编码后形成的。
Meta 表里有一个列族 info。info 包含了三个列,分别为 Regioninfo、Server 和 Serverstartcode。
- info:regioninfo 记录 region详细信息,存储一个 HRegionInfo 对象(包括行键范围 StartKey 和 EndKey、列族列表和属性。)。
- info:server 记录了管理该 Region 的 Region 服务器的地址,包括端口号,如 localhost:16201。
- info:serverstartcode 当前 region 被分到 RegionServer 的起始时间。
当用户表特别大时,用户表的 Region 也会非常多。Meta 表存储了这些 Region 信息,也变得非常大。Meta 表也需要划分成多个 Region,每个 Meta 分区记录一部分用户表和分区管理的情况。(有了meta表,就可以得到region和HRegionServer的对应关系,可以进行Region定位:客户端通过 ZooKeeper 获取 Meta 表(分区表)存储的地址,首先在对应的 Regionserver上获取 Meta 表的信息(meta表存在Regionserver上),得到所需的Region对应的Regionserver的信息,然后从Region 服务器上找到所需的数据)
注意:在客户端对元数据进行操作的时候才会连接 master,如果对数据进行读写,直接连接zookeeper 读取目录/hbase/meta-region-server 节点信息,会记录 meta 表格的位置。直接读取即可,不需要访问 master,这样可以减轻 master 的压力,相当于 master 专注 meta 表的写操作,客户端可直接读取 meta 表。
在 HBase 的 2.3 版本更新了一种新模式:Master Registry。客户端可以访问 master 来读取meta 表信息。加大了 master 的压力,减轻了 zookeeper 的压力。
1.5 RegionServer 内部组件
Region
Hbase表的分片,HBase 表会根据 RowKey 值被切分成不同的 region 存储在 RegionServer中,在一个 RegionServer 中可以有多个不同的 region。同一个行键的 Region 不会被拆分到多个 Region 服务器上。 一个HBase表被划分成多个Region,开始只有一个Region,后台不断分裂。
一个表中包含多个列族,一个列族一个文件存储,region的切分是横向切分的(切了store),那么包含了多个列族。
按照RowKey切分为Region,再按照列族切分为store
Store
HFile 存储在 Store 中,一个 Store 对应 HBase 表中的一个列族。(不全)
一个列族对应多个store,这多个store一定在不同的region里面。(region横向切分造成的)
同一个Region里的不同的store一定来自不同的列族。
StoreFile
保存实际数据的物理文件,StoreFile 以 HFile 的形式存储在 HDFS 上。每个 Store 会有一个或多个 StoreFile(HFile),数据在每个 StoreFile 中都是有序的。
HFile
这是在磁盘上保存原始数据的实际的物理文件,是实际的存储文件(K-V格式)。StoreFile 是以 HFile的形式存储在 HDFS 的。
HFile是Google SSTable的开源实现,它是一种有序K-V磁盘存储格式,带有多级索引,以方便定位数据,中的多级索引类似于B+树。
Block Cache
读缓存,负责缓存频繁读取的数据,每次查询出的数据会缓存在 BlockCache 中,采用了LRU置换策略(Least Recently Used最近最少使用),方便下次查询。
MemStore
写缓存,由于 HFile 中的数据要求是有序的,所以数据是先存储在 MemStore 中(每个region内的每个column family拥有一个MemStore ),排好序后,等到达刷写时机才会flush刷写到 HFile,每次刷写都会形成一个新的 HFile。
每次 Flush 的最小单位是 Region。每个 Column family 维护一个 MemStore。
WAL(Write-Ahead logs)
预写日志 用来容灾,以便RegionServer宕机后恢复。由于数据要经 MemStore 排序后才能刷写到 HFile,写进磁盘。但把数据保存在内存中会有很高的概率导致数据丢失,为了解决这个问题,数据会先写在一个叫做 Write-Ahead logfile 的文件中,然后再写入 MemStore 中。所以在系统出现故障的时候,数据可以通过这个日志文件重建。https://www.jianshu.com/p/65cb8cd81f40
实现 WAL 的类叫做 HLog,每个 HRegionServer 中都有一个 HLog 对象,当 HRegion 实例化时, HLog 对象会传入 HRegion 的构造器。当 HRegion 接受到一个更新操作,通过 HLog 对象将操作写入 WAL(核心是append()方法),HLog 文件定期会滚动出新的文件,并删除旧的文件(已持久化到 HFile 中的数据)。
第二章 HBase 进阶
2.1 写流程
为了提升HBase写效率,避免随机写性能低下,RegionServer将所有收到的写请求暂时写入内存,之后再顺序刷新到磁盘上,进而将随机写转化成顺序写以提升性能。
写流程:
1)Client 先访问 ZooKeeper,获取 hbase:meta 表位于哪个 Region Server(102)。
2)访问对应的 Region Server,获取 hbase:meta 表,将其缓存到连接中,作为连接属性 MetaCache,由于 Meta 表格具有一定的数据量,导致了创建连接比较慢;
之后使用创建的连接获取 Table,这是一个轻量级的连接,只有在第一次创建的时候会检查表格是否存在访问 RegionServer,之后在获取 Table 时不会访问 RegionServer;
3)调用Table的put方法写入数据,此时还需要解析RowKey,对照缓存的MetaCache,查看具体写入的位置有哪个 RegionServer。
4)将数据顺序写入(追加)到 WAL(WAL主要作用是当RegionServer突然宕机后重新恢复丢失的数据),,此处写入是直接落盘的,并设置专门的线程控制 WAL 预写日志的滚动(类似 Flume)。
5)将数据写入对应的 MemStore(根据写入命令的 RowKey 和 ColumnFamily 查看具体写入到哪个 MemStory),并且在 MemStore 进行排序;(源码中 确保写入WAL和MemStore都写入成功,类似事务,WAL失败要回滚MemStore)
6)向客户端发送 ack;(对客户端来说 写流程已经结束了)
7)等达到 MemStore 的刷写时机后,将数据刷写到对应的 story 中,保存成HFile格式。
2.2 MemStore Flush 刷写
MemStore 刷写由多个线程控制,条件互相独立:
上图中,一个Region中有两个store(两个列族),在flush的时候会往hdfs的不同文件夹(datanode)中写入。
每一个列族中的列具有高的查询同时性,不同列族中的列再一次查询中不同时查询,所以可以存放在hdfs的不同DataNode节点上。(隔离存储的好处)
主要的刷写规则是控制刷写文件的大小,在每一个刷写线程中都会进行监控,MemStore 刷写时机:
1.(单个)当某个 memstroe 的大小达到了hbase.hregion.memstore.flush.size(默认值 128M), 其所在 region 的所有 memstore 都会刷写。
当 memstore 的大小达到了
- hbase.hregion.memstore.flush.size(默认值 128M)
- * hbase.hregion.memstore.block.multiplier(默认值 4)
时,会阻止继续往该 memstore 写数据。
2.(总)当 region server 中 memstore 的总大小达到
- java_heapsize
- *hbase.regionserver.global.memstore.size(默认值 0.4)
- *hbase.regionserver.global.memstore.size.lower.limit(默认值 0.95),
region 会按照其所有 memstore 的大小顺序(由大到小)依次进行刷写。直到 region server中所有 memstore 的总大小减小到上述值以下。 当 region server 中 memstore 的总大小达到
- java_heapsize
- *hbase.regionserver.global.memstore.size(默认值 0.4)
时,会阻止继续往所有的 memstore 写数据。
- (时间)到达自动刷写的时间,也会触发 memstore flush。自动刷新的时间间隔由该属性进行配置
- hbase.regionserver.optionalcacheflushinterval(默认 1 小时)。
- 当 WAL 文件的数量超过 hbase.regionserver.max.logs,region 会按照时间顺序依次进行刷写,直到 WAL 文件数量减小到 hbase.regionserver.max.log 以下(该属性名已经废弃,不再暴露给用户,现无需手动设置,最大值为 32)。
2.3 读流程
2.3.1 HFile结构
在了解读流程之前,需要先知道读取的数据是什么样子的。
HFile 是存储在 HDFS 上面每一个 store 文件夹下实际存储数据的文件。里面存储多种内容。包括数据本身(keyValue 键值对)、元数据记录、文件信息、数据索引、元数据索引和一个固定长度的尾部信息(记录文件的修改情况)。
键值对按照块大小(默认 64K)保存在文件中,数据索引按照块创建,块越多,索引越大。每一个 HFile 还会维护一个布隆过滤器(就像是一个很大的地图,文件中每有一种 key,就在对应的位置标记,读取时可以大致判断要 get 的 key 是否存在 HFile 中)。
KeyValue 内容如下:
rowlength -----------→ key 的长度
row -----------------→ key 的值
columnfamilylength --→ 列族长度
columnfamily --------→ 列族
columnqualifier -----→ 列名
timestamp -----------→ 时间戳(默认系统时间)
keytype -------------→ Put
由于 HFile 存储经过序列化,所以无法直接查看。可以通过 HBase 提供的命令来查看存储在 HDFS 上面的 HFile 元数据内容。
[atguigu@hadoop102 hbase]$ bin/hbase hfile -m -f /hbase/data/命名空间/表名/regionID/列族/HFile 名
2.3.2 读流程
HBase是一个读比写慢的框架。由于写流程使得数据位于内存中或者磁盘中,因此读取数据时,需要从多个数据存放位置中寻找数据。
1)Client 先访问 ZooKeeper,获取 hbase:meta 表位于哪个 Region Server。
2)访问对应的 Region Server,获取 hbase:meta 表,根据读请求的 namespace:table/rowkey,查询出目标数据位于哪个 Region Server 中的哪个 Region 中。并将该 table 的 region 信息以及 meta 表的位置信息缓存在客户端的 meta cache,方便下次访问。
3)与目标 Region Server 进行通讯;
4)分别在 Block Cache(读缓存),MemStore 和 Store File(HFile)(一起读,并不是先读内存再读磁盘)中查询目标数据,并将查到的所有数据进行合并(Merge,返回最大的时间戳)。
5) 将从文件中查询到的数据块(Block,HFile 数据存储单元,默认大小为 64KB)缓存到Block Cache。
6)将合并后的最终结果返回给客户端。
2.3.3 Block Cache
HBase 在实现中提供了两种缓存结构 MemStore(写缓存) 和 BlockCache(读缓存)。写缓存前面说过不再重复。
- HBase 会将一次文件查找的 Block块 缓存到 Cache中,以便后续同一请求或者邻近数据查找请求,可以直接从内存中获取,避免昂贵的IO操作。
- BlockCache是Region Server级别的,
- 一个Region Server只有一个Block Cache,在 Region Server 启动的时候完成 Block Cache 的初始化工作。
- HBase对Block Cache的管理分为如下三种。
- LRUBlockCache 是最初的实现方案,也是默认的实现方案,将所有数据都放入JVM Heap中,交给JVM进行管理。
- SlabCache 实现的是堆外内存存储,不再由JVM管理数据内存。一般跟第一个组合使用,单它没有改善 GC 弊端,引入了堆外内存利用率低。
- BucketCache 缓存淘汰不再由 JVM 管理 降低了Full GC 发生的频率。
读数据时不要理解
为先从 MemStore 中读取,读不到再读 BlockCache 中,还读不到再从HFile中读取,然后将数据写入到 BlockCache 中。因为如果人为设置导致磁盘数据new,内存数据old。你读取的时候会出错的!
HBase 把磁盘跟内存数据一起读,然后把磁盘数据放到 BlockCache中,BlockCache 是磁盘数据的缓存。HBase 是个读比写慢的工具。
2.3.4 合并读取数据优化
每次读取数据都需要读取三个位置,最后进行版本的合并。效率会非常低,所有系统需要对此优化。
(1)HFile 带有索引文件,读取对应 RowKey 数据会比较快。
(2)Block Cache 会缓存之前读取的内容和元数据信息,如果 HFile 没有发生变化(记录在 HFile 尾信息中),则不需要再次读取。
(3)使用布隆过滤器能够快速过滤当前 HFile 不存在需要读取的 RowKey,从而避免读取文件。(布隆过滤器使用 HASH 算法,不是绝对准确的,出错会造成多扫描一个文件,对读取数据结果没有影响)
2.4 StoreFile Compaction 合并
由于memstore每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(Put/Delete)有可能会分布在不同的 HFile 中,因此查询时需要遍历所有的 HFile。
为了减少 HFile 的个数,以及清理掉过期和删除的数据,会进行 StoreFile Compaction。
Compaction 分为两种,分别是 Minor Compaction 和 Major Compaction。
- Minor Compaction会将临近的若干个较小的 HFile 合并成一个较大的 HFile,并清理掉部分过期和删除的数据,有系统使用一组参数自动控制。
- Major Compaction 会将一个 Store 下的所有的 HFile 合并成一个大 HFile,并且会清理掉过期和删除的数据,由参数 hbase.hregion.majorcompaction控制,默认 7 天
Minor Compaction 控制机制:
参与到小合并的文件需要通过参数计算得到,有效的参数有 5 个
(1)hbase.hstore.compaction.ratio(默认 1.2F)合并文件选择算法中使用的比率。
(2)hbase.hstore.compaction.min(默认 3) 为 Minor Compaction 的最少文件个数。
(3)hbase.hstore.compaction.max(默认 10) 为 Minor Compaction 最大文件个数。
(4)hbase.hstore.compaction.min.size(默认 128M)为单个 Hfile 文件大小最小值,小于这个数会被合并。
(5)hbase.hstore.compaction.max.size(默认 Long.MAX_VALUE)为单个 Hfile 文件大小最大值,高于这个数不会被合并。小合并机制为拉取整个 store 中的所有文件,做成一个集合。之后按照从旧到新的顺序遍历。
判断条件为:
① 过小合并,过大不合并
② 文件大小/ hbase.hstore.compaction.ratio < (剩余文件大小和) 则参与压缩。所有把比值设置过大,如 10 会最终合并为 1 个特别大的文件,相反设置为 0.4,会最终产生 4 个 storeFile。
不建议修改默认值
③ 满足压缩条件的文件个数达不到个数要求(3 <= count <= 10)则不压缩。
2.4.1 数据真正的删除时间
在hbaseAPI层面,delete其实是put的过程,普通的put是添加一个value值,而delete的过程是添加一个Type为delete的过程。所以delete之后数据并没有真正被删除。还需要Flush和Compact的配合。
Flush会进行删数据
Flush只会删除当前memStore中重复的数据(timestamp较小的删除)
StoreFile重复的并不会删
被标记为DeleteColumn的不会被删除
Major Compact也会进行删数据(当文件数>=3时,compact调用的也是major compact)
major compact 会将全部重复的数据进行删除,包括Storefile中的
major compact会将被标记为DeleteColumn的删除
在建表时如果指定存储2个版本,那么put进去相同rowkey的数据时,只会保留两个timestamp大的
想要数据真正删除,用delete删除,然后flush刷写到storefile,然后使用compact(Major Compaction)
案例参考
https://blog.csdn.net/qq_46548855/article/details/108718950
2.5 Region Split 切分
默认情况下,每个 Table 起初只有一个 Region,随着数据的不断写入,Region 会自动进行拆分。刚拆分时,两个子 Region 都位于当前的 Region Server,但处于负载均衡的考虑,HMaster 有可能会将某个 Region 转移给其他的 Region Server。
2.5.1 Region Split 时机
Region 的拆分是由 HRegionServer 完成的,在操作之前需要通过 ZK 汇报 master,修改对应的 Meta 表信息添加两列 info:splitA 和 info:splitB 信息。之后需要操作 HDFS 上面对应的文件,按照拆分后的 Region 范围进行标记区分,实际操作为创建文件引用,不会挪动数据。刚完成拆分的时候,两个 Region 都由原先的 RegionServer 管理。之后汇报给 Master, 由Master将修改后的信息写入到Meta表中。等待下一次触发负载均衡机制,才会修改Region的管理服务者,而数据要等到下一次压缩时,才会实际进行移动。
不管是否使用预分区,系统都会默认启动一套 Region 拆分规则。不同版本的拆分规则有差别。系统拆分策略的父类RegionSplitPolicy。
0.94 版本之前 => ConstantSizeRegionSplitPolicy
( 1 ) 当 1 个 region 中 的 某 个 Store 下 所 有 StoreFile 的 总 大 小 超 过 hbase.hregion.max.filesize (10G),该 Region 就会进行拆分。
0.94 版本之后,2.0 版本之前 => IncreasingToUpperBoundRegionSplitPolicy
( 2 ) 当1个region 中的某个 Store 下所有 StoreFile 的总大小超过Min(initialSize*R^3 ,hbase.hregion.max.filesize"),该Region就会进行拆分。其中 initialSize 的默认值为 2*hbase.hregion.memstore.flush.size,R 为当前 Region Server 中属于该 Table 的Region 个数(0.94 版本之后)。
hbase.hregion.max.filesize (10G)
hbase.hregion.memstore.flush.size(默认值 128M)
具体的切分策略为:
第一次 split:1^3 * 256 = 102MB
第二次 split:2^3 * 256 = 2048MB
第三次 split:3^3 * 256 = 6912MB
第四次 split:4^3 * 256 = 16384MB > 10GB,因此取较小的值 10GB
后面每次 split 的 size 都是 10GB 了
2.0 版本之后 => SteppingSplitPolicy
(3)Hbase 2.0 引入了新的 split 策略:如果当前 RegionServer 上该表只有一个 Region,按照 2 * hbase.hregion.memstore.flush.size 分裂,否则按照 hbase.hregion.max.filesize 分裂。
接着按这种规律下去,最后会形成一个 256M…10G 的这样Region队列,会产生数据倾斜问题。
解决方法: 建表时做好预分区,提前做好Region组的规划,0-1k,1k-2k,2k-3k这样的。
官方不建议用多个列族,比如有CF1,CF2,CF3,但是 CF1数据很多而CF2跟CF3数据很少,那么当触发了region切分的时候,会把CF2跟CF3分成若干小份,不利于系统维护。
优秀文章:https://www.cnblogs.com/sunsky303/p/14312350.html
2.5.2 预分区(自定义分区)
每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。
1)手动设定预分区
create 'staff1','info', SPLITS => ['1000','2000','3000','4000']
2)生成 16 进制序列预分区
create 'staff2','info',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}
3)按照文件中设置的规则预分区
(1)创建 splits.txt 文件内容如下:
aaaa
bbbb
cccc
dddd
(2)然后执行:
create 'staff3', 'info',SPLITS_FILE => 'splits.txt'
4)使用 JavaAPI 创建预分区
package com.atguigu.hbase;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
public class HBaseConnect {
public static void main(String[] args) throws IOException {
// 1.获取配置类
Configuration conf = HBaseConfiguration.create();
// 2.给配置类添加配置
conf.set("hbase.zookeeper.quorum","hadoop102,hadoop103,hadoop104");
// 3.获取连接
Connection connection = ConnectionFactory.createConnection(conf);
// 3.获取 admin
Admin admin = connection.getAdmin();
// 5.获取 descriptor 的 builder
TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(TableName.valueOf("bigdata", "staff4"));
// 6. 添加列族
builder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(
Bytes.toBytes("info")).build());
// 7.创建对应的切分
byte[][] splits = new byte[3][];
splits[0] = Bytes.toBytes("aaa");
splits[1] = Bytes.toBytes("bbb");
splits[2] = Bytes.toBytes("ccc");
// 8.创建表
admin.createTable(builder.build(),splits);
// 9.关闭资源
admin.close();
connection.close();
}
}
2.6 Hbase对LSM-Tree的使用
详情参考:https://zhuanlan.zhihu.com/p/135371171
传统关系型数据库使用B树或一些变体作为存储结构,能高效进行查找。但保存在磁盘中时它也有一个明显的缺陷,那就是逻辑上相离很近但物理却可能相隔很远,这就可能造成大量的磁盘随机读写。随机读写比顺序读写慢很多,为了提升IO性能,我们需要一种能将随机操作变为顺序操作的机制,于是便有了LSM树。LSM树能让我们进行顺序写磁盘,从而大幅提升写操作,作为代价的是牺牲了一些读性能。
磁盘读写时涉及到磁盘上数据查找,地址一般由柱面号、盘面号和块号三者构成。也就是说移动臂先根据柱面号移动到指定柱面,然后根据盘面号确定盘面的磁道,最后根据块号将指定的磁道段移动到磁头下,便可开始读写。整个过程主要有三部分时间消耗,查找时间(seek time) +等待时间(latency time)+传输时间(transmission time) 。分别表示定位柱面的耗时、将块号指定磁道段移到磁头的耗时、将数据传到内存的耗时。整个磁盘IO最耗时的地方在查找时间,所以减少查找时间能大幅提升性能。
LSM树的全称是 Log Structed Merge Tree,翻译过来就是Log结构合并树。数据写入的时候以Log方式连续写入,然后异步对磁盘上的多个LSM树进行合并。
LSM树可以看作是一个 N 阶合并树。数据写操作(包括插入、修改、删除)都在内存中进行,并且都会创建一个新记录(修改会记录新的数据值,而删除会记录一个删除标志)。这些数据在内存中仍然还是一棵排序树,当数据量超过设定的内存阈值后,会将这棵排序树和磁盘上最新的排序树合并。当这棵排序树的数据量也超过设定阈值后,会和磁盘上下一级的排序树合并。合并过程中,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。
在需要进行读操作时,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘 上的排序树顺序查找。
在 LSM 树上进行一次数据更新不需要磁盘访问,在内存即可完成。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用 LSM 树可以极大程度地减少磁盘的访问次数,加快访问速度。
整体上HBase就是用了LSM树的思路。
-
因为小树先写到内存中,为了防止内存数据丢失,写内存的同时需要暂时持久化到磁盘,对应了HBase的HLog(WAL)和MemStore。
-
MemStore上的树达到一定大小之后,需要flush到HRegion磁盘中(一般是Hadoop DataNode),这样MemStore就变成了DataNode上的磁盘文件StoreFile,定期HRegionServer对DataNode的数据做merge操作,彻底删除无效空间,多棵小树在这个时机合并成大树,来增强读性能。
数据写(插入,更新):数据首先顺序写如hlog (WAL), 然后写到MemStore, 在MemStore中,数据是一个2层B+树(图中的C0树)。MemStore满了之后,数据会被刷到storefile (hFile),在storefile中,数据是3层B+树(图中的C1树),并针对顺序磁盘操作进行优化。