Skip to main content

介绍Hbase

· 21 min read

详细介绍HBase

HBase是一个分布式的、可伸缩的、基于列的存储系统,它是由Apache软件基金会开发的一个开源项目,主要用于大数据的实时随机读写访问。HBase构建在Hadoop文件系统(HDFS)之上,并提供了对大规模数据集的类似于Google的Bigtable的随机实时读/写访问。它是一个分布式数据库,适用于非结构化数据存储,并且能够处理非常大量的数据。 以下是HBase的一些关键特性:

  1. 可伸缩性:HBase是为水平扩展设计的,可以通过添加更多的服务器来增加存储和处理能力。
  2. 分布式存储:数据自动切分到多个服务器上,并且可以在多个服务器上进行读写操作。
  3. 高吞吐量:HBase可以支持大量的数据读写操作,适合于需要高吞吐量的应用场景。
  4. 列式存储:与传统的行式数据库不同,HBase是列式存储的,这意味着它可以非常高效地处理稀疏数据,并且可以单独访问表中的特定列。
  5. 版本控制:HBase中的每个单元格都可以存储多个版本的数据,这使得它可以用于存储有时间戳的数据。
  6. 一致性:HBase提供了强一致性读写,确保了数据的准确性。
  7. 容错性:HBase设计时就考虑了容错性,即使某个服务器出现故障,整个系统仍然可以正常运行。
  8. 与Hadoop生态系统集成:HBase可以与Hadoop生态系统中的其他工具(如Hive、Pig、MapReduce等)无缝集成,便于进行复杂的数据处理和分析。 HBase的数据模型包括以下几个基本概念:
  9. 表(Table):HBase中的数据存储在表中,每个表由行和列组成。
  10. 行(Row):表中的每一行由一个唯一的行键(Row Key)标识。
  11. 列族(Column Family):列族是一组相关列的集合,通常一起被访问和存储。列族必须在创建表时定义,但列可以动态添加。
  12. 列限定符(Column Qualifier):列限定符实际上是列族中的一个列,它可以动态创建。
  13. 单元格(Cell):单元格是行、列族和列限定符的组合,存储了具体的数据。
  14. 时间戳(Timestamp):每个单元格都可以存储多个版本的数据,每个版本都对应一个时间戳。 HBase在处理大规模数据集时非常有用,尤其是在需要随机读写访问的场景下。它常用于大数据应用,如实时分析、日志处理、用户行为分析等领域。

如果使用 column family,那是怎么存储的

在HBase中,使用列族(Column Family)存储数据时,数据实际上是以列族为单位进行组织和存储的。每个列族都有自己的存储和配置设置,例如压缩选项、TTL(Time To Live,即数据存活时间)和版本管理等。以下是使用列族存储数据的基本概念和组织方式:

  1. 列族定义:在创建HBase表时,需要定义列族。列族是不可变的,一旦创建后,不能更改其配置。例如,我们可以创建一个用户表,其中包含两个列族:info用于存储用户信息,stats用于存储用户统计信息。
  2. 列族存储:在HBase中,每个列族的数据物理上是分开存储的。这意味着属于同一个列族的所有列的数据会被存储在同一个存储单元中,这样可以针对列族进行独立的存储和访问优化。
  3. 列限定符:在列族内部,可以使用列限定符(Column Qualifier)来区分不同的列。列限定符是动态的,不需要预先定义。在上述例子中,info列族可能包含nameageemail等列限定符,而stats列族可能包含post_countlike_count等列限定符。
  4. 单元格:每个单元格由行键(Row Key)、列族、列限定符和时间戳唯一确定,存储了具体的数据值。在HBase中,每个单元格都可以存储多个版本的数据,每个版本对应一个时间戳。
  5. 数据存储和检索:当向HBase表写入数据时,数据会根据列族和列限定符被写入相应的存储单元。当读取数据时,可以指定要访问的列族和列限定符,HBase会根据这些信息检索数据。
  6. 存储优化:由于列族是独立存储的,可以为不同的列族配置不同的存储设置,如压缩算法和TTL。这样可以针对不同的数据特性进行优化,提高存储效率和性能。 例如,如果我们有一个用户表,其中包含两个列族infostats,数据可能会按照以下方式组织:
Row Key: user1
- info:
- name: Alice
- age: 30
- email: [email protected]
- stats:
- post_count: 100
- like_count: 200

在这个例子中,info列族存储了用户的基本信息,而stats列族存储了用户的统计信息。这种组织方式允许我们独立地访问和优化每个列族的数据。

它们会连续存储吗

