【 DataFunTalk】HBase RowKey 与索引设计



转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com

AIQ 机器学习大数据 知乎专栏 点击关注

分享嘉宾:毕杰山** **华为云 主任工程师

内容来源:HBase MeetUp《HBase RowKey 与索引设计》

出品社区:DataFun
今天分享的内容主要是 HBase RowKey 与索引设计相关的一些技巧、原则和相关案例。将分以下四部分分析,第一部分简单介绍 HBase 基础知识,第二部分探讨合理的需求调研方法,第三部分是关于 RowKey 与索引设计的一些技巧、原则,第四部分是关于 OpenTSDB/JanusGraph/GeoMesa 典型案例的设计分享。

一、HBase 基础

第一部分包括基础概念与数据模型介绍、快速浏览读写流程、介绍 RowKey 在读写流程中发挥的作用。

首先是基本概念介绍。

Table: 同传统数据库中的表是类似的,但他的不同之处在于它是基于 SchemaLess 的设计,比传统数据库表更加灵活。

Region 将表横向切割成一个个子表,从而实现分布式的存储,子表在 HBase 中称作 Region,它关联了数据的一个区间。

Column Family: HBase 可以将一行数据分成不同列的集合,这些列的集合称为 Column Family,不同的 Column Family 文件被存储在不同的路径中。

RegionServer: 指 HBase 里面数据服务的进程,每个 Region 必须要分到 RegionServer 上才能提供正常的读写服务。

MemStore: 用来在内存中缓存一定大小的数据,达到一定大小后批量写入到底层文件系统中。

HFile: HBase 数据在底层分布式文件系统中的文件组织格式。

关于进程角色如下图,主要有 ZooKeeper、Master、RegionServer 等角色。Meta 表的路由信息在 ZooKeeper 中;Master 负责表管理操作,Region 到各个 RegionServer 的分配以及 RegionServer Failover 的处理等;RegionServer 提供数据读写服务。HBase 的所有数据文件都存放在 HDFS 中。

我们先来理解一下 KeyValue 的概念。在 HBase 中所存储的数据,是以 KeyValue 形式存在的。KeyValue 拥有特定的组织结构,如下图所示。一个 KeyValue 可以理解成 HBase 表中的一个列,当一行存在多个列时,将包含多个 KeyValue。同一行的 KeyValue 有可能存在于不同的文件中,但在读取的时候,将会按需合并在一起返回给客户端。用户写数据时,需要定义用户数据的 RowKey,指定每一列所存放的 Column Family,并且为其定义相应的 Qualifier(列名),Value 部分存放用户数据。HBase 中每一行可拥有不同的 KeyValues,这就是 HBase Schema-Less 的特点。

下面介绍 KeyValue 多版本。HBase 中支持数据的多版本,通过带有不同时间戳的多个 KeyValue 版本来实现的。HBase 所保存的版本数目是可配置的,默认存放 3 个版本。在普通的读取流程中,旧版本的数据是不可见的,但通过指定版本数或者版本号的读取,可以获取旧版本数据。下图是普通读取流程与多版本读取流程的对比。

