HBase架构


HBase的特点

  1. 大:一个表可以有上亿行,上百万列。
  2. 面向列:面向列簇的存储和权限控制,列簇独立检索。
  3. 稀疏:对于空(null)的列,并不占用存储空间,因此,表可以设计的非常稀疏。
  4. 无模式(no Schema):每一行都有一个可以排序的主键和任意多的列,列可以根据需要动态增加,同一张表中不同的行可以有截然不同的列。
  5. 数据多版本:每个单元格中的数据可以有多个版本,默认情况下,版本号自动分配,版本号就是单元格插入时的时间戳。
  6. 数据类型单一:HBase中的数据都是字节数组,没有类型。

HBase架构组件

  • Region Server:提供数据的读写服务,当客户端访问数据时,直接和Region Server通信。
  • HBase Master:Region的分配,DDL操作(创建表,删除表)。
  • Zookeeper:是HDFS的一部分,维护一个活跃的集群状态。

  Hadoop DataNode存储着Region Server管理的数据,所有HBase数据存储在HDFS文件系统中,Region Server在HDFS DataNode中是可配置的,并使数据存储靠近在它所需要的地方,就近服务,当往HBase写数据时Local,但是当一个region被移动之后,HBase的数据就不是Local的,除非做了压缩(compaction)操作。NameNode维护物理数据块的元数据信息。


Regions

  HBase Table 通过rowkey range 的范围被水平切分成多个Region,一个Region包含了所有的,在Region开始键和结束之内的行,Regions被分配到集群的节点上,成为Region Servers,提供数据的读写服务,一个region server 可以服务1000个Region。

Region的分裂策略

  region中存储的是一张表的数据,当region中的数据条数过多的时候,会直接影响查询效率。当region过大的时候,region会拆分为两个region,HMaster会将分裂的region分配到不同的region server上,这样可以让请求分散到不同的Region Server上,已达到负载均衡,这也是HBase的一个优点。

  • ConstantSizeRegionSplitPolicy

    0.94版本前,HBase region的默认切分策略

    当region中最大的store大小超过某个阈值(hbase.hregion.max.filesize=10G)之后就会触发切分,一个region等分为2个region。

    但是在生产线上这种切分策略却有相当大的弊端(切分策略对于大表和小表没有明显的区分):

    • 阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就1个,形成热点,这对业务来说并不是什么好事。
    • 如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的region,这对于集群的管理、资源使用、failover来说都不是一件好事。
  • IncreasingToUpperBoundRegionSplitPolicy

    0.94版本~2.0版本默认切分策略

    ​ 总体看和ConstantSizeRegionSplitPolicy思路相同,一个region中最大的store大小大于设置阈值就会触发切分。
    但是这个阈值并不像ConstantSizeRegionSplitPolicy是一个固定的值,而是会在一定条件下不断调整,调整规则和region所属表在当前regionserver上的region个数有关系.

    region split阈值的计算公式是:

    • 设regioncount:是region所属表在当前regionserver上的region的个数

    • 阈值 = regioncount^3 * 128M * 2,当然阈值并不会无限增长,最大不超过MaxRegionFileSize(10G),当region中最大的store的大小达到该阈值的时候进行region split

    例如:

    • 第一次split阈值 = 1^3 * 256 = 256MB
    • 第二次split阈值 = 2^3 * 256 = 2048MB
    • 第三次split阈值 = 3^3 * 256 = 6912MB
    • 第四次split阈值 = 4^3 * 256 = 16384MB > 10GB,因此取较小的值10GB
    • 后面每次split的size都是10GB了

    特点

    • 相比ConstantSizeRegionSplitPolicy,可以自适应大表、小表;
    • 在集群规模比较大的情况下,对大表的表现比较优秀
    • 对小表不友好,小表可能产生大量的小region,分散在各regionserver上
    • 小表达不到多次切分条件,导致每个split都很小,所以分散在各个regionServer上
  • SteppingSplitPolicy

    2.0版本默认切分策略

    ​ 相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些
    ​ region切分的阈值依然和待分裂region所属表在当前regionserver上的region个数有关系

    • 如果region个数等于1,切分阈值为flush size 128M * 2
    • 否则为MaxRegionFileSize。

    这种切分策略对于大集群中的大表、小表会比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小region,而是适可而止。

  • KeyPrefixRegionSplitPolicy

    根据rowKey的前缀对数据进行分区,这里是指定rowKey的前多少位作为前缀,比如rowKey都是16位的,指定前5位是前缀,那么前5位相同的rowKey在相同的region中。

  • DelimitedKeyPrefixRegionSplitPolicy

    保证相同前缀的数据在同一个region中,例如rowKey的格式为:userid_eventtype_eventid,指定的delimiter为 _ ,则split的的时候会确保userid相同的数据在同一个region中。
    按照分隔符进行切分,而KeyPrefixRegionSplitPolicy是按照指定位数切分。

  • BusyRegionSplitPolicy

    按照一定的策略判断Region是不是Busy状态,如果是即进行切分

    如果你的系统常常会出现热点Region,而你对性能有很高的追求,那么这种策略可能会比较适合你。它会通过拆分热点Region来缓解热点Region的压力,但是根据热点来拆分Region也会带来很多不确定性因素,因为你也不知道下一个被拆分的Region是哪个。

  • DisabledRegionSplitPolicy

    不启用自动拆分, 需要指定手动拆分