在HBase中,同一个列族内的数据是按照行键(Row Key)排序并连续存储的,这意味着同一个列族中的所有数据都存储在同一个存储单元中。这种设计允许对同一个列族内的数据进行高效的单次扫描,因为相关的数据都存储在连续的磁盘空间上。 然而,不同的列族之间的数据并不是连续存储的。每个列族都有自己独立的存储空间,因此属于不同列族的数据可能会分散在磁盘上的不同位置。这是因为HBase允许每个列族有不同的存储和访问策略,例如不同的压缩设置、TTL(Time To Live)和版本管理等。 当HBase写入数据时,它会根据行键和列族将数据写入相应的存储单元。由于行键是排序的,所以同一个列族内的数据会按照行键的顺序连续存储。这种设计有助于提高读取效率,尤其是当应用程序需要扫描整个列族或特定范围内的行时。 总的来说,HBase中的列族内部数据是连续存储的,但不同列族之间的数据则是分开存储的。这种组织方式允许HBase针对每个列族进行独立的存储优化,同时也支持高效的列族内数据访问。

一个 column family 是怎么存储的

在HBase中,一个列族(Column Family)的数据存储是由多个组件共同工作的,这些组件包括MemStore、StoreFiles(HFiles)、HFile索引和BlockCache。以下是列族数据存储的详细过程:

  1. MemStore
    • 当数据被写入HBase时,首先会进入MemStore,这是一个在内存中的缓冲区。
    • MemStore按照列族来组织,每个列族都有自己的MemStore。
    • MemStore中的数据是按照key-value对排序的,key由行键(Row Key)、列族、列限定符(Column Qualifier)和时间戳组成。
    • 当MemStore达到一定大小(例如128MB)时,它会被刷新到磁盘上,形成一个新的StoreFile。
  2. StoreFiles(HFiles)
    • StoreFiles是MemStore刷新到磁盘上的文件,也称为HFiles。
    • HFiles是HBase中的核心数据存储格式,它是一种有序的、不可变的文件格式,支持快速查找和读取操作。
    • HFiles包含了一系列的数据块(Blocks),每个数据块包含了一系列的key-value对。
    • HFiles有一个独立的文件索引,用于快速定位数据块。
  3. HFile索引
    • HFile索引是一个独立的文件,它包含了HFile中每个数据块的起始位置和关键字的范围。
    • 当查询数据时,HBase使用HFile索引来快速定位到包含所需数据的数据块。
  4. BlockCache
    • BlockCache是HBase中的读缓存,它缓存了最近从HFiles中读取的数据块。
    • 当HBase读取数据时,如果数据块已经在BlockCache中,则直接从缓存中读取,避免磁盘I/O。
    • BlockCache是按照LRU(Least Recently Used)算法管理的,以确保最常用的数据块被缓存。
  5. Compaction
    • 随着数据的不断写入,会产生大量的HFiles,为了优化读取性能,HBase会定期进行Compaction操作。
    • Compaction包括Minor Compaction和Major Compaction。Minor Compaction会将多个小的HFiles合并成一个大的HFile,而Major Compaction会合并所有的HFiles,并且清理掉已经删除或过期的数据。
  6. WAL(Write-Ahead Log)
    • 在数据写入MemStore之前,为了保证数据的持久性,HBase会先将数据写入WAL。

    • WAL是一个磁盘上的日志文件,它记录了所有数据的变更操作。

    • 在发生故障时,HBase可以使用WAL来恢复未刷新到HFiles的数据。 总的来说,HBase中的列族数据存储是一个复杂的过程,涉及内存缓冲、磁盘文件、索引和缓存等多个组件。这些组件共同工作,以确保数据的快速写入、读取和持久性。

单元格 的作用

在HBase中,单元格(Cell)是数据存储和检索的基本单位。每个单元格由行键(Row Key)、列族(Column Family)、列限定符(Column Qualifier)、时间戳(Timestamp)唯一确定,并存储了具体的数据值。单元格在HBase中扮演着关键角色,具有以下作用:

  1. 数据存储:单元格是实际存储数据的地方。在HBase中,数据以键值对的形式存储在单元格中,其中键是由行键、列族、列限定符和时间戳组成的复合键。
  2. 数据版本管理:HBase中的单元格支持多版本数据存储。每个单元格可以存储多个版本的数据,每个版本对应一个不同的时间戳。这种版本控制机制使得HBase能够存储数据的历史变化,并且允许应用程序根据需要检索特定版本的数据。
  3. 数据检索:当从HBase表中读取数据时,可以通过指定行键、列族、列限定符和时间戳来精确地检索特定的单元格。如果省略时间戳,HBase会返回单元格的最新版本的数据。
  4. 稀疏数据存储:由于单元格只在实际存储数据时占用空间,HBase可以高效地处理稀疏数据集,即那些有很多空值或未定义字段的数据集。在列式存储系统中,只有非空值会被实际存储,这有助于节省存储空间。
  5. 数据一致性:HBase中的单元格保证了对同一行的写操作是原子性的,这意味着对单元格的更新要么完全成功,要么完全失败,不会出现部分更新导致的中间状态。
  6. 数据安全性:单元格可以与HBase的安全特性相结合,实现对数据的访问控制。例如,可以为不同的用户或用户组设置不同的访问权限,以控制他们对特定单元格的读写访问。
  7. 数据压缩和编码:由于单元格内的数据通常是相同类型的,HBase可以对单元格内的数据进行高效压缩和编码,减少存储需求并提高读取效率。 总的来说,单元格是HBase中数据模型的核心,它不仅负责存储数据,还提供版本管理、稀疏数据存储、数据一致性和安全性等关键功能。通过设计,单元格使得HBase能够高效地处理大规模的分布式数据集。

