HBase,全称Hadoop数据库,是一个分布式、可扩展、面向列集群的数据库,是一种分布式数据库解决方案,通过大量廉价的机器解决海量数据的高速存储和读取问题。 这篇文章会像剥洋葱一样,一层一层地剥开她的心。
首先,我们来看看HBase的特点:
性能。 基于LSM树的数据结构设计,保证了顺序写入,并通过Bloom滤波、Compaction等内部优化方法优化了读取性能,使HBase具有较高的读写性能。
可靠性高。 HBase在写入数据之前写入WAL预写日志,以防止计算机宕机时内存中的数据丢失。
易于扩展。 底层依赖HDFS,当磁盘空间不足时,可直接水平扩容。
稀疏。 稀疏性是HBase的一个突出特点,在其他数据库中,null值一般都是用null填充的,而对于百万列的表,通常有大量的null值,如果采用填充null的策略,必然会造成大量的空间浪费。 对于 HBase 空值,不需要填充,因此稀疏性是 HBase 列可无限扩展的重要条件。
列群集存储。 使用集群存储,用户可以自由选择将哪些列放入同一集群中。
多个版本。 HBase支持多版本保存数据,支持按时间戳排序。 用户可以根据需要选择最新版本或历史版本。
如前所述,HBase 符合列群集存储。 为了存储数据,我们首先比较一下基于行的存储、列式存储和列集群存储之间的区别。
行存储
传统的关系数据库是按行存储的,这意味着每一行数据都存储在一起。
列式存储
数据存储在列中,即每列数据存储在一起。 基于列的存储有哪些优势?
1.您可以节省存储空间并仅存储有用的内容,这些内容需要作为 null 值进行处理,以便进行行存储,但不能作为列存储的 null 值进行处理。
2.每列类型一致,当数据落到磁盘时,可以获得更好的数据压缩效率。
3.大多数情况下,我们再次查询时不需要查询整个表的所有列,只会使用部分列,但是由于是基于行的存储方式,每次都必须查询出所有字段,才能过滤出我们需要的字段。 但按列存储就不一样了,就好比吃自助餐,按需查询,只查询需要的东西,明显减少了磁盘io。
列群集存储。
那么什么是列集群存储呢?对于字段较多的表,如果为查询设计的列数过大,必然会造成磁盘IO过大,从而影响查询性能。 列簇存储是指一个列簇下可以存储多个列,每个列簇可以存储在一个文件中,这样可以减少一些磁盘IO,提高查询性能。
每个列簇的数据都存储在一起,每个列簇可以自由选择要存储的列。 因此,列集群存储实际上为用户提供了一个自由选择,如果将所有列都放到一个列集群中,实际上相当于按行存储,每个查询都需要找出所有列,如果每个列单独存储一个列集群,则类似于按列存储。
在目前的系统中,不建议设置过多的列簇,后面会提到,因为刷新时 memstore 的最小单位不是 memstore,而是整个区域,所以设置过多的列簇会非常耗能,但这种架构会演变成 htap(混合事务和分析)系统,为系统提供了最核心的基础。
HBase 架构分为三个部分:Zookeepper 集群、hmaster 和区域服务器。 架构图看起来很复杂,但核心是区域服务器,然后会从区域服务器 -> region ->store ->hfile-> 数据块中移除。
ZooKeeper是一种分布式的无中心元数据存储服务,用于检测和记录HBase集群中服务器的状态信息。
客户端会先访问 zk,查询 hbase:meta 表信息,并将 hbase:meta 表信息缓存在客户端上,以提高查询性能。
HBaster是HBase的佼佼者,不参与具体表数据的管理,只负责宏控,主要有两大任务,一是管理区域服务器,二是执行一些高风险操作,如DDL(建表、删除表等)。
HBase Master的特点:
监控所有区域服务器的状态,并在集群处于数据恢复状态或动态调整负载时将区域分配给区域服务器。
提供DDL相关接口,支持创建、删除、更新表结构。
区域服务器是整个 Habse 架构的核心模块,负责读取和写入实际数据,当访问数据时,客户端直接与 HBase 区域服务器通信。
HBase 的表根据行键区域划分为多个区域,一个区域包含该区域中的所有数据。 一个区域服务器负责管理多个区域,并负责区域服务器上所有区域的读写操作,一个区域服务器最多可以管理1000个区域。
如果服务器既是区域服务器又是 HDFS 数据节点,则每个区域服务器在 HDFS 中存储自己的数据然后,区域服务器的数据将存储在本地 HDFS 中,以加快访问速度。
但是,如果是新迁移的区域服务器,则没有区域服务器数据的本地副本。 在 HBase 运行压缩之前,副本将迁移到本地数据节点。
RegionServer 主要用于响应用户 IO 请求,是 HBase 的核心模块,由 WAL(hlog)、blockcache 和多个区域组成。
hlog 实际上是一个 WAL(write-ahead-log)预写日志,在写入 memstore 之前先写入 hlog 进行数据备份,hlog 存储在磁盘上以保证高可靠性。 hlog 位于区域服务器级别,这意味着整个区域服务器共享一个 hlog。
hlog 在 HBase 中有两个函数:
当区域服务器宕机时,仍在内存中且未写入磁盘的数据不会丢失,仍可以通过 hlog 恢复。
用于实现HBase集群之间的主从复制,通过播放Master集群推送的hlog日志来实现主从复制。
hlog 的日志文件存放在 HDFS 中,HBase 集群默认会在 HDFS 上创建一个 HBase 文件夹,该文件夹下有一个 WAL 目录,用于存放所有相关的 hlog,并且 hlog 不会永久存在,整个 HBase 的总 hlog 会经过以下过程:
1.发生写入操作时,首先构建 hlog。
2.因为 hlog 会不断追加,整个文件会越来越大,所以需要支持滚动日志文件存储,所以 hbase 后台每隔一段时间(默认一小时)就会生成一个新的 hlog 文件,历史 hlog 会被标记为历史文件。
3.一旦数据进入磁盘并形成 hfile,hlog 中的数据就不需要存在了,因为 hfile 存储在 hdfs 中,而 hdfs 文件系统保证了它的可靠性,所以当 hlog 中的数据登陆磁盘时,hlog 就会失效,对应的操作就是将文件从 wal 移动到 oldwal 目录下, 并且该文件此时仍然存在,不会被删除。
4.HBase 有一个后台进程,默认每分钟默认一次失效日志文件,如果没有引用操作,该文件将从物理体中完全删除。
HBase 将从 hfile 查询到的数据缓存到 BlockCache,以便将来直接从内存中读取,从而减少磁盘 IO。
BlockCache 是区域级的,每个区域只有一个 blockcache。
HBase的数据只独立存在于MEMSTORE和HBown中,BlockCache只缓存HBfile中的一些热数据。
每个地域由一个或多个存储组成,该地域是集群负载均衡的基本单元,也是memstore刷新的基本单元。 整个区域实际上是一个 LSM 树,MEMSTORE 对应 C0 树,作为写缓存存储在内存中,hfile 对应 CN 树,存储在磁盘上。 通过保证顺序写入,在牺牲部分读取性能的前提下,大大提升了写入性能,并通过内部优化如布隆滤波器、压缩等方式补偿读取性能,从而实现较高的读写性能。
每个 store 由一个 memstore 和多个 storefile 组成,storefile 的底层其实就是 hfile,storefile 是 hbase 对 hfile 的封装。 每个列簇都存储在一个存储中,所以如果有多个列簇,就会有多个存储,如果列簇太多,就会有太多的内存,这会占用太多的内存,而且在刷新时,也会造成更大的能耗。
3.4.1.1 memstore
Memstore 是 HBase 的写缓存,当 hlog 写入成功后,会先写入 Memstore,按照 rowkey 字典顺序进行排序,当达到一定阈值时,数据会刷新到 HDFS 中,形成一个 hfile 文件。 HBase 的 Memstore 使用了跳转表的数据结构,所以这里就不多介绍了。
memstore的作用:
写入缓存:批量累加数据,批量放置磁盘存储,减少磁盘IO,提高性能。
排序:按照行键的字典顺序保留数据。
Memstore 在读写方面起着很大的作用,最大的耗能操作是在 flush 操作中,下面我们将详细介绍。
Memstore 刷新触发条件。
我们已经知道,当 memstore 的大小达到某个阈值时,数据会被刷新到 HDFS 中,形成一个 hfile 文件,但需要注意的是,memstore 刷新的最小操作单元不是 memstore,而是整个 hregion。 也就是说,如果有一个 memstore 需要刷新,整个 hregion 都会受到影响,所以如果一个 hregion 中的 memstore 太多,每次刷新的开销必然会很大,所以每个表不应该设置太多的列簇。 触发刷新操作的具体条件如下:
1.什么时候区域中的内存存储高达 HBasehregion.memstore.flush.size(默认值为 128MB)会触发该区域中的所有 Memstore 刷新,并且不会阻止写入操作。
2.什么时候一个区域中的所有内存存储大小之和达到 HBasehregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size(默认值 2 * 128 = 256 MB),则会触发该区域的所有 memstore 刷新,从而阻止该区域的写入操作。
3.什么时候当区域准备好脱机时memstore 大小的总和达到 HBasehregion.preclose.flush.size(默认值为 5MB),则触发该区域内的所有 memstore 刷新,然后可以禁用该区域。
4.什么时候regionserver 中的所有内存存储大小之和达到 HBaseregionserver.global.memstore.大小 * HBase HeapSize (默认值 0..)4 * Heap size),从memstore最大的区域开始触发regionserver中所有区域的刷新,并阻塞整个regionserver的写入操作。直到 memstore 大小回退到 HBase 的上一个参数值regionserver.global.memstore.size.lower.limit(默认值为 0。95) 次,然后才解除堵塞物。
5.什么时候区域服务器中的 WAL(即 hlog)到 HBaseregionserver.maxlogs(默认值 32),HBase 会为 Memstore Flush 选择最早的 WAL 对应的区域,同时也会阻塞对应区域的写入操作。
6.RegionServer 将定期刷新内存存储,周期为 HBaseregionserver.OptionalCacheFlushInterval(默认值为 1 小时)。 为了避免同时刷新所有区域,定期刷新将具有随机延迟。
7.用户可以通过执行 flush [table] 或 flush [region] 命令来执行此操作手动冲洗区域中的表或内存存储。
3.4.1.2 hfile
hfile 是 HBase 存储数据的文件组织形式,它通过参考 Bigtable 的 Sstable 和 Hadoop 的 Tfile 来实现。 下图是hfile的物理结构示意图,如图所示,hfile会被分成多个大小相等的块,hfile的内部结构还是比较复杂的,感兴趣的同学可以看一下(在这篇文章中,我们主要看一下存储实际数据的数据块。
3.4.1.2.1 datablock
数据块是 HBase 中最小的数据存储单元。 datablock 主要存储用户的 keyvalue 数据(keyvalue 后面通常跟一个时间戳,图中不标注),keyvalue 结构是 HBase 存储的核心,每个数据都以 keyvalue 结构存储在 HBase 中。 键值结构可以表示如下:
每个键值由 4 个部分组成,即键长度、值长度、键和值。 其中 key length 和 value length 是两个固定长度的数值,而 key 是一个复杂的结构,首先是 rowkey 的长度,然后是 rowkey,然后是 columnfamily 的长度,然后是 columnfamily,然后是 columnqualifier,最后是 timestamp 和 keytype(keytype 有四种类型,分别是 put、delete、deleteColumn 和 deletefamily),value 没有那么复杂,它是纯二进制数据的字符串。
3.4.1.2.2 基本概念。
从上图可以看出,有些长度是固定值,所以稍微简化一下键,然后重点放在这些内容的具体含义上。 键由 rowkey + columnfamily + column qualifier + timestamp + keytype 组成,值为实际值。
rowkey:每行数据的主键是HBase的唯一查询条件,因此行键的设计至关重要。
column family:列簇,每个列簇的数据存储在一起,每个列簇用户可以自由选择存储哪些列,同一列簇的所有成员都有相同的列簇前缀,通常相同类型的列簇下都存储在一个列簇下,但注意不要设置太多的列簇, 稍后将讨论。
qualifier:列,即以列簇为前缀的特定列,格式为 column family:qualifier。
timestamp:Timestamp,插入单元格时的时间戳,默认用作单元格的版本号。 不同版本的数据按相反的时间顺序排序,即最新的数据排在第一位。 当数据需要更新时,不会像MySQL那样直接更新到源数据,而是会添加一条新的数据,并且新数据的时间戳会更大,因此会排在第一位。 您可以根据时间戳和保存的版本数设置 TTL。
type:数据类型,用于区分是否为删除数据,删除数据与更新数据相同,不会直接删除数据的物理数据,会插入一条新的数据,但该数据的类型会标记为删除,在查询中会默认过滤。 仅在执行中major compaction仅在操作期间清除删除数据。
cell:cell,在 HBa 中,该值将保存为单元格中的单元格。 要定位单元格,需要满足“rowkey + column family + qualifier + timestamp + keytype”这五个要素。 每个单元格都包含相同数据的多个版本。 单元格中没有数据类型,它完全是字节存储。
在介绍读写过程之前,我们先介绍一下元表,为什么我们要先介绍一下,因为它是唯一的读写方式,而且是rowkey的指南。
元表存储了所有的区域信息,元表存储在 zk 中,当第一次读写访问或区域失败时,客户端会先访问 zk,然后客户端会根据 rowkey 访问对应的区域服务器,然后客户端会访问对应区域服务器进行相应的读写操作。
1.客户端首先访问ZooKeeper,获取元表所在的地域服务器。
2.访问对应的地域服务器,获取元表表,查询目标数据所在的地域在哪个地域服务器。 表的区域信息和元表的位置信息缓存在客户端的元缓存中,方便下次访问。
3.客户端直接与目标区域服务器通信。
4.数据按顺序写入(附加)到 hlog (wal) 中,以防止机器停机和内存中的数据丢失。
5.将数据写入对应的 memstore,数据会在 memstore 中排序。
6.向客户端发送 ACK。
7.达到 memstore 的触发条件后,数据会刷新到 hfile。
1.客户端首先访问ZooKeeper,获取元表所在的地域服务器。
2.访问对应的地域服务器,获取元表表,查询目标数据所在的地域在哪个地域服务器。 表的区域信息和元表的位置信息缓存在客户端的元缓存中,方便下次访问。
3.客户端直接与目标区域服务器通信。
4.分别在它尚未放置在内存中memstore 和它已掉落到磁盘上(对于 hfile 数据优先从内存中的 blockcache 缓存缓存中读取,读取缓存不从 hfile 加载)并且所有找到的数据都会被合并。 需要注意的是,HBase的数据只独立存在于内存和磁盘中,即MEMSTORE和HFILE中的数据一定是全量数据,而BlockCache(读缓存)只是HFILE缓存在内存中的热数据的一部分,而BLOCKcache的功能是直接从内存中读取HFILE的数据, 减少磁盘 IO 的数量。
5.将从文件 hfile 查询到的块(hfile 数据存储单元,默认大小为 64kb)缓存到块缓存中。
6.然后,合并的最终结果将包含最新数据返回给客户端。
阅读该过程的详细说明。
HBase的写入操作非常方便,更新其实只是数据的一个新时间戳,而数据的删除只是一个删除标记,只是再次major compaction在物理删除之前。 而且因为 hbase 中的同一个 rowkey 是保存多个版本的数据,以及不同的 keytype 数据,所以同一个 rowkey 会对应多条数据,所以这里并不像我们想象的那样,如果 rowkey 命中 blockcache 或者 memstore,就会直接返回,远没有想象中那么简单, 所以没有先从 BlockCache 或者 Memstore 读取的概念,这本身就不对。
读取数据时,必须读取内存 memstore 和磁盘 hfile 中的数据,会创建两种类型的扫描器(storefilescanner 和 memstorescanner)来分别探索 hfile 和 memstore 中的数据,然后会根据用户选择的时间范围和行键范围过滤掉一些扫描器,最后会先为 hfile 数据找到对应的块, 并且 blockcache 在查找区块时会优先搜索,找不到后再从 hfile 加载。memstorescanner 会从 memstore 中查询数据,最后将内存和磁盘中的数据合并,将最新的数据返回给客户端。 简化流程如下:
1.构建扫描程序
每个 storescanner 都会为当前 store 中的每个 hfile 构造一个 storefilescanner,用于检索相应的文件。 同时,为对应的 memstore 构建 memstorescanner,用于对存储中的 memstore 进行数据检索。
1.筛选扫描仪
根据“时间范围”和“行键范围”筛选 StoreFilesCanner 和 MemstoresCanner,以消除绝对没有要检索的结果的扫描程序。
1.seek rowkey
所有 storefilescanner 都开始准备并找到负责的 hfile 中满足条件的起始行。 Seek过程(这里省略了Lazy Seek优化)也是一个非常核心的步骤,它由以下三个步骤组成:
定位块偏移量:读取 BlockCache 中 hfile 的索引树结构,根据索引树获取对应行键所在的块偏移量和块大小
加载块:首先根据 blockoffset 在 blockcache 中找到数据块,如果不在缓存中,则将其加载到 hfile 中。
查找键:在数据块内使用二进制搜索方法查找特定的行键。
当达到触发条件时,HBase 的 MEMSTORE 会将 MEMSTORE 中的数据刷新到 HDFS,每次都会形成一个新的 HBase 文件,所以随着时间的不断积累,同一存储下的 HBase 文件会越来越多,这会降低 HBase 查询的性能,这主要体现在查询数据的 IO 数量增加上。 为了优化查询性能,HBase 合并了较小的 HFLifer 以减少文件数量,这种合并 HBase 操作称为压缩。
minor compaction:将合并多个相邻的 hfile,并在合并过程中清理 TTL 数据,但不会清理已删除的数据。 小压缩消耗资源少,IO量小,文件数量少,提高读操作性能,适合高频运行。
major compaction:一个 store 下的所有 hfile 都会被合并,过期和删除的数据会被清理干净,也就是说,所有需要删除的数据都会在 major 压缩中被删除。 一般来说,主要的压缩时间会持续很长时间,整个过程会消耗大量的系统资源,对上层业务的影响会很大。 因此,在生产环境中,大压实的自动触发通常被禁用,并在非高峰时段手动触发。
数据类型:没有数据类型,都是字节数组(有一个实用程序类字节将 j**a 对象序列化为字节数组),而传统的关系型数据库具有丰富的数据类型。
数据操作:HBase只有插入、查询、删除等非常基础的操作,表之间没有关系,而传统数据库一般功能很多,相互关联。
存储模式:HBase基于列集群存储,传统关系型数据库基于行存储。
数据更新:在数据更新的情况下,Habse 不会像传统数据库那样直接修改记录,而是在 DELETE 状态下插入一条新数据,旨在保证顺序写入,提高数据 I/O 效率,提高性能。 并且旧数据不会立即删除,只会在压缩过程中删除。
时间版本:当HBase数据写入单元时,会附加一个时间戳,默认为regionserver写入数据的时间,但也可以指定不同的时间。 数据可以有多个版本,可以设置 TTL,也可以设置要保留的版本数。
索引:MySQL支持多种类型的索引,而HBase只有一个行键
易于扩展。 从事过MySQL库表分片的同学一定体会过它的魅力,很麻烦,但是HBase可以动态、水平扩展,非常方便。
只有 3 种方法可以访问 HBase。
1.通过单个行键进行访问。
2.通过 rowkey 进行范围
3.全表扫描。
长度原则。 RowKey是一个二进制代码流,可以是任意字符串,最大长度为64KB,在实际应用中一般为10-100bytes。 建议越短越好,不超过 16 个字节。
唯一的原则。 行键的唯一性必须通过设计来保证。 由于 HBase 中的数据存储是键值的形式,因此如果将相同的行键数据插入到 HBase 中的同一个表中,则现有数据将被新数据覆盖。
排序原则。 HBase 的行键按 ASCII 排序。
散列原理。 行键应均匀分布在 HBase 节点上,防止热数据导致数据倾斜。
reversing
顾名思义,直接反转的意思。 比如用户ID、手机号等,rowkey 的头部没有随机性,但尾部的随机性很好,那么我们可以直接翻转rowkey。 反转可以有效地使行键随机分布,但会牺牲行键的有序性。 它适用于获取操作,但不适用于扫描操作,因为原始行键上数据的自然顺序已被打乱。
salting
加盐的原理是在原始行键前面添加一个固定长度的随机数,以确保数据在所有区域之间进行负载均衡。
hashing
哈希与加盐类似,只是哈希要求前缀不能是随机的,需要使用一些哈希算法,以便客户端在哈希后可以重构行键。
推荐程序:
在大多数情况下,可以选择服务主键的最后三位数字,将 HBase 分区的其余部分作为加盐前缀,然后使用服务主键作为串联的行键。
数据分析能力弱:数据分析是HBase的弱点,如聚合操作、多维复杂查询、多表关联查询等。 因此,我们一般在HBase之上构建Phoenix或Spark等组件,以增强HBase的数据分析和处理能力。
二级索引本身不支持:HBase 默认只索引单列行键,因此正常情况下查询非行键列较慢。 因此,我们一般选择HBase二级索引方案,目前比较成熟的方案是Phoenix,也可以选择Elasticsearch Solr等搜索引擎自行设计和实现。
不支持原生 SQL:SQL 查询也是 HBase 的一个弱点,但幸运的是,这可以通过引入 Phoenix 来解决,这是一个为 HBase 设计的 SQL 层。
HBase 本身不支持全球跨行交易,仅支持单行交易模型。 同样,Phoenix 提供的全局事务模型组件可以用来弥补 HBase 的这一缺点。
故障恢复时间长。
1.在设计表结构时,一定不要设置太多的列簇,不要超过三个,最好只有一个。
2.rowkey的设计一定要注意加盐或哈希,避免数据热的问题。
3.默认情况下,需要关闭 Major Compaction,因为它会消耗资源并影响写入,因此您可以在非高峰时段手动执行。
4.如果触发了 memstore 的刷新,需要注意 region server 下的所有 memstore 是否都达到阈值,因为这会影响整个 region 服务器的写入。
5.BlockCache 不能有偏差,HBase 中的数据要么在写缓存 memstore 中没有写入磁盘,要么是已经写入磁盘的数据,这两个部分必须包含所有数据,blockcache 只是 hfile 缓存在内存中的热数据的一部分。
1.人们常说HBase数据读取memstore、hfile和blockcache,为什么只有storefilescanner和memstorescanner两种类型的扫描器?没有blockcachescanner?
HBase 中的数据只独立存在于 memstore 和 storefile 中,blockcache 中的数据只是 storefile 中数据的一部分(热数据),也就是说,blockcache 中的所有数据都必须存在于 storefile 中。 因此,MemStoresCanner 和 StoreFilesCanner 可以覆盖所有数据。 当 storefilescanner 通过索引定位到要查找的键所在的块时,它首先检查该块是否存在于块缓存中,如果存在则直接检索,如果不存在,则在对应的 storefile 中读取。
2.数据更新操作在将数据放入磁盘之前将数据写入内存存储。 下单后需要在BlockCache中更新对应的KV吗?如果我不更新,我会读取脏数据吗?
如果你理解了第一个问题,相信很容易得出这个答案:不需要在blockcache中更新对应的kv,脏数据也不会被读取。 写入 memstore 的数据将形成一个新文件,该文件独立于 blockcache 中的数据,并且存在多个版本。
作者:京东物流于建飞.
*:京东云开发者社区自猿齐说技术**请注明**。