HBase读写过程详解

在底层,HBase将数据存储于HDFS中。

客户端是整个HBase系统的入口,用户直接通过客户端对HBase进行操作。HBase Client通过RPC方式和HMaster、RegionServer通信。对于管理类操作,与HMaster进行通信;对于数据读写操作,与HRegionservers进行通信。

目录表

在HBase中,有一个表叫.META.,它保存关于HMaster所引用的regions的所有信息,以获取关于数据的信息。

0.96.0版本之前

在HBase 0.96.0版本之前,它维护两个目录表:-ROOT-和.META.。这两个表是作为HBase表存在的。其中:

  • ROOT: 记录了META表的位置信息(Region信息),-ROOT-只有一个region。
  • META: 记录了用户表的位置信息(Region信息),.META.可以有多个regoin。

Client访问用户数据之前需要首先访问zookeeper,然后访问-ROOT-表,接着访问.META.表,最后才能找到用户数据的位置去访问(需要三次请求找到用户Table所在的位置,然后第四次请求开始获取真正的数据),不过client端会做cache缓存。

0.96.0版本及之后

从HBase 0.96.0版本开始,它维护一个目录表hbase:meta,作为一个HBase表存在。这个表在HBase shell中list命令中会过滤掉,但实际和普通表无任何区别。

hbase:meta表(0.96.0之前叫做 .META.) 维护一个系统中所有region的列表。

hbase:meta的位置之前是在-ROOT-表内跟踪的,现在存储在ZooKeeper中(Zookeeper的/hbase/meta-region-server节点中)。

hbase:meta表存储了所有用户HRegion的位置信息。它的结构如下:

  • Key:格式为([table],[region start key],[region id])
  • Values,它只有info列族,这个列族包含三个列,他们分别是:
    • info:regioninfo (用于这个region的序列化的 HRegionInfo实例,是RegionInfo的proto格式:regionId,tableName,startKey,endKey,offline,split,replicaId)
    • info:server(包含这个region的RegionServer的server:port)
    • info:serverstartcode (包含这个region的RegionServer进程的start-time,格式是HRegionServer的启动时间戳)

HRegionServer

HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地化。(数据本地化并不是总能实现,比如在HRegion移动(如因Split)时,需要等下一次Compact才能继续回到本地化。)

HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成。

1. WAL即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94之前,存储在/hbase/.logs/目录中),一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的),这当然会引起性能问题,因而在HBase 1.0之后,通过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。

2. BlockCache是一个读缓存,即“引用局部性”原理,将数据预读取到内存中,以提升读的性能。HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(通常是off-heap)。通常BucketCache的性能要差于LruBlockCache,然而由于GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache由于是自己管理BlockCache,而不需要GC,因而它的延迟通常比较稳定,这也是有些时候需要选用BucketCache的原因。

3. HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table可以有一个或多个Region,他们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上,一个HRegionServer可以有多个HRegion,他们分别属于不同的Table。HRegion由多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成。

4. MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会写入MemStore中,由MemStore根据一定的算法将数据Flush到底层HDFS文件中(HFile),通常每个HRegion中的每个 Column Family有一个自己的MemStore。

5. HFile(StoreFile) 用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列。

HRegionServer内部存储结构

HBase使用的主要数据结构是LSM-tree(Log-Structured Merge Tree)。

HBase使用LSM-tree存储文件,LSM-tree将数据维护为两个独立的部分,这些部分针对底层存储进行了优化。这种类型的数据结构依赖于两种结构,一种是位于内存中的当前的、较小的结构,另一种是位于持久磁盘中的较大的结构,一旦内存中的部分超过了一定的限制,使用合并排序算法将其与存储在磁盘上的更大的结构合并,并为新的插入请求创建一个新的内存树。它将随机数据访问转换为顺序数据访问,提高了读性能,而合并是一种后台进程,不会影响前台处理。

HRegionServer中数据写入流程

当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终需要去的HRegionServer。然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)。

写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。

MemStore Flush

MemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序。

每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。有以下三种情况可以触发MemStore的Flush动作,需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。

  • 当一个MemStore的大小超过了hbase.hregion.memstore.flush.size的大小,默认128MB。此时当前的HRegion中所有的MemStore会Flush到HDFS中。
  • 当全局MemStore的大小超过了hbase.regionserver.global.memstore.upperLimit的大小,默认40%的内存使用量。此时当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush顺序是MemStore大小的倒序,直到总体的MemStore使用量低于hbase.regionserver.global.memstore.lowerLimit,默认38%的内存使用量。
  • 当前HRegionServer中WAL的大小超过了hbase.regionserver.hlog.blocksize * hbase.regionserver.max.logs的数量,当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush使用时间顺序,最早的MemStore先Flush直到WAL的数量少于hbase.regionserver.hlog.blocksize * hbase.regionserver.max.logs。

在MemStore Flush过程中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列,那么在Recover时就直接从哪里开始。在HRegion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence。

HFile格式

HBase的数据以Key-Value(Cell)的形式顺序的存储在HFile中。

在MemStore的Flush过程中生成HFile,由于MemStore中存储的Cell遵循相同的排列顺序,因而Flush过程是顺序写。(磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针)

