一条数据的HBase之旅,简明HBase入门教程14:Scan全流程

举报
Jaison 发表于 2018/05/24 17:02:27 2018/05/24
【摘要】 本文详细介绍RegionServer侧如何处理读取请求(Get & Scan)的详细实现思路。

华为云上的NoSQL数据库服务CloudTable,基于Apache HBase,提供全托管式集群服务,集成了时序数据库OpenTSDB与时空数据库GeoMesa,在TB/PB级别的海量数据背景下,可提供ms级查询以及千万级TPS,点我了解详情


NoSQL漫谈-HBase.png


Client发送读取请求到RegionServer


无论是Get,还是Scan,Client在发送请求到RegionServer之前,也需要先获取路由信息:


1.定位该请求所关联的Region


因为Get请求仅关联一个RowKey,所以,直接定位该RowKey所关联的Region即可。

对于Scan请求,先定位Scan的StartRow所关联的Region。


2.往该Region所关联的RegionServer发送读取请求


该过程与《一条数据的HBase之旅,简明HBase入门教程8 - 数据路由与分组打包》的"数据路由"章节所描述的流程类似,不再赘述。


如果一次Scan涉及到跨Region的读取,读完一个Region的数据以后,需要继续读取下一个Region的数据,这需要在Client侧不断记录和刷新Scan的进展信息。如果一个Region中已无更多的数据,在scan请求的响应结果中会带有提示信息,这样可以让Client侧切换到下一个Region继续读取。


RegionServer如何处理读取请求


关于Read的命题


通过前面的文章,我们已经知道了如下这些信息:


1.一个表可能包含一个或多个Region


将HBase中拥有数亿行的一个大表,横向切割成一个个”子表“,这一个个”子表“就是Region

3.Regions


2.每一个Region中关联一个或多个列族


如果将Region看成是一个表的横向切割,那么,一个Region中的数据列的纵向切割,称之为一个Column Family。每一个列,都必须归属于一个Column Family,这个归属关系是在写数据时指定的,而不是建表时预先定义。

4.RegionAndColumnFamilies


3.每一个列族关联一个MemStore,以及一个或多个HFiles文件


上面的关于“Region与多列族”的图中,泛化了Column Family的内部结构。下图是包含MemStore与HFile的Column Family组成结构:

ColumnFamilyComponents


HFile数据文件存在于底层的HDFS中,上图中只是为了方便阐述HFile与Column Family之间的关系。


在HBase的源码实现中,将一个Column Family抽象成一个Store对象。可以这么简单理解Column Family与Store的概念差异:Column Family更多的是面向用户层可感知的逻辑概念,而Store则是源码实现中的概念,是关于一个Column Family的抽象。


4.每一个MemStore中可能涉及一个Active Segment,以及一个或多个Immutable Segments


ColumnFamilyWithSegments


扩展到一个Region包含两个Column Family的情形:

RegionWithMultipleFamilies


5.HFile由Block构成,默认地,用户数据被按序组织成一个个64KB的Block


HFile V1的结构虽已过时,但非常有助于你理解HFile的核心设计思想:


HFileV1


  • Data Block(上图中左侧的Data块):保存了实际的KeyValue数据。

  • Data Index:关于Data Block的索引信息。


HFile V2只不过在HFile V1基础上做的演进,将Data Index信息以及BloomFilter的数据也分成了多层。


当前阶段,你只需要了解到:基于一个给定的RowKey,HFile中提供的索引信息能够快速查询到对应的Data Block


在重新温习了上述内容以后,我们也大致了解了关于HBase读取我们所面临的问题是什么。关于HBase Read的命题可以定义为:如何从包含1个或多个列族(每个列族包含1个或多个MemStore Segments,以及1个或多个HFiles)的Region中读取用户所期望的数据?默认情况下,这些数据必须是未被标记删除的、未过期的而且是最新版本的数据。


将Get看作一类特殊的Scan


无论是读取一行数据,还是读取指定RowKey范围的读取一系列数据,所面临的问题其实是类似的,因此,可以将Get看作是一种特殊的Scan,只不过它的StartRow与StopRow重叠,事实上,RegionServer侧处理Get请求时的确先将Get先转换成了一个Scan操作。