用户数据存入到 HBase 表中时,需要进行 Qualifier (KeyValue/Qualifier(KeyValue/ 列) 设计。一个最简单的设计是保持 HBase 的列与用户数据一致,如下图 1 的设计。这种,基本上与关系型数据库的设计是一致,但这种会带来较大冗余 (KeyValue 结构化开销)。但 HBase 基于 KeyValue 的接口,决定了这种设计可以是非常灵活的,例如我们也可以考虑为 HBase 的每一行只设置两个列,其中 Name 为一个列,其它内容合并到一个列中,如下图 2 所示。

尽管我们在使用 HBase 表存放数据的时候,需要预先做好列设计。但这个设计仅仅由应用层感知,HBase 并没有存放任何的 Schema 信息来描述这个设计。也就是说,应用层需要知道为每一表 / 每一行设计了什么样的列 (KeyValue),然后在读取的时候做相应的解析。然 HBase 中并没有 Schema 信息,那么每一行中的列,也可以是任意添加的。如上图所示,绿色背景的 KeyValue 为后续增加的。

关于 Column Family,前面提到它是列的集合。每个 Column Family 里面关联了一个 MemStore,关联了多个 HFile 文件。当我们在选择是否要应用多个 Column Family 的时候,需要调研所读写应用的业务特点,有些数据可能会一起写入,有时候临时增加数据,此时可以考虑用两个 Column Family。如下图所示,假设为表设置了两个列族,而且定义每一个列族中要存放的列:{Name}->Column Family-A,{City,Phone,Gender}->Column Family-B 不同列族的数据会被存储在不同的路径中。即设置多个列族时一行数据可能存在于两个路径中。整行读取的时候,需要将两个路径中的数据合并在一起才可以获取到完整的一行记录。但如果仅仅读取 Name 一列的话,只需要读取 Column Family-A 即可。

下图所示,读写数据的简单路由机制。一开始会先去 ZooKeeper 中获取 Meta 表的路由信息,然后在 Meta 中定位每条数据关联的用户 Region 路径。下面这部分是基于 RowKey 从 Meta 表定位关联 Region 方法,通过一个反向扫描的方式进行。

下面介绍一下写入流程。客户端通过发请求到 RegionServer 端,写入的时候会先写入 WAL 日志中,其次会将数据写入 memstore 内存,当数据达到一定大小后会 flush 成一个个的 HFile 文件,当文件达到一定数量后,通过 compaction 的操作合并成更大文件,这样数据读取会更快。

Region Split。有人可能会有疑问,分裂的时候需不需搬迁数据?当一个 Region 变的过大后,会触发 Split 操作,将一个 Region 分裂成两个子 Region。Region Split 过程并不会真正将父 Region 中的 HFile 数据搬到子 Region 目录中。Split 过程仅是在子 Region 中创建了到父 Region 的 HFile 引用文件,子 Region1 中的引用文件指向原 HFile 的上部,而子 Region2 的引用文件指向原 HFile2 的下部。数据的真正搬迁工作是在 Compaction 过程完成的。

下面是读取流程。当进行读取时,客户端会先发送 scan 请求到 RegionServer,打开 scanner,然后调 next 请求获取一行数据,也可以将一批数据先放入 Result 的 Cache 中去, 客户端不断迭代获取内容,scan 每次获取多少行数据通常需要结合自己业务特点去获取合理的值。

关于 Scanner 的抽象。由于数据一开始会先写入 MemStore,当数据达到一定大小以后再 Flush 成底层文件,那么在读取的时候首先需要解决的问题是什么?因为数据可能存在于多个列族中,然后每个列族里又有内存里面的数据,还有些数据可能存在于多个文件中,那么应该如何读取呢?这里涉及到数据的抽象,这里将 Region 的读取会封装成 ResultScanner 对象,每个列族封装成 StoreScanner 对象,每个 StoreScanner 里面又有多个 HFile、MemStore 等。StoreScanner 包含一个 SegmentScanner 和多个 StoreFileScanner,这些 Scanner 会被组织在优先级队列里面,在 Scan 的时候一定会优先指定一个起始 Key 的值,Scanner 在打开的时候会将指针定位到指定 Key 的位置,每个 Scanner 在打开的时候会对 KeyValue 进行排序,然后放入一个优先级队列中。然后客户端每次通过 Next 请求驱动 Scan 的调用,Scanner Next 请求调用如下图所示,用 ScannerA-D 表示上面提到的各种 Scanner,当依次 Next 请求调用时,会判断哪个 Scanner 的数据是最小的。比如 ScannerA 先读,读取 KeyValue 数据,然后判断 Scanner 是否读取完毕,是否超出了 Scan 范围,假如没有读完会被再次丢回队列,重新排序,如此循环获取数据。

关于 HFile 的组织结构。HFile 中数据按 Block 组织,一个 Data Block 的默认大小为 64KB,Data Block 中直接存储了 KeyValue 信息,最底层的 Block 是 Leaf Index Block,Data Block 的索引信息存储在 Leaf Index Block 中。而 Leaf Index Block 的信息存储在 Root Index 中 (不考虑 Intermediate Index Block 情形)。从 Root Index Block 到 Leaf Index Block Leaf Index BlockLeaf Index Block Leaf Index Block Leaf Index BlockLeaf Index Block 再到 Data Block,以及从 Data Block 到用户数据 KeyValue 的 数据组织,正是一种典型的 B + Tree 结构。

下面我们回顾一下 RowKey 在读写流程中发挥的作用:

读写数据时通过 RowKey 路由到对应的 Region,MemStore 中的数据按 RowKey 排序,HFile 中的数据按 RowKey 排序。

RowKey 的设计直接关乎 Region 的划分,我们如何划分 Region?

首先通过分析业务读写吞吐量以及总的数据量信息,设定合理的 Region 数量目标,接下来预先定义 RowKey 的结构以及数据分布特点划分 RowKey 区间,然后按照设定的 Split 信息建表。

RowKey 查询的局限性。根据下表信息,基于 Name+Phone+ID 构建 RowKey。如果提供的查询条件能够尽可能丰富的描述 RowKey 的前缀信息,则查询时延越能得到保障。如下面几种组合条件场景:Name+Phone+ID、Name+Phone、Name。如果查询条件不能提供 Name 信息,则 RowKey 的前缀条件是无法确定的,此时只能通过全表扫描的方式来查找结果。一种业务模型的用户数据 RowKey,只能采用单一结构设计。但事实上,查询场景可能是多维度的。例如在上面的场景基础上,还需要单独基于 Phone 列进行查询。这是 HBase 二级索引出现的背景。即二级索引是为了让 HBase 能够提供更多维度的查询能力。

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

常见的两种二级索引。方案一全局索引(下右图)、方案二本地索引(下左图)。方案一优点:数据按索引字段全部排序,基于索引字段的小批次查询性能高。能够支持更大的查询并发数。方法的复杂度较低。缺点是:生成索引数据对实时写入的影响较大。方案二优点:每一个用户 Region 都拥有独立的索引数据,目前最佳的实践是将这部分索引数据存放于一个独立的列族中。方案缺点:按索引字段进行查询时,需要访问所有的索引 Region,随着数据量和 Region 数目的不断增多,查询时延无保障,查询所支持的并发数也会降低。

二、合理的需求调研

这部分主要介绍一下在设计 RowKey 之前如何合理地调研,RowKey 设计的目标是将数据合理地分配到每一个 Region 中,从而很好地满足业务的读写需求。索引设计目标是为 HBase 提供更多维度的查询能力,在实际应用中应该通过构建尽量少的索引,来满足更多的查询场景。

需求调研的关键维度有:负载特点、查询场景、数据特点

负载特点指的是读写 TPS 大小以及读写比重,数据负载均衡与高校读取时常是矛盾的,在重度轻写的大数据场景中,RowKey 设计应该更侧重于如何高校读取,而在重写轻读的大数据场景中,在满足基本查询需求的前提下,应该更关注整体的吞吐量,这就对数据的负载均衡提出了很高的要求。

查询场景指需要支持哪些查询场景?时延要求?最高频的查询场景是什么?是否有其它维度的价值查询场景?频度?是否有组合字段场景?各个字段的匹配类型?

数据特点,涉及到的问题有:

1. 查询条件字段的离散度信息?字段离散度是指字段 A 的离散度等于字段 A 的可能枚举值数目除以数据总记录条数。

2. 查询条件字段的数据分布特点?数据分布影响 RowKey 的设计,更进一步影响如何合理的划分 Region 信息。

3. 数据生命周期?因为生命周期影响到一个表的一次 Major Compaction 发生时涉及到的最大数据量。

三、RowKey 与索引设计

首先看一下影响查询性能的关键因素是什么?基于某一个索引 /RowKey 进行查询时,影响查询的关键因素在于是否将扫描的候选结果集限定在一个合理的范围内,如下图所示。知识点备注:直接影响数据扫描范围的查询条件,称之为查询驱动条件。而其它的能够起到过滤作用的查询条件,则称之为查询过滤条件。影响查询的关键因素在于如何合理的设置查询驱动条件。

RowKey 字段的选取,遵循的基本原则是唯一性,RowKey 必须能够唯一的识别一行数据。无论应用是什么样的负载特点,RowKey 字段都应该参考最高频的查询场景。数据库通常都是以如何高效的读取和消费数据为目的,而不是数据存储本身。而后,结合具体的负载特点,再对选取的 RowKey 字段值进行改造,组合字段场景下需要重点考虑字段的顺序。

避免数据热点的方法:

(1) Reversing

如果经初步设计出的 RowKey 在数据分布上不均匀,但 RowKey 尾部的数据却呈现出了良好的随机性,此时,可以考虑将 RowKey 的信息翻转,或者直接将尾部的 bytes 提前到 RowKey 的前部。

(2) Salting

Salting 的原理是在原 RowKey 的前面添加固定长度的随机 bytes,随机 bytes 能保障数据在所有 Regions 间的负载均衡。缺点:既然是随机 bytes,基于原 RowKey 查询时无法获知随机 bytes 信息是什么,也就需要去各个可能的 Regions 中去查看。可见 Salting 对于读取是利空的。

(3) Hashing

基于 RowKey 的完整或部分数据进行 Hash,而后将 Hashing 后的值完整替换原 RowKey 或部分替换 RowKey 的前缀部分。缺点是与 Reversing 类似,Hashing 也不利于 Scan,因为打乱了原 RowKey 的自然顺序。

接下来介绍一个小的知识点,分布式数据库的常见数据分片方式。两种常见的基础数据分片方式有 Hash 分片、Range 分片。原理与区别见下图,在实际应用中,两者还可以结合应用。HBase 采用了基于 RowKey 分区的方式。

二级索引 RowKey 设计常见方法:

(1) 无 Schema 模式

下图是常见设计思路,如果原数据 RowKey 中已经包含了索引列的信息,该设计容易导致数据冗余。

(2) 有 Schema 模式

当原数据 RowKey 中的列与索引列有重叠时,该设计能避免一个列在索引列中被重复存储。但该设计需要事先支持 Schema,也就是需要事先定义原数据的 RowKey 结构以及索引的结构信息。

二级索引字段的选取:

对所有的价值查询场景进行详细分析,基于确实能够缩小查询范围的一部分列来构建二级索引。即我们应该基于离散度较好的一些列来构建索引。如下图所示,字段 ID,PHONE 的离散度为 1,基于这些字段构建索引是最佳的,而字段 PROVINCE 与 GENDER,AGE 的离散度较差,不适合用来构建二级索引。

组合索引适用场景 / 构建原则:

组合索引的创建,取决于对用户查询场景的详细分析。组合索引的确可极大的优化这些字段组合时的查询场景,但却会带来相对较大的数据膨胀。在不了解用户数据特点以及用户查询场景的情形下,盲目的构建组合索引,是要坚决避免的。举例如下图,假设查询条件为 NAME=”Wang” && Phone=”1388888” 如果基于 NAME 构建索引,则依然可能需要扫描大量的数据,才可以找到一条目标记录,如下图 1 所示。如果基于 NAME+Phone 构建索引,则可以更精确地命中目标记录,如下图 2 所示。查询性能可大幅度提升。

组合索引中字段组合的顺序:

先导列的选取:

(1) 被选作先导列的列,一定是经常被用到的列;

(2) 应选择设置了 EQUALS 查询条件的列作为先导列;

(3) 先导列应该具备较好的离散度;

(4) 尽量不要重复选择其它索引的先导列作为本索引的先导列。

附加列的选取:

(1) 提供了 EQUALS 查询条件的列,应该放在前面部分;

(2) 由于列的组合顺序将会影响到数据的排序,我们也应该考虑业务场景关于排序的诉求;

(3) 几何各个列的离散度进一步分析,将这些组合之后,能否将数据限定在一个合理的范围之内?如果不能,需要结合查询场景设置更合理的组合列。

关于索引的其他建议:

1. 严格控制二级索引的数量。每一个索引所关联的数据总条数,与用户是 1:1 的。因此,在为一个用户表定义二级索引时,应该要考虑多个索引所带来的存储空间膨胀以及性能下降问题。建议在充分分析了各种查询场景的情况下,通过构尽量少索引来满足更多的查询场景。

2. Global Index。在高并发 / 小批次查询场景中更有利。查询一个 Local Index 时,需要去查看每一个 Index Region 才能获取到符合条件的完整结果集,这对高并发查询不利。但 Local Index 对于并发分析场景却是有利的。

3. 全文检索需求可考虑与 Elasticsearch/Solr 对接。Elasticsearch/Solr 是专业的索引引擎,但不适合作为数据主存系统,因此,将核心数据存放在 HBase 中,而通过 ES/Solr 构建全文索引能力,是一种常见的组合应用场景。

四、设计案例分享

关于 OpenTSDB 设计分析、JanusGraph 设计分析、GeoMesa 设计分析三个案例,将从数据模型、典型查询场景、RowKey 设计三个方面介绍。

1. OpenTSDB

OpenTSDB 数据模型如下图,一个 Time Series 可以理解成是一个数据源的一个指标按时间产生的指标数据序列,每一个指标称之为一个 Data Point。OpenTSDB 使用一个 Metric Name 以及一组 Tags 信息来唯一确定一个 Time Series。每一条记录成为 Data Point。

典型场景分析:给定 Metric Name 以及一组 Tags 信息,查询某时间范围的所有的 Data Points;给定 Metric Name 以及一组 Tags 信息,查询某时间范围的聚合结果;给定 Metric Name, 查询所有相关 Time Series 在某时间范围的统计信息。

RowKey 设计如下图,第一部分有一个 SALT 决定了数据的分片,第二部分是有关查询都会提供的 Metric ID 信息,其他包括 Timestamp 以及 tags 信息。

2. JanusGraph

JanusGraph—数据模型。主要包含两类数据:顶点 (Vertex) 和边(Edge)。顶点包含属性信息。边拥有 EdgeLabel 信息,EdgeLabel 拥有 Multiplicity 属性,用来定义任意两个顶点之间具有统一 EdgeLabel 的边数量信息,以及定义任意一个顶点的出入度信息(入边数量与出边数量)。边也可以包含属性信息,边分为单向边与无向边(双向边)。

JanusGraph—典型查询场景。简单关系查询如“A 的同事有哪些人?”;多层关系查询(扩线查询)如“A 的朋友的朋友的朋友”;关系 (多层) 查询 + 属性条件过滤如“A 的朋友的朋友中,有哪些拥有本科学历并且在深圳工作?”;基于属性的查询如“姓名为 XXX,学历为本科,居住地在深圳龙岗区,年龄在 30~40 岁左右的人?”

JanusGraph—RowKey 设计。在 JanusGraph 中,无论是用户数据还是元数据信息,都以 Vertex 形式存在,因此 JanusGraph 包含了各色各样的 Vertex 类型,这些类型在 ID Padding 部分体现出来。如:

000:Normal Vertices;

010:Partitioned Vertices;

100:Unmodifiable vertices;

101:Schema Type Vertices;

000101:User Property Key;

100101:System Property Key

3. GeoMesa

GeoMesa—数据模型。GeoMesa 是基于 HBase 构建的时空数据库,GeoMesa 的一条数据,称之为一个 SimpleFeature,SimpleFeature 中主要包含如下数据:时间信息、空间信息、其他属性。

GeoMesa—典型查询场景。查询类型有:区域 + 时间区间的时空查询、区域信息的空间查询、指定对象轨迹的时序查询、主题属性查询。例如查询某一个地理区域在某个时间范围内发生的关键事件;热力图信息,如一个任意区域的流量信息;给出 2015 年受血吸虫病影响的区域;查找最近 10 分钟进入飓风区域的汽车?查找某一个点附近所有的酒店;查找某嫌疑人在 2018 年 9 月的移动轨迹,等等。

GeoMesa—RowKey 设计。我们看一下指定对象的 RowKey 设计有以下几种:

(1) 针对 Point 的时间 + 空间三维索引(Z3)

ShardKey(1byte)+

Epoch Week(2bytes)+

Z3(x,y,t)(8bytes)+

FeatureID;

(2) 针对 Point 的空间索引 (Z2)

ShardKey(1byte)+

Z2(x,y)(2bytes)+

FeaturelD;

(3) 针对复杂空间对象 (如 Polygon) 的时间 + 空间三维索引(XZ3)

ShardKey(1byte)+

Epoch Week(2bytes)+

XZ2(minX,minY,maxX,maxY)(8bytes)+

FeatureID;

(4) 针对复杂空间对象 (如 Polygon) 的空间二维索引(XZ2)

ShardKey(1byte)+

XZ2(minX,minY,maxX,maxY)(8bytes)+

FeatureID

(5) Attribute 索引

ldxBytes(2bytes)+

ShardKey(1byte)+

AttrValue+

SplitByte(1byte)+

SecondaryIndex(Z3/XZ3/Z2/XZ2)+

FeatureID

(6) ID 索引

FeatureID

注意:Z2/Z3 的核心思想是基于 Z-Order 空间填充曲线将二维 / 三维信息映射成一维信息,在这个一维信息中能够很好的保持时空距离。而 XZ2/XZ3 的核心思想是 XZ -Order 空间填充曲线,XZ -Order 是基于 Z-Order 做的扩展,从而能够支持将 Polygon/Rectangle 等复杂空间对象映射成一维信息。

内容总结:

1. HBase 基础概念 / 读写流程回顾,探讨 RowKey 的关键作用;

2. RowKey 与索引设计前需求调研的几个关键维度;

3. RowKey 与索引设计的技巧原则;

4. 围绕 OpenTSDB/JanusGraph/GeoMesa 的数据模型与查询场景,简单探讨了它们的 RowKey 结构设计。配套 PPT 下载,请识别底部二维码关注社区公众号,后台回复1207

作者介绍:

毕杰山**,华为云 CloudTable(表格存储服务)主任工程师 **。长期聚焦于 HBase 及其它开源 NoSQL 技术,对各种分布式存储技术 (KeyValue 存储,文档存储,图存储,搜索引擎,时序 / 时空数据库等) 抱有浓厚的兴趣。

内推信息:

我们正在招聘大数据 /AI相关开发工程师,欢迎感兴趣的同学加入我们,base 深圳。简历投递:yuanchunfeng@huawei.com


更多高质资源 尽在AIQ 机器学习大数据 知乎专栏 点击关注

转载请注明 AIQ - 最专业的机器学习大数据社区  http://www.6aiq.com