Compaction

MemStore每次Flush会创建新的HFile,而过多的HFile会引起读的性能问题,那么如何解决这个问题呢?HBase采用Compaction机制来解决这个问题,有点类似Java中的GC机制。

在HBase中Compaction分为两种:Minor Compaction和Major Compaction。

Minor Compaction是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次Minor Compaction的结果是更少并且更大的StoreFile。

Major Compaction是指将所有的StoreFile合并成一个StoreFile,在这个过程中,标记为Deleted的Cell会被删除,而那些已经Expired的Cell会被丢弃,那些已经超过最多版本数的Cell会被丢弃。一次Major Compaction的结果是一个HStore只有一个StoreFile存在(In this, all the HFiles of a column family are merged.)。Major Compaction可以手动或自动触发,然而由于它会引起很多的IO操作而引起性能问题,因而它一般会被安排在周末、凌晨等集群比较闲的时间。

默认, major compactions每7天一个周期,会被调度运行一次。HBase 0.96.x之前, major compactions默认每天被调度执行一次。

HRegion Split

最初,一个Table只有一个HRegion,随着数据写入增加,如果一个HRegion到达一定的大小,就需要Split成两个HRegion,这个大小由hbase.hregion.max.filesize指定,默认为10GB。

当split时,两个新的HRegion会在同一个HRegionServer中创建,它们各自包含父HRegion一半的数据,当Split完成后,父HRegion会下线,而新的两个子HRegion会向HMaster注册上线,处于负载均衡的考虑,这两个新的HRegion可能会被HMaster分配到其他的HRegionServer中,此时会引起有些HRegionServer处理的数据在其他节点上,直到下一次Major Compaction将数据从远端的节点移动到本地节点。

HRegionServer中数据读取流程

在HBase写时,相同Cell(RowKey/ColumnFamily/Column相同)并不保证在一起,甚至删除一个Cell也只是写入一个新的Cell,它含有Delete标记,而不一定将一个Cell真正删除了,因而这就引起了一个问题,如何实现读的问题?

要解决这个问题,我们先来分析一下相同的Cell可能存在的位置:

  • 首先对新写入的Cell,它会存在于MemStore中;
  • 然后对之前已经Flush到HDFS中的Cell,它会存在于某个或某些StoreFile(HFile)中;
  • 最后,对刚读取过的Cell,它可能存在于BlockCache中。

既然相同的Cell可能存储在三个地方,在读取的时候只需要扫瞄这三个地方,然后将结果合并即可(Merge Read)。

在HBase中扫瞄的顺序依次是:BlockCache、MemStore、StoreFile(HFile)。

其中StoreFile的扫瞄先会使用Bloom Filter过滤那些不可能符合条件的HFile,然后使用Block Index快速定位Cell,并将其加载到BlockCache中,然后从BlockCache中读取。

HRegionServer中数据删除流程

当HBase收到一个删除请求(对某些数据)时,它并不会马上就删除。需要被删除的数据会被做上标记(tombstone marker) 。这是因为HFile本身是不可变的,这样的删除在HDFS上的HFile内是不可用的。

当发生major compactions时,被标记过的记录或数据会被丢弃,一个新的HFile文件会被创建(不带被标记的数据)。

HRegionServer Recovery(故障恢复)

当一台HRegionServer宕机时,由于它不再发送Heartbeat给ZooKeeper而被监测到,此时ZooKeeper会通知HMaster,HMaster会检测到哪台HRegionServer宕机,它将宕机的HRegionServer中的HRegion重新分配给其他的HRegionServer,同时HMaster会把宕机的HRegionServer相关的WAL拆分分配给相应的HRegionServer(将拆分出的WAL文件写入对应的目的HRegionServer的WAL目录中,并且写入对应的DataNode中),从而这些HRegionServer可以Replay分到的WAL来重建MemStore。

HBase的优缺点

HBase的优点:

  • HBase采用强一致性模型,在一个写返回后,保证所有的读都读到相同的数据(行级别原子性,即,PUT 操作一定是完全成功或者完全失败);
  • 通过HRegion动态Split和Merge实现数据存储自动水平扩展,并使用HDFS提供的多个数据备份功能,实现高可用性;
  • 列可以动态增加,并且列为空就不存储数据,节省存储空间;
  • 采用HRegionServer和DataNode运行在相同的服务器上实现数据的本地化,提升读写性能,并减少网络压力;
  • 内建HRegionServer的宕机自动恢复。采用WAL来Replay还未持久化到HDFS的数据。(容错性,自动故障转移) ;
  • 可以无缝的和Hadoop/MapReduce集成,有利于数据分析;
  • Hbase 可以提供高并发读写操作的支持;
  • 非常灵活的模式设计(或者说没有固定模式的限制)。

HBase的缺点:

  • 不能支持条件查询,只支持按照 row key 来查询;
  • 不支持事务;
  • JOIN 不是数据库层支持的,而需要用 MapReduce;
  • 只能在行键上索引和排序;
  • WAL的Replay过程可能会很慢;
  • 灾难恢复比较复杂,也会比较慢;
  • Major Compaction会引起IO Storm

《PySpark原理深入与编程实战》