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

一、前言

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

二、 HBase 简介

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

2.1 HBase 关键进程

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

  • Master 是一个轻量级进程,负责所有 DDL 操作,负载均衡, region 信息管理,并在宕机恢复中起主导作用。

  • RegionServer 管理 HRegion,与客户端点对点通信,负责实时数据的读写,。

  • zookeeper 做 HMaster 选举,关键信息如 meta-region 地址,replication 进度,Regionserver 地址与端口等存储。

2.2 HBase 架构

首先给出架构图如下

      架构浅析: HBase 数据存储基于 LSM 架构,数据先顺序写入 HLog,默认情况下 RegionServer 只有一个 Hlog 实例,之后再写入 HRegion 的 MemStore 之中。HRegion 是一张 HBase 表的一块数据连续的区域,数据按照 rowkey 字典序排列,RegionServer 管理这些 HRegion 。当MemStore达到阈值时触发flush操作,刷写为一个 HFile 文件,众多 HFile 文件会周期性进行 major, minor compaction 合并成大文件。所有 HFile 与日志文件都存储在HDFS之上。

     至此,我们对 HBase 的关键组件和它的角色以及架构有了一个大体的认识,下面重点介绍下 HBase 的读路径。

三、读路径解析

     客户端读取数据有两种方式, GetScan。 Get 是一种随机点查的方式,根据 rowkey 返回一行数据,也可以在构造 Get 对象的时候传入一个 rowkey 列表,这样一次 RPC 请求可以返回多条数据。Get 对象可以设置列与 filter,只获取特定 rowkey 下的指定列的数据、Scan 是范围查询,通过指定 Scan 对象的 startRow 与 endRow 来确定一次扫描的数据范围,获取该区间的所有数据。

     一次由客户端发起的完成的读流程,可以分为两个阶段。第一个阶段是客户端如何将请求发送到正确的 RegionServer 上,第二阶段是 RegionServer 如何处理读取请求。

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.1 客户端层面

      HBase 读数据共有两种方式,Get 与 Scan。

      在通用层面,在客户端与服务端建连需要与 zookeeper 通信,再通过 meta 表定位到 region 信息,所以在初次读取 HBase 的时候 rt 都会比较高,避免这个情况就需要客户端针对表来做预热,简单的预热可以通过获取 table 所有的 region 信息,再对每一个 region 发送一个 Scan 或者 Get 请求,这样就会缓存 region 的地址;

      rowkey 是否存在读写热点,若出现热点则失去分布式系统带来的优势,所有请求都只落到一个或几个 HRegion 上,那么请求效率一定不会高;

     读写占比是如何的。如果写重读轻,浏览服务端 RegionServer 日志发现很多 MVCC STUCK 这样的字样,那么会因为 MVCC 机制因为写 Sync 到 WAL 不及时而阻塞读,这部分机制比较复杂,考虑之后分享给大家,这里不详细展开。

Get 请求优化

  • 将 Get 请求批量化,减少 rpc 次数,但如果一批次的 Get 数量过大,如果遇到磁盘毛刺或者 Split 毛刺,则 Get 会全部失败(不会返回部分成功的结果),抛出异常。

  • 指定列族,标识符。这样可以服务端过滤掉很多无用的 scanner,减少 IO 次数,提高效率,该方法同样适用于 Scan。

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,这部分内容又会比较艰深复杂,同样不再展开。下面结合业务需求讲下我们做的优化实践

猜你喜欢

转载自blog.csdn.net/qq_23160237/article/details/87873822