Compaction操作

Minor Compaction:
  • 指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次 Minor Compaction 的结果是更少并且更大的StoreFile。
Major Compaction:
  • 指将所有的StoreFile合并成一个StoreFile,这个过程会清理三类没有意义的数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。另外,一般情况下,major compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发major compaction功能,改为手动在业务低峰期触发。

HBase HMaster

  分配Region,DDL操作(创建表,删除表)
  协调各个Region Server

在启动时分配Region、在恢复或是负载均衡时重新分配Region。
监控所有集群当中的Region Server实例,从Zookeeper中监听通知。

  管理功能:

提供创建、删除、更新表的接口。


Zookeeper 协调器

  Zookeeper一般在分布式系统中的成员之间协调共享的状态信息,Region Server和活跃的HMaster通过会话连接到Zookeeper,Zookeeper维护短暂的阶段,通过心跳机制用于活跃的会话。


组件之间如何工作

  Zookeeper一般在分布式系统中的成员之间协调共享的状态信息,Region Server活跃的HMaster通过会话连接到Zookeeper,Zookeeper维护短暂的阶段,通过心跳机制用于活跃的会话。

  每个Region Server创建一个短暂的节点,HMaster监控这些节点发现可用的Region Server,同时HMaster 也监控这些节点的服务器故障。HMaster 通过撞见一个临时的节点,Zookeeper 决定其中一个HMaster 作为活跃的。活跃的HMaster 给 Zookeeper发送心跳信息,不活跃的HMaster在活跃的HMaster出现故障时,接受通知。

   如果一个Region Server 或是一个活跃的HMaster 在发送心跳信息失败或是出现了故障,则会话过期,相应的临时节点将被删除,监听器将因这些删除的节点更新通知信息,活跃的HMaster将监听Region Server ,并且将会恢复出现故障的 Region Server ,不活跃的HMaster 监听活跃的HMaster 故障,如果一个活跃的HMaster出现故障,则不活跃的HMaster将会变得活跃。


HBase 的首次读写

  有一个特殊的HBase目录表叫做Meta表,它拥有Region在集群中的位置信息,Zookeeper存储着Meta表的位置。

  如下就是客户端首次读写HBase 所发生的事情:

  1. 客户端从Zookeeper的Meta表中获取Region Server。
  2. 客户端将查询 META 服务器,获取它想访问的相对应的Region Server 的行键,客户端将这些信息以及META表的位置信息。
  3. 客户端将从相应的Region Server获取行。

  如果再次读取,客户端将使用缓存来获取META 的位置及之前的行键。这样时间久了,客户端不需要查询META表,除非Region移动所导致的丢失,这样的话,则会重新查询更新缓存。


