08 HBase如何组织与存储数据?
你好,我是彭旭。
上节课,我们分析了云服务的数据存储需求,了解了之前基于MySQL的分库分表支撑海量数据的存储与读写的过程。但是,因为硬件与运维上的巨大成本,我们不得不谋求一个新的存储方案。
这两节课,我们就一起学习HBase的架构和原理,看看HBase是怎么解决之前的问题的。
今天我们会聚焦在HBase组织数据的方式上,解决上节课提到的运维成本问题。希望通过这节课的学习,你能了解两个关键知识点。
- HBase如何存储数据,给数据分区。在集群扩容的过程中,HBase自动迁移分区从而实现负载均衡的过程。
- HBase物理上存储数据的方法,HBase集群包含的组件以及组件之间的协作方式。
HBase数据组织的逻辑模型
使用一个数据库,第一步可以去理解它的数据模型,还有它的数据是怎么组织的。
HBase和关系型数据库一样,以数据表的形式组织数据。一张表包含多行数据,每行数据都由一个唯一的行键来标识。
数据在表中以行键的字典序排序,像这张图一样。
- 行键类似关系型数据库主键,通过行键可以定位到一行数据。对HBase来说,通过行键来读取单行或者多行数据最高效。当然,它也支持基于行键区间的扫描。
- HBase是一个宽列族存储数据库,数据按列族聚簇存储在一起。虽然HBase是列式存储数据库,但不建议一个表有多个列族。所以,在单个列族的情况下,基本上就跟行式存储类似了。
- HBase建表的时候需要至少指定一个列族,但是不需要声明列。每一行数据都可以定义属于自己的列,也就是支持动态的Schema。但是每行的列不一定有值,所以HBase是一个稀疏表。
看到这里你应该还记得上一节课我们讲到的需要兼容多版本的客户端。之前MySQL场景下增减字段都需要走大表DDL的需求,而这种动态Schema的设计,就很好地解决了这个问题。
- 每一列其实都是一个KeyValue键值对,但是可以有多个版本。每个版本会有一个时间戳,你可以在建表的时候指定需要保留最近的N个版本。
HBase的数据模型非常灵活,你可以按需为每行数据定义不同的字段,即使定义了较多的列,只要列没有实际存储数据,就不会占用存储空间。
HBase不提供多表关联查询的能力,所以比较适合单表存储能搞定的业务,比如云服务场景下,通话记录、联系人、短信等都可以用一张表来解决,就很适合用HBase存储。
总结一下,HBase的逻辑模型其实类似一个嵌套了多层的Map,你可以根据行键、列族、列限定符、数据版本一层层递进,找到存储的行、列的数据。
分区,弹性伸缩的基本单元
数据行分片后就形成了分区,那么一个分区自然也就包含多行数据。那HBase是怎么管理分区的呢?
在基础篇里面我们讲到HBase是基于行键,按区间分区的。
事实上,默认情况下,你甚至都不需要关注HBase是如何分区的,当分区文件达到配置的大小后,HBase会自动将大分区从中间分裂成两个小的分区。
如果某个区间段的分区数据被删除了很多,HBase又会自动把相邻的小分区合并成大一点的分区。因为分区太多对集群的管理来说也是负担。
HBase数据分布像下面这张图一样,每个分区大小很均匀,而且是均衡地分布在集群的各个节点上。这里需要注意的是,如果集群节点做了扩容,HBase可能需要一定的时间来将分区进行relocate以及本地化,达到最佳性能。
总结一下,分区是HBase中数据分布与负载均衡的最小单元,HBase能够自动帮我们拆分、合并这些分区。当数据量增长到当前机器配置无法支撑的情况时,我们只需要在集群中增加一些服务器节点,HBase就可以自动帮我们迁移分区、平衡数据与请求。
还是回到在云服务的场景中,联系人、通话记录等都很适合用用户ID做分区,而且你会发现,之前需要手动分库分表的情况在HBase场景下完全自动化了,这样就再也不用凌晨停机去做数据迁移了。
HBase数据存储的物理模型
那HBase是如何存储这些分区的呢?再来看看HBase数据存储的物理模型。
先来看一张HBase集群的架构图。
可以看到,这里涉及3个物理组件。Master对应进程HMaster,RegionServer对应进程HRegionServer以及Zookeeper,以及存储上HBase所依赖的Hadoop DFS组件。
首先来看一下集群中最繁忙,也是物理上的弹性伸缩单元RegionServer。它的工作最明确,就是给客户端提供数据的读写服务。一个RegionServer通常可以负责成百上千个分区,这些分区的读写请求就由它来提供服务。
反过来,一个分区同时只会由一个RegionServer提供服务,而不像某些存储引擎(比如StarRocks、Kudu)一个分区可以由多个节点提供服务,其中一个作为主节点提供数据读写能力,其他节点作为从节点提供数据读取能力。
再来看一下集群的“管家” Master。你可以看到,图中有active和standBy两个HMaster进程,实际上,一个HBase集群可以有多个Master来竞争成为active的Master。只要active的Master宕机了,或者在Zookeeper上的租期过期了,其他standBy的Master就会竞争成为新的active Master,实现高可用,确保集群正常运行。
Master实际上主要负责这么几个工作。
- 监控RegionServer的状态。在RegionServer故障时,将故障RegionServer的分区重新分配给其他活跃的RegionServer。
- 分配管理分区。将分区在集群内均衡地分配给RegionServer。
- 涉及集群管理的操作,如管理表与列族的变更、分区的Split合并等都由Master负责。
你可以参考HMaster.java类的方法来看看Master实际上负责了哪些事情。
事实上,Master和RegionServer已经提供了HBase作为一个数据库需要的所有服务。但是,和所有分布式系统一样,它还需要一个分布式协调服务来提供配置管理、Master选举、元数据存储等功能。而Zookeeper作为Hadoop生态里备受青睐的分布式协调服务,被HBase选用就顺理成章了。
Zookeeper在HBase集群中主要用来做分布式协调。举几个例子吧。
- 确保同时只有一个active的Master。
- 集群一些元数据的存储,比如活跃的RegionServer、分区Split过程中的一些数据等,实际上集群节点的故障检测与恢复,都依赖这些元数据来实现。
- 存储客户端会引导数据hbase:meta表所在的RegionServer。这个hbase:meta元数据表是用来让客户端搞清楚操作的数据分布在哪个分区,应该与哪个RegionServer打交道。在下节课会详细介绍。
从物理进程上来说,这3个组件组成了整个集群。RegionServer的工作最为直接,但是也是最为复杂的一个组件。我们再来细看一下RegionServer内部有哪些组件。
第一个是 WAL(Write-Ahead-Log,预写式日志),这个我们在第2节课介绍LSM的时候有说到,这里复习一下。
WAL类似关系型数据库的Redo日志,数据写入MemStore之前,会将对数据的操作(如Put、Delete等操作)记录到WAL预写入日志文件。这个文件是以追加的形式批量顺序写入磁盘的,所以速度很快。当程序崩溃导致内存MemStore数据丢失后,HBase RegionServer会重放这个WAL日志文件中的数据操作,在内存中重建MemStore的数据。
为了提升并发,每个分区Region会有一个WAL。所以,不同的分区可以并发地写入自己的WAL,以免写入磁盘竞争太大,这对性能也是一个提升。
第二个是 Store存储仓库,每个列族对应一个存储仓库,一个存储仓库包含一个MemStore和多个存储文件。当MemStore的大小达到了配置的阈值后,会作为一个文件刷新、输出、存储到磁盘里。这个存储文件按顺序写入磁盘,不支持修改,以HFile的形式存储在Hadoop的DataNode中。
这个存储仓库又可以细分为 MemStore 和 StoreFile。
MemStore 位于分区服务器的堆内存,它是一个基于NavigableSet存储的有序数据结构。数据在写入MemStore的时候就会按行键排序,刷新输出到存储文件StoreFile的时候会按顺序遍历写入,提高写性能。同时,根据局部性原理,新插入数据总是比老数据使用频繁。所以,MemStore新写入数据的访问速度也是不低的。
另一个 StoreFile 由数据块 Block 组成,而数据块就是HBase数据读取的最小单元,你可以在建表时按列族指定表的数据块大小。如果表启用了前面提到的数据压缩,那么数据在写入StoreFile之前就会按数据块进行压缩,读取时,也同样是对数据块解压后再放入缓存。
这里提一下,理想情况下,每次读取数据的大小都是指定的数据块大小的倍数,这样可以避免一些无效的I/O,效率最高。所以,如果你的程序里面针对较少行的随机读取较多,就可以设置小一点的数据块。反过来,如果按顺序的扫描较多,则可以设置大一点的数据块。
最后我用一棵树来结合整理一下HBase逻辑上的表、分区和物理上存储模型的关系。
再强调一点,HBase本身并不提供存储功能,而是基于HDFS存储。StoreFile在HDFS中对应HFile,WAL生成的日志对应HLog。因为是基于HDFS存储数据,因此在进行分区迁移时,无需真正移动数据。然而,换个角度看,这也意味着HBase与HDFS紧密耦合,无法实现存储与计算的分离,节省成本。
小结
好了,到这里你应该对HBase的逻辑模型跟物理模型都有了一个了解。
HBase的逻辑模型其实跟关系型数据库类似,但是HBase会保存多个版本的数据,默认情况下会读取数据的最新版本,你也可以指定版本或者时间区间来读取一个历史的数据版本。
分区类似关系型数据库的分库分表,但是在HBase中实现了分区的自动管理与负载均衡,这样在增加服务器节点时,再也不需要做数据迁移了。
HBase集群物理上由3个组件组成,负责提供读写服务的RegionServer,负责分区管理与调度的Master以及负责分布式协调的Zookeeper。
此外,HBase依赖于Hadoop HDFS作为数据存储,这也是HBase被人诟病的一个瓶颈。因为它使得HBase与HDFS紧密耦合,无法实现存算分离的架构,增加了系统的复杂性和维护成本。
最后给你一个建议,虽然HBase能够自动管理分区,自动拆分与合并并且负载均衡,但是我还是建议你在定义表的时候就想清楚数据该如何分布,为数据表预先定义好分区。这样可以避免业务高峰期分区的自动拆分与合并引起系统负载过高,导致服务响应时长变长,甚至引起雪崩。
思考题
在逻辑模型中提到,一个HBase表不建议有多个列族,你知道这是为什么吗?
欢迎你在留言区和我交流。如果觉得有所收获,也可以把课程分享给更多的朋友一起学习。欢迎你加入我们的读者交流群,我们下节课见!
- Geek_03c08d 👍(2) 💬(1)
有个疑问,不建议多个列族,为什么还要设计多个列族这个东西?
2024-06-29 - Geek_03c08d 👍(2) 💬(1)
在HBase中,列族(Column Family)是一个重要的概念,但通常建议一个HBase表尽量少使用多个列族,甚至只使用一个列族。原因如下: 存储效率和性能: HBase中的列族是以HFile的形式存储在HDFS上的,不同的列族会被存储在不同的HFile中。每个HFile都有一定的存储开销,包括元数据、索引等。 当一个表有多个列族时,写入和读取操作会涉及多个HFile,增加了I/O开销和复杂性,影响整体性能。 Compaction(合并)开销: HBase定期执行合并操作,将多个小的HFile合并成一个大的HFile。多个列族意味着需要对每个列族分别执行合并操作,这会增加系统的负担和延迟。 内存使用: 每个列族都需要维护独立的MemStore(内存存储),这会增加内存的使用量。如果表中有多个列族,每个列族的MemStore都需要占用内存,导致内存资源的分配更加复杂和紧张。 一致性和原子性: HBase的写操作是以行级别为原子单位的,但这仅限于同一个列族内的操作。如果表中有多个列族,跨列族的写操作无法保证原子性和一致性,这可能导致数据的不一致性。 Schema设计复杂性: 多个列族的Schema设计和管理会更加复杂。维护多个列族的定义、配置和优化需要更多的工作量,增加了开发和运维的复杂度。 基于以上原因,通常建议HBase表尽量使用单一列族,除非有非常明确的需求和理由需要使用多个列族。在设计Schema时,应尽量简化列族的使用,以提升性能和简化管理。 以上回答来自gpt4-o
2024-06-28 - 密码123456 👍(1) 💬(1)
由于不同的列族存储在不同的文件,是不是担心跨文件读取数据?
2024-06-27 - lufofire 👍(0) 💬(1)
思考题感觉本身有点奇怪了。先说疑问: 本文中说过,HBase 的数据模型非常灵活,你可以按需为每行数据定义不同的字段,即使定义了较多的列,只要列没有实际存储数据,就不会占用存储空间, 所以HBase使用的稀疏表, 这个表结构适合存储那些列数非常多但每行只有少数几个列实际有值的场景。而为什么会有列族的概念呢?这是因为列族内的列通常具有相似的访问模式和存储需求。列族在创建表时就需要定义,并且在物理上存储在一起。 这样就会有一个实际场景的矛盾,假如我们业务有很多列, 但是其访问模式差异很大,这该如何处理呢? 回到这个问题,为什么HBase表不建议多列族? 因为列族创建可能是存储需求的不同,所以通常列族存储在不同的HFiles中, 如果有多个列族,那么对于每一次读取操作,HBase可能需要打开多个文件来获取数据,这会增加磁盘I/O,降低读取性能。另外就是不同HFiles本身意味着每个列族有不同的Memstore, 这个也就意味着更耗内存。最后就是因为HBase本身就存在写放大的问题, 多个列族的合并操作,势必带来更复杂的管理。
2024-06-28