hbase中的row key是啥_Hbase 架构通读理解

本文详细介绍了HBase的架构体系,包括Region查找、预写日志WAL机制和HLog相关类。讨论了HBase如何通过WAL保证数据一致性和高可用性,以及Region的生命周期、读写路径、性能优化和Row Key设计原则。内容涵盖HBase的故障恢复、日志滚动、归档和删除,以及读写性能的优化策略。
摘要由CSDN通过智能技术生成

几乎接触hbase都会看过的一本书《HBase权威指南》。

为什么要用Hbase

  • - Hbase的诞生是因为现有的关系型数据库已经无法在硬件上满足疯狂增长的数据了,而且因为需要实时的数据提取Memcached也无法满足
  • - Hbase适合于无结构或半结构化数据,适合于schema变动的情况
  • - Hbase天生适合以时间轴做查询

分布式计算系统的CAP定理

  • 在理论计算机科学中, CAP定理(CAP theorem), 又被称作布鲁而定理(Brewer's theorem), 它指出对于一個分布式计算系统来说,不可能同时是满足一下三点:
  • - 一致性(Consistency) (所有节点在同一时间拥有相同的数据)
  • - 可用性(Availability) (保证每个请求不管成功或失败都有相应)
  • - 分隔容忍(Partition tolerance) (系统中仁义的信息丢失或者失败不会影响系统继续运行)

HBase使用文件系统

HBase底层实质是HDFS,而这里的文件系统就是指的HDFS上的文件系统,和常规选择操作系统一样,对于用户来说,磁盘文件系统也有多种选择。

常见的文件系统ext3,ext4,XFS

Linux操作系统中使用最普遍的文件系统是ext3.它已经被证明是稳定可靠的文件系统,这意味着生产机器中采用ext3为本地文件系统是一个相对安全的选择。使用ext3注意如下优化点:

  • 用户在挂载文件系统时应该设置noatime属性来禁止记录文件访问时间戳,以减少内核的管理的开销。HBase不需要记录每个文件访问时间,并且禁止用这个选项可以大幅提高磁盘的读取性能。/dev/sdd1 /data ext3 default,noatime 0 0
  • 更好利用ext3提供的磁盘空间,默认情况下,磁盘为每个块的关键进程保留了固定空间,以保证在磁盘已满的情况下不影响关键进程的使用。这个功能对于系统磁盘比较有用,而对于存储磁盘来说几乎无用。并且对一个大型集群中的可用存储空间造成了极大影响。

ext3的下一代就是ext4,最初两者代码是相同的,但是随后ext4被移到独立项目中,08年之后正式成为了Linux内核的一部分。短短几年时间ext4就达到这个程度,足以证明其稳定可靠性,而且ext4可以在不转存文件情况下升级的。

  • 性能上ext4打败了ext3并接近了高性能文件系统XFS,同时兼顾了很多高级功能。如允许单文件达到16TB的大小,支持EB的存储空间。
  • ext4延迟分配,采用延迟分配策略数据会被保存在内存中,内存中会有若干数据块,直到数据最终被刷写到磁盘。这个策略会帮助块中文件保持连续,并在某一个时刻整块写入磁盘,减少了碎片文件并提高的文件读取性能。当然同时带来的服务器内存不够溢出甚至崩溃风险。

XFS和ext3几乎是在同一时间开始被Linux支持的。几乎和ext4功能类似但是有它特点:

  • XFS在引导服务器格式化非常快,这样可以减少使用磁盘组建新服务器的时间
  • XFS也有缺点,众所周知它的一些操作涉及到了元数据的变更,比如删除大量文件的操作。不过一般HBase处理的文件都是少而大的。因此用户在XFS上不受太多影响

HBase的架构体系

ea41f1d46c47b37c82cc06b89764bf66.png
图1.0 HBase对接HDFS 架构

上图中已经很清晰描述出HBase的架构以及和HDFS交互

  • HBase中有大量的HRegionServer
  • 每个HRegionServer中包含了两个部分HLog和HRegion
  • 每个HRegion中包含很多Store
  • 每个Store中包含很多StoreFile和一个MemStore
  • 而每个StoreFile包含一个HFile

那么HBase最小的单元就是HFile或者说是StoreFile吧。我们知道HDFS中是由DataNode组成的。而每个DataNode中包含了很多Block,所以Block是HDFS最小单元。

这里就很好理解HBase和HDFS交互的点了,就是两者最小单元对接地方。HBase的HFile和HDFS的Block之间交互。当然不是直接交互而是通过DFS Client来进行交互。脑袋突然奔出来HFile和Block是牛郎和织女,DFS Client是他们之间那座相会的乌鹊桥 - -

基于zookeeper一次简单客户端读写流程:

客户端联系zookeeper子集群查找行键。上述过程是通过zookeeper获取含有-ROOT-的region服务器(主机名)来完成的。通过含有-ROOT-的region服务器可以查询到.META.表中的对应的region服务器名,其中包含请求行键信息,这联储的主要内容都被缓存下来,并且都只查询一次。最终,通过查询.META.服务器来获取客户端查询的行键数据所在的region的服务器名。

一旦知道了数据实际位置,即region的位置,HBase会缓存这次查询的信息,同时直接联系管理实际数据的HRegionServer。所以,之后客户端可以通过缓存信息很好的定位所需的数据位置,而不用再次查找.META.表

HRegionServer负责打开region,并创建对应的HRegion实例。当HRegion被打开后,它会为每个表的HColumnFamily创建一个Store实例。这些列族是用户之前创建表时定义的 。每个Store实例包含一个或多个StoreFile实例。它们是实际数据存储文件HFile的轻量级封装。每个Store还有其对应的一个MemStore,一个HRegionServer分享了一个HLog实例。

Region查找

HBase提供了两张特殊的目录表-ROOT-和.META.

-ROOT-表用来查询所有.META.表中region的位置。HBase的设计中只有一个root region,即 root region从不拆分,从而保证类似B+树结构的三层查找结构:第一层是Zookeeper种包含root region位置信息的节点,第二层是从-ROOT-表中查找对应的meta region 的位置,第三层是从.META.表中查找用户表对应的region的位置。

2487690f09a0f954b4b62fabe378c4ef.png
图1.1 基于zookeeper的Region查找

-ROOT-表的一个row代表着META的一个region信息,其key的结构是META表名,META表Region的startkey,RegionId。其value的主要保存regioninfo和server信息。ROOT表不能split

.META.表的一个row代表着用户表的一个region信息,其key的结构是其实就是用户表的regionName,用户表名,startKey,RegionId。其value同样保存着regioninfo和server信息。META表可以split,但是一个region默认有128M,可以存上亿个用户表的region信息,所以一般不会split。

其查找过程如下:

  1. 通过zk getData拿-ROOT-表的location
  2. RPC -ROOT-表的rs,getClosestRowBefore,拿row对应的meta表的region location
  3. RPC .META.表的某一个region,拿该row在真实table所在的region location
  4. RPC对应region

Region的生命周期

Region的各种状态均由master触发,并使用AssignmentManager类进行管理。这个类会从Region的下线(offline)状态开始一直跟踪,并管理它的状态。

286a080bc2de634ce5d42bd17df9819d.png
图1.2 Region生命周期状态

状态可能由master发起,也可能由region发起服务器发起。例如,当master把region分配到一个服务器后,由服务器来完成打开过程。此外,拆分过程由region服务器发起,这个过程可能引起一些列的region关闭和打开事件。

由于事件都是分布式的,服务器使用zookeeper来跟踪一个特定znode状态。

预写日志 WAL (Write Ahead Log)

WAL是一种高并发、持久化的日志保存与回放机制。每一个业务数据的写入操作(PUT / DELETE)执行前,都会记账在WAL中。类似Mysql中的bing log

5db1852d9c24eb3a29e0bbc15f61327a.png
图 1.3 WAL

WAL最重要的作用是灾难恢复。和MySQL 的BIN log类似,它记录所有的数据改动。一旦服务器崩溃,通过重放log,我们可以恢复崩溃之前的数据。这也意味如果写入WAL失败,整个操作将认为失败。
我们先看看HBase是如何做到的。首先,客户端初始化一个可能对数据改动的操作,如put(Put),delete(Delete) 和 incrementColumnValue()。这些操作都将被封装在一个KeyValue对象实例中,通过RPC 调用发送给HRegionServer(最好是批量操作)。 一旦达到一定大小,HRegionServer 将其发送给HRegion。这个过程中,数据会首先会被写入WAL,之后将被写到实际存放数据的MemStore中。
当MemStore到达一定大小,或者经过一段时间后,数据将被异步地写入文件系统中。然而,在两次写入文件系统之间的数据,是保留在内存中的。如果这个时候系统崩溃,那数据···,别急,我们有WAL!WAL几个重要的类。

  • HLog
  • HLogKey
  • LogFlusher
  • LogRoller
  • Replay

HLog类

实现了WAL的类是HLog类。源码中,当HRegion在构造器中被实例化时,HLog实例会被当做一个参数传入HRegion的构造器中。当一个region接受到一个更新操作时候,它可以直接将数据保存到一个共享的WAL实例中去。

HLog的核心功能是append()方法。仔细阅读源码中发现在Put,Delte,Increment的方法中又一个额外的参数集合:setWriteToWAL(false)。如果用户设置调用这个方法时候会导致向WAL写入数据的过程会被停止。这个将获得很多额外的性能提升,但是这种操作是不建议无脑停用的,因为数据迟早会丢失,并且HBase不能恢复丢失的且未写入WAL日志中的数据

另一个重要的特性是HLog将通过“sequence number”追踪数据改变。它内部使用AtomicLong保证线程安全。sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。Region打开存储文件,读取每个HFile中的最大的sequence number,如果该值大于HLog 的sequence number, 就将它作为HLog 的sequence number的值。最后,HLog将得到上次存入文件和继续记log的点。

93635607cc4cfc934c53e4078a1dd79f.png
图 1.4 WAL中HLog 修改

图 1.4展示的是3个不同的Region,它存储在同一个region服务器上,并且每一个都包含不同的RowKey范围。三个Region共享一个HLog实例。这意味着数据按照到达的顺序写入WAL中,当日志需要回放时会产生额外的工作,但是由于这很少发生,所以最优做法是按照顺序存储,这样能提供最好I/O。

Hlog利用HMaster恢复和切分一个由一个崩溃的HRegionServery遗留下来的Log。之后,重新部署regions。

HLogKey类

WAL当前使用的是HADOOP的SequenceFile,这种文件格式是按照键值对方式存储保存的记录。对WAL来说,值仅仅是客户端发送的修改不请求。Key被HLogKey实例代表,由于RowKey仅仅代表行键,列簇,列限定词,时间戳,类型,以及值。所以要有一个地方来存储KeyValue的归属,即Region和表名,这个信息存储在HLogKey中。

6741ae9e431caf19e20ed283b4f96fe5.png
图 1.5 HLogKey

HLogKey还存储了上面提到的序列号。每一条记录的数字是递增的,以保持一个连续的编辑状态。

WALEdit类

客户端发送的每一个修改都会被封装到一个WALEdit实例。它通过日志级别来管理原子性,假设更新了一行中的10列,每一列或每一个单元格都是一个单独的KeyValue实例。如果服务器将它们中的5个写入到WAL后就失败了,用户就会得到一般的修改把内容被持久化了的行。

它可以通过将包含多个单元格的,且所有被认为是原子的更新都写入到一个WALEdit实例中啦解决。这一组的修改都会在一次操作中被写入,以保证日志的一致性。

LogSyncer类

Table在创建的时候,有一个参数可以设置,是否每次写Log日志都需要往集群里的其他机器同步一次,默认是每次都同步,同步的开销是比较大的,但不及时同步又可能因为机器宕而丢日志。同步的操作现在是通过Pipeline的方式来实现的,Pipeline是指datanode接收数据后,再传给另外一台datanode,是一种串行的方式;n-Way Writes是指多datanode同时接收数据,最慢的一台结束就是整个结束。差别在于一个延迟大,一个并发高,hdfs现在正在开发中,以便可以选择是按Pipeline还是n-Way Writes来实现写操作。

Table如果设置每次不同步,则写操作会被RegionServe缓存,并启动一个LogSyncer线程来定时同步日志,定时时间默认是1秒,也可由hbase.regionserver.optionallogflushinterval设置。

LogSyncer只对于用户表起作用,所有目录表始终是一直同步状态。

f766e52f592ec74df14a6f0d652fedc2.png
图 1.6 LogSyncer HLog

LogRoller类

日志写入的大小是有限制的。LogRoller类会作为一个后台线程运行,在特定的时间间隔内滚动日志。通过hbase.regionserver.logroll.period属性控制,默认1小时。

WAL滚动

就是上文提到的LogRoller类。

通过wal日志切换,这样可以避免产生单独的过大的wal日志文件,这样可以方便后续的日志清理(可以将过期日志文件直接删除)另外如果需要使用日志进行恢复时,也可以同时解析多个小的日志文件,缩短恢复所需时间。

触发滚动的条件:

  1. SyncRunner线程在处理日志同步后, 检查当前在写的wal的日志大小是否超过配置{hbase.regionserver.hlog.blocksize默认为hdfs目录块大小}*{hbase.regionserver.logroll.multiplier默认0.95},超过后同样调用requestLogRoll发起日志滚动请求
  2. SyncRunner线程在处理日志同步后,如果有异常发生,就会调用requestLogRoll发起日志滚动请求

WAL归档和删除

归档

WAL创建出来的文件都会放在/hbase/.log下,在WAL文件被定为归档时,文件会被移动到/hbase/.oldlogs下

删除

1.判断:是否此WAL文件不再需要,是否没有被其他引用指向这个WAL文件

会引用此文件的服务:

  • TTL进程:该进程会保证WAL文件一直存活直到达到hbase.master.logcleaner.ttl定义的超时时间(默认10分钟)为止
  • 备份(replication)机制:如果你开启了HBase的备份机制,那么HBase要保证备份集群已经完全不需要这个WAL文件了,才会删除这个WAL文件。这里提到的replication不是文件的备份数,而是0.90版本加入的特性,这个特性用于把一个集群的数据实时备份到另外一个集群。如果你的手头就一个集群,可以不用考虑这个因素。

2.删除

  • Write-Ahead-Log(WAL)保证数据的高可用性。
  • 如果没有 WAL,当RegionServer宕掉的时候,MemStore 还没有写入到HFile,或者StoreFile还没有保存,数据就会丢失。
  • HBase中的HLog机制是WAL的一种实现,每个RegionServer中都会有一个HLog的实例,RegionServer会将更新操作(如 Put,Delete)先记录到 WAL(也就是HLog)中,然后将其写入到Store的MemStore,最终MemStore会将数据写入到持久化的HFile中(MemStore 到达配置的内存阀值)。这样就保证了HBase的高可用性。

Replay类

当HRegionServer启动,打开所管辖的region,它将检查是否存在剩余的log文件,如果存在,将调用Store.doReconstructionLog()。重放一个日志只是简单地读入一个日志,将日志中的条目加入到Memstore中。最后,flush操作将Memstore中数据flush到硬盘中。


旧日志往往由region server 崩溃所产生。当HMaster启动或者检测到region server 崩溃,它将日志文件拆分为多份文件,将其存储在region所属的文件夹。之后,根据上面提到的方法,将日志重放。需要指出的是,崩溃的服务器中的region只有在日志被拆分和拷贝之后才能被重新分配。拆分日志利用HLog.splitLog()。旧日志被读入主线程内存中,之后,利用线程池将其写入所有的region文件夹中,一个线程对应于一个region。

重放过程:HRegionServer启动,打开所管辖的Region,检查是否存在剩余的log文件,如果存在,调用Store.doReconstructionLog()。重放一个日志只是简单地读入一个日志,将日志中的条目加入到Memstore中。最后,flush操作将Memstore中数据flush到硬盘中。

HBase读写路径的工作机制

HBase写路径工作机制

在HBase 中无论是增加新行还是修改已有的行,其内部流程都是相同的。HBase 接到命令后存下变化信息,或者写入失败抛出异常。默认情况下,执行写入时会写到两个地方:预写式日志(write-ahead log,也称HLog)和MemStore。HBase 的默认方式是把写入动作记录在这两个地方,以保证数据持久化。只有当这两个地方的变化信息都写入并确认后,才认为写动作完成。

MemStore 是内存里的写入缓冲区,HBase 中数据在永久写入硬盘之前在这里累积。当MemStore 填满后,其中的数据会刷写到硬盘,生成一个HFile。HFile 是HBase 使用的底层存储格式。HFile 对应于列族,一个列族可以有多个HFile,但一个HFile 不能存储多个列族的数据。在集群的每个节点上,每个列族有一个MemStore。

大型分布式系统中硬件故障很常见,HBase 也不例外。设想一下,如果MemStore还没有刷写,服务器就崩溃了,内存中没有写入硬盘的数据就会丢失。HBase 的应对办法是在写动作完成之前先写入WAL。HBase 集群中每台服务器维护一个WAL 来记录发生的变化。WAL 是底层文件系统上的一个文件。直到WAL 新记录成功写入后,写动作才被认为成功完成。这可以保证HBase 和支撑它的文件系统满足持久性。大多数情况下,HBase 使用Hadoop 分布式文件系统(HDFS)来作为底层文件系统。

如果HBase 服务器宕机,没有从MemStore 里刷写到HFile 的数据将可以通过回放WAL 来恢复。你不需要手工执行。Hbase 的内部机制中有恢复流程部分来处理。每台HBase 服务器有一个WAL,这台服务器上的所有表(和它们的列族)共享这个WAL。

f1798a72a077e2920df1c3e76f8faddc.png

写操作会写入WAL和内存写缓冲区MemStore,客户端在写的过程中不与底层的HFile直接交互

bcb7a2171c981b9c435d345223adac66.png

当MemStore写满时,会刷写到硬盘,生成一个新的HFile

你可能想到,写入时跳过WAL 应该会提升写性能。但我们不建议禁用WAL,除非你愿意在出问题时丢失数据。如果你想测试一下,如下代码可以禁用WAL:

Put p = new Put();
p.setWriteToWAL(false);

HBase 读路径工作机制

如果你想快速访问数据,通用的原则是数据保持有序并尽可能保存在内存里。HBase实现了这两个目标,大多情况下读操作可以做到毫秒级。HBase 读动作必须重新衔接持久化到硬盘上的HFile 和内存中MemStore 里的数据。HBase 在读操作上使用了LRU(最近最少使用算法)缓存技术。这种缓存也叫做BlockCache,和MemStore 在一个JVM 堆里。BlockCache 设计用来保存从HFile 里读入内存的频繁访问的数据,避免硬盘读。每个列族都有自己的BlockCache。

掌握BlockCache 是优化HBase 性能的一个重要部分。BlockCache 中的Block 是HBase从硬盘完成一次读取的数据单位。HFile 物理存放形式是一个Block 的序列外加这些Block的索引。这意味着,从HBase 里读取一个Block 需要先在索引上查找一次该Block 然后从硬盘读出。Block 是建立索引的最小数据单位,也是从硬盘读取的最小数据单位。Block大小按照列族设定,默认值是64 KB。根据使用场景你可能会调大或者调小该值。如果主要用于随机查询,你可能需要细粒度的Block 索引,小一点儿的Block 更好一些。Block变小会导致索引变大,进而消耗更多内存。如果你经常执行顺序扫描,一次读取多个Block,大一点儿的Block 更好一些。Block 变大意味着索引项变少,索引变小,因此节省内存。

从HBase 中读出一行,首先会检查MemStore 等待修改的队列,然后检查BlockCache看包含该行的Block 是否最近被访问过,最后访问硬盘上的对应HFile。HBase 内部做了很多事情,这里只是简单概括。读路径如图所示

49249cf3ec9015c2b4fa67eca256834a.png

注意,HFile 存放某个时刻MemStore 刷写时的快照。一个完整行的数据可能存放在多个HFile 里。为了读出完整行,HBase 可能需要读取包含该行信息的所有HFile。


性能优化

垃圾回收机制优化

1、为何HMaster一般不需调整垃圾回收机制

HMaster 没有处理过重的负载,并且实际的数据服务不经过 HMaster,所以垃圾回收时 HMaster 通常不会产生问题。

2、描述JRE的启发式算法

JRE 在默认情况下会按照一般情况下来估计程序在做什么、怎么创建对象、如何分配堆内存处理数据,这些假设在多数情况下都是对的。JRE能够运用启发式算法,根据运行进程的状况进行调整。设置当启发式的学习调整受限于具体实现时候,JRE 也能够更好地处理某些特殊情况。

3、为何需要调整RegionServer 的垃圾回收机制

JRE的默认算法和启发式学习调整功能不能很好地处理RegionServer。当RegionServer 在写入量过大的负载时候,繁重的负载使得JRE通过对程序行为的各种假设进行内存分配的策略不再有效。

在RegionServer写入数据时,数据会先保存在memstore 中,当大于阈值时候,再写入到磁盘。因为写入的数据是由客户端在不同时间写入的,故而他们占据的Java堆空间很可能是不连续的,会出现孔洞。

4、RegionServer的新生代和老生代的特点比较

新生代大小在128M~512M;老生代大小在几GB。

最初写入的数据会保存在新生代,再刷写到磁盘;当数据刷写到磁盘的速度较慢时候,新生代中的数据停留时间过长,会被移到老生代。

新生代空间可以被迅速回收,对内存管理没有影响;老生代数据量大,回收慢,对内存管理影响大。所以二者需要不同的垃圾回收策略。

本地 memstore 分配缓冲区(Memstore-Local Allocation Buffers, MSLAB)

memstore 的扰动(不断创建和释放内存空间),会造成 region server 的内存碎片问题, JVM 老生代的Heap产生孔洞。本地memstore 分配缓存可以解决这个问题。

MSALB 只允许从堆中分配相同大小的对象,一旦这些对象分配并且最终回收,它们将在堆中留下固定大小的孔洞。之后相同大小的新对象会重新使用这些孔洞,不产生提升错误。

  • 配置 hbase.hregion.memstore.mslab.enabled 默认false
  • 配置 hbase.hregion.memstore.mslab.chunksize 默认2M
  • 配置hbase.hregion.memstore.mslab.max.allocation 默认256k

同时使用MSLAB是有代价的:它们将更加浪费堆空间,因为用户不太可能把缓冲区都用到最后一个字节,剩余没有使用空间则将被浪费。用户需要在压缩收集器造成的服务停止和额外空间消耗之间找到一个平衡。

同时使用缓冲区需要额外的内存复制工作,所以使用缓冲区比直接使用KeyValue实例要稍慢一些,用户需要需要衡量这些工作负载是否产生负面影响。

负载均衡

在分布式系统中,负载均衡是一个非常重要的功能,在HBase中通过Region的数量来实现负载均衡,HBase中可以通过hbase.master.loadbalancer.class来实现自定义负载均衡算法。

master有一个内置的叫做均衡器的特性。在默认情况下,均衡器每五分钟运行一次,这是通过hbase.balancer.period属性设置的。一旦均衡器启动,它将会尝试均匀分配region到所有的region服务器。启动均衡器时,均衡器首先会确定一个region分配计划,该计划用于描述region如何移动。然后通过迭代器调用管理API中的unassign()方法开始移动。

f54dafc983fe1330ef9d3d167e5ad80e.png

HBase合并Region

Region的合并(merge)并不是为了性能考虑而是处于维护的目的被创造出来的。比如删了大量的数据,导致每个Region都变小了,这个时候合并Region就比较合适了。

fe2e2baa81c9df57def4a1d817f90945.png

从流程图中可以看到,MERGING_NEW是一个初始化状态,在Master的内存中,而处于Backup状态的Master内存中是没有这个新Region的MERGING_NEW状态的,那么可以通过对HBase的Master进行一个主备切换,来临时消除这个永久RIT状态。而HBase是一个高可用的集群,进行主备切换时对用户应用来说是无感操作。因此,面对MERGING_NEW状态的永久RIT可以使用对HBase进行主备切换的方式来做一个临时处理方案。之后,我们在对HBase进行修复BUG,打Patch进行版本升级。


HBase之Rowkey设计

HBase由于其存储和读写的高性能,在OLAP即时分析中越来越发挥重要的作用,在易观精细化运营产品--易观方舟也有广泛的应用。作为Nosql数据库的一员,HBase查询只能通过其Rowkey来查询(Rowkey用来表示唯一一行记录),Rowkey设计的优劣直接影响读写性能。HBase中的数据是按照Rowkey的ASCII字典顺序进行全局排序的,有伙伴可能对ASCII字典序印象不够深刻,下面举例说明:

假如有5个Rowkey:"012", "0", "123", "234", "3",按ASCII字典排序后的结果为:"0", "012", "123", "234", "3"。

Rowkey排序时会先比对两个Rowkey的第一个字节,如果相同,然后会比对第二个字节,依次类推... 对比到第X个字节时,已经超出了其中一个Rowkey的长度,短的Rowkey排在前面。

由于HBase是通过Rowkey查询的,一般Rowkey上都会存一些比较关键的检索信息,我们需要提前想好数据具体需要如何查询,根据查询方式进行数据存储格式的设计,要避免做全表扫描,因为效率特别低。

Rowkey设计原则

  1. Rowkey的唯一原则
  2. Rowkey的排序原则
  3. Rowkey的长度原则
  4. Rowkey的散列原则

唯一原则:必须在设计上保证其唯一性。由于在HBase中数据存储是Key-Value形式,若HBase中同一表插入相同Rowkey,则原先的数据会被覆盖掉(如果表的version设置为1的话),所以务必保证Rowkey的唯一性

排序原则:HBase的Rowkey是按照ASCII有序设计的,我们在设计Rowkey时要充分利用这点。比如视频网站上对影片《泰坦尼克号》的弹幕信息,这个弹幕是按照时间倒排序展示视频里,这个时候我们设计的Rowkey要和时间顺序相关。可以使用"Long.MAX_VALUE - 弹幕发表时间"的 long 值作为 Rowkey 的前缀

长度设计原则:Rowkey是一个二进制,Rowkey的长度被很多开发者建议说设计在10~100个字节,建议是越短越好。

原因有两点:

  1. HBase的持久化文件HFile是按照KeyValue存储的,如果Rowkey过长比如500个字节,1000万列数据光Rowkey就要占用500*1000万=50亿个字节,将近1G数据,这会极大影响HFile的存储效率
  2. MemStore缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统无法缓存更多的数据,这会降低检索效率

需要指出的是不仅Rowkey的长度是越短越好,而且列族名、列名等尽量使用短名字,因为HBase属于列式数据库,这些名字都是会写入到HBase的持久化文件HFile中去,过长的Rowkey、列族、列名都会导致整体的存储量成倍增加。

散列原则:我们设计的Rowkey应均匀的分布在各个HBase节点上。拿常见的时间戳举例,假如Rowkey是按系统时间戳的方式递增,Rowkey的第一部分如果是时间戳信息的话将造成所有新数据都在一个RegionServer上堆积的热点现象,也就是通常说的Region热点问题, 热点发生在大量的client直接访问集中在个别RegionServer上(访问可能是读,写或者其他操作),导致单个RegionServer机器自身负载过高,引起性能下降甚至Region不可用,常见的是发生jvm full gc或者显示region too busy异常情况,当然这也会影响同一个RegionServer上的其他Region。

通常有3种办法来解决这个Region热点问题:

1、Reverse反转

针对固定长度的Rowkey反转后存储,这样可以使Rowkey中经常改变的部分放在最前面,可以有效的随机Rowkey。

反转Rowkey的例子通常以手机举例,可以将手机号反转后的字符串作为Rowkey,这样的就避免了以手机号那样比较固定开头(137x、15x等)导致热点问题,

这样做的缺点是牺牲了Rowkey的有序性。

2、Salt加盐

Salting是将每一个Rowkey加一个前缀,前缀使用一些随机字符,使得数据分散在多个不同的Region,达到Region负载均衡的目标。

比如在一个有4个Region(注:以 [ ,a)、[a,b)、[b,c)、[c, )为Region起至)的HBase表中,

加Salt前的Rowkey:abc001、abc002、abc003

我们分别加上a、b、c前缀,加Salt后Rowkey为:a-abc001、b-abc002、c-abc003

可以看到,加盐前的Rowkey默认会在第2个region中,加盐后的Rowkey数据会分布在3个region中,理论上处理后的吞吐量应是之前的3倍。由于前缀是随机的,读这些数据时需要耗费更多的时间,所以Salt增加了写操作的吞吐量,不过缺点是同时增加了读操作的开销。

ΩΩ3、Hash散列或者Mod

用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推断。确定性Hash(比如md5后取前4位做前缀)能让客户端重建完整的RowKey,可以使用get操作直接get想要的行。

例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下

9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0)

