hbase 是一个基于java、开源、NoSql、非关系型、面向列的、构建与hadoop 分布式文件系统(HDFS)上的、仿照谷歌的BigTable的论文开发的分布式数据库。
列式存储数据库以列为单位聚合数据,然后将列值顺序地存入磁盘,这种存储方法不同于行式存储的传统数据库,行式存储数据库连续地存储整行。图1-1形象地展示了列式存储和行式存储的不同物理结构。
列式存储的出现主要基于这样一种假设:对于特定的查询,不是所有的值都是必需的。尤其是在分析型数据库里,这种情形很常见,因此需要选择一种更为合适的存储模式。
在这种新型的设计中,减少I/O只是众多主要因素之一,它还有其他的优点:因为列的数据类型天生是相似的,即便逻辑上每一行之间有轻微的不同,但仍旧比按行存储的结构聚集在一起的数据更利于压缩,因为大多数的压缩算法只关注有限的压缩窗口。
像增量压缩或前缀压缩这类的专业算法,是基于列存储的类型定制的,因而大幅度提高了压缩比。更好的压缩比有利于在返回结果时降低带宽的消耗。
值得注意的是,从典型RDBMS的角度来看,HBase并不是一个列式存储的数据库,但是它利用了磁盘上的列存储格式,这也是RDBMS与HBase最大的相似之处,因为HBase以列式存储的格式在磁盘上存储数据。但它与传统的列式数据库有很大的不同:传统的列式数据库比较适合实时存取数据的场景,HBase比较适合键值对的数据存取,或者有序的数据存取。
HBase | hive | |
---|---|---|
类型 | 列式存储 | 数据仓库 |
内部机制 | 数据库引擎 | MapReduce |
增删改查 | 都支持 | 只支持导入和查询 |
schema | 只需要预先定义列族,不需要具体到列,列可以动态添加 | 需要预先定义表格 |
应用场景 | 实时 | 离线处理 |
特点 | 以K-V形式存储 | 类sql |
Hive和Hbase是两种基于Hadoop的不同技术–Hive是一种类SQL的引擎,并且运行MapReduce任务,Hbase是一种在Hadoop之上的NoSQL 的Key/vale数据库。当然,这两种工具是可以同时使用的。就像用Google来搜索,用FaceBook进行社交一样,Hive可以用来进行统计查询,HBase可以用来进行实时查询,数据也可以从Hive写到Hbase,设置再从Hbase写回Hive。
HBase客户端(Client)提供了Shell命令行接口、原生Java API编程接口、Thrift/REST API编程接口以及MapReduce编程接口。HBase客户端支持所有常见的DML操作以及DDL操作,即数据的增删改查和表的日常维护等。其中Thrift/REST API主要用于支持非Java的上层业务需求,MapReduce接口主要用于批量数据导入以及批量数据读取。
HBase客户端访问数据行之前,首先需要通过元数据表定位目标数据所在RegionServer,之后才会发送请求到该RegionServer。同时这些元数据会被缓存在客户端本地,以方便之后的请求访问。如果集群RegionServer发生宕机或者执行了负载均衡等,从而导致数据分片发生迁移,客户端需要重新请求最新的元数据并缓存在本地。
Master主要负责HBase系统的各种管理工作:
RegionServer主要用来响应用户的IO请求,是HBase中最核心的模块,由WAL(HLog)、BlockCache以及多个Region构成。
WAL(HLog):HLog在HBase中有两个核心作用——其一,用于实现数据的高可靠性,HBase数据随机写入时,并非直接写入HFile数据文件,而是先写入缓存,再异步刷新落盘。为了防止缓存数据丢失,数据写入缓存之前需要首先顺序写入HLog,这样,即使缓存数据丢失,仍然可以通过HLog日志恢复;其二,用于实现HBase集群间主从复制,通过回放主集群推送过来的HLog日志实现主从复制。
BlockCache :HBase系统中的读缓存。客户端从磁盘读取数据之后通常会将数据缓存到系统内存中,后续访问同一行数据可以直接从内存中获取而不需要访问磁盘。对于带有大量热点读的业务请求来说,缓存机制会带来极大的性能提升。
BlockCache缓存对象是一系列Block块,一个Block默认为64K,由物理上相邻的多个KV数据组成。BlockCache同时利用了空间局部性和时间局部性原理,前者表示最近将读取的KV数据很可能与当前读取到的KV数据在地址上是邻近的,缓存单位是Block(块)而不是单个KV就可以实现空间局部性;后者表示一个KV数据正在被访问,那么近期它还可能再次被访问。当前BlockCache主要有两种实现——LRUBlockCache和BucketCache,前者实现相对简单,而后者在GC优化方面有明显的提升。
Region:数据表的一个分片,当数据表大小超过一定阈值就会“水平切分”,分裂为两个Region。Region是集群负载均衡的基本单位。通常一张表的Region会分布在整个集群的多台RegionServer上,一个RegionServer上会管理多个Region,当然,这些Region一般来自不同的数据表。
一个Region由一个或者多个Store构成,Store的个数取决于表中列簇(column family)的个数,多少个列簇就有多少个Store。HBase中,每个列簇的数据都集中存放在一起形成一个存储单元Store,因此建议将具有相同IO特性的数据设置在同一个列簇中。
每个Store由一个MemStore和一个或多个HFile组成。MemStore称为写缓存,用户写入数据时首先会写到MemStore,当MemStore写满之后(缓存数据超过阈值,默认128M)系统会异步地将数据flush成一个HFile文件。显然,随着数据不断写入,HFile文件会越来越多,当HFile文件数超过一定阈值之后系统将会执行Compact操作,将这些小文件通过一定策略合并成一个或多个大文件。
HBase底层依赖HDFS组件存储实际数据,包括用户数据文件、HLog日志文件等最终都会写入HDFS落盘。HDFS是Hadoop生态圈内最成熟的组件之一,数据默认三副本存储策略可以有效保证数据的高可靠性。HBase内部封装了一个名为DFSClient的HDFS客户端组件,负责对HDFS的实际数据进行读写访问。
容量巨大:HBase的单表可以支持千亿行、百万列的数据规模,数据容量可以达到TB甚至PB级别。传统的关系型数据库,如Oracle和MySQL等,如果单表记录条数超过亿行,读写性能都会急剧下降,在HBase中并不会出现这样的问题。
良好的可扩展性:HBase集群可以非常方便地实现集群容量扩展,主要包括数据存储节点扩展以及读写服务节点扩展。HBase底层数据存储依赖于HDFS系统,HDFS可以通过简单地增加DataNode实现扩展,HBase读写服务节点也一样,可以通过简单的增加RegionServer节点实现计算层的扩展。
稀疏性:HBase支持大量稀疏存储,即允许大量列值为空,并不占用任何存储空间。这与传统数据库不同,传统数据库对于空值的处理要占用一定的存储空间,这会造成一定程度的存储空间浪费。因此可以使用HBase存储多至上百万列的数据,即使表中存在大量的空值,也不需要任何额外空间。
高性能:HBase目前主要擅长于OLTP场景,数据写操作性能强劲,对于随机单点读以及小范围的扫描读,其性能也能够得到保证。对于大范围的扫描读可以使用MapReduce提供的API,以便实现更高效的并行扫描。
多版本:HBase支持多版本特性,即一个KV可以同时保留多个版本,用户可以根据需要选择最新版本或者某个历史版本。
支持过期:HBase支持TTL过期特性,用户只需要设置过期时间,超过TTL的数据就会被自动清理,不需要用户写程序手动删除。
Hadoop原生支持:HBase是Hadoop生态中的核心成员之一,很多生态组件都可以与其直接对接。HBase数据存储依赖于HDFS,这样的架构可以带来很多好处,比如用户可以直接绕过HBase系统操作HDFS文件,高效地完成数据扫描或者数据导入工作;再比如可以利用HDFS提供的多级存储特性(Archival Storage Feature),根据业务的重要程度将HBase进行分级存储,重要的业务放到SSD,不重要的业务放到HDD。或者用户可以设置归档时间,进而将最近的数据放在SSD,将归档数据文件放在HDD。另外,HBase对MapReduce的支持也已经有了很多案例,后续还会针对Spark做更多的工作。
HBase本身不支持很复杂的聚合运算(如Join、GroupBy等)。如果业务中需要使用聚合运算,可以在HBase之上架设Phoenix组件或者Spark组件,前者主要应用于小规模聚合的OLTP场景,后者应用于大规模聚合的OLAP场景。
HBase本身并没有实现二级索引功能,所以不支持二级索引查找。好在针对HBase实现的第三方二级索引方案非常丰富,比如目前比较普遍的使用Phoenix提供的二级索引功能。
HBase原生不支持全局跨行事务,只支持单行事务模型。同样,可以使用Phoenix提供的全局事务模型组件来弥补HBase的这个缺陷。
一个hbase 表由一个或多个列族(CF)组成,一个列族又包含多个列(我们称列为列限定符,CQ),每列存储相应的值。和传统数据库不一样。hbase 表是稀疏表,一些列可能根本不存值。在这种情况下也不会存储null 值。而且该列不会添加到表中。一旦一个rowkey及相应的列值生成了,将会被存储到表中。
在HBase 中,人们使用很多单词来描述不同的部分:行、列、键、单元格、值、行键、时间戳等。为了确保我们说的是同一个事情,我们将对HBase 的术语进行统一,一行由很多列组成,全部由相同的键引用。 一个特定的列和一个键称为单元格。一个单元格可以有很多版本,由不同时间戳的版本来区分。单元格可以叫做键值对。因此,一行由一个键引用,每行由一组单元格组成,其中每一个单元格又由指定的行名和特定列名确定。
有值的列才会被存储到底层的文件系统。另外,即使在创建表时需要定义列族,也不需要提前定义列名称。当列族插入数据时,列名称可以动态生成。因此,在不同的行中会有可能存在数百万动态创建的列名。
为了更快的查询,键和列会按照字母排序存储在表中,同时也存储在内存中
表存储有多个方面 。第一个方面是HBase如何将单独的一个值存储至表中。第二个面活及如何将所有这些单元格存储在一起,以形成 一个表。
从外到内,一个表是由一个或者多个region组成,一个region由一个或者多个列族组成 ,一个列族由一个 store 组成, 一个store 由唯一的 memstore 加上一个或者多个HFile组成, HFlie又是由block组成,而block是由cell组成。
所有的行以及相关的列一起形成了一张表。但是,为了提供可扩展以及快速随机访问的功能,HBase不得不将数据分布在多个服务器中。为了达到这个目的,表被分割成多个region存储,每个region将会存储一个指定区间的数据。region将会被分配到RegionServer以服务于每个region的内容。当新的region被创建后,过了配置的那段时间,HBase的负载平衡器将会把数据移动到其他的RegionServer上,以确保HBase集群负载均衡。类似预分区,还有很多有效的关于region优化的策略。相关的知识点将会在下面几章陈述。
每个region都有一个起始键和一个结束键来定义它的边界。所有这些信息将随着文件保存在region中,也会保存在hbase:meta表中(对于HBase 0.96之前的版本则保存在.META.中)。通过这张表能够跟踪所有的region信息。当它们变得太大,region可以分裂。如果需要,region也可以合并。
列族是HBase中独特的、其他的关系型数据库应用中没有的概念。对于同一个region,不同的列族会将数据存储在不同的文件中,而且它们的配置也可以不同。具有相同访问模式和相同格式的数据应该被划分到同一个列族中。关于格式的一个例子:对于每个用户画像信息,你除了要存储用户图片文件,还需要存储很多文本元数据信息,你可以将它们存储在不同的列族中:一个做了压缩的列族(所有文本信息存储的地方),另一个没有被压缩的列族(所有图片存储的地方)。关于访问模式的例子:如果一些信息大部分情况下被读取并且几乎从未被写入更新;另外-部分也是多数时候被写入却几乎未被读取,你就可以将它们分在不同的列族中。如果你想存储的列具有相似的存储格式和访问模式,就对它们进行重组并划分到同-个列族中。
对于一个给定的RegionServer,它的写入缓存区是由所有的列族共享,这些列族的配置适用该主机托管的所有region。过度使用列族将对缓存产生压力,这将会产生很多小文件,导致很多次的合并,反过来将会影响性能。当你对表的列族进行配置时,技术上并没有数目的限制。但是,综观过去的几年,我们工作中遇到的绝大部分的用户案例都是需要仅要求一个列族。有时有些需要两个列族,但是每次我们看到超过两个列族时,通常都是建议其减少列族的个数来改善效率。如果你的设计包括三个以上的列族,你可能需要认真考虑下是否真的需要这么多列族,大部分情况下它们可以重新分组。如果你没有对两个列族之间做一致性约束,而且数据也将在不同的时间存储到各自的列族中,你可以创建两个表,每个表都有一个列族,而不是给一个表创建两个列族。这个策略在决定region的大小的时候很有效。实际上,虽然保持两个列族大小相同更好。通过把它们分割成两个不同的表,这样使其更容易独自增长。
我们发现一个store对应着一个列族。一个store对象由一个memstore和零个或更多的strore File(称为HFile)组成。store是存储所有写入表中的信息的实体,并且当数据需要从表中读取时也将被用到。
当内存写满必须要刷新到磁盘的时候,HFlie就会被创建。随着时间的推移,HFlie最终会被压缩成大的文件。它们是HBase用来存储表数据的文件格式。HFlie由不同种类的block块组成(如索引块和数据块)。HFlie存储在HDFS上,因此它们也能够获得Hadoop持久化和多副本的益处。
HFile由block组成。这些block不应该和HDFS的block混淆。一个HDFS block可以包含很多HFile block。HFile block通常在8KB和1MB之间,但是默认大小是64KB,然而,如果一个表配置了压缩选项,HBase仍然会产生64KB 大小的block,然后压缩block。根据数据的大小以及压缩的格式,压缩后的block存储在磁盘大小也不一样。大的block将会创建数量较少的索引数据,这将有助于顺序表访问,而小一点的block将创建更多的索引值,有利于随机访问。
如果你将block size配置得很小,将会产生过多的HFlie block索引,这样给内存带来很大的压力,将会取得和预期相反的效果。同时,由于压缩的数据很小,压缩率也很低,数据容量将会增大。当你决定修改默认值的时候,需要考虑到所有的细节信息。在你做任何决定性的变化的时候,需要使用不同的配置并测试你的应用负载。然而,大部分情况下,建议使用默认值。
下面主要的块类型会在HFile文件涉及(因为它们大多是内部的细节实现,我们只会提供一个概述;如果想对具体的块类型了解更多,参考HBase的源代码):
数据块采用逆序存储。这就意味着,数据块也按照逆序写入,而不是先在文件开头放入索引后将其他数据写入。先存储数据块然后存储数据块索引,Trailer数据块存储在最后。
HBase是面向列存储的数据库。这就意味着每一列将单独存储,而不是单独存储整个行。因为数据值能在不同的时间插入,因此在HDFS上最终变成不同的文件。
HBase引入了列簇的概念,列簇下的列可以动态扩展;另外,HBase使用时间戳实现了数据的多版本支持。
视图解说:
示例表中包含两行数据,两个rowKey分别为 com.cnn.www 和 com.example.www,按照字典序由小到大排列。每行数据有三个列簇,分别为anchor、contents以及people,其中列簇anchor下有两列,分别为cnnsi.com以及my.look.ca,其他两个列簇都仅有一列。可以看出 根据行 com.cnn.www 以及列 anchor:cnnsi.com 可以定位到数据CNN,对应的时间戳信息是t9,而同一行的另一列 contents:html 下有三个版本的数据,版本号分别为 t5,t6和t7。
Base是由一系列KV构成的。然而HBase这个Map系统却并不简单,有很多限定词——稀疏的、分布式的、持久性的、多维的以及排序的。接下来,我们先对这个Map进行解析,这对于之后理解HBase的工作原理非常重要。
HBase中Map的key是一个复合键,由rowkey、column family、qualifier、type以及timestamp组成,value即为cell的值。
例子:
上节逻辑视图中行"com.cnn.www"以及列"anchor:cnnsi.com"对应的数值"CNN"实际上在HBase中存储为如下KV结构:
{“com.cnn.www”,“anchor”,“cnnsi.com”,“put”,“t9”} -> “CNN”
同理,其他的KV还有:
{“com.cnn.www”,“anchor”,“my.look.ca”,“put”,“t8”} -> “CNN.com”
{“com.cnn.www”,“contents”,“html”,“put”,“t7”} -> “…”
{“com.cnn.www”,“contents”,“html”,“put”,“t6”} -> “…”
{“com.cnn.www”,“contents”,“html”,“put”,“t5”} -> “…”
{“com.example.www”,“people”,“author”,“put”,“t5”} -> “John Doe”
多维
这个特性比较容易理解。HBase中的Map与普通Map最大的不同在于,key是一个复合数据结构,由多维元素构成,包括rowkey、column family、qualifier、type以及timestamp。
稀疏
对于hbase来说,空值不需要任何填充。因为 hbase 的列在理论上是可以无限扩容的,对于百万列的表来说,通常都会存在大量的空值,如果使用填充null的策略,势必会造成大量空间的浪费。因此稀疏性是Hbase的列可以无限扩展的一个重要条件
排序
构成HBase的KV在同一个文件中都是有序的,但规则并不是仅仅按照rowkey排序,而是按照KV中的key进行排序——先比较rowkey,rowkey小的排在前面;如果rowkey相同,再比较column,即column family:qualifier,column小的排在前面;如果column还相同,再比较时间戳timestamp,即版本信息,timestamp大的排在前面。这样的多维元素排序规则对于提升HBase的读取性能至关重要
分布式
构成HBase的所有Map并不集中在某台机器上,而是分布在整个集群中。
与大多数数据库系统不同,HBase中的数据是按照列簇存储的,即将数据按照列簇分别存储在不同的目录中。
行式存储
行式存储系统会将一行数据存储在一起,一行数据写完之后再接着写下一行,最典型的如MySQL这类关系型数据库,如图1-4所示。
**行式存储在获取一行数据时是很高效的,但是如果某个查询只需要读取表中指定列对应的数据,那么行式存储会先取出一行行数据,再在每一行数据中截取待查找目标列。**这种处理方式在查找过程中引入了大量无用列信息,从而导致大量内存占用。因此,这类系统仅适合于处理OLTP类型的负载,对于OLAP这类分析型负载并不擅长。
列式存储
列式存储理论上会将一列数据存储在一起,不同列的数据分别集中存储,最典型的如Kudu、Parquet on HDFS等系统(文件格式),如图所示。
**列式存储对于只查找某些列数据的请求非常高效,只需要连续读出所有待查目标列,然后遍历处理即可;但是反过来,列式存储对于获取一行的请求就不那么高效了,需要多次IO读多个列数据,最终合并得到一行数据。**另外,因为同一列的数据通常都具有相同的数据类型,因此列式存储具有天然的高压缩特性。
列簇式存储
从概念上来说,列簇式存储介于行式存储和列式存储之间,可以通过不同的设计思路在行式存储和列式存储两者之间相互切换。比如,一张表只设置一个列簇,这个列簇包含所有用户的列。HBase中一个列簇的数据是存储在一起的,因此这种设计模式就等同于行式存储。再比如,一张表设置大量列簇,每个列簇下仅有一列,很显然这种设计模式就等同于列式存储。上面两例当然是两种极端的情况,在当前体系中不建议设置太多列簇,但是这种架构为HBase将来演变成HTAP(Hybrid Transactional and Analytical Processing)系统提供了最核心的基础。
命名空间,类似于关系型数据库的DatabBase 概念,每个命名空间下有多个表。HBase有两个自带的命名空间,分别是hbase 和default,hbase 中存放的是HBase 内置的表,default 表是用户默认使用的命名空间。
表,一个表包含多行数据
一行数据包含一个唯一标识RowKey。多个column以及对应的值。在hbase中,一张表中所有row都按照rowKey的字典序由小到大排序
column family在表创建的时候需要指定,用户不能随意增减。一个column family下可以设置任意多个qualifier,因此可以理解为HBase中的列可以动态增加,理论上甚至可以扩展到上百万列。
与关系型数据库中的列不同,HBase中的column由column family(列簇)以及qualifier(列名)两部分组成,两者中间使用":"相连。比如contents:html,其中contents为列簇,html为列簇下具体的一列。
单元格,由五元组(row,column,timestamp,type,value)组成的结构,其中type表示Put/Delete这样的操作类型,timestamp代表这个cell的版本。这个结构在数据库中实际是以KV结构存储的,其中(row,column,timestamp,type)是K,value字段对应KV结构的V。
时间戳,每个cell在写入HBase的时候都会默认分配一个时间戳作为该cell的版本,当然,用户也可以在写入的时候自带时间戳。HBase支持多版本特性,即同一rowkey、column下可以有多个value存在,这些value使用timestamp作为版本号,版本越大,表示数据越新。
RegionServer是HBase系统中最核心的组件,主要负责用户数据写入、读取等基础操作。RegionServer组件实际上是一个综合体系,包含多个各司其职的核心模块:HLog、MemStore、HFile以及BlockCache。
一个RegionServer由一个(或多个)HLog、一个BlockCache以及多个Region组成。其中,HLog用来保证数据写入的可靠性;BlockCache可以将数据块缓存在内存中以提升数据读取性能;Region是HBase中数据表的一个数据分片,一个RegionServer上通常会负责多个Region的数据读写。一个Region由多个Store组成,每个Store存放对应列簇的数据,比如一个表中有两个列簇,这个表的所有Region就都会包含两个Store。每个Store包含一个MemStore和多个HFile,用户数据写入时会将对应列簇数据写入相应的MemStore,一旦写入数据的内存大小超过设定阈值,系统就会将MemStore中的数据落盘形成HFile文件。HFile存放在HDFS上,是一种定制化格式的数据存储文件,方便用户进行数据读取。
当hbase读写数据的时候,数据会先写在Write-Ahead logfile的文件中,然后再写入内存中。再系统出现异常的情况下可以根据这个文件重新构建
HBase中系统故障恢复以及主从复制都基于HLog实现。默认情况下,所有写入操作(写入、更新以及删除)的数据都先以追加形式写入HLog,再写入MemStore。大多数情况下,HLog并不会被读取,但如果RegionServer在某些异常情况下发生宕机,此时已经写入MemStore中但尚未flush到磁盘的数据就会丢失,需要回放HLog补救丢失的数据。此外,HBase主从复制需要主集群将HLog日志发送给从集群,从集群在本地执行回放操作,完成集群之间的数据复制。
说明如下:
每个RegionServer拥有一个或多个HLog(默认只有1个,1.1版本可以开启MultiWAL功能,允许多个HLog)。每个HLog是多个Region共享的,图5-2中Region A、Region B和Region C共享一个HLog文件。
HLog中,日志单元WALEntry(图中小方框)表示一次行级更新的最小追加单元,它由HLogKey和WALEdit两部分组成,其中HLogKey由table name、region name以及sequenceid等字段构成。
HLog文件生成之后并不会永久存储在系统中,它的使命完成后,文件就会失效最终被删除。
HLog生命周期包含4个阶段:
HLog构建:HBase的任何写入(更新、删除)操作都会先将记录追加写入到HLog文件中。
HLog滚动:HBase后台启动一个线程,每隔一段时间(由参数’hbase.regionserver.logroll.period’决定,默认1小时)进行日志滚动。日志滚动会新建一个新的日志文件,接收新的日志数据。日志滚动机制主要是为了方便过期日志数据能够以文件的形式直接删除。
HLog失效:写入数据一旦从MemStore中落盘,对应的日志数据就会失效。为了方便处理,HBase中日志失效删除总是以文件为单位执行。查看某个HLog文件是否失效只需确认该HLog文件中所有日志记录对应的数据是否已经完成落盘,如果日志中所有日志记录已经落盘,则可以认为该日志文件失效。一旦日志文件失效,就会从WALs文件夹移动到oldWALs文件夹。注意此时HLog并没有被系统删除。
HLog删除:Master后台会启动一个线程,每隔一段时间(参数’hbase.master.cleaner.interval’,默认1分钟)检查一次文件夹oldWALs下的所有失效日志文件,确认是否可以删除,确认可以删除之后执行删除操作。确认条件主要有两个:
一张表会被水平切分成多个Region,每个Region负责自己区域的数据读写请求。水平切分意味着每个Region会包含所有的列簇数据,HBase将不同列簇的数据存储在不同的Store中,每个Store由一个MemStore和一系列HFile组成
更新数据存储在 MemStore 中,HBase基于LSM树模型实现,所有的数据写入操作首先会顺序写入日志HLog,再写入MemStore,当MemStore中数据大小超过阈值之后再将这些数据批量写入磁盘,生成一个新的HFile文件。LSM树架构有如下几个非常明显的优势:
总结:
MemStore的主要作用:
MemStore中数据落盘之后会形成一个文件写入HDFS,这个文件称为HFile。
HBase实现了一种读缓存结构——BlockCache。客户端读取某个Block,首先会检查该Block是否存在于Block Cache,如果存在就直接加载出来,如果不存在则去HFile文件中加载,加载出来之后放到Block Cache中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的IO操作。
BlockCache主要用来缓存Block。需要关注的是,Block是HBase中最小的数据读取单元,即数据从HFile中读取都是以Block为最小单元执行的。一个RegionServer只有一个BlockCache,在RegionServer启动时完成BlockCache的初始化工作。
写入流程可以概括为三个阶段。
1)客户端处理阶段:客户端将用户的写入请求进行预处理,并根据集群元数据定位写入数据所在的RegionServer,将请求发送给对应的RegionServer。
2)Region写入阶段:RegionServer接收到写入请求之后将数据解析出来,首先写入WAL,再写入对应Region列簇的MemStore。
3)MemStore Flush阶段:当Region中MemStore容量超过一定阈值,系统会异步执行flush操作,将内存中的数据写入文件,形成HFile。
用户写入请求在完成Region MemStore的写入之后就会返回成功。MemStore Flush是一个异步执行的过程。
客户端处理阶段:
HBase客户端处理写入请求的核心流程基本上可以概括为三步。
用户提交put请求后,HBase客户端会将写入的数据添加到本地缓冲区中,符合一定条件就会通过AsyncProcess异步批量提交。HBase默认设置autoflush=true,表示put请求直接会提交给服务器进行处理;用户可以设置autoflush=false,这样,put请求会首先放到本地缓冲区,等到本地缓冲区大小超过一定阈值(默认为2M,可以通过配置文件配置)之后才会提交。很显然,后者使用批量提交请求,可以极大地提升写入吞吐量,但是因为没有保护机制,如果客户端崩溃,会导致部分已经提交的数据丢失
在提交之前,HBase会在元数据表hbase:meta中根据rowkey找到它们归属的RegionServer,这个定位的过程是通过HConnection的locateRegion方法完成的。如果是批量请求,还会把这些rowkey按照HRegionLocation分组,不同分组的请求意味着发送到不同的RegionServer,因此每个分组对应一次RPC请求。
Client与ZooKeeper、RegionServer的交互过程
客户端根据写入的表以及rowkey在元数据缓存中查找,如果能够查找出该rowkey所在的RegionServer以及Region,就可以直接发送写入请求(携带Region信息)到目标RegionServer。
·如果客户端缓存中没有查到对应的rowkey信息,需要首先到ZooKeeper上/hbase-root/meta-region-server节点查找HBase元数据表所在的RegionServer。向hbase:meta所在的RegionServer发送查询请求,在元数据表中查找rowkey所在的RegionServer以及Region信息。客户端接收到返回结果之后会将结果缓存到本地,以备下次使用。
·客户端根据rowkey相关元数据信息将写入请求发送给目标RegionServer,RegionServer接收到请求之后会解析出具体的Region信息,查到对应的Region对象,并将数据写入目标Region的MemStore中。
HBase会为每个HRegionLocation构造一个远程RPC请求MultiServerCallable,并通过rpcCallerFactory.newCaller()执行调用。将请求经过Protobuf序列化后发送给对应的RegionServer。
Region写入阶段
服务器端RegionServer接收到客户端的写入请求后,首先会反序列化为put对象,然后执行各种检查操作,比如检查Region是否是只读、MemStore大小是否超过blockingMemstoreSize等。检查完成之后,执行一系列核心操作
branch-1的写入流程设计为:先在第6步释放行锁,再在第7步Sync WAL,最后在第8步打开mvcc让其他事务可以看到最新结果。正是这样的设计,导致了第4章4.2节中提到的“CAS接口是Region级别串行的,吞吐受限”问题。这个问题已经在branch-2中解决。
MemStore Flush阶段
随着数据的不断写入,MemStore中存储的数据会越来越多,系统为了将使用的内存保持在一个合理的水平,会将MemStore中的数据写入文件形成HFile。
数据写入Region的流程可以抽象为两步:追加写入HLog,随机写入MemStore。
追加写入HLog
HLog保证成功写入MemStore中的数据不会因为进程异常退出或者机器宕机而丢失,但实际上并不完全如此,HBase定义了多个HLog持久化等级,使得用户在数据高可靠和写入性能之间进行权衡。
HLog持久化等级
HBase可以通过设置HLog的持久化等级决定是否开启HLog机制以及HLog的落盘方式。HLog的持久化等级分为如下五个等级。
HLog写入模型
在HBase的演进过程中,HLog的写入模型几经改进,写入吞吐量得到极大提升。之前的版本中,HLog写入都需要经过三个阶段:首先将数据写入本地缓存,然后将本地缓存写入文件系统,最后执行sync操作同步到磁盘。
很显然,三个阶段是可以流水线工作的,基于这样的设想,写入模型自然就想到“生产者-消费者”队列实现。然而之前版本中,生产者之间、消费者之间以及生产者与消费者之间的线程同步都是由HBase系统实现,使用了大量的锁,在写入并发量非常大的情况下会频繁出现恶性抢占锁的问题,写入性能较差。
当前版本中,HBase使用LMAX Disruptor框架实现了无锁有界队列操作。基于Disruptor的HLog写入模型
最左侧部分是Region处理HLog写入的两个前后操作:append和sync。当调用append后,WALEdit和HLogKey会被封装成FSWALEntry类,进而再封装成Ring
BufferTruck类放入Disruptor无锁有界队列中。当调用sync后,会生成一个SyncFuture,再封装成RingBufferTruck类放入同一个队列中,然后工作线程会被阻塞,等待notify()来唤醒。
最右侧部分是消费者线程,在Disruptor框架中有且仅有一个消费者线程工作。这个框架会从Disruptor队列中依次取出RingBufferTruck对象,然后根据如下选项来操作:
随机写入MemStore
KeyValue写入Region分为两步:首先追加写入HLog,再写入MemStore。MemStore使用数据结构ConcurrentSkipListMap来实际存储KeyValue,优点是能够非常友好地支持大规模并发写入,同时跳跃表本身是有序存储的,这有利于数据有序落盘,并且有利于提升MemStore中的KeyValue查找性能。
KeyValue写入MemStore并不会每次都随机在堆上创建一个内存对象,然后再放到ConcurrentSkipListMap中,这会带来非常严重的内存碎片,进而可能频繁触发Full GC。HBase使用MemStore-Local Allocation Buffer(MSLAB)机制预先申请一个大的(2M)的Chunk内存,写入的KeyValue会进行一次封装,顺序拷贝这个Chunk中,这样,MemStore中的数据从内存flush到硬盘的时候,JVM内存留下来的就不再是小的无法使用的内存碎片,而是大的可用的内存片段。
基于这样的设计思路,MemStore的写入流程可以表述为以下3步。
1)检查当前可用的Chunk是否写满,如果写满,重新申请一个2M的Chunk。
2)将当前KeyValue在内存中重新构建,在可用Chunk的指定offset处申请内存创建一个新的KeyValue对象。
3)将新创建的KeyValue对象写入ConcurrentSkipListMap中。
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)中查询目标数据,并将
查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不
同的类型(Put/Delete)。
5) 将从文件中查询到的数据块(Block,HFile 数据存储单元,默认大小为64KB)缓存到
Block Cache。
6)将合并后的最终结果返回给客户端。
prepare阶段:遍历当前Region中的所有MemStore,将MemStore中当前数据集CellSkipListSet(内部实现采用ConcurrentSkipListMap)做一个快照snapshot,然后再新建一个CellSkipListSet接收新的数据写入。prepare阶段需要添加updateLock对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
flush阶段:遍历所有MemStore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及磁盘IO操作,因此相对比较耗时。
commit阶段:遍历所有的MemStore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,把storefile添加到Store的storefiles列表中,最后再清空prepare阶段生成的snapshot。
由于memstore 每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(Put/Delete)有可能会分布在不同的HFile 中,因此查询时需要遍历所有的HFile。为了减少HFile 的个数,以及清理掉过期和删除的数据,会进行StoreFile Compaction。
Compaction 分为两种,分别是Minor Compaction 和Major Compaction。
Compaction是从一个Region的一个Store中选择部分HFile文件进行合并。合并原理是,先从这些待合并的数据文件中依次读出KeyValue,再由小到大排序后写入一个新的文件。之后,这个新生成的文件就会取代之前已合并的所有文件对外提供服务。
Base根据合并规模将Compaction分为两类:Minor Compaction和Major Compaction。
Minor Compaction是指选取部分小的、相邻的HFile,将它们合并成一个更大的HFile。
Major Compaction是指将一个Store中所有的HFile合并成一个HFile,这个过程还会完全清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。
合并小文件,减少文件数,稳定随机读延迟。
提高数据的本地化率。
清除无效数据,减少数据存储量。
最常见的时机有如下三种:MemStore Flush、后台线程周期性检查以及手动触发。
当一些region被不断的写数据,达到regionSplit的阀值时(默认10GB),就会触发region分裂,一旦集群中region很多,就会导致集群管理运维成本增加。可以使用在线合并功能将这些Region与相邻的Region合并,减少集群中空闲Region的个数。
Region合并的主要流程如下:
ConstantSizeRegionSplitPolicy
一个Region中最大Store的大小超过阈值之后就会触发分裂。该策略最简单,但弊端相当大。阈值设置大,对大表友好,小表可能不会触发分裂,极端情况下可能只有一个region。阈值设置小,对小表友好,但一个大表可能在集群中产生大量的region。对于集群管理不是好事。
IncreasiongToUpperBoundRegionSplitPolicy
一个Region中最大Store的大小超过阈值之后就会触发分裂。阈值不是固定的值,而是在一定情况下不断调整的,调整后的阈值大小和Region所属表在当前region server上的region个数有关系。
调整后的阈值 = regions regions flushsize * 2
阈值不会无限增大,maxRegionFileSize来做限制。能够自适应大小表,集群规模大的情况下,对大表很优秀,对小表会产生大量小region
SteppingSplitPolicy
分裂阈值大小和待分裂Region所属表在当前Region Server上的region个数有关系。
如果region个数为1,分裂之为flushsize * 2。
否则为 maxRegionFileSize
大表小表都不会产生大量的region
整个分裂事务过程分为三个阶段:prepare、execute和rollback。
prepare阶段
在内存中初始化两个子region,具体是生成两个HRegionInfo对象,包含tableName、regionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录切分的进展。
execute阶段
rollback阶段
如果execute阶段出现异常,则执行rollback操作。为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据,根据切分进展来做不同的回滚操作。
布隆过滤器由一个长度为N的01数组array组成。首先将数组array每个元素初始设为0。
对集合A中的每个元素w,做K次哈希,第i次哈希值对N取模得到一个index(i),即index(i)=HASH_i(w)%N,将array数组中的array[index(i)]置为1。最终array变成一个某些元素为1的01数组。
下面举个例子,如图2-9所示,A={x,y,z},N=18,K=3。
初始化array=000000000000000000。
对元素x,HASH_0(x)%N=1,HASH_1(x)%N=5,HASH_2(x)%N=13。因此array=010001000000010000。
对元素y,HASH_0(y)%N=4,HASH_1(y)%N=11,HASH_2(y)%N=16。因此array=010011000001010010。
对元素z,HASH_0(z)%N=3,HASH_1(y)%N=5,HASH_2(y)%N=11。因此array=010111000001010010。
最终得到的布隆过滤器串为:010111000001010010。
此时,对于元素w,K次哈希值分别为:
HASH_0(w)%N=4
HASH_1(w)%N=13
HASH_2(w)%N=15
可以发现,布隆过滤器串中的第15位为0,因此可以确认w肯定不在集合A中。因为若w在A中,则第15位必定为1。
如果有另外一个元素t,K次哈希值分别为:
HASH_0(t)%N=5
HASH_1(t)%N=11
HASH_2(t)%N=13
我们发现布隆过滤器串中的第5、11、13位都为1,但是却没法肯定t一定在集合A中。
因此,布隆过滤器串对任意给定元素w,给出的存在性结果为两种:
·w可能存在于集合A中。
·w肯定不在集合A中。
当N取K*|A|/ln2时(其中|A|表示集合A元素个数),能保证最佳的误判率,所谓误判率也就是过滤器判定元素可能在集合中但实际不在集合中的占比。
举例来说,若集合有20个元素,K取3时,则设计一个N=3×20/ln2=87二进制串来保存布隆过滤器比较合适。
代码示例:
<dependencies>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.1-jreversion>
dependency>
dependencies>
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
/**
* 布隆过滤器
*
* @author Mr.Zhang
* @date 2022/8/15 18:17
*/
public class BloomFilterTest {
private static int size = 1000000;
/**
* 期望的误判率
*/
private static double fpp = 0.01;
/**
* 布隆过滤器
*/
private static BloomFilter<Integer> bloomFilter = null;
private static int total = 1000000;
public static void main(String[] args) {
bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
// 插入一百万样板数据
for (int i = 0; i < total; i++) {
bloomFilter.put(i);
}
// 用另外十万测试数据,判断误判率
int count = 0;
for (int i = total; i < total + 100000; i++) {
if (bloomFilter.mightContain(i)){
count++;
System.out.println(i+"误判了");
}
}
System.out.println("总共的误判数"+count);
}
}
HBase Coprocessor分为两种:Observer和Endpoint
Client写入 -> 存入MemStore,一直到MemStore满 -> Flush成一个StoreFile,直至增长到一定阈值 -> 触发Compact合并操作 -> 多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除 -> 当StoreFiles Compact后,逐步形成越来越大的StoreFile -> 单个StoreFile大小超过一定阈值后(默认10G),触发Split操作,把当前Region Split成2个Region,Region会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer 上,使得原先1个Region的压力得以分流到2个Region上
由此过程可知,HBase只是增加数据,没有更新和删除操作,用户的更新和删除都是逻辑层面的,在物理层面,更新只是追加操作,删除只是标记操作。
用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能。
HBase的可扩展性是基于其对数据的重组以合并成更大的文件,然后将表里面的数据分散到很多服务器上的能力。为了达到这个目标, HBase具有三大机制:合井、分裂、重平衡。这 个机制对用户是透明的。但是如果设计不好或者使用不当,将可能影响服务器的性能表现。因此,通过了解这些机制将有助于了解服务器的反应。
HBase将所有接收到的操作保存到memstore内存区。当内存缓冲区满了之后,会将数据刷新到磁盘。随着时间的推移,这样的操作会不停地在HDFS上创建很多小文件,并且根据我们稍后会提到的具体标准,HBase将会选择相应的文件组合成更大的文件。这会在多个方面让HBase受益。首先,新的文件将由托管的RegionServer进行写入,保证数据存储在HDFS本地磁盘上。本地写操作将允许RegionServer在本地查询文件,而不是通过网络查询。其次,在用户查询数据的时候,将会减少查询文件的数量。提高HBase的查询效率,减少在HDFS上保持寻址所有小文件的压力。最后,它允许HBase对存储到这些文件的数据做一些清理工作。如果生存周期(TTL)导致这些文件失效,它们将不会被再次写到新的目标文件。同样适用在某些详细情况下的删除操作。
合并类型有两种
当HBase选择部分而不是所有的来进行合并的时候,这种合并被称为小合并。当前的region有三个或者三个以上的HFile,默认的配置将会触发合并。如果合并被触发,HBase将会基于合并规则选择一些文件进行合并。如果store中的所有文件都被选中,那么这个合并将会演变成大合并。
当HBase选择部分而不是所有的来进行合并的时候,这种合并被称为小合并。当前的region有三个或者三个以上的HFile,默认的配置将会触发合并。如果合并被触发,HBase将会基于合并规则选择一些文件进行合并。如果store中的所有文件都被选中,那么这个合并将会演变成大合并。小合并对数据进行一些清理操作,但不是所有的信息都会被清理。当你对一个单元格执行删除操作的时候,HBase将会存储一个标记,来表示所有比该单元格旧的同一单元格已经被删除。因此,相同key所对应的位于该时间徽之前的所有单元格应该被删除。当HBase在执行合并操作时发现删除标记,将会确保删除所有比该标记旧的,具有相同的key和列限定词的单元格。然而,由于一些单元格数据可能仍然存在于其他的文件中,而这些文件还未被选中进行合并,所以HBase不能删除这些标记,因为它仍然适用,并无法确定是否还有其他的单元格需要被删除。对于那些删除标记存在于未被选中合并的文件中情况也一样。如果是这种情况,那些由于删除标记而应该被移除的单元格将仍然保留到执行大合并为止。基于列族级定义的TTL的过期单元格将被删除,因为它们不依赖其他未被选中的文件的内容,除非该表已被配置成保持最少的版本数。
理解单元格的版本总数和合并的关系很重要。当决定需要保留数据的版本数后,最好就是将该数字视为在给定时间内的最小版本数。一个很好的例子就是只有单个列族的表,该表被配置为保留最大为3的版本数。只有两种情况下HBase将会删除多余的版本。第一种是刷新数据到磁盘的时候,第二种是执行大合并的时候。返回给客户端的单元格版本的个数,通常是根据表的配置而定,然而,当使用RAW=>true的时候,你可以获得所有版本的数据,下面深入探究下几种情况:
- 执行四次put,并不刷到磁盘,紧接着执行scan,不论版本数,将会返回四个版本的数据。
- 执行四次put,刷到磁盘,紧接着执行scan,将会返回三个版本的数据。
- 执行四次put并刷到磁盘,再执行四次put,并刷到磁盘,紧接着执行scan,将会返回六个版本的数据。
- 执行四次put并刷到磁盘,再执行四次put并刷新到磁盘,再进行大合并,紧接着执行scan,将会返回三个版本的数据。
当所有的文件被选中进行合并的时候,我们称为大合并。大合并和小合并工作原理类似,除了前者会将应用于所有相关单元格的delete market删除,此外同一个单元格所有多余的版本也将被丢弃。大合并可以对指定的region的列族级别或在region级别或者表级别进行手动触发。HBase也能够配置,以便于周期性地执行大合并。
自动每周一次的合并能够在任何情况下发生,这个时间取决于你的集群启动时间。这就意味着,当你几乎没有HBase的通信量时,自动合并可以突然发生,但是这也意味着,它可以精确的发生在峰值活动正在进行的时候。因为合并需要读取和重写所有的数据,大合并是I/O密集型操作,将会对你的集群反应时间和SLA产生很大影响。因此,强烈建议完全关闭自动合并,并且在当你知道在对集群影响最小的时候,你自己启动定时任务来触发合并操作。我们也建议不要同时合并所有的表。与将所有表的操作放在一周的同一天执行相反,可以将合并操作分散到整个周。最后,如果你的集群真的含有很多表和分区,建议开发一个程序检查每个分区的文件个数以及最旧文件的生命周期;在region级别上,只要超过你设定的文件数或最旧的文件的生命周期超过你预先配置的期限(即使只有一个文件)都可以触发一个合并操作。这样有助于保持region数据的locality值,同时降低你集群的IO。
分裂操作是合并的相反操作。当HBase将多个文件合并在一起的时候,如果在合并过程中没有太多的值被丢弃,它将创建一个更大的文件。输入的文件越大,就需要更多的时间去解析合并它们等。因此,HBase试图将它们保持在可配置的最大值之下。在HBase0.94这个版本以及更旧的版本中,默认值是1GB,然后在后面的版本中,这个值增加到10GB。当其中一个region的列族达到10GB之后,为了优化负载均衡,HBase将会对指定的region触发拆分机制,将region分裂成两个新region。因为region边界适用给定region的所有列族,所有的列族将会按照同样的方式进行分裂,即使它们远小于配置的最大值。当一个region分裂后,将会分割成两个新的较小的region,第一个region的start key为原来分区的start key,第二个region的end key是原来分区的end key。第一个region的end key以及第二个region的start key由HBase决定,它将会选出最优中点。HBase将会尽量选择中间点,然而,我们不想这个过程耗费太多的时间,所以它不会在一个HFile块内分裂。
关于拆分有些事情需要注意。首先HBase从不会在同一行的两列之间进行分裂。所有的列将会被保存在同一个region里面。因此,如果你有很多列或者非常大的列,单个行的值可能比配置的最大值都大,而HBase不能分裂它。你要避免这种整个region只服务于一行的情况。
同时,你还需要记住HBase将会拆分所有列族。即使你的第一个列族达到10GB的阙值,而第二个列族却只有少数几行或者几千字节,这两列族都会被拆分。分裂后的第二个列族将会在所有的region里面分布着微小的文件。这不是你想看到的状态,同时,你也想重新检查你的表设计来避免这样的事情。当你遇到这样的情况,并且.在你的两个列族之间也没有很强的一致性需求时,可以考虑将它们分裂成两个表。
最后,不要忘了分裂表是需要付出代价的。当一个region分裂并均衡后,region数据在本地的百分比将会降低,直到下次合并。这会影响读取性能,因为客户端会到达托管着region的RegionServer去获得数据,然而当region做完均衡之后,将需要通过网络从其他的RegionServer上获得数据以服务请求。同时,更多的region将会对master、HBase: meta表,以及region服务产生更大的压力。在HBase的世界,region分裂是合理并正常的,但是你还需要关注它们。
Region分裂后,服务器有可能宕机,新的服务器可能加入到集群中,因此,在某种程度上,数据将不会很合理地分布在你所有的RegionServer上。为了帮助集群保持合理的分布数据,每5分钟(默认配置的调度时间)HBase Master将会运行一个负载均衡器来保证所有的RegionServer管理和服务着近乎相同数目的region。
HBa s e有几种不同的负载均衡算法。0.94版本之前,HBas e使用的都是SimpleLoadBalancer,但是从0.96版本之后开始使用StochasticLoadBalancer。尽管推荐使用默认配置的负载均衡器,但你也可以开发自己的负载均衡器并要求HBase使用它。
当一个region被负载均衡器从一个服务器移动到另外一个新的服务器时,在几毫秒内该region将会不可用,同时丢弃它本地的数据,直到下一次做大合并操作的时候。
图2-7展示的是master如何将region从负载最重的服务器分配到负载压力小点的服务器。超负荷的服务器接受来自master的命令将region关闭并转移到目标服务器。
Rowkey长度原则
Rowkey 是一个二进制码流,Rowkey 的长度被很多开发者建议说设计在10~100 个字节,不过建议是越短越好,不要超过16 个字节。
原因如下:
HBase的查询实现只提供两种方式:
Hbase在海量的表数据中,是如何找到用户所需要的表数据的呢?这里是通过索引的机制解决了这个问题。
Client访问用户数据之前需要首先访问zookeeper集群,通过zookeeper集群首先确定-ROOT-表在的位置,然后在通过访问-ROOT- 表确定相应.META.表的位置,最后根据.META.中存储的相应元数据信息找到用户数据的位置去访问。通过 这种索引机制解决了复杂了寻址问题。
减少调整
减少调整这个如何理解呢?HBase中有几个内容会动态调整,如region(分区)、HFile,所以通过一些方法来减少这些会带来I/O开销的调整。
减少启停
数据库事务机制就是为了更好地实现批量写入,较少数据库的开启关闭带来的开销,那么HBase中也存在频繁开启关闭带来的问题。
减少数据量
虽然我们是在进行大数据开发,但是如果可以通过某些方式在保证数据准确性同时减少数据量,何乐而不为呢?
合理设计
在一张HBase表格中RowKey和ColumnFamily的设计是非常重要,好的设计能够提高性能和保证数据的准确性
Column Family的个数具体看表的数据,一般来说划分标准是根据数据访问频度,如一张表里有些列访问相对频繁,而另一些列访问很少,这时可以把这张表划分成两个列族,分开存储,提高访问效率
region中的rowkey是有序存储,若时间比较集中。就会存储到一个region中,这样一个region的数据变多,其它的region数据很少,加载数据就会很慢,直到region分裂,此问题才会得到缓解。
Region过大会发生多次compaction,将数据读一遍并重写一遍到hdfs 上,占用io,region过小会造成多次split,region 会下线,影响访问服务,最佳的解决方法是调整hbase.hregion. max.filesize 为256m。
实时查询,可以认为是从内存中查询,一般响应时间在 1 秒内。HBase 的机制是数据先写入到内存中,当数据量达到一定的量(如 128M),再写入磁盘中, 在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了 HBase I/O 的高性能。
对表内数据的增删查改是可以正常进行的,因为hbase client 访问数据只需要通过 zookeeper 来找到 rowkey 的具体 region 位置即可. 但是对于创建表/删除表等的操作就无法进行了,因为这时候是需要HMaster介入, 并且region的拆分,合并,迁移等操作也都无法进行了
hbase为了保证随机读取的性能,所以hfile里面的rowkey是有序的。当客户端的请求在到达regionserver之后,为了保证写入rowkey的有序性,所以不能将数据立刻写入到hfile中,而是将每个变更操作保存在内存中,也就是memstore中。memstore能够很方便的支持操作的随机插入,并保证所有的操作在内存中是有序的。当memstore达到一定的量之后,会将memstore里面的数据flush到hfile中,这样能充分利用hadoop写入大文件的性能优势,提高写入性能。
由于memstore是存放在内存中,如果regionserver因为某种原因死了,会导致内存中数据丢失。所有为了保证数据不丢失,hbase将更新操作在写入memstore之前会写入到一个write ahead log(WAL)中。WAL文件是追加、顺序写入的,WAL每个regionserver只有一个,同一个regionserver上所有region写入同一个的WAL文件。这样当某个regionserver失败时,可以通过WAL文件,将所有的操作顺序重新加载到memstore中。
① 开启bloomfilter过滤器,开启bloomfilter比没开启要快3、4倍
② Hbase对于内存有特别的需求,在硬件允许的情况下配足够多的内存给它
③ 通过修改hbase-env.sh中的 export HBASE_HEAPSIZE=3000 #这里默认为1000m
④ 增大RPC数量
通过修改hbase-site.xml中的hbase.regionserver.handler.count属性,可以适当的放大RPC数量,默认值为10有点小。
热点现象:
某个小的时段内,对HBase的读写请求集中到极少数的Region上,导致这些region所在的RegionServer处理请求量骤增,负载量明显偏大,而其他的RgionServer明显空闲。
热点现象出现的原因:
HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。然而糟糕的rowkey设计是热点的源头。
热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。
热点现象解决办法:
为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。常见的方法有以下这些:
not only sql (不仅仅是sql)