HBase 读流程解析与优化的最佳实践

1、前言

本文首先对 HBase 作简单的介绍,包括其总体架构、依赖组件、核心服务类的相关解析。再重点介绍 HBase 读取数据的流程分析,并根据此流程介绍如何在客户端以及服务端优化性能,同时结合有赞线上 HBase 集群的实际应用状况,将理论和实践结合,但愿能给读者带来启发。如文章有纰漏请在下面留言,咱们共同探讨共同窗习。html

2、 HBase 简介

HBase 是一个分布式,可扩展,面向列的适合存储海量数据的数据库,其最主要的功能是解决海量数据下的实时随机读写的问题。 一般 HBase 依赖 HDFS 作为底层分布式文件系统,本文以此作前提并展开,详细介绍 HBase 的架构,读路径以及优化实践。node

2.1 HBase 关键进程

HBase是一个 Master/Slave 架构的分布式数据库,内部主要有 Master, RegionServer 两个核心服务,依赖 HDFS 作底层存储,依赖 zookeeper 作一致性等协调工做。sql

  • Master 是一个轻量级进程,负责全部 DDL 操做,负载均衡, region 信息管理,并在宕机恢复中起主导做用。
  • RegionServer 管理 HRegion,与客户端点对点通讯,负责实时数据的读写,。
  • zookeeper 作 HMaster 选举,关键信息如 meta-region 地址,replication 进度,Regionserver 地址与端口等存储。

2.2 HBase 架构

首先给出架构图以下数据库

架构浅析: HBase 数据存储基于 LSM 架构,数据先顺序写入 HLog,默认状况下 RegionServer 只有一个 Hlog 实例,以后再写入 HRegionMemStore 之中。 HRegion 是一张 HBase 表的一块数据连续的区域,数据按照 rowkey 字典序排列,RegionServer 管理这些 HRegion 。当MemStore达到阈值时触发flush操做,刷写为一个 HFile 文件,众多 HFile 文件会周期性进行 major, minor compaction 合并成大文件。全部 HFile 与日志文件都存储在HDFS之上。
至此,咱们对 HBase 的关键组件和它的角色以及架构有了一个大致的认识,下面重点介绍下 HBase 的读路径。

3、读路径解析

客户端读取数据有两种方式, GetScan。 Get 是一种随机点查的方式,根据 rowkey 返回一行数据,也能够在构造 Get 对象的时候传入一个 rowkey 列表,这样一次 RPC 请求能够返回多条数据。Get 对象能够设置列与 filter,只获取特定 rowkey 下的指定列的数据、Scan 是范围查询,经过指定 Scan 对象的 startRow 与 endRow 来肯定一次扫描的数据范围,获取该区间的全部数据。
一次由客户端发起的完成的读流程,能够分为两个阶段。第一个阶段是客户端如何将请求发送到正确的 RegionServer 上,第二阶段是 RegionServer 如何处理读取请求。apache

3.1 客户端如何发送请求到指定的 RegionServer

HRegion 是管理一张表一块连续数据区间的组件,而表是由多个 HRegion 组成,同时这些 HRegion 会在 RegionServer 上提供读写服务。因此客户端发送请求到指定的 RegionServer 上就须要知道 HRegion 的元信息,这些元信息保存在 hbase:meta 这张系统表以内,这张表也在某一个 RegionServer 上提供服务,而这个信息相当重要,是全部客户端定位 HRegion 的基础所在,因此这个映射信息是存储在 zookeeper 上面。 客户端获取 HRegion 元信息流程图以下: 后端

咱们以单条 rowkey 的 Get 请求为例,当用户初始化到 zookeeper 的链接以后,并发送一个 Get 请求时,须要先定位这条 rowkey 的 HRegion 地址。若是该地址不在缓存之中,就须要请求 zookeeper (箭头1),询问 meta 表的地址。在获取到 meta 表地址以后去读取 meta 表的数据来根据 rowkey 定位到该 rowkey 属于的 HRegion 信息和 RegionServer 的地址(箭头2),缓存该地址并发 Get 请求点对点发送到对应的 RegionServer(箭头3),至此,客户端定位发送请求的流程走通。