合理组织所有的KeyValue数据源


在Store/Column Family内部,KeyValue可能存在于MemStore的Segment中,也可能存在于HFile文件中,无论是Segment还是HFile,我们统称为KeyValue数据源


在本文的第一部分介绍如何执行Scan操作时,我们讲到了Client侧使用一个ResultScanner来抽象地描述一次Scan操作,ResultScanner屏蔽掉了往RegionServer发送请求以及一个Region读取完成以后切换到下一个Region等细节信息。


初次阅读RegionServer/Region的读取流程所涉及的源码时,会被各色各样的Scanner类整的晕头转向,HBase使用了各种Scanner来抽象每一层/每一类KeyValue数据源的Scan操作:


  • 关于一个Region的读取,被封装成一个RegionScanner对象。

  • 每一个Store/Column Family的读取操作,被封装在一个StoreScanner对象中。

  • SegmentScanner与StoreFileScanner分别用来描述关于MemStore中的Segment以及HFile的读取操作。

  • StoreFileScanner中关于HFile的实际读取操作,由HFileScanner完成。


RegionScanner的构成如下图所示:

RegionScanner


在StoreScanner内部,多个SegmentScanner与多个StoreFileScanner被组织在一个称之为KeyValueHeap的对象中:

KeyValueHeapInStoreScanner


每一个Scanner内部有一个指针指向当前要读取的KeyValue,KeyValueHeap的核心是一个优先级队列(PriorityQueue),在这个PriorityQueue中,按照每一个Scanner当前指针所指向的KeyValue进行排序:


    // 用来组织所有的Scanner 

    protected PriorityQueue<KeyValueScanner> heap = null;  



    // PriorityQueue当前排在最前面的Scanner 

    protected KeyValueScanner current = null;


同样的,RegionScanner中的多个StoreScanner,也被组织在一个KeyValueHeap对象中:

KeyValueHeapInRegionScanner


KeyValueScanner接口


KeyValueScanner定义了读取KeyValue的基础接口:


  /**

    * 查看当前Scanner中当前指针位置的KeyValue,该接口不会移动指针.

    */

  Cell peek();

  


  /**

    * 返回Scanner当前指针位置的KeyValue,而后移动指针到下一个KeyValue.

    */

  Cell next() throws IOException;

  


  /**

    * 将当前Scanner的指针定位到指定的KeyValue的位置,如果不存在,则定位到

    * 比该Cell大的下一个KeyValue位置.Seek操作会从当前的HFile Block的开

    * 始位置查找.

    */

  boolean seek(Cell key) throws IOException;

  


  /**

    * 与seek接口类似,也是将当前Scanner的指针定位到指定的KeyValue的位置,

    * 如果不存在,则定位到比该KeyValue大的下一个KeyValue位置.与seek接口不

    * 同点在于,该操作会从上一次读到的HFile Block的位置开始查找.

    */

  boolean reseek(Cell key) throws IOException;

  


  /**

    * 与seek/reseek类似,但不同点在于采用了Lazy Seek机制来降低磁盘IO请求,

    * 其原理在于充分利用Bloom Filter的判断结果,以及待Seek的KeyValue与该

    * Scanner中最大时间戳的对比,减少一些不必要的Seek操作

    */

  boolean requestSeek(Cell kv, boolean forward,  boolean useBloom) throws IOException;



实现了KeyValueScanner接口类的主要Scanner包括:


  • StoreFileScanner

  • SegmentScanner

  • StoreScanner



RegionScanner初始化


RegionScanner初始化过程,包括几个关键操作:


1.获取ReadPoint

ReadPoint决定了此次Scan操作能看到哪些数据。Scan过程中新写入的数据,对此次Scan是不可见的。


2.按需选择对应的Store,并初始化对应的StoreScanner

StoreScanner在初始化的时候,也会按需选择对应的SegmentScanner以及StoreFileScanner,筛选规则包括:


  • 如果一次Scan操作指定了Time Range,则只选择与该Time Range有关的Scanners。

  • 对于Get操作,可以通过BloomFilter过滤掉不符合条件的Scanners。