来自note文档

Rowkey应当如何设计

从性能上来看,Rowkey设计的最大原则是要保证落在每个Region上的概率是一致的,即Rowkey要完全打散。

不打散有什么坏处:造成数据集中的Region所在RegionServer的读写热点,甚至该Server可能被打挂。打挂后,热点Region又会移到其他Server上,让其他Server也有了被打挂的风险。

建表时,会选择预分区的方式——Hex或Decimal,这个是实际影响表的分区结果,而不只是一个展示。

如果用Hex预分区建表的话,常见的打散方式是md5取个前几位(一般不取全部,是因为会使得rowkey太长);如果用Decimal建表的话,可能要自己构造一些打散方法。

通常不建议用取模的方式(比如uid取模)做前缀打散,因为我们实践中取模不够平均,还是建议用md5。

rowkey长度原则上不允许> 32KB

Column Family

一个Column Family可以理解为一个存储单元,存储属性也是在这一层面上设置的,因此表设计创建时应将具有相同I/O特效的列(Column)放在同一个Column Family上以提高访问性能。

几个额外注意点:

  • 命名越短越好,尽量使用一个字符,如“d”代替“data”

  • 一张表内数量尽量控制在3个以内

  • 表内每一行都拥有相同的Column Family,但某一行对某一Column Family的存储可为空

  • 增加或修改Column Family会导致Region上下线,期间影响业务访问,因此在建表时请尽量确认其设计,避免日后修改

行键设计

PHBase查询数据时会使用行键、行前缀或行范围来检索数据,为使PHBase性能达到最佳,缜密的行键设计是至关重要的,避免查询数据时必须触发全表扫描的情况。

概念须知

  • 表的数据被划分在若干个Region中,均匀分散在各个RegionServer上

  • Region维护的Row Key范围:Start Key ~ End Key

  • Row Key根据字典顺序排列

  • 读写请求根据Row Key定位到对应的Region上

散列 (数据热点问题)

不良的行键设计可能导致同一时间大量访问相邻Row Key,访问聚集在某个RegionServer,最终引发访问性能下降甚至某些Region不可用的后果。

选择散列手段前,确认访问是否依赖有序性:

  • 读请求只有get时通常不依赖有序性,一般可选择hash前缀打散

  • scan一块连续数据时依赖有序性,此时需要寻求一种折中有序性和热点打散的方案,如下文经典案例二。

避免数据热点问题的有效手段:

  • Hash 或者 Mod

    • 前缀使用Hash (md5后取前5-8位) 或者 Mod (数字类型)

    • 前缀长度根据region数来确定

      • 如前缀长度为3时HexString最多可分为4096个region,DecimalString可分为1000个region

      • 但因region后续可能发生split,因此建议在上述计算结果的基础上再增加2~3位

    • 若不考虑有序性,推荐优先使用md5来打散

经典案例

  • 通过uid进行单条查询或扫描

    • 各uid分段流量不均匀分布,如0开头的uid流量明显高于其他,则可对uid做md5后作为前缀

    • 各uid分段流量均匀分布,则可用uid直接作为行键前缀

  • 通过某个特殊id进行单条查询或扫描

    • 某些业务场景需要扫描某个tagid下所有uid的信息,行键的初步设计为tagid_uid

    • 当tagid的数量很少或流量分布不均匀时,很难通过tagid进行合理的分区

    • 此时可对uid做mod作为前缀来实现打散,前缀的位数根据region数决定

    • 最终行键格式:uid%100_tagid_uid (region数最大为100)

    • 扫描方式:发起100个scan,00_tagid_uid, 01_tagid_uid, ..., 99_tagid_uid