3.2 RegionServer 处理读请求

首先在 RegionServer 端,将 Get 请求当作特殊的一次 Scan 请求处理,其 startRow 和 StopRow 是同样的,因此介绍 Scan 请求的处理就能够明白 Get 请求的处理流程了。缓存

3.2.1 数据组织

让咱们回顾一下 HBase 数据的组织架构,首先 Table 横向切割为多个 HRegion ,按照一个列族的状况,每个 HRegion 之中包含一个 MemStore 和多个 HFile 文件, HFile 文件设计比较复杂,这里不详细展开,用户须要知道给定一个 rowkey 能够根据索引结合二分查找能够迅速定位到对应的数据块便可。结合这些背景信息,咱们能够把一个Read请求的处理转化下面的问题:如何从一个 MemStore,多个 HFile 中获取到用户须要的正确的数据(默认状况下是最新版本,非删除,没有过时的数据。同时用户可能会设定 filter ,指定返回条数等过滤条件)
在 RegionServer 内部,会把读取可能涉及到的全部组件都初始化为对应的 scanner 对象,针对 Region 的读取,封装为一个 RegionScanner 对象,而一个列族对应一个 Store,对应封装为 StoreScanner,在 Store 内部,MemStore 则封装为 MemStoreScanner,每个 HFile 都会封装为 StoreFileScanner 。最后数据的查询就会落在对 MemStoreScanner 和 StoreFileScanner 上的查询之上。
这些 scanner 首先根据 scan 的 TimeRange 和 Rowkey Range 会过滤掉一些,剩下的 scanner 在 RegionServer 内部组成一个最小堆 KeyValueHeap,该数据结构核心一个 PriorityQueue 优先级队列,队列里按照 Scanner 指向的 KeyValue 排序。网络

// 用来组织全部的Scanner 
protected PriorityQueue<KeyValueScanner> heap = null; 
// PriorityQueue当前排在最前面的Scanner 
protected KeyValueScanner current = null;    
复制代码

3.2.2 数据过滤

咱们知道数据在内存以及 HDFS 文件中存储着,为了读取这些数据,RegionServer 构造了若干 Scanner 并组成了一个最小堆,那么如何遍历这个堆去过滤数据返回用户想要的值呢。 咱们假设 HRegion 有4个 Hfile,1个 MemStore,那么最小堆内有4个 scanner 对象,咱们以 scannerA-D 来代替这些 scanner 对象,同时假设咱们须要查询的 rowkey 为 rowA。每个 scanner 内部有一个 current 指针,指向的是当前须要遍历的 KeyValue,因此这时堆顶部的 scanner 对象的 current 指针指向的就是 rowA(rowA:cf:colA)这条数据。经过触发 next() 调用,移动 current 指针,来遍历全部 scanner 中的数据。scanner 组织逻辑视图以下图所示。 数据结构

第一次 next 请求,将会返回 ScannerA中的rowA:cf:colA,然后 ScannerA 的指针移动到下一个 KeyValue rowA:cf:colB,堆中的 Scanners 排序不变;
第二次 next 请求,返回 ScannerA 中的 rowA:cf:colB,ScannerA 的 current 指针移动到下一个 KeyValue rowB:cf:ColA,由于堆按照 KeyValue 排序可知 rowB 小于 rowA, 因此堆内部,scanner 顺序发生改变,改变以后以下图所示
scanner 内部数据彻底检索以后会 close 掉,或者 rowA 全部数据检索完毕,则查询下一条。默认状况下返回的数据须要通过 ScanQueryMatcher 过滤返回的数据须要知足下面的条件

  • keyValue类型为put
  • 列是Scanner指定的列
  • 知足filter过滤条件
  • 最新的版本
  • 未删除的数据

若是 scan 的参数更加复杂,条件也会发生变化,好比指定 scan 返回 Raw 数据的时候,打了删除标记的数据也要被返回,这部分就再也不详细展开,至此读流程基本解析完成,固然本文介绍的仍是很粗略,有兴趣的同窗能够本身研究这一部分源码。架构

4、读优化

