1.基本概念
- 简介 Apache HBase(Hadoop DataBase)是一个开源的、高可靠性、高性能、面向列(这里指列族,非列式存储)、可伸缩、实时读写的分布式数据库,其设计思想来源于 Google 的 BigTable 论文。利用 Hadoop HDFS 作为其文件存储系统,利用 ZooKeeper 作为其分布式协同服务。主要用来存储非结构化和半结构化的松散数据(列式存储 NoSQL 数据库)。
HBase 良好的分布式架构设计为海量数据的快速存储、随机访问提供了可能,基于数据副本机制和分区机制可以轻松实现在线扩容、缩容和数据容灾,是大数据领域中 Key-Value 数据结构存储最常用的数据库方案。
注意:HBase 是列族数据库(Column-Family Database),不是列式数据库(Column-Oriented Database)。
总结:HBase 是运行在 HDFS 之上的面向列(列族)的数据库管理系统。
2.特点
- 易扩展- 2方面:一个是基于运算能力的扩展,通过增加HRegionServer节点数量,提升HBase上层处理能力;一个是基于存储能力的扩展,通过增加DataNode节点数量对存储层扩容,提升HBase数据存储能力。
- 容量大 - HBase 单表可以有十亿行、百万列,数据矩阵横向和纵向两个维度所支持的数据量级都非常具有弹性。HBase 的主要作用就是面向 PB 级别数据的实时入库和快速随机访问。这主要源于上述易扩展的特点,使得 HBase 通过扩展来存储海量的数据。
- 面向列 - HBase是面向列族存储的,列族下面有很多的列,列式存储的最大好处就是,其数据在表中是按照某列 存储的,这样在查询只需要少数几个字段时,能大大减少读取的数据量。还可以动态增加列,单独对列进行各方面的操作。
- 多版本 - HBase 的每个列的数据存储支持多个 Version,比如住址列,可能有多个变更。
- 稀疏性 - 为空的列并不占用存储空间,表的设计可以非常稀疏。不必想关系型数据库那样需要预先知道所有列名然后再进行NULL填充。
- 高可靠 - WAL(Write Ahead Log)日志先行机制,保证数据写入的时候不会因为集群异常而导致写入数据丢失。Replication 机制,保证了集群在出现严重问题的时候,数据不会发生丢失或者损坏。HBase 底层使用 HDFS,本身也有备份。
- 高性能 - HBase 底层采用 LSM 树(Log-Structured Merge-Tree)数据结构进行存储,底层的 LSM 树数据结构和 RowKey 有序排列等架构上的独特设计,使得 HBase 写入性能非常高。HRegion 切分、主键索引、缓存机制使得 HBase 在海量数据下具备一定的随机读取性能,该性能针对 RowKey 的查询能够到达毫秒级别。LSM 树属于树形结构,最末端的子节点是以内存的方式进行存储的,内存中的小树会 Flush 到磁盘中(当子节点达到一定阈值以后,会放到磁盘中,且存入的过程会进行实时 Merge 成一个主节点,然后磁盘中的树定期会做 Merge 操作,合并成一棵大树,以优化读性能)。
3.应用
HBase 是一种 NoSQL 数据库,这意味着它不像传统的 RDBMS 数据库那样支持 SQL 作为查询语言。HBase 是一种分布式存储的数据库,技术上来讲,它更像是分布式存储而不是分布式数据库,它缺少很多 RDBMS 系统的特性,比如列类型,辅助索引,触发器和高级查询语言等。
HBase 不适合解决所有的问题,首先数据量要足够大,如果有十亿或百亿行数据,那么 HBase 是一个很好的选择,如果只有几百万行甚至不到的数据量,RDBMS 是一个很好的选择。因为数据量小的话,真正能工作的机器量少,剩余的机器都处于空闲的状态。其次,如果你不需要辅助索引,静态类型的列,事务等特性可以考虑 HBase。但是一个已经用 RDBMS的系统想要切换到 HBase,则需要重新设计系统。最后,保证硬件资源足够,每个 HDFS 集群在少于 5 个节点的时候,都不能表现的很好。因为 HDFS 默认的复制数量是 3,再加上一个 NameNode。其实 HBase 在单机环境下也能运行,但是请在开发环境中进行使用。
适合 HBase 的应用:
- 存储业务数据:车辆 GPS 信息,司机点位信息,用户操作信息,设备访问信息。
- 存储日志数据:架构监控数据(登录日志,中间件访问日志,推送日志,短信邮件发送记录),业务操作日志信息。
- 存储业务附件:UDFS 系统(去中心化文件系统)存储图像,视频,文档等附件信息。
HBase和RDBMS的区别:
属性HBaseRDBMS数据类型只有字符串数据类型丰富数据操作增删改查,不支持Join各种操作及表连接存储结构列式存储表结构和行式存储数据保护更新后仍保留旧版本直接覆盖可伸缩性轻易增加节点需要中间层,牺牲性能
2.数据模型
在HBase表中,一条数据拥有一个全局唯一的主键(RowKey)和任意数量列,每个列数据存储支持多个版本,一列或多列组成一个列族,同一个列族中列的数据在物理上都存储在同一个 HFile 中。这样基于列存储的数据结构有利于数据缓存和查询。
所以,在 HBase 中定位一条数据需要通过:RowKey → Column Family → Column Qualifier → Version。
HBase表中的数据是稀疏地存储的,用户可以动态地创建不同的列,HBase表中数据按主键排序(字典序),根据主键划分为不同的HRegion存储在不同的HRegionServer上,已完成数据的分布式存储和读取。
1.NameSpace
命名空间类似于关系型数据库中的数据库的概念,他其实是表的逻辑分组。这种抽象为多租户相关功能奠定了基础。
命名空间是可以管理维护的,可以创建,删除或更改命名空间。HBase 有两个特殊预定义的命名空间:
- default:没有明确指定命名空间的表将自动落入此命名空间
- hbase:系统命名空间,用于包含 HBase 的内部表和元数据表
2.Table
Table 和关系型数据库中的表一个意思,由行和列组成。
3.RowKey
RowKey 的概念与关系型数据库中的主键相似,是一行数据的唯一标识。RowKey 可以是任意字符串(最大长度是64KB,实际应用中长度一般为 10-100 Bytes),RowKey 以字节数组保存。存储数据时,数据会按照 RowKey 的字典序排序存储,所以设计 RowKey 时,要充分利用排序存储这个特性,将经常一起读取的行存放到一起。
访问 HBase 数据的方式有三种:
- 基于 RowKey 的单行查询;
- 基于 RowKey 的范围查询;
- 全表扫描查询。
4.Column Family
Column Family 即列族,HBase 基于列划分数据的物理存储,同一个列族中列的数据在物理上都存储在同一个 HFile中。一个列族可以包含任意多列,一般同一类的列会放在一个列族中,每个列族都有一组存储属性:
- 是否应该缓存在内存中;
- 数据如何被压缩或行键如何编码等。
HBase 在创建表的时候就必须指定列族。HBase 的列族不是越多越好,官方推荐一个表的列族数量最好小于或者等于三,过多的列族不利于 HBase 数据的管理和索引。
5.Column Qualifier
列族的限定词,理解为列的唯一标识。但是列标识是可以改变的,因此每一行可能有不同的列标识。使用的时候必须 列族:列 ,列可以根据需求动态添加或者删除,同一个表中不同行的数据列都可以不同。
6.TimeStamp
TimeStamp是支持不同版本的标识,在 HBase 中,使用不同的 Timestamp 来标识相同 RowKey 对应的不同版本的数据。相同RowKey的数据按TimeStamp倒序排序,默认查询最新的版本,也可以指定TimesTtamp值来制定版本。
HBase 通过 RowKey 和 Column Family,Column Qualifier 来确定一个存贮单元,然后再通过时间戳来进行索引。时间戳的类型是 64 位整型,时间戳默认是精确到毫秒的当前系统时间。时间戳也可以由客户显式赋值,如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳。
为了避免数据存在过多版本而造成管理(包括存贮和索引)负担,HBase 提供了两种数据版本回收方案:
- 一是保留最后的n个版本
- 二是保留最近一段时间的版本
7.Cell
Cell 由 Row,Column Family,Column Qualifier,Version 组成。Cell 中的数据是没有类型的,全部使用字节码形式存贮,因为 HDFS 上的数据都是字节数组。
3.架构模型
HBase 可以将数据存储在本地文件系统,也可以存储在 HDFS 文件系统。在生产环境中,HBase 一般运行在 HDFS上,以 HDFS 作为基础的存储设施。用户通过 HBase Client 提供的 Shell 或 Java API 来访问 HBase 数据库,以完成数据的写入和读取。HBase 集群主要由 HMaster、HRegionServer 和 ZooKeeper 组成。
1.ZooKeeper
职责如下:
- 负责HMaster的选举,保证HMaster的主备切换。
- 实时监控HRegionServer节点状态,将节点上下线信息实时报告给HMaster。
- 维护元数据和集群配置:存储了hbase:meta元数据表的Region寻址,存储 HBase 的 Schema,包括有哪些 Table,每个 Table 有哪些 Column Family。
2.Client
HBase Client 为用户提供了访问 HBase 的接口,可以通过元数据表(客户端负责发送请求到数据库)来定位到目标数据的 HRegionServer。客户端连接的方式有很多种:
HBase Shell
Java API
发送的请求主要包括:
- DDL:数据库定义语言(表的建立,删除,添加删除列族,控制版本)
- DML:数据库操作语言(增删改)
- DQL:数据库查询语言(查询,全表扫描,基于主键,基于过滤器)
Client 维护着一些 Cache 来加快对 HBase 的访问,比如 HRegione 的位置信息。
3.HMaster
HMaster是HBase的主节点,负责集群的管理工作,实现了高可用(Active和Backup),通过Zookeeper完成主备切换,其职责如下:
- 管理和分配HRegion,负责启动的时候分配HRegion到具体的HRegionServer,或者是在分割HRegion时对于新HRegion的分配。还管理用户对表的DDL操作。- 表的元数据信息存储在 ZooKeeper- 表的数据存储在 HRegionServer 上(实际存储在 HDFS 上)
- 负载均衡- 将数据均衡地分布于各个HRegionServer上,防止HRegionServer数据倾斜过载。- 负责将用户请求均衡地分布于各个HRegionServer上,防止HRegionServer请求过热。
- 维护数据发现失效的HRgion,并将失效的 HRegion 分配到正常的 HRegionServer 上,当某个HRegionServer下线时,迁移其内部的HRegion到其他HRegionServer上
- 权限控制
4.HRegionServer
HRegionServer对接用户请求,是真正干活的节点,属于HBase具体数据的管理者。职责如下:
- 实时与HMaster保持心跳,汇报自己的状态信息。
- 当接收到HMaster的命令创建一张表时,会分配一个HRegion对应一张表。
- 当HRegion变大时负责HRegion的切分。
- 当自己下线时,其内部的HRegion会被其他HRegionServer所接收和管理。
- 维护HMaster分配给他的HRegion,处理对这些HRegion的IO请求 - 当客户端发送DQL或者DML请求时,他负责与客户端建立连接。- WAL:Write Ahead Log 日志先行。记录了数据写入、更新日志,他被用来作故障恢复;- MemStore:写缓存,当写入数据时,会先写入写缓存,然后再写入HFile,每个HRegion的列族下都有一个MemStore.- 负责与底层的HDFS交互,将(HLog,HFile)存入HDFS。- BlockCache:读缓存,存储了经常被访问的数据,采用LRU算法淘汰数据。
当某个HRegionServer宕机时,Zookeeper会通知HMaster进行失效备援。下线的HRegionServer暂时停止对外服务,HMaster会将宕机的HRegionServer上的HRegion转移到其他HRegionServer,并且对下线的HRegionServer进行日志重放,将MemStore中还未持久化到HDFS的数据进行恢复。
当某台 HRegionServer Failover 的时候,整个过程中 HRegion 中的数据是不可用的,因为它是缺失的。因此,HBase 属于 CP 架构,降低了可用性,具备强一致性读/写。设想一下,如果 Redo 过程中的 HRegion 能够响应请求,那么可用性提高了,则必然返回不一致的数据(因为 Redo 可能还没完成),那么 HBase 的一致性就降低了。
5.HRegion
一个 HRegionServer 包含了多个 HRegion。HBase 将表中的数据基于 RowKey 的不同范围划分到不同 HRegion 上,每个HRegion 都负责一定范围的数据存储和访问。
HRegion 是 HBase 中分布式存储和负载均衡的最小单元,不同的 HRegion 可以分布在不同的 HRegionServer 上。每个表一开始只有一个 HRegion,随着数据不断插入表,HRegion 不断增大,当增大到指定阀值(10G)的时候,HRegion会进行二等分,切分后的其中一个HRegion会发送给其他HRegionServer,以实现负载均衡。
当 Table 中的行不断增多,就会有越来越多的 HRegion。为了防止前期数据的处理都集中在一个 HRegionServer,我们可以根据自己的业务进行预分区(按RowKey预分区)
这样即使有一张百亿条数据的表,由于数据被划分到不同的 HRegion上,每个 HRegion 都可以独立地进行读写,HBase 读写数据的时候还可以与多 HRegion 分布式并发操作,所以访问速度并不会有太大的降低。
负载均衡:
Split
在 HBase 中 Split 是一个很重要的功能,HBase 是通过把数据分配到一定数量的 HRegion 来达到负载均衡的。一个Table 会被分配到一个或多个 HRegion 中,这些 HRegion 会被分配到一个或者多个 HRegionServer 中。在自动 Split 策略中,当一个 HRegion 达到一定的大小就会自动 Split 成两个 HRegion。Table 在 HRegion 中是按照 RowKey 来排序的,并且一个 RowKey 所对应的行只会存储在一个 HRegion 中,这一点保证了 HBase 的强一致性。
当一个 Table 刚被创建的时候,HBase 默认的分配一个 HRegion 给 Table。也就是说这个时候,所有的读写请求都会访问到同一个 HRegionServer 的同一个 HRegion 中,这个时候就达不到负载均衡的效果了,集群中的其他 HRegionServer 可能处于比较空闲的状态。解决这个问题可以用 pre-splitting 在创建 Table 时提前生成多个 HRegion。
在 Table 初始化的时候如果不配置的话,HBase 是不知道如何去 Split HRegion 的,因为 HBase 不知道应该把哪个RowKey 作为 Split 的开始点。如果我们可以大概预测到 RowKey 的分布,我们可以使用 pre-spliting 来帮助我们提前 Split HRegion。
如果我们的预测不是特别准确,还是会导致某个 HRegion 过热被集中访问,不过还好我们还有 auto-split,默认按 10G自动切分。但是如果文件到达 9G 后迟迟未到 10G 此时对于 HBase 来说是比较难受的。最好的办法就是首先预测 Split 的切分点,做 pre-splitting,后面再交给 auto-split 来处理。
HBase 在每次数据合并之后都会针对相应 HRegion 生成一个 requestSplit 请求,requestSplit 首先会执行 checkSplit,检测 FileSize 是否达到阈值,如果超过阈值,就进行切分。
HBase 自带了两种 pre-split 的算法,分别是 HexStringSplit 和 UniformSplit 。如果我们的 RowKey 是十六进制的字符串作为前缀的,就比较适合用 HexStringSplit 作为 pre-split 的算法。例如,我们使用HexHash(prefix) 作为 RowKey的前缀,其中 HexHash 为得到十六进制字符串的 hash 算法。我们也可以用我们自己的 Split 算法。
当一个 HRegion 达到一定的大小,他会自动 Split 成两个 HRegion。如果我们的 HBase 版本是 0.94 之后,那么默认的有三种自动 Split 的策略,ConstantSizeRegionSplitPolicy,IncreasingToUpperBoundRegionSplitPolicy 还有KeyPrefixRegionSplitPolicy。
在 0.94 版本之前 ConstantSizeRegionSplitPolicy 是默认和唯一的 Split 策略。当某个 Store(对应一个 Column Family)的大小大于配置值 hbase.hregion.max.filesize 的时候(默认 10G)HRegion 就会自动分裂。
而 0.94 版本之后 IncreasingToUpperBoundRegionSplitPolicy 是默认的 Split 策略。这个策略中,最小的分裂大小和Table 的某个 HRegionServer 的 HRegion 个数有关,当 StoreFile 的大小大于以下公式得出的值的时候就会 Split。公式如
下:
# R 为同一个 Table 中在同一个 HRegionServer 中的 HRegion 的个数
Min(R^2 * "hbase.hregion.memstore.flush.size", "hbase.hregion.max.filesize")
例如:
- hbase.hregion.memstore.flush.size 默认值 128MB。
- hbase.hregion.max.filesize 默认值为 10GB。
- 如果初始时 R=1 ,那么 Min(128MB, 10GB)=128MB ,也就是说在第一个 Flush 的时候就会触发分裂操作。
- 当 R=2 的时候 Min(22128MB, 10GB)=512MB ,当某个 StoreFile 大小达到 512MB 的时候,就会触发分裂。
- 如此类推,当 R=9 的时候,StoreFile 达到 10GB 的时候就会分裂,也就是说当 R>=9 的时候,StoreFile 达到 10GB 的时候就会分裂。
KeyPrefixRegionSplitPolicy 可以保证相同的前缀的 RowKey 保存在同一个 HRegion 中。指定 RowKey 前缀位数划分HRegion,通过读取 KeyPrefixRegionSplitPolicy.prefix_length 属性,该属性为数字类型,表示前缀长度,在进行 Split 时,按此长度对 SplitPoint 进行截取。此种策略比较适合固定前缀的 RowKey。当 Table 中没有设置该属性,指定此策略效果等同与使用 IncreasingToUpperBoundRegionSplitPolicy。
我们可以通过配置 hbase.regionserver.region.split.policy 来指定 Split 策略,也可以写我们自己的 Split 策略。
6.Store
一个 HRegion 由多个 Store 组成,每个 Store 都对应一个 Column Family,Store 包含 1 个 MemStore 和 0 或多个StoreFile 组成。
- MemStore:作为 HBase 的内存数据存储,数据的写操作会先写到 MemStore 中,当 MemStore 中的数据增长到指定阈值(默认 128M)后,HRegionServer会启动FlushCache进程将MemStore中的数据持久化到StoreFile,每次写入都会有一个StoreFile。当客户端检索数据时,先到MemStore检索,若没有,则检索StoreFile。
- StoreFile:MemStore 中的数据写到文件后就是 StoreFile,StoreFile 底层是以 HFile 格式保存的。HBase以StoreFile的大小来判断是否需要切分HRegion。当一个 HRegion 中所有 StoreFile 的大小和数量都增长到超过指定阈值时,HMaster会把当前 HRegion 分割为两个,切分后其中一个 HRegion 会被转移到其他的HRegionServer 上,实现负载均衡。
- HFile:HFile 和 StoreFile 是同一个文件,只不过站在 HDFS 的角度称这个文件为 HFile,站在 HBase 的角度就称这个文件为 StoreFile。是 HBase 在 HDFS 中存储数据的格式,它包含多层的索引,这样在 HBase 检索数据的时候就不用完全的加载整个文件。
7.HFile
8.BlockCache
众所周知,提升数据库读取性能的一个核心方法是,尽可能将热点数据存储到内存中,以避免昂贵的 IO 开销。现代系统架构中,诸如 Redis 这类缓存组件已经是体系中的核心组件,通常将其部署在数据库的上层,拦截系统的大部分请求,保证数据库的“安全”,提升整个系统的读取效率。
同样为了提升读取性能,HBase 也实现了一种读缓存结构 BlockCache。客户端读取某个 Block,首先会检查该 Block是否存在于 BlockCache,如果存在就直接加载出来,如果不存在就去 HFile 文件中加载,加载出来后再放到 BlockCache中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的 IO 开销。从字面意思可以看出来,BlockCache 主要用来缓存 Block。
HBase 在实现中提供了两种缓存结构:MemStore 和 BlockCache。MemStore 作为 HBase 的写缓存,保存着数据的最近一次更新;BlockCache 作为 HBase 的读缓存,保存着最近被访问的数据块。
需要关注的是, Block 是 HBase 中最小的数据读取单元,即数据从 HFile 中读取都是以 Block 为最小单元执行的。BlockCache 是 RegionServer 级别的,一个 RegionServer 只有一个 BlockCache,在 RegionServer 启动时完成BlockCache 的初始化工作。
BlockCache 名称中的 Block 指的是 HBase 的 Block,分别有以下几种:
- DATA
- ENCODED_DATA
- LEAF_INDEX
- BLOOM_CHUNK
- META
- INTERMEDIATE_INDEX
- RO0T_INDEX
- FILE_INFO
- GENERAL_BLOOM_META
- DELETE_FAMILY_BLOOM_META
- TRAILER
- INDEX_V1
- …
到目前为止,HBase 先后实现了 3 种 BlockCache 方案:
- LRUBlockCache :最早的实现方案,也是默认的实现方案(LRUBlockCache + BucketCache);
- SlabCache:HBase 0.92 版本实现的第二种方案,参见 HBase-4027;
- BucketCache :HBase 0.96 之后官方提供的另一种可选方案,参见 HBase-7404。
这三种方案的不同之处在于内存管理模式,其中LRUBlockCache是将所有数据都放入JVMHeap中,交给JVM进行管理。而后2种方案采用的机制允许将部分数据存储在堆外。
这种演变本质上是因为 LRUBlockCache 方案中JVM垃圾回收机制经常导致程序长时间暂停,而采用堆外内存对数据进行管理可以有效缓解系统长时间GC。
BlockCache 默认是开启的,不需要做额外的事情去开启 BlockCache。如果想让某个列族不使用 BlockCache,可以通过以下命令关闭它:
create'my-table', {NAME =>'my-cf', BLOCKCACHE =>'false'}
alter'my-table', CONFIGURATION => {NAME =>'my-cf', BLOCKCACHE =>'false'}
1.LRUBlockCache
LRUBlockCache是完全基于JVM Heap的LRU方案,在 0.92 版本之前只有这种 BlockCache 的实现方案,LRU 就是
Least Recently Used,即近期最少使用算法。读出来的 Block 会被放到 BlockCache 中待下次查询使用。当缓存满了的时候,会根据 LRU 的算法来淘汰 Block。每次访问数据都会将其放在我们的队首,如果需要淘汰数据,就只需要淘汰队尾即可。这里有个问题,如果有个数据在 1 分钟内被访问了 1000 次,接下来 1 分钟没有访问该数据,但是有其他的数据被大量访问,就会导致这个热点数据被淘汰。
# Java API 设置 in-memory
HColumnDescriptor.setInMemory(true);# Shell API 设置 in-memorycreate't', {NAME =>'f', IN_MEMORY =>'true'}
看起来是不是很像 JVM 的新生代、老年代、永久代(JDK1.8 已彻底移除,替换为了元空间)?没错,这个方案就是模拟 JVM 的分代设计而做的。
列族被设置为 IN-MEMORY 并不是意味着这个列族是存储在内存中的,这个列族依然是跟别的列族一样存储在硬盘上。一般的 Block 被第一次读出后是放到 single-access ,只有当被访问多次后才会放到 multi-access ,而带有IN_MEMORY 属性的列族中的 Block 一开始就会被放到 in-memory 区域。这个区域的缓存有最高的存活时间,在需要淘汰Block 的时候,这个区域的 Block 是最后被考虑到的,所以这个属性仅仅是为了 BlockCache 而创造的。
目前 BlockCache 的堆内内存方案就只有 LRUBlockCache,可以通过将 hfile.block.cache.size 设置为 0 来关闭该方案,但是不推荐。相关配置如下,默认为 0.4,表示分配给 StoreFile 使用的块缓存的最大堆(-Xmx setting)。
<property><name>hfile.block.cache.size</name><value>0.4</value></property>
计算 HBase 中有多少内存可用于缓存的方法如下:
number of region servers * heap size(-Xms1024m初始堆和-Xmx1024m最大堆) * hfile.block.cache.size * 0.99
块缓存的默认值为 0.4,表示可用堆的 40%。最后一个值(99%)是在驱逐开始之后 LRU 缓存中的默认可接受加载因子。它被包含在这个等式中的原因是,说可以使用 100% 的可用内存是不现实的,因为这会使得该过程从加载新块的位置阻塞。下面是一些示例:
- 1 个 RegionServer,堆大小设置为 1GB,BlockCache 大小约为 405MB。
- 20 个 RegionServer,堆大小设置为 8GB,BlockCache 大小约为 63.3GB。
- 100 个 RegionServer,堆大小设置为 24GB,并且 hfile.block.cache.size 为 0.5,BlockCache 大小约为 1.16TB。
设置 hfile.block.cache.size 的时候要注意在 HBase 的内存使用上有一个规则那就是 MemStore + BlockCache 的内存占用比例不能超过 0.8 (即 80%),否则就会报错。因为必须要留 20% 作为机动空间。用配置项来说明就是:
hbase.regionserver.global.memstore.size (默认为 0.4) + hfile.block.cache.size (默认为 0.4) <= 0.8。
值得一提的是,这两个配置项的默认值都是 0.4,也就是说默认项的总和就己经达到了他们俩可以占用的内存比例上限了,所以基本没事就不用去加大这两个配置项,你调大哪一个,都必须相应地调小另外一个。
BlockCache 可以带来很多好处,就是一个菜鸟都可以想到用内存来做缓存提高读取性能,但是 LRUBlockCache 有什么坏处呢?完全基于 JVM Heap 的缓存,势必带来一个后果:随着内存中对象越来越多,每隔一段时间都会引发一次 Full GC。在 Full GC 的过程中,整个 JVM 完全处于停滞状态(Stop the Word),有的时候长达几分钟。
2.SlabCache
为了解决 LRUBlockCache 方案中因 JVM 垃圾回收导致的服务中断问题,SlabCache 方案提出使用 Java NIO
DirectByteBuffer 技术实现堆外内存存储,不再由 JVM 管理数据内存。
默认情况下,系统在初始化的时候会分配2个缓存区,分别占整个BlockCache大小的80%和20%,每个缓存区分别存储固定大小的Block,其中前者主要存储小于等于64K的Block,后者主要存储小于等于128K的Block,如果一个Block太大会导致2个缓存区都无法存储。
和 LRUBlockCache 相同,SlabCache 也使用 Least-Recently-Used 算法淘汰过期的 Block。和 LRUBlockCache 不同的是,SlabCache 淘汰 Block 时只需要将对应的 BufferByte 标记为空闲,后续 Cache 对其上的内存直接进行覆盖即可。
线上集群环境中,不同表不同列簇设置的 BlockSize 都可能不同,很显然,默认只能存储小于等于 128KB Block 的SlabCache 方案不能满足部分用户场景。比如,用户设置 BlockSize = 256K,简单使用 SlabCache 方案就不能达到缓存这部分 Block 的目的。因此 HBase 在实际实现中将 SlabCache 和 LRUBlockCache 搭配使用,称为 DoubleBlockCache 。
具体查询流程:在一次随机读中,一个 Block 从 HDFS 中加载出来之后会在两个 Cache 中分别存储一份。缓存读时首先在 LRUBlockCache 中查找,如果 Cache Miss 再在 SlabCache 中查找,此时如果命中,则将该 Block 放入LRUBlockCache 中。
但是,经过实际测试,DoubleBlockCache 方案有很多弊端。比如,SlabCache 中固定大小内存设置会导致实际内存使用率比较低,而且使用 LRUBlockCache 缓存 Block 依然会因为 JVM GC 产生大量内存碎片。因此在 HBase 0.98 版本之后,
已经不建议使用该方案(已废弃)。
3.BucketCache
4.3.1. 基本介绍
BucketCache 借鉴了 SlabCache 的创意,也用上了堆外内存。不过它是这么用的:相比起只有 2 个区域的
SlabeCache,BucketCache 一上来就分配了 14 种区域。
这 14 种区域分别放的是大小为 4KB、8KB、16KB、32KB、40KB、 48KB、56KB、64KB、96KB、128KB、192KB、256KB、384KB、 512KB 的 Block。而且这个种类列表还是可以手动通过设置 hbase.bucketcache.bucket.sizes 属性
来定义,如下。这 14 种类型可以分配出很多个 Bucket。
BucketCache 的存储不一定要使用堆外内存,是可以自由在 3 种存储介质直接选择:堆外(offheap)、文件(file)、mmaped file(memory-mapped file 内存映射文件)。通过设置 hbase.bucketcache.ioengine 为 offheap、file 或者mmaped file 来配置,如下。
<property><name>hbase.bucketcache.ioengine</name><value>offheap</value></property>
系统一启动 BucketCache 就会把可用的存储空间按照每个 Bucket 的大小上限均分为多个 Bucket。如果划分完的数量比你的种类还少,比如比 14 (默认的种类数量)少,就会直接报错,因为每一种类型的 Bucket 至少要有一个 Bucket。BucketCache 实现起来的样子就像如下图所示(每个区域的大小都是 512 * 4)。
BucketCache 还有一个特别的长处,那就是它自己来划分内存空间、自己来管理内存空间,Block 放进去的时候是考虑到 offset 偏移量的,所以内存碎片少,发生 GC 的时间很短。
还有一点是为什么存储介质会有 file?我们用缓存不就是为了使用内存,然后利用内存比硬盘快得优势来提高读写的性能吗?大家不要忘记了还有 SSD 硬盘,最开始设计这种策略的初衷就是想把 SSD 作为一层比传统机械硬盘更快的缓存层来使用,所以你可以把 file 这种类型等同于 SSD-file。
使用 BucketCache 有以下的好处:
- 这是第一个可以使用 SSD 硬盘的缓存策略,这是最大的亮点;
- 这种策略极大地改进了 SlabCache 使用率低的问题;
- 配置极其灵活,可以适用于多种场景。
在实际测试中也表现出了很高的性能,所以 HBase 就顺理成章地把 SlabCache 废弃了。off-heap 的延迟可以接近于on-heap 的延迟,因为 off-heap 并不会引起 GC 操作。
3.读写流程
HBase 中单表的数据量通常可以达到 TB 级或 PB 级,但大多数情况下数据读取可以做到毫秒级。HBase 是如何做到的呢?要想实现表中数据的快速访问,通用的做法是数据保持有序并尽可能的将数据保存在内存里。HBase 也是这样实现的(HRegionServer 的 BlockCache)。
对于海量级的数据,首先要解决的是存储的问题。数据存储上,HBase 将表切分成一个个的 HRegion 托管到
HRegionServer 上,类似关系型数据库的分区表,但比关系型数据库分区、分库易用。
解决完存储接下来聊访问。数据表被切分成多个 HRegion,用户在访问数据时,如何找到某条数据对应的 HRegion呢?这里我们分别讲解 0.96 以前和 0.96 以后两种情况。
1.三层索引
1.HBase 0.96 以前
HBase 0.96以前内部维护了两张特殊的表:-ROOT-表和.META.表,用来查找各种表的HRegion位置。这两张特殊的表也会进行切分成多个HRegion。-ROOT-表比.META.表更特殊一些,永远不会切分超过一个HRegion,这样就保证了只需要3次跳转,就能定位到任意HRegion。
- -ROOT-:维护了.META.表的HRegion信息。
- .META.:维护了其他表的HRegion信息。
而-ROOT-表的HRegion信息存储在Zookeeper,通过 ZooKeeper 可以找到 -ROOT- 的 HRegion 托管的HRegionServer。再通过 -ROOT- 表找到 .META. 表的 HRegion 位置。 .META. 表中存放着用户的表的 HRegion 切分信息。
整个流程为: Client → ZooKeeper → -ROOT- → .META. → 用户的表的 HRegion 。
当用户的表特别大时,用户的表的 HRegion 也会非常多。.META. 表存储了这些 HRegion 信息,也会变得非常大,这时.META. 自己也需要划分成多个 HRegion,托管到多个 HRegionServer 上。这时就出现了一个问题:当 .META. 被托管在多个RegionServer 上,如何去定位 .META. 呢? HBase 的做法是用另外一个表来记录 .META. 的 HRegion 信息,就和 .META. 记录用户的表的 HRegion 一样,这个表就是 -ROOT- 表。
-ROOT- 表永远只有一个 HRegion,也就是说只会存放在一台 HRegionServer 上,这个信息至关重要,是所有客户端定位 HRegion 的入口,所以这个映射信息存储在 ZooKeeper 上面。
2.HBase 0.96 以后
HBase 0.96 以后,-ROOT- 表被移除,直接将 .META. 表 HRegion 位置信息存放在 ZooKeeper 中,并将 .META. 表更名为hbase:meta 。
此时整个流程为: Client → ZooKeeper → hbase:meta → 用户的表的 HRegion 。
hbase:meta 表结构如下:
- rowkey:表名,格式为 表名,起始键,HRegion的时间戳.Encode编码. ;
- table:state:表的状态,启用还是禁用状态;
- info:state:HRegion 的状态,正常情况下为 OPEN;
- info:server:HRegionServer 的地址和端口,如 node03:16020;
- info:serverstartcode:HRegionServer 启动的 13 位时间戳;
- info:sn:server 和 serverstartcode 的组合,如 node03:16020,1662183040273;
- info:seqnumDuringOpen:HRegion 在线时长的二进制串;
- info:regioninfo:HRegion 的详细信息,如:ENCODED、NAME、STARTKEY、ENDKEY 等。
3.数据读取过程
1.第一步:
项目有100亿业务数据,存储在一个HBase集群上(由多个服务器节点构成),每个数据节点上有若干个HRegion,每个HRegion实际上就是HBase中一批数据的集合(一段连续范围的RowKey的数据)。
现在我们根据主键RowKey来查询对应的记录,通过hbase:meta表可以帮我们迅速定位到该记录所在数据节点。以及数据节点中的HRegion,目前我们有100亿条记录,占空间10TB。所有记录被切分成 5000 个 HRegion,每个 HRegion大约 2G。
由于记录在1个HRegion中,所以我们只需要查询这2G的HRegion,就可以找到对应记录。
2.第2步:
由于HBase按列族存储。比如一条记录有 400 个字段,前 100 个字段是人员信息相关,这是一个列族(列的集合);中间 100 个字段是公司信息相关,是一个列族。另外 100 个字段是人员交易信息相关,也是一个列族;最后还有 100 个字段是其他信息,也是一个列族。
这四个列族是分开存储的,假设2G的HRegion文件中,分为4个列族,那么每个列族就是500M。我们只需要遍历这500M的数据就可以找到对应记录。
3.第3步:
一个列族在HDFS中会包含1个或者多个HFiile(StoreFile)。如果一个HFile 的大小为 100 M,那么该列族包含 5 个HFile 在磁盘上或内存中。由于 HBase 内存 Flush 到磁盘中的数据是排好序的,要查询的记录有可能在最前面,也有可能在最后面,按平均来算,我们只需遍历 2.5 个 HFile 共 250M,就可以找到对应的记录。
4.第4步:
每个HFile中,是以键值对(key/value)方式存储,只需要遍历文件中的Key位置并判断符合条件即可。一般Key是有限长度,假设key/value=1:24,最终只需要10M的数据量,就可以找到对应记录。
在这个过程中,还会有布隆过滤器帮助我们快速判断不正确的HFile,这样速率再一次提升,以及HBase是有缓存机制的,如果数据在内存中(BlockCache),效率更高。
HFile 继续划分,有 Data Block,Data Block Index,Trailler 等组成,已经定位到 RowKey 所在的 HFile 时,会先读取HFile 的 Trailer 的信息以获取 Data Block Index 的位置。Data Block Index 的 Key 就是 Data Block 的 RowKey,所以通过 Data Block Index 的 Key 就能精确的定位到要检索的 RowKey 在哪个 Data Block 上,然后直接将该 Data Block 读取到内存,需要注意的是这里的 Data Block 已经很小了(默认是 64K,不同于 HDFS 上的 Block 默认为 128M,HBase 的 HFile 中的 Block 要小的多)这样子足以读取该 Block 到内存中,将该 Block 进行遍历就能获取到需要的 RowKey 并取出数据。因为这里的Block 只有 64K,所以遍历会非常迅速。这就是为什么 HFile 的 Data Block 要设置的如此之小的原因。
正因为以上流程,即使数据量剧增,也不会导致 HBase 的查询性能下降。同时,HBase 是一个面向列存储的数据库(列族机制),当表字段非常多时,可以把其中一些字段独立出来放在一部分机器上,而另外一些字段放到另一部分机器上,分散存储,分散列查询。
正是由于这样复杂的存储结构和分布式的存储方式,保证了 HBase 海量数据下的查询效率。
4.写入数据流程
- Client 访问 ZooKeeper,获取 hbase:meta 所在 HRegionServer 的节点信息;
- Client 访问 hbase:meta 所在的 HRegionServer,获取 hbase:meta 记录的元数据后先加载到内存中,然后再从内存中查询出 RowKey 所在的 HRegion (HRegion 所在的 HRegionServer);
- Client 对 RowKey 所在的 HRegion 对应的 HRegionServer 发起写入数据请求;
- 建立连接后,首先将 DML 要做的操作写入到日志 HLog;
- 然后将数据的修改更新到 MemStore 中,本次操作结束。一个 HRegion 由多个 Store 组成,一个 Store 对应一个列族,Store 包括位于内存中的 Memstore 和位于磁盘的 StoreFile,写操作先写入 MemStore;
- 当 MemStore 数据达到阈值后(默认 128M),创建一个新的 MemStore;
- 旧的 MemStore 将刷写为一个独立的 StoreFile(HRegionServer 会启动 FlushCache 进程写入 StoreFile)并存放到HDFS,最后删除 HLog 中的历史数据。
- 当 StoreFile 文件的数量增长到一定阈值后,系统会进行合并(次/小 Minor Compaction、主/大 Major Compaction);
- 在合并过程中会进行版本合并和删除工作,形成更大的 StoreFile;
- 当一个 HRegion 所有 StoreFile 的大小和数量超过一定阈值后,会把当前的 HRegion 分割为两个,并由 HMaster 分配到相应的 HRegionServer 服务器,实现负载均衡。
4.数据刷写
1.触发时机
1.内存阈值
HRegion 中的每个 MemStore 占用的内存超过相关阈值 hbase.hregion.memstore.flush.size 时会触发刷写,默认为 128MB。
如果我们的数据增加得很快,达到了 hbase.hregion.memstore.flush.size *hbase.hregion.memstore.block.multiplier(默认为 4) 的大小,也就是 128 * 4 = 512MB 的时候,除了触发 MemStore 刷写之外,HBase 还会在刷写的时候阻塞所有写入该Store 的请求。
2.内存总和
整个 HRegionServer 的 MemStore 占用内存总和大于相关阈值时会触发刷写。如果达到了 HRegionServer 级别的刷写,当前 HRegionServer 的所有写操作将会被阻塞,这个阻塞可能会持续到分钟级别。
HBase 为 HRegionServer 所有的 MemStore 分配了一定的写缓存,大小等于 hbase_heapsize(HRegionServer 占用的堆内存大小) * hbase.regionserver.global.memstore.size(默认为 0.4)。
相关阈值计算公式为: hbase_heapsize * hbase.regionserver.global.memstore.size *
hbase.regionserver.global.memstore.size.lower.limit(默认为 0.95) = MAX_SIZE 。例如:HBase 堆内存总
共是 32G,MemStore 占用内存为:32 * 0.4 * 0.95 = 12.16G 将触发刷写。
3.日志阈值
HBase 使用了 WAL 机制(日志先行),当数据到达 HRegion 时是先写入日志的,然后再被写入到 MemStore。如果日志的数量越来越大,这就意味着 MemStore 中未持久化到磁盘的数据越来越多。当 HRegionServer 挂掉的时候,恢复时间将会变得很长,所以有必要在日志到达一定的数量时进行一次刷写操作。相关公式为:Math.max(32, hbase_heapsize *hbase.regionserver.global.memstore.size * 2 / logRollSize)
4.定期刷写
当定时器到达 hbase.regionserver.optionalcacheflushinterval (默认值 3600000 毫秒,即 1 小时)时,
HBase 会自动触发刷写。一般建议调大,比如 10 小时,因为很多场景下 1 小时 Flush 一次会产生很多小文件,一方面导致Flush 比较频繁,另一方面导致小文件很多,影响随机读性能。
5.更新频率
如果 HBase 的某个 HRegion 更新的很频繁,而且既没有达到自动刷写阀值,也没有达到内存的使用限制,但是内存中的更新数量已经足够多,比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为 30000000 次,也会触发刷写。
6.手动刷写
Shell 中通过 flush 命令。
hbase> flush 'TABLENAME'
hbase> flush 'REGIONNAME'
hbase> flush 'ENCODED_REGIONNAME'
hbase> flush 'REGION_SERVER_NAME'
7.注意
以上所有条件触发的刷写操作最后都会检查对应的 Store 包含的 StoreFiles 文件数是否超过
hbase.hstore.blockingStoreFiles 参数配置的个数,默认为 16。如果满足这个条件,那么当前刷写会被推迟到
hbase.hstore.blockingWaitTime 参数设置的时间后再刷写。
如果是阻塞刷写,HBase还会请求Compaction压实处理或者Split分割操作。
2.刷写策略
HBASE 1.1 之前:MemStore 刷写是 HRegion 级别的。就是说,如果要刷写某个 MemStore ,MemStore 所在的 HRegion中其他的 MemStore 也是会被一起刷写的(简单的理解:Flush 一个列族时其它列族也会一起 Flush)。
HBASE 2.x 之后:
- FlushAllStoresPolicy:HBASE 1.1 的策略。
- FlushAllLargeStoresPolicy:判断 HRegion 中每个 MemStore 的使用内存是否大于指定阀值,大于阀值的 MemStore 将会被刷写。阈值计算公式: flushSizeLowerBound = max((long)128 / 3, 16) = 42 。
- FlushNonSloppyStoresFirstPolicy:将 Region 中的 MemStore 按照 isSloppyMemStore 分到两个 HashSet 里面( sloppyStores 和 regularStores )然后: 判断 regularStores 里面是否有 MemStore 内存占用大于相关阀值的 MemStore,有的话就会对这些 MemStore进行刷写,其他的不做处理,这个阀值计算和 FlushAllLargeStoresPolicy 的阀值计算逻辑一致。 如果 regularStores 里面没有 MemStore 内存占用大于相关阀值的 MemStore,这时候就开始在 sloppyStores 里面寻找是否有 MemStore 内存占用大于相关阀值的 MemStore,有的话就会对这些 MemStore 进行刷写,其他的不做处理。 如果上面 sloppyStores 和 regularStores 都没有满足条件的 MemStore 需要刷写,这时候就将 FlushNonSloppyStoresFirstPolicy 策略久退化成 FlushAllStoresPolicy 策略了。
3.刷写流程
1.prepareFlush阶段
刷写的第一步是对 MemStore 做 Snapshot,为了防止刷写过程中更新的数据同时在 Snapshot 和 MemStore 中而造成后续处理的困难,所以在刷写期间需要持有 updateLock。持有了 updateLock 之后,将阻塞客户端的写操作。所以只在创建 Snapshot 期间持有 updateLock,而且 Snapshot 的创建非常快,所以此锁期间对客户的影响一般非常小。对 MemStore做 Snapshot 是在 internalPrepareFlushCache 里面进行的。
2.flushCache阶段
如果创建快照没问题,那么返回的 result 将为 null。这时候我们就可以进行下一步 internalFlushCacheAndCommit。其
实 internalFlushCacheAndCommit 里面包含两个步骤:flushCache 阶段和 commit 阶段。
- flushCache 阶段:将 prepareFlush 阶段创建好的快照写到临时文件里面,临时文件是存放在对应 HRegion 文件夹下面的 .tmp 目录里面。
- commit 阶段:将 flushCache 阶段生产的临时文件移到(rename)对应的列族目录下面,并做一些清理工作,比如删除第一步生成的 Snapshot。
5.数据合并
HBase根据合并文件规模,分为Mino Compaction和Major Compaction
1.Minor Compaction(次要/小)
选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不做任何的数据删除、多版本数据的清理工作。但是对于minVersoion=0并且设置了TTL的过期版本数据进行清理。
一次 Minor Compaction 的结果是让小的StoreFile 变的更少并且产生更大的 StoreFile。
2. Major Compaction(主要/大)
将所有的 StoreFile 合并成一个 StoreFile 清理三类无意义数据:==被删除的数据、TTL过期的数据、版本号超过设定版本号的数据。==一般情况下,Major Compaction时间比较长,整个过程消耗大量系统资源,对上层业务有比较大的影响,所以一般会关闭自动Major Compaction功能,改为手动在低峰期触发。
总结:
1.Mino Compaction:快速合并小文件
2.Major Compaction:合并成大文件,清理大文件不必要的数据
3.合并时机
1.MemStore 刷盘
MemStore Flush 会产生 HFile 文件,文件越来越多就需要 Compact。每次执行完 Flush 操作之后,都会对当前 Store 中的文件数进行判断,一旦文件数大于配置,就会触发 Compaction。Compaction 都是以 Store 为单位进行的,整个 HRegion的所有 Store 都会执行 Compact。
2. 周期性检查
后台线程定期触发检查是否需要执行 Compaction,检查周期可配置。线程先检查文件数是否大于配置,一旦大于就会触发 Compaction。如果不满足,它会接着检查是否满足 Major Compaction 条件(默认 7 天触发一次,可配置手动触发)。
周期性检查线程 CompactionChecker 大概 2hrs 46mins 40sec 执行一次。计算公式为:
hbase.server.thread.wakefrequency(默认为 10000 毫秒) * hbase.server.compactchecker.interval.multiplier(默认1000)。
3. 手动执行
一般来讲,手动触发 Compaction 通常是为了执行 Major Compaction,一般有这些情况需要手动触发合并:因为很多业务担心自动 Major Compaction 影响读写性能(可以选择直接关闭),因此会选择低峰期手动触发;用户在执行完 alter 操作之后希望立刻生效,手动执行触发 Major Compaction;
发 Compaction。Compaction 都是以 Store 为单位进行的,整个 HRegion的所有 Store 都会执行 Compact。
2. 周期性检查
后台线程定期触发检查是否需要执行 Compaction,检查周期可配置。线程先检查文件数是否大于配置,一旦大于就会触发 Compaction。如果不满足,它会接着检查是否满足 Major Compaction 条件(默认 7 天触发一次,可配置手动触发)。
周期性检查线程 CompactionChecker 大概 2hrs 46mins 40sec 执行一次。计算公式为:
hbase.server.thread.wakefrequency(默认为 10000 毫秒) * hbase.server.compactchecker.interval.multiplier(默认1000)。
3. 手动执行
一般来讲,手动触发 Compaction 通常是为了执行 Major Compaction,一般有这些情况需要手动触发合并:因为很多业务担心自动 Major Compaction 影响读写性能(可以选择直接关闭),因此会选择低峰期手动触发;用户在执行完 alter 操作之后希望立刻生效,手动执行触发 Major Compaction;
HBase 管理员发现硬盘容量不够的情况下手动触发 Major Compaction 删除大量过期数据。
版权归原作者 m0_63924864 所有, 如有侵权,请联系我们删除。