HBase META 表

  • META 表集群中所有Region的列表
  • META 表像是一个B树
  • META 表结构为:
    • Key:region start key,region id
    • Values:Region Server

RegionServer 的组件

  Region Server 运行在HDFS DataNode上,并有如下组件:

WAL:Write Ahead Log 提前写日志是一个分布式文件系统上的文件,WAL存储没有持久化的新数据,用于故障恢复,类似Oracle 的Redo Log。

  • BlockCache:读缓存,它把频繁读取的数据放入内存中,采用LRU。
  • MemStore:写缓存,存储来没有来得及写入磁盘的新数据,每一个region的每一个列族有一个MemStore。
  • Hfiles存储行,作为键值对,在硬盘上。

HBase写步骤

  当客户端提交一个Put 请求,第一步是把数据写入WAL:

  • 编辑到在磁盘上的WAL的文件,添加到WAL文件的末尾
  • WAL用于宕机恢复

  一旦数据写入WAL,将会把它放到MemStore里,然后将返回一个ACk给客户端


MemStore

  MemStore 存储以键值对的方式更新内存,和存储在HFile是一样的。每一个列簇就有一个MemStore ,以每个列簇顺序的更新。


HBase Region 刷新(Flush)

  当MemStore 积累到足够的数据,则整个排序后的集合被写到HDFS的新的HFile中,每个列簇使用多个HFiles,列簇包含真实的单元格,或者是键值对的实例,随着KeyValue键值对在MemStores中编辑排序后,作为文件刷新到磁盘上。

  注意列簇是有数量限制的,每一个列族有一个MemStore,当MemStore满了,则进行刷新。它也会保持最后一次写的序列号,这让系统知道直到现在都有什么已经被持久化了。

  最高的序列号作为一个meta field 存储在HFile中,来显示持久化在哪里结束,在哪里继续。当一个region 启动后,读取序列号,最高的则作为新编辑的序列号。


HBase HFile

  数据存储在HFile,HFile 存储键值,当MemStore 积累到足够的数据,整个排序的键值集合会写入到HDFS中新的HFile 中。这是一个顺序的写,非常快,能避免移动磁头。


HFile 的结构

  HFile 包含一个多层的索引,这样不必读取整个文件就能查找到数据,多层索引像一个B+树。

  • 键值对以升序存储
  • 在64K的块中,索引通过行健指向键值对的数据。
  • 每个块有自己的叶子索引。
  • 每个块的最后的键被放入到一个中间索引中。
  • 根索引指向中间索引。

  trailer (追踪器)指向 meta的块,并在持久化到文件的最后时被写入。trailer 拥有 bloom过滤器的信息以及时间范围(time range)的信息。Bloom 过滤器帮助跳过那些不含行健的文件,时间范围(time range)则跳过那些不包含在时间范围内的文件。


HFile Index

  索引是在HFile 打开并放入内存中时被加载的,这允许在单个磁盘上执行查找。


HBase 读合并

  一个行的键值单元格可以被存储在很多地方,行单元格已经被存储到HFile中、在MemStore最近被更新的单元格、在Block cache最佳被读取的单元格,所以当你读取一行数据时,系统怎么能把相对应的单元格内容返回呢?一次读把block cache, MemStore, and HFiles中的键值合并的步骤如下:

  1. 首先,扫描器(scanner )在读缓存的Block cache寻找行单元格,最近读取的键值缓存在Block cache中,当内存需要时刚使用过的(Least Recently Used )将会被丢弃。
  2. 接下来,扫描器(scanner)将在MemStore中查找,以及在内存中最近被写入的写缓存。
  3. 如果扫描器(scanner)在MemStore 和Block Cache没有找到所有的数据,则HBase 将使用 Block Cache的索引以及bloom过滤器把含有目标的行单元格所在的HFiles 加载到内存中。

  每个MemStore有许多HFiles 文件,这样对一个读取操作来说,多个文件将不得不被多次检查,势必会影响性能,这种现象叫做读放大(read amplification)。