StoreScanner中筛选除了Scanner以后,会将每一个Scanner seek到Scan的StartRow位置:

Seek_Scanner


通过next请求读取一行行数据


如果将RegionScanner理解成一个内部构造复杂的机器,而驱动这个机器运转的动力源自Client侧的一次次scan请求,scan请求通过调用RegionScanner的next方法来获取一行行结果。


为了简单的解释该流程,我们先假定一个RegionScanner中仅包含一个StoreScanner,那么,这个RegionScanner中的核心读取操作,是由StoreScanner完成的,我们进一步假定StoreScanner由4个Scanners组成(我们泛化了SegmentScanner与StoreFileScanner的区别,统称为Scanner),直观起见,在下图中我们使用了四种不同的颜色:

StoreScanner-next-1


每一个Scanner中都有一个current指针指向下一个即将要读取的KeyValue,KeyValueHeap中的PriorityQueue正是按照每一个Scanner的current所指向的KeyValue进行排序


第一次next请求,将会返回ScannerA中的Row01:FamA:Col1,而后ScannerA的指针移动到下一个KeyValue Row01:FamA:Col2,PriorityQueue中的Scanners排序依然不变:

StoreScanner-next-2


第二次next请求,依然返回ScannerA中的Row01:FamA:Col2,ScannerA的指针移动到下一个KeyValue Row02:FamA:Col1,此时,PriorityQueue中的Scanners排序发生了变化:

StoreScanner-next-3


下一次next请求,将会返回ScannerB中的KeyValue…..周而复始,直到某一个Scanner所读取的数据耗尽,该Scanner将会被close,不再出现在上面的PriorityQueue中。


SegmentScanner/StoreFileScanner中返回的KeyValue,包含了各种类型的KeyValue:


  • 已被更新过的旧KeyValue

  • 已被标记删除但尚未被及时清理的KeyValue

  • 已过期的尚未被及时清理的KeyValue

  • 用来描述一次删除操作的KeyValue(删除还包含了多种类型)

  • 承载最新用户数据的普通KeyValue



因此,在StoreScanner层,需要对这些KeyValue做更复杂的逻辑校验,这些校验由ScanQueryMatcher完成。默认地,可作为返回数据的KeyValue,应该满足如下条件:


  • KeyValue类型为Put

  • KeyValue所关联的列为用户Scan所涉及的列

  • KeyValue的时间戳符合Scan的TimeRange要求

  • 版本最新

  • 未被标记删除

  • 通过了Filter的过滤条件


上述条件,只针对一些普通的Scan,不同的Scan参数配置,可能会导致条件集发生变化,如Scan启用了Raw Scan模式时,Delete类型的KeyValue也会被返回。另外,上面的这些条件所罗列的顺序,也未遵循实际的检查顺序,而实际的检查顺序也是严格的,如果颠倒就可能会导致Bug。小米的同学就曾发现了这样的一个Bug:


假设某一个列共有T1~T5五个版本, Column Family中设置的MaxVersions=3(即最大允许保留的版本)

T5 -> Value=5

T4 -> Value=4

T3 -> Value=3

T2 -> Value=2

T1 -> Value=1

如果Scan中采用了一个SingleColumnValueFilter,要求返回满足Value<=3的所有结果。


因为MaxVersions为3,我们所期望的返回结果应该为:

 T5 -> Value=5 (Value不满足条件)

 T4 -> Value=4 (Value不满足条件)

T3 -> Value=3

 T2 -> Value=2 (Version不满足条件)

 T1 -> Value=1 (Version不满足条件)


关于多版本检查以及Filter检查,这里有两种可能的顺序:


Opt 1:先检查Filter,再检查多版本。这种情况下的返回结果为:

 T5 -> Value=5 (Value不满足条件)

 T4 -> Value=4 (Value不满足条件)

T3 -> Value=3

T2 -> Value=2

T1 -> Value=1

这种情况的返回结果就是错误的。


Opt 2: 先检查多版本,再检查Filter。这种情况下的返回结果才是预期的。