在介绍读流程以后,咱们再结合有赞业务上的实践来介绍如何优化读请求,既然谈到优化,就要先知道哪些点可会影响读请求的性能,咱们依旧从客户端和服务端两个方面来深刻了解优化的方法。

4.1客户端层面

HBase 读数据共有两种方式,Get 与 Scan。
在通用层面,在客户端与服务端建连须要与 zookeeper 通讯,再经过 meta 表定位到 region 信息,因此在初次读取 HBase 的时候 rt 都会比较高,避免这个状况就须要客户端针对表来作预热,简单的预热能够经过获取 table 全部的 region 信息,再对每个 region 发送一个 Scan 或者 Get 请求,这样就会缓存 region 的地址;
rowkey 是否存在读写热点,若出现热点则失去分布式系统带来的优点,全部请求都只落到一个或几个 HRegion 上,那么请求效率必定不会高; 读写占比是如何的。若是写重读轻,浏览服务端 RegionServer 日志发现不少 MVCC STUCK 这样的字样,那么会由于 MVCC 机制由于写 Sync 到 WAL 不及时而阻塞读,这部分机制比较复杂,考虑以后分享给你们,这里不详细展开。

4.1.1请求优化

  • 将 Get 请求批量化,减小 rpc 次数,但若是一批次的 Get 数量过大,若是遇到磁盘毛刺或者 Split 毛刺,则 Get 会所有失败(不会返回部分红功的结果),抛出异常。
  • 指定列族,标识符。这样能够服务端过滤掉不少无用的 scanner,减小 IO 次数,提升效率,该方法一样适用于 Scan。

4.1.2 Scan 请求优化

  • 设定合理的 startRow 与 stopRow 。若是 scan 请求不设置这两个值,而只设置 filter,则会作全表扫描。
  • 设置合理的 caching 数目, scan.setCaching(100)。 由于 Scan 潜在会扫描大量数据,所以客户端发起一次 Scan 请求,实际并不会一次就将全部数据加载到本地,而是分红屡次 RPC 请求进行加载。默认值是100。用户若是确实须要扫描海量数据,同时不作逻辑分页处理,那么能够将缓存值设置到1000,减小 rpc 次数,提高处理效率。若是用户须要快速,迭代地获取数据,那么将 caching 设置为50或者100就合理。

4.2 服务端优化

相对于客户端,服务端优化可作的比较多,首先咱们列出有哪些点会影响服务端处理读请求。

  • gc 毛刺
  • 磁盘毛刺
  • HFile 文件数目
  • 缓存配置
  • 本地化率
  • Hedged Read 模式是否开启
  • 短路读是否开启
  • 是否作高可用

gc 毛刺没有很好的办法避免,一般 HBase 的一次 Young gc 时间在 20~30ms 以内。磁盘毛刺发生是没法避免的,一般 SATA 盘读 IOPS 在 150 左右,SSD 盘随机读在 30000 以上,因此存储介质使用 SSD 能够提高吞吐,变向下降了毛刺的影响。HFile 文件数目由于 flush 机制而增长,因 Compaction 机制减小,若是 HFile 数目过多,那么一次查询可能通过更多 IO ,读延迟就会更大。这部分调优主要是优化 Compaction 相关配置,包括触发阈值,Compaction 文件大小阈值,一次参与的文件数量等等,这里再也不详细展开。读缓存能够设置为为 CombinedBlockCache,调整读缓存与 MemStore 占比对读请求优化一样十分重要,这里咱们配置 hfile.block.cache.size 为 0.4,这部份内容又会比较艰深复杂,一样再也不展开。下面结合业务需求讲下咱们作的优化实践
咱们的在线集群搭建伊始,接入了比较重要的粉丝业务,该业务对RT要求极高,为了知足业务需求咱们作了以下措施。

4.2.1 异构存储