7006-abc002

95e6-abc003

若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,这种设计会使得分区之间更加均衡。

如果Rowkey是数字类型的,也可以考虑Mod方法。

HBase索引设计

数据库查询可简单分解为两个步骤:

  1. 键的查找
  2. 数据的查找

因这两种数据组织方式的不同,在RDBMS领域有两种常见的数据组织表结构:

索引组织表:键与数据存放在一起,查找到键所在的位置则意味着查找到数据本身。

堆表:键的存储与数据的存储是分离的。查找到键的位置,只能获取到数据的物理地址,还需要基于该地址去获取数据。

HBase数据表其实是一种索引组织表结构:查找到RowKey所在的位置则意味着找到数据本身。因此,RowKey本身就是一种索引

RowKey查询的局限性/二级索引需求背景

如果提供的查询条件能够尽可能丰富的描述RowKey的前缀信息,则查询时延越能得到保障。如下面几种组合条件场景:

  • Name + Phone + ID
  • Name + Phone
  • Name

如果查询条件不能提供Name信息,则RowKey的前缀条件是无法确定的,此时只能通过全表扫描的方式来查找结果。

一种业务模型的用户数据RowKey,只能采用单一结构设计。但事实上,查询场景可能是多纬度的。例如,在上面的场景基础上,还需要单独基于Phone列进行查询。这是HBase二级索引出现的背景。即,二级索引是为了让HBase能够提供更多纬度的查询能力。

注:HBase原生并不支持二级索引方案,但基于HBase的KeyValue数据模型与API,可以轻易的构建出二级索引数据。Phoenix提供了两种索引方案,而一些大厂家也都提供了自己的二级索引实现。

常见两种索引

6149590352cb8becd1a06464bb543438.png

方案一:

优点:数据按索引字段全部排序,基于索引字段的小批次查询性能高。能够支撑更大的查询并发数。方法的复杂度低。

缺点:生成索引数据对实时写入影响较大

60e7d0b641f0ce4b7140f3ba1fb17832.png

方案二:

优点:每个用户的Region都拥有独立的索引数据,目前最佳的实践是将这部分索引数据存放于一个独立的列族中。

缺点:按索引字段进行查询,需要访问所有的索引的Region,随着数据量和Region数目的不断增多,查询时延无保障,查询所支持的并发数也会降低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值