本文首先对 HBase 作简单的介绍,包括其总体架构、依赖组件、核心服务类的相关解析。再重点介绍 HBase 读取数据的流程分析,并根据此流程介绍如何在客户端以及服务端优化性能,同时结合有赞线上 HBase 集群的实际应用状况,将理论和实践结合,但愿能给读者带来启发。如文章有纰漏请在下面留言,咱们共同探讨共同窗习。html
HBase 是一个分布式,可扩展,面向列的适合存储海量数据的数据库,其最主要的功能是解决海量数据下的实时随机读写的问题。 一般 HBase 依赖 HDFS 作为底层分布式文件系统,本文以此作前提并展开,详细介绍 HBase 的架构,读路径以及优化实践。node
HBase是一个 Master/Slave 架构的分布式数据库,内部主要有 Master, RegionServer 两个核心服务,依赖 HDFS 作底层存储,依赖 zookeeper 作一致性等协调工做。sql
首先给出架构图以下数据库
客户端读取数据有两种方式, Get 与 Scan。 Get 是一种随机点查的方式,根据 rowkey 返回一行数据,也能够在构造 Get 对象的时候传入一个 rowkey 列表,这样一次 RPC 请求能够返回多条数据。Get 对象能够设置列与 filter,只获取特定 rowkey 下的指定列的数据、Scan 是范围查询,经过指定 Scan 对象的 startRow 与 endRow 来肯定一次扫描的数据范围,获取该区间的全部数据。
一次由客户端发起的完成的读流程,能够分为两个阶段。第一个阶段是客户端如何将请求发送到正确的 RegionServer 上,第二阶段是 RegionServer 如何处理读取请求。apache
HRegion 是管理一张表一块连续数据区间的组件,而表是由多个 HRegion 组成,同时这些 HRegion 会在 RegionServer 上提供读写服务。因此客户端发送请求到指定的 RegionServer 上就须要知道 HRegion 的元信息,这些元信息保存在 hbase:meta 这张系统表以内,这张表也在某一个 RegionServer 上提供服务,而这个信息相当重要,是全部客户端定位 HRegion 的基础所在,因此这个映射信息是存储在 zookeeper 上面。 客户端获取 HRegion 元信息流程图以下: 后端
首先在 RegionServer 端,将 Get 请求当作特殊的一次 Scan 请求处理,其 startRow 和 StopRow 是同样的,因此介绍 Scan 请求的处理就能够明白 Get 请求的处理流程了。缓存
让咱们回顾一下 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;
复制代码
咱们知道数据在内存以及 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 组织逻辑视图以下图所示。 数据结构
若是 scan 的参数更加复杂,条件也会发生变化,好比指定 scan 返回 Raw 数据的时候,打了删除标记的数据也要被返回,这部分就再也不详细展开,至此读流程基本解析完成,固然本文介绍的仍是很粗略,有兴趣的同窗能够本身研究这一部分源码。架构
在介绍读流程以后,咱们再结合有赞业务上的实践来介绍如何优化读请求,既然谈到优化,就要先知道哪些点可会影响读请求的性能,咱们依旧从客户端和服务端两个方面来深刻了解优化的方法。
HBase 读数据共有两种方式,Get 与 Scan。
在通用层面,在客户端与服务端建连须要与 zookeeper 通讯,再经过 meta 表定位到 region 信息,因此在初次读取 HBase 的时候 rt 都会比较高,避免这个状况就须要客户端针对表来作预热,简单的预热能够经过获取 table 全部的 region 信息,再对每个 region 发送一个 Scan 或者 Get 请求,这样就会缓存 region 的地址;
rowkey 是否存在读写热点,若出现热点则失去分布式系统带来的优点,全部请求都只落到一个或几个 HRegion 上,那么请求效率必定不会高; 读写占比是如何的。若是写重读轻,浏览服务端 RegionServer 日志发现不少 MVCC STUCK 这样的字样,那么会由于 MVCC 机制由于写 Sync 到 WAL 不及时而阻塞读,这部分机制比较复杂,考虑以后分享给你们,这里不详细展开。
相对于客户端,服务端优化可作的比较多,首先咱们列出有哪些点会影响服务端处理读请求。
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要求极高,为了知足业务需求咱们作了以下措施。
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集群存储架构示意图如图所示:
<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 。
该特性由 HDFS-2246 引入。咱们集群的 RegionServer 与 DataNode 混布,这样的好处是数据有本地化率的保证,数据第一个副本会优先写本地的 Datanode。在不开启短路读的时候,即便读取本地的 DataNode 节点上的数据,也须要发送RPC请求,通过层层处理最后返回数据,而短路读的实现原理是客户端向 DataNode 请求数据时,DataNode 会打开文件和校验和文件,将两个文件的描述符直接传递给客户端,而不是将路径传递给客户端。客户端收到两个文件的描述符以后,直接打开文件读取数据,该特性是经过 UNIX Domain Socket进程间通讯方式实现,流程图如图所示:
开启短路读须要修改 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>
复制代码
当咱们经过短路读读取本地数据由于磁盘抖动或其余缘由读取数据一段时间内没有返回,去向其余 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 监控查看 hedgedReadOps 与 hedgedReadOps 两个指标项,查看启用 Hedged read 的效果,前者表示发生了 Hedged read 的次数,后者表示 Hedged read 比原生读要快的次数。
HBase是一个CP系统,同一个region同一时刻只有一个regionserver提供读写服务,这保证了数据的一致性,即不存在多副本同步的问题。可是若是一台regionserver发声宕机的时候,系统须要必定的故障恢复时间deltaT, 这个deltaT时间内,region是不提供服务的。这个deltaT时间主要由宕机恢复中须要回放的log的数目决定。集群复制原理图以下图所示:
应用冷启动预热不生效问题。该问题产生的背景在于应用初始化以后第一次访问 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 之内。
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…