HBase 辅压缩(minor compaction)

  HBase将会自动把小HFiles 文件重写为大的HFiles 文件,这个过程叫做minor compaction。

  辅助压缩减少文件的数量,并执行合并排序。


HBase 主压缩(Major Compaction)

  主压缩将会合并和重写一个region 的所有HFile 文件,根据每个列族写一个HFile 文件,并在这个过程中,删除deleted 和expired 的单元格,这将提高读性能。

  然而因为主压缩重写了所有的文件,这个过程中将会导致大量的磁盘IO操作以及网络拥堵。我们把这个过程叫做写放大(write amplification)。


Region = 临近的键

  • 一个表将被水平分割为一个或多个Region,一个Region包含相邻的起始键和结束键之间的行的排序后的区域。
  • 每个region默认1GB。
  • 一个region的表通过Region Server 向客户端提供服务。
  • 一个region server可以服务1000 个region。

Region 分裂

  初始时一个table在一个region 中,当一个region 变大之后,将会被分裂为2个子region,每个子Region 代表一半的原始Region,在一个相同的 Region server中并行打开。

  然后把分裂报告给HMaster。因为需要负载均衡的缘故,HMaster 可能会调度新的Region移动到其他的Server上。


读负载均衡(Read Load Balancing)

  分裂一开始发生在相同的region server上,但是由于负载均衡的原因。HMaster 可能会调度新的Region被移动到其他的服务器上。导致的结果是新的Region Server 提供数据的服务需要读取远端的HDFS 节点。直到主压缩把数据文件移动到Regions server本地节点上,Hbase数据当写入时是本地的,但是当一个region 移动(诸如负载均衡或是恢复操作等),它将不会是本地的,直到做了主压缩的操作(major compaction.)


HDFS数据复制

  所有的读写操作发生在主节点上,HDFS 复制WAL和HFile 块,HFile复制是自动发生的,HBase 依赖HDFS提供数据的安全,当数据写入HDFS,本地化地写入一个拷贝,然后复制到第二个节点,然后复制到第三个节点。WAL 文件和 HFile文件通过磁盘和复制进行持久化,那么HBase怎么恢复还没来得及进行持久化到HFile中的MemStore更新呢?


HBase 故障恢复

  当一个RegionServer 挂掉了,坏掉的Region 不可用直到发现和恢复的步骤发生。Zookeeper 决定节点的失败,然后失去region server的心跳。

  然后HMaster 将会被通知Region Server已经挂掉了。

  当HMaster检查到region server已经挂掉后,HMaster 将会把故障Server上的Region重写分配到活跃的Region servers上。为了恢复宕掉的region server,memstore 将不会刷新到磁盘上,HMaster 分裂属于region server 的WAL 到单独的文件,然后存储到新的region servers的数据节点上,每个Region Server从单独的分裂的WAL回放WAL。来重建坏掉的Region的MemStore。


数据恢复

  WAL 文件包含编辑列表,一个编辑代表一个单独的put 、delete.Edits 是按时间的前后顺序排列地写入,为了持久化,增加的文件将会Append到WAL 文件的末尾。

  当数据在内存中而没有持久化到磁盘上时失败了将会发生什么?通过读取WAL将WAL 文件回放,添加和排序包含的edits到当前的MemStore,最后MemStore 刷新将改变写入到HFile中。


HBase架构的优点

  • 强一致模型:当写操作返回时,所有的读将看到一样的结果
  • 自动扩展:Regions 随着数据变大将分裂;使用HDFS传播和复制数据
  • 内建的恢复机制:使用WAL
  • Hadoop的集成:直接使用mapreduce

HBase架构的缺点

  • WAL回放较慢
  • 故障恢复较慢
  • 主压缩导致IO瓶颈

HBase 读写流程

HBase写数据和存数据的过程

HBase数据的写入过程

  1. Client访问zookeeper,获取元数据存储所在的regionserver
  2. 通过刚刚获取的地址访问对应的regionserver,拿到对应的表存储的regionserver
  3. 去表所在的regionserver进行数据的添加
  4. 查找对应的region,在region中寻找列族,先向memstore中写入数据
  5. 当memstore写入的值变多,触发溢写操作(flush),进行文件的溢写,成为一个StoreFile
  6. 当溢写的文件过多时,会触发文件的合并(Compact)操作,合并有两种方式(major,minor)