在Scanner中,如果允许读取多个版本(由Scan#readVersions配置),那正常的读取顺序应该为:


上面这种读取的顺序与实际存在的数据的逻辑顺序也是相同的。


由于不同的Scan所读取的每一行中的数据不同,有的限定了列的数量,有的限定了版本的数量,这使得读取时可以通过一些优化,减少不必要的数据扫描。如某次Scan在允许读多个版本的同时,限定了只读取C1~C3,那么,读取顺序应该为:

Scan-Order-LimitColumns


最普通的Scan,其实只需要读取每一列的最新版本即可,那读取的顺序应该为:

Scan-Order-LimitVersions


通过上面几张图,我们其实是想说明在Scanner内部需要具备这样的一些基础能力:


  • 如果只需要当前列的最新版本,那么Scanner应该可以跳过当前列的其它版本,而且将指针移到下一列的开始位置。

  • 如果当前行的所要读取的列都已读完,那么,Scanner应该可以跳过该行剩余的列,将指针移动到下一行的开始位置。


我们知道KeyValueScanner定义了基础的seek/reseek/requestSeek等接口,可以将指针移动到指定KeyValue位置。但关于指针如何移动的决策信息,由谁来提供?


这些信息也是由ScanQueryMatcher提供的。ScanQueryMatcher对每一个KeyValue的逻辑检查结果称之为MatchCode,MatchCode不仅包含了是否应该返回该KeyValue的结果,还可能给出了Scanner的下一步操作的提示信息。关于它的枚举值,简单举例如下:


  • INCLUDE_AND_SEEK_NEXT_ROW

    包含当前KeyValue,并提示Scanner当前行已无需继续读取,请Seek到下一行。

  • INCLUDE_AND_SEEK_NEXT_COL

    包含当前KeyValue,并提示Scanner当前列已无需继续读取,请Seek到下一列。



无论是StoreScanner还是RegionScanner,返回的都是符合条件的KeyValue列表。这些KeyValues在RSRpcServices层被进一步组装成Results响应给Client侧。


总结


Scan涉及了太多的细节内容,本文只粗略介绍了Scan的一些核心思路,这与本系列文章最初的定位有关,当然也受限于本文的篇幅。 本文主要介绍了如下内容:


  1. 介绍HBase的两种读取模式:Get与Scan

    Client如何发起一次Get请求,Get的关键参数

    Client如何发起一次Scan请求,Scan的关键参数

  2. 重点介绍了RegionServer侧关于Scan的处理流程

    如何用Scanner来抽象描述关于Region的读取操作

    关于读取KeyValue的基础Scanner接口定义

    RegionScanner初始化时的关键操作

    Client侧的一次次scan请求如何驱动RegionScanner内部的读取操作

    从StoreFileScanner/SegmentScanner中读取出来的原始KeyValue如何被合理的校验

    Scanner读取时如何跳过一些不必要的数据


关于Scan的更多细节,感兴趣的同学可以自己去源码中探寻答案:


  • 如果第一次scan请求不能取回所有的数据,下一次scan如何快速有效继承上一次的进度?

  • Get/Small Scan/Large Scan在实现上有何不同?

  • ScanQueryMatcher中校验KeyValue的详细逻辑

  • 关于Filter涉及多步校验,每一步校验是在什么地方完成的?

  • MinVersion与MaxVersion的定义是什么?

  • ScanQueryMatcher中关于多种删除类型的语义是如何定义的?

  • 如何限制一次Scan所占用的内存大小以及执行的时间?

  • BloomFilter在Get/Scan流程中是如何被应用的?

  • Scan过程中如果正在读取的HFile文件被Compaction合并了,如何处理?

  • 正在Scan的Region突然被迁移到其它的RegionServer中,如何继续原来的进度继续读取?

  • Reverse Scan与普通的Scan在实现上有何不同?


HFile的内容在本文只粗略提及,在RegionServer侧的处理流程中,关于BlockCache部分更是只字未提。本文将重点放在介绍Scan的核心思路上。


下篇文章将单独介绍HFile的核心原理。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。