HBase 资源隔离+异构存储。SATA 磁盘的随机 iops 能力,单次访问的 RT,读写吞吐上都远远不如 SSD,那么对RT极其敏感业务来讲,SATA盘并不能胜任,因此咱们须要HBase有支持SSD存储介质的能力。
为了 HBase 能够支持异构存储,首先在 HDFS 层面就须要作响应的支持,在 HDFS 2.6.x 以及以后的版本,提供了对SSD上存储文件的能力,换句话说在一个 HDFS 集群上能够有SSD和SATA磁盘并存,对应到 HDFS 存储格式为 [ssd] 与 [disk]。然而 HBase 1.2.6 上并不能对表的列族和 RegionServer 的 WAL 上设置其存储格式为 [ssd], 该功能在社区 HBase 2.0 版本以后才开放出来,因此咱们从社区 backport 了对应的 patch ,打到了咱们有赞本身的 HBase 版本之上。支持 [ssd] 的 社区issue 以下: issues.apache.org/jira/browse…
添加SSD磁盘以后,HDFS集群存储架构示意图如图所示:

理想的混合机型集群异构部署,对于 HBase 层面来看,文件存储可选三种策略:HOT, ONE_SSD, ALL_SSD,其中 ONE_SSD 存储策略既能够把三个副本中的两个存储到便宜的SATA磁盘介质之上来减小 SSD 磁盘存储成本的开销,同时在数据读取访问本地 SSD 磁盘上的数据能够得到理想的 RT ,是一个十分理想的存储策略。HOT 存储策略与不引入异构存储时的存储状况没有区别,而 ALL_SSD 将全部副本都存储到 SSD 磁盘上。 在有赞咱们目前没有这样的理想混合机型,只有纯 SATA 与 纯 SSD 两种大数据机型,这样的机型对应的架构与以前会有所区别,存储架构示意图如图所示:
基于这样的场景,咱们作了以下规划:1.将SSD机器规划成独立的组,分组的 RegionServer 配置 hbase.wal.storage.policy=ONE_SSD, 保证 wal 自己的本地化率;2. 将SSD分组内的表配置成 ONE_SSD 或者 ALL_SSD;3. 非SSD分组内的表存储策略使用默认的 HOT 具体的配置策略以下:在 hdfs-site.xml 中修改

<property>
      <name>dfs.datanode.data.dir</name>
      <value>[SSD]file:/path/to/dfs/dn1</value>
 </property>
复制代码

在 SSD 机型 的 RegionServer 中的 hbase-site.xml 中修改

<property>
      <name>hbase.wal.storage.policy</name>
      <value>ONE_SSD</value>
 </property>
复制代码

其中ONE_SSD 也能够替代为 ALL_SSD。 SATA 机型的 RegionServer 则不须要修改或者改成 HOT 。

4.2.2 HDFS短路读

该特性由 HDFS-2246 引入。咱们集群的 RegionServer 与 DataNode 混布,这样的好处是数据有本地化率的保证,数据第一个副本会优先写本地的 Datanode。在不开启短路读的时候,即便读取本地的 DataNode 节点上的数据,也须要发送RPC请求,通过层层处理最后返回数据,而短路读的实现原理是客户端向 DataNode 请求数据时,DataNode 会打开文件和校验和文件,将两个文件的描述符直接传递给客户端,而不是将路径传递给客户端。客户端收到两个文件的描述符以后,直接打开文件读取数据,该特性是经过 UNIX Domain Socket进程间通讯方式实现,流程图如图所示:

该特性内部实现比较复杂,设计到共享内存段经过 slot 放置副本的状态与计数,这里再也不详细展开。

开启短路读须要修改 hdfs-site.xml 文件

<property>
        <name>dfs.client.read.shortcircuit</name>
        <value>true</value>
    </property>
    <property>
        <name>dfs.domain.socket.path</name>
         value>/var/run/hadoop/dn.socket</value>
    </property>
复制代码

4.2.3 HDFS Hedged read

当咱们经过短路读读取本地数据由于磁盘抖动或其余缘由读取数据一段时间内没有返回,去向其余 DataNode 发送相同的数据请求,先返回的数据为准,后到的数据抛弃,这也能够减小磁盘毛刺带来的影响。默认该功能关闭,在HBase中使用此功能须要修改 hbase-site.xml

<property>
       <name>dfs.client.hedged.read.threadpool.size</name>
       <value>50</value> 
    </property>
    <property>
       <name>dfs.client.hedged.read.threshold.millis</name>
       <value>100</value>
    </property>
复制代码