多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除

  • minor compaction:小范围合并,默认是3-10个文件进行合并,不会删除其他版本的数据。
  • major compaction:将当前目录下的所有文件全部合并,一般手动触发,会删除其他版本的数据(不同时间戳的)
  1. 当region中的数据逐渐变大之后,达到某一个阈值,会进行裂变(一个region等分为两个region,并分配到不同的regionserver),原本的Region会下线,新Split出来的两个Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。

  由此可知HBase只是增加数据,所有的更新和删除操作,都是在Compact阶段做的,所以用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能读写。


  1. HStore存储是HBase存储的核心,其中由两部分组成,一部分是Memstore,一部分是StoreFile。
  2. HLog的功能: 宕机数据恢复

  在分布式系统环境中,我们是无法避免系统出错或者宕机的,一旦HRegionServer意外退出,MemStore中的内存数据就会丢失,而引入HLog就是为了防止这种情况。

工作机制:每个HRegionServer中都会有一个HLog对象,HLog是一个实现Write Ahead Log的类,每次用户操作写入Memstore的同时,也会写一份数据到HLog文件中,HLog文件定期会滚动出新,并删除旧的文件(已持久化到Storefile中的数据),当HRegionServer意外终止后,HMaster会通过Zookeeper感知,HMaster首先处理遗留的HLog文件,将不同region的log数据拆分,分别放在相应region目录下,然后再将失效的region(带有刚刚拆分的log)重新分配,领取到这些region的HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到Memstore中,然后flush到StoreFile,完成数据恢复。

  1. Region就是StoreFiles,StoreFiles里由HFile构成,HFile里由hbase的data块构成,一个data块里面又有很多的keyvalue对,每个keyvalue里存了我们需要的值。
  2. 一 张表,有两个列族(红颜色的一个,黄颜色的一个),一个列族有两个列,从图中可以看出,这就是列式数据库的最大特点,同一个列族的数据在在一起的,我们还 发现如果是有多个版本,同时也会存多个版本。最后我们还发现里面存了这样的值:r1:键值,cf1:列族的名字,c1:列名。t1:版本号,value值 (最后一幅图说明的是value值可以存放的位置)。通过这样的看法,我们会发现如果在设计表的时候把这几个东西:r1:键值,cf1:列族的名 字,c1:列明的名字取短一点,我们就可以节省出好多存储的空间!

  我们看倒数第二张图,字段筛选的效率从左到右明显下降,所以在keyvalue的设计时用户可以考虑把一些重要的筛选信息左移到合适的位置,从而在不改变数 据量的情况下,提高查询性能。那么简单的说就是用户应当尽量把查询维度或信息存储在行健中,因为它筛选数据的效率最高。

  HBase 的数据存储时会被有顺序的存储到一个特定的范围,因为我们存储的时候一般都是按顺序的,所以会一直存到同一个region上,由于一个region只能由 一个服务器管理,这样我们老是添加到同一个region上,会造成读写热点,从而使集群性能下降。

  那么解决这个问题的办法还是有的,假如我们 有9台服务器,那么我们就会去当前时间,然后mod9,加到行键前缀,这样就会被平均的分到不同的region服务器上了,这样带来的好处是,因为相连的数据 都分布到不同的服务器上了,用户可以多线程并行的读取数据,这样查询的吞吐量会提高。关于版本的控制,我们要么就让多台服务器上的时间都同步,要么干脆就在put插入数据时,设置一个客户端的时间戳来代替。

关于表的设计

  设计表的时候,有两种设计方式,一种是高表设计,一种是胖表设计。根据HBase的拆分规则,我们的高表设计更容易拆分(使用组合键),不过如果我们设计成胖表,而我们这个胖表里的数据需要经常修改,这样的设计是很合理的。以为HBase保证了行级原子性,如果设计成高表,反而就不合适了,因为不能保证跨行的原子性。