线程池大小能够与读handler的数目相同,而超时阈值不适宜调整的过小,不然会对集群和客户端都增长压力。同时能够经过 Hadoop 监控查看 hedgedReadOpshedgedReadOps 两个指标项,查看启用 Hedged read 的效果,前者表示发生了 Hedged read 的次数,后者表示 Hedged read 比原生读要快的次数。

4.2.4 高可用读

HBase是一个CP系统,同一个region同一时刻只有一个regionserver提供读写服务,这保证了数据的一致性,即不存在多副本同步的问题。可是若是一台regionserver发声宕机的时候,系统须要必定的故障恢复时间deltaT, 这个deltaT时间内,region是不提供服务的。这个deltaT时间主要由宕机恢复中须要回放的log的数目决定。集群复制原理图以下图所示:

HBase提供了HBase Replication机制,用来实现集群间单方向的异步数据复制咱们线上部署了双集群,备集群 SSD 分组和主集群 SSD 分组有相同的配置。当主集群由于磁盘,网络,或者其余业务突发流量影响致使某些 RegionServer 甚至集群不可用的时候,就须要提供备集群继续提供服务,备集群的数据可能会由于 HBase Replication 机制的延迟,相比主集群的数据是滞后的,按照咱们集群目前的规模统计,平均延迟在 100ms 之内。因此为了达到高可用,粉丝业务能够接受复制延迟,放弃了强一致性,选择了最终一致性和高可用性,在初版采用的方案以下:
粉丝业务方不想感知到后端服务的状态,也就是说在客户端层面,他们只但愿一个 Put 或者 Get 请求正常送达且返回预期的数据便可,那么就须要高可用客户端封装一层降级,熔断处理的逻辑,这里咱们采用 Hystrix 作为底层熔断处理引擎,在引擎之上封装了 HBase 的基本 API,用户只须要配置主备机房的 ZK 地址便可,全部的降级熔断逻辑最终封装到 ha-hbase-client 中,原理相似图9,这里再也不赘述。

4.2.5 预热失败问题修复

应用冷启动预热不生效问题。该问题产生的背景在于应用初始化以后第一次访问 HBase 读取数据时候须要作寻址,具体流程见图2,这个过程涉及屡次 RPC 请求,因此耗时较长。在缓存下全部的 Region 地址以后,客户端与 RegionServer 就会作点对点通讯,这样 RT 就有所保证。因此咱们会在应用启动的时候作一次预热操做,而预热操做咱们一般作法是调用方法 getAllRegionLocations 。在1.2.6版本getAllRegionLocations 存在 bug(后来通过笔者调研,1.3.x,以及2.x版本也都有相似问题),该方案预期返回全部的 Region locations 而且缓存这些 Region 地址,但实际上,该方法只会缓存 table 的第一个 Region, 笔者发现此问题以后反馈给社区,并提交了 patch 修复了此问题,issue链接:issues.apache.org/jira/browse… 。这样经过调用修复 bug 以后的 getAllRegionLocations 方法,便可在应用启动以后作好预热,在应用第一次读写HBase时便不会产生 RT 毛刺。
粉丝业务主备超时时间都设置为 300ms。通过这些优化,其批量 Get 请求 99.99% 在 20ms 之内,99.9999% 在 400ms 之内。

5、总结

HBase 读路径相比写路径更加复杂,本文只是简单介绍了核心思路。也正是由于这种复杂性,在考虑优化的时候须要深刻了解其原理,且目光不能仅仅局限于自己的服务组件,也要考虑其依赖的组件,是否也有可优化的点。最后,本人能力有限,文中观点不免存在纰漏,还望交流指正。

最后打个小广告,有赞大数据团队基础设施团队,主要负责有赞的数据平台(DP), 实时计算(Storm, Spark Streaming, Flink),离线计算(HDFS,YARN,HIVE, SPARK SQL),在线存储(HBase),实时 OLAP(Druid) 等数个技术产品,欢迎感兴趣的小伙伴联系 zhaoyuan@youzan.com

参考
www.nosqlnotes.com/technotes/h…
hbasefly.com/2016/11/11/ hadoop.apache.org/docs/stable… www.cloudera.com/documentati…

相关文章
相关标签/搜索