写缓存

  每一个put的操作实际上是RPC的操作,它将客户端的数据传送到服务器然后返回,这只适合小数据量的操作,如果有个应用程序需要每秒存储上千行数据到HBase中,这样处理就不太合适了。HBase的API配备了一个客户端的写缓冲区,缓冲区负责收集put操作,然后调用RPC操作,一次性将put送往服务器。默认情况下,客户端缓冲区是禁止的。可以通过自动刷写设置为FALSE来激活缓冲区。

  table.setAutoFlush(false);void flushCommits () throws IOException这个方法是强制 将数据写到服务器。用户还可以根据下面的方法来配置客户端写缓冲区的大小。

  void setWritaeBufferSize(long writeBufferSize) throws IOException;默认大小是 2MB,这个也是适中的,一般用户插入的数据不大,不过如果你插入的数据大的话,可能要考虑增大这个值。从而允许客户端更高效地以一定数量的数据组成一组,然后通过一次RPC请求来执行。

  在每一个客户端设置一个写缓冲区是一件麻烦的事,我们可以在Hbase-site.xml中给用户设置一个较大的预设值。

<property>
<name>hbase.client.write.buffer</name>
<value>20971520</value>
</property>

压缩

  hbase支持大量的算法,并且支持列族级别以上的压缩算法,除非有特殊原因,不然我们应该尽量使用压缩,压缩通常会带来较好的性能。通过一些测试,我们推荐使用SNAPPY这种算法来进行我们hbase的压缩。


HBase数据的读取流程

  1. Client访问zookeeper,获取元数据存储所在的regionserver
  2. 通过刚刚获取的地址访问对应的regionserver,拿到对应的表存储的regionserver
  3. 去表所在的regionserver进行数据的读取
  4. 查找对应的region,在region中寻找列族,先找到memstore,找不到去blockcache中寻找,再找不到就进行storefile的遍历
  5. 找到数据之后会先缓存到blockcache中,再将结果返回blockcache逐渐满了之后,会采用LRU的淘汰策略。

  • 在HBase中,所有的存储文件都被划分成了若干个小存储块,这些存储块在get或scan操作时会加载到内存中,他们类似与RDBMS中的存储单元页,这个参数的默认大小是64k,HBase会顺序的读取一个数据块到内存缓存中,其读取相邻的数据时就可以在内存中读取而不需要从磁盘中再次读取,有效的减少了磁盘的I/O的次数。这个参数默认为TRUE,这意味着每次读取的块都会缓存到内存中。
    • 但是:如果用户顺序读写某个特定的列族,这个时候,这个机制就会把其他我们不需要的列族的数据也加载到内存中,增加了我们的负担,那么1就需要将其关闭。void setBlockCacheEnable(boolean blockCacheEnable);
  • 禁止自动刷写:
    • 我们有大批数据要插入时,如果我们没有禁止,Put实例会被逐个的传送到region服务器,(一条一条的往磁盘中写,害怕不?)如果用户禁止了自动刷写的功能,put操作会在写缓冲区被填满时才会被送出。
    • 使用扫描缓存,如果HBase被用作一个mapreduce作业的输入源,请最好将作为mapreduce作业输入扫描器实例的缓存用setCaching()方法设置为默认值1更大的数。使用默认值意味着map任务会在处理每条记录时都请求region服务器,不过,这个值是500的话,则一次可传送500条数据到客户端进行处理,当然了这数据也是根据具体情况而定。
  • 限定扫描范围:
    • 比如我们要处理大量行(特别是作为mapreduce的输入源),其中用到scan的时候我们有Scan.addFamily();的方法,这个时候我们如果只是需要到这几个列族中的几个列,那么我们一定要精确,因为过多的列会导致效率的损失。
  • 块缓存的用法:
    • 首先我们的块缓存是通过Scan.setCacheBlocks();启动的,那么被频繁访问的行,我们应该使用缓存块,但是MapReduce作业使用扫描大量的行,我们就不该使用这个了。

到底啦!关注靓仔学习更多的大数据知识!😊

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