数据仓库之HBase技术框架剖析

Hbase 核心架构

1.Hbase 框架基础

HBase 的数据在存放时会按照Rowkey 进行排序,所以HBase 中的数据都是按照Rowkey有序的。
HBase 的 Region 采用先横向拆分再纵向拆分的方式。
在这里插入图片描述
所谓先横向,再纵向,就是先按照行对数据进行划分,再按列对数据进行划分
在这里插入图片描述
按行分隔后,就得到了两个 Region,此时每一个 Region 就需要一个 server 对它进行服务,比如管理和读写服务,也就是 RegionServer(一个 RegionServer 会管理多个 Region)。
在这里插入图片描述
按行拆分后,开始按列拆分,所谓的按列拆分,就是按照列族进行拆分:
在这里插入图片描述
HBase 在存储数据的时候,会按照列族去存储数据,每个列族被存到不同的文件中去:
(我们可以将访问频率相近的列放入到同一个列族中
在这里插入图片描述
实际的 HDFS 存储文件内容如下图所示:(可以看出 HDFS 中的目录是以列族作为文件夹)
HFile存储格式

根据上图可知,每一条记录都会记录对应的 RowKey、ColumnFamily、Column,因此, Row Key 设计的长度建议在 16 字节以内,而 ColumnFamily、Column 的长度也应该尽可能的短小,否则会导致存储的数据量剧增,虽然这会使得可读性变差。

2.Hbase 物理存储

在这里插入图片描述
① HBase 中,Table 的所有行都按照 RowKey 的字典序排列;
② Table 在行的方向上被分割为多个 Region;
③ Region 按大小进行分割的,每个表一开始只有一个 Region ,随着数据不断插入表,
Region 不断增大,当增大到一个阀值的时候,Region 会等分为两个新的 Region 。当 table 中的行不断增多,就会有越来越多的 Region 。
在这里插入图片描述

Region 是 HBase 中分布式存储和负载均衡的最小单元。
最小单元就表示不同的 Region可以分布在不同的 HRegionServer 上,但一个 Region 是不会拆分到多个 server 上的。

  • HRegion 是 HBase 分布式存储和负载均衡的最小单元(不是存储的最小单元)
  • Region 由一个或者多个 Store 组成,每个 store 保存一个 columns family
  • 每个 Store 又由一个 memStore 和 0 至多个 StoreFile 组成
  • StoreFile 以 HFile 格式保存在 HDFS 上
    在这里插入图片描述
    在这里插入图片描述
3.Hbase 用户请求数据流程

HBase 在 0.96 之后去掉了-ROOT- Table,只剩下 Meta Table(hbase:meta),它存储了集群中所有用户 HRegion 的HregionServer位置信息。Zookeeper 的节点中(/hbase/meta-region-server)存储的直接是这个 META Table 的位置。

  • ① 从 Zookeeper(/hbase/meta-region-server)中获取 hbase:meta
    的位置(HRegionserver 的位置),缓存该位置。
  • ② 从 HRegionserver 中查询用户 Table 对应请求的 RowKey 所在的 HRegionserver,缓存该位置信息。
  • ③ 从查询到 HRegionserver 中读取 Row。
  • ④ 客户会缓存这些位置信息,只是缓存当前 RowKey 对应的 HRegion 的位置
  • ⑤ 如果下一个要查询的 RowKey 不在同一个 HRegion 中,则需要继续查询 hbase:meta 所在的 HRegion。
  • ⑥ 然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table 的信息,除非某个 HRegion 因为宕机或 Split 被移动,此时需要重新查询并更新缓存

META Table 中记录的是哪一条数据被放到哪一个Region 中去了,它的存储格式是K-V 的形式,K 是 table,key,region,这里的 key 指的是这个 Region 的第一条数据的 RowKey, 通过两个连续的 Region 的 key,就可以知道前一个 Region 的 RowKey 范围,但是由于只知道范围,你只能确定查找的 RowKey 对应的数据可能在某一个 Region 上,也可能不在, 但是如果不在这个 Region 上也不会在其他的 Region 上。

4.Hbase 数据写入流程
  • ① 当客户端发起 Put 请求时,第一步是将数据写入write-ahead 日志(WAL):

  • 发布的内容将被添加到存储在磁盘上的 WAL 文件末尾。
    WAL 用于在服务器崩溃的情况下恢复尚未保存的数据。

  • ② 数据写入 WAL 后,被写入 MemStore 中,然后放入 Put 请求确认信息(ack)返回客户端。

在这里插入图片描述

5.Hbase 数据读取流程

确定数据所在的 Region 后,开始读取指定的数据。

row 对应的 KeyValue cell 可以在多个位置:

  • ① 最近读取的 cell 在 Block Cache 中;
  • ② 最近更新的 cell 在 MemStore 中;
  • ③ Hfile 中;

因此,当读取一行数据时,系统是如何获得相应的 cell 并返回的?读取操作按照以下步骤从 BlockCache,MemStore 和 HFile 合并关键值:

  • 首先,扫描器在 BlockCache(读取缓存)中,查找 Row Cells,最近读取的 Key Values被缓存在这里,并且当需要内存时,最近最少使用的被清除。
  • 其次,扫描器在 MemStore 中查找,内存写入缓存包含最近的写入。
  • 最后,如果扫描器在 MemStore 和 BlockCache 中没有找到 row cells,然后 HBase 使用Block
    Cache 的 HFile 索引和布隆过滤器把可能存在目标 row cells 的 HFile 加载到内存中, 并在加载到内存中的HFile 中进行查找。
    在这里插入图片描述
6.HBase Compaction 过程
  • ① Minor Compaction
    HBase 会自动选择一些较小的 HFile,并将它们重写成更少且更大的 Hfiles,这个过程称为 Minor Compaction。Minor Compaction 通过将较小的文件重写为较少但较大的文件来减少存储文件的数量,执行合并排序。
    Minor 通常会把数个小的相邻的 StoreFile 合并成一个大的 StoreFile,Minor 不会删除标识为删除的数据和过期的数据,只做部分文件的合并,不做其他事情

  • ② Major Compaction
    Major compaction 将 Region 的每一个 Store 的所有的 StoreFile 合并,并重写到一个StoreFile 中,也就是对 Store 的所有数据进行重写,即每个列族对应这样的一个 StoreFile, 并在此过程中,删除已删除或过期的 Cell(TTL 设置),这样提升了读取性能。由于 Major compaction 重写了所有StoreFile 文件,因此在此过程中可能会发生大量磁盘 I/O 和网络流量, 有较大的性能消耗,这被称为写入放大

    Major compaction 执行计划可以自动运行。由于写入放大,通常计划在周末或晚上进行Major compaction,由于服务器故障或负载平衡,Major compaction 还会使任何远程数据文件成为本地服务器的本地数据文件。

  • ③ Compaction 综述
    一般情况下都是做 Minor 合并,Major 不少集群都是禁止,然后在集群负载较小时,进行手动 Major 合并。在数据立方公司,也是配置了一个datacube.hregion.majorcompaction0,这是配置 Major 的合并周期(默认为 7 天),如果配置成 0 即关闭 Major 合并。需要注意,既然 Major 合并是把所有 HFile 都合并成一个文件,可想对集群负载不可小觑。

    Minor 则只会选择数个 HFile 文件 compact 为一个 HFile,Minor 的过程一般较快,而且IO 相对较低。在日常任务时间,都会禁止 Mjaor 操作,只在空闲的时段定时执行

7.Hbase RowKey 设计

HBase 是三维有序存储的,通过 rowkey(行键),column key(column family 和 qualifier) 和
imeStamp(时间戳)这个三个维度可以对 HBase 中的数据进行快速定位。

HBase 中 rowkey 可以唯一标识一行记录,在 HBase 查询的时候,有以下几种方式:

- 通过 get 方式,指定 rowkey 获取唯一一条记录;
- 通过 scan 方式,设置 startRow 和 stopRow 参数进行范围匹配;
- 全表扫描,即直接扫描整张表中所有行记录。

(1) Rowkey 长度原则

rowkey 是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般10-100bytes,以 byte[] 形式保存,一般设计成定长。建议越短越好,不要超过 16 个字节,原因如下:

  • 1.数据的持久化文件 HFile 中是按照 KeyValue 存储的,如果 rowkey 过长,比如超过100 字节,1000w 行数据,光 rowkey 就要占用 100*1000w=10 亿个字节,将近 1G 数据,这样会极大影响 HFile 的存储效率;

  • 2.MemStore 将缓存部分数据到内存,如果 rowkey 字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率;

  • 3.目前操作系统都是 64 位系统,内存 8 字节对齐,控制在 16 个字节,8 字节的整数倍利用了操作系统的最佳特性。

(2) RowKey 散列原则

必须在设计上保证 RowKey 唯一性,RowKey 是按照字典顺序排序存储的,因此,设计 RowKey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。

(3) RowKey 唯一原则

必须在设计上保证 RowKey 唯一性,RowKey 是按照字典顺序排序存储的,因此,设计
RowKey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。

(4) 针对热点问题的 RowKey 设计原则

HBase 中的行是按照 RowKey 的字典顺序排序的,这种设计优化了 scan 操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于 scan。然而糟糕的 Region 设计是热点的源头。 热点发生在大量的 client 直接访问集群的一个或极少数个节点(访问可能是读, 写或者其他操作)。大量访问会使热点 Region 所在的单个机器超出自身承受能力,引起性能下降甚至 Region 不可用,这也会影响同一个 RegionServer 上的其他 Region,由于主机无法服务其他 Region 的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。

为了避免写热点,设计 RowKey 使得不同行在同一个 Region,但是在更多数据情况下, 数据应该被写入集群的多个 Region,而不是一个。

下面是一些常见的避免热点的方法以及它们的优缺点:

  • 1)加盐
    这里所说的加盐不是密码学中的加盐,而是在 rowkey 的前面增加随机数,具体就是给
    rowkey 分配一个随机前缀以使得它和之前的 rowkey 的开头不同。分配的前缀种类数量应该和你想使用数据分散到不同的 region 的数量一致。加盐之后的 rowkey 就会根据随机生成的前缀分散到各个 region 上,以避免热点。加随机数可以使数据均匀分布,但是不可重构, 失去 get 快速定位数据的能力。

  • 2)哈希
    哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的 rowkey,可以使用 get 操作准确获取某一个行数据。

  • 3)反转
    第三种防止热点的方法时反转固定长度或者数字格式的 rowkey。这样可以使得 rowkey 中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机 rowkey,但是牺牲了 rowkey 的有序性。
    反转 rowkey 的例子以手机号为 rowkey,可以将手机号反转后的字符串作为 rowkey,这样的就避免了以手机号那样比较固定开头导致热点问题。

  • 4)时间戳反转
    一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为 RowKey 的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到 key 的末尾(由于 Hbase 数据按照 RowKey 自然排序,timestamp 越大代表数据越新,Long.Max_Value –timestamp 就越小,就可以保证最新的数据排在 ),例如 [key][ Long.Max_Value - timestamp] ,[key] 的最新值可以通过 scan [key]获得[key]的第一条记录,因为 HBase 中 rowkey 是有序的, 第一条记录是最后录入的数据。

    比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计 RowKey 的时候, 可以这样设计:
    [userId 反转][Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候, 直接指定反转后的 userId,startRow 是[userId 反转][000000000000],stopRow 是[userId 反转][Long.Max_Value - timestamp]。
    如果需要查询某段时间的操作记录,startRow 是[user 反转][Long.Max_Value - 起始时间],stopRow 是[userId 反转][Long.Max_Value - 结束时间]。

(5) 组合 RowKey 设计原则

RowKey 为多个属性拼接而成时,将具有高标识度的、经常使用检索列的属性放在组合RowKey 的前面。

(6) 应用场景分析
  • ① 针对事务数据 RowKey 设计

事务数据是带时间属性的,建议将时间信息存入到 Rowkey 中,这有助于提示查询检索速度。对于事务数据建议缺省就按天为数据建表,这样设计的好处是多方面的。按天分表后, 时间信息就可以去掉日期部分只保留小时分钟毫秒,这样 4 个字节即可搞定。加上散列字段2 个字节一共 6 个字节即可组成唯一 Rowkey。如下图所示:
事务数据 RowKey 设计
这样的设计从操作系统内存管理层面无法节省开销,因为 64 位操作系统是必须8 字节对齐,但是对于持久化存储中 Rowkey 部分可以节省 25%的开销。也许有人要问为什么不将时间字段以主机字节序保存,这样它也可以作为散列字段了。这是因为时间范围内的数据还是尽量保证连续,相同时间范围内的数据查找的概率很大,对查询检索有好的效果,因此使用独立的散列字段效果更好,对于某些应用,我们可以考虑利用散列字段全部或者部分来存储某些数据的字段信息,只要保证相同散列值在同一时间(毫秒)唯一。

  • ② 针对统计数据的 RowKey 设计

统计数据也是带时间属性的,统计数据最小单位只会到分钟(到秒预统计就没意义了)。同时对于统计数据缺省我们也采用按天数据分表,这样设计的好处无需多说。按天分表后, 时间信息只需要保留小时分钟,那么 0~1400 只需占用两个字节即可保存时间信息。由于统计数据某些维度数量非常庞大,因此需要 4 个字节作为序列字段,因此将散列字段同时作为序列字段使用也是 6 个字节组成唯一 Rowkey。如下图所示:
统计数据 RowKey 设计

  • ③ 针对通用数据的 RowKey 设计

通用数据采用自增序列作为唯一主键,用户可以选择按天建分表也可以选择单表模式。这种模式需要确保同时多个入库加载模块运行时散列字段(序列字段)的唯一性。可以考虑给不同的加载模块赋予唯一因子区别。设计结构如下图所示。
通用数据 RowKey 设计

8.HBase列族分析

在大多数的工厂环境下,往往只会设计一个列族,因为列族数量过多会导致如下的性能问题:

  • 1.Flush 会产生大量 IO
    Flush 的最小单元是 region,也就是说一个 region 中的某个列族做 Flush 操作,其他的列族也会 Flush,对每个列族而言,每次 Flush 都会产生一个文件,频繁 Flush 必然会产生更多的 StoreFile,StoreFile 数量增多又会产生更多的 Compact 操作,Flush 和 Compact 都是很重的 IO 操作。

  • 2.Split 操作可能会导致数据访问性能低下
    Split 的最小单元是 region, 如果这个 region 有两个列族 A、B,列族 A 有 100 亿条
    录,列族 B 有 100 条记录,如果最终 Split 成 20 个 region, 那么列族 B 的 100 条记录会分
    布到 20 个 region 上, 扫描列族 B 的性能低下。

因此,在设计列族时,过多的列族会导致很多性能问题,列族设计最重要的一点就是减少列族数量

9.Hive on HBase

HBase 用于在线业务服务,不适合做统计分析。(使用 HBase 进行查询的条件比较苛刻,只 能根据 RowKey 去进行查询)
Hive 用于离线分析,适合数据分析,统计。

因此,我们一般使用 Hive on HBase 完成数据的加载,也就是关系型数据库、Hive 数据库或者文件中的数据向 HBase 的导入。我们一般会创建一张映射表,然后往映射表里灌入数据,后台就会帮我们把数据灌入 HBase 中。

10.HBase二级索引

目前 HBase 主要应用在结构化和半结构化的大数据存储上,其在插入和读取上都具有极高的性能表现,这与它的数据组织方式有着密切的关系,在逻辑上,HBase 的表数据按RowKey 进行字典排序, RowKey 实际上是数据表的一级索引(Primary Index),由于HBase 本身没有二级索引(Secondary Index)机制,基于索引检索数据只能单纯地依靠RowKey,为了能支持多条件查询,开发者需要将所有可能作为查询条件的字段一一拼接到RowKey 中,这是 HBase 开发中极为常见的做法,但是无论怎样设计,单一 RowKey 固有的局限性决定了它不可能有效地支持多条件查询。

通常来说,RowKey 只能针对条件中含有其首字段的查询给予令人满意的性能支持,在查询其他字段时,表现就差强人意了,在极端情况下某些字段的查询性能可能会退化为全表扫描的水平,这是因为字段在 RowKey 中的地位是不等价的,它们在 RowKey 中的排位决定了它们被检索时的性能表现,排序越靠前的字段在查询中越具有优势,特别是首位字段具有特别的先发优势,如果查询中包含首位字段,检索时就可以通过首位字段的值确定RowKey的前缀部分,从而大幅度地收窄检索区间,如果不包含则只能在全体数据的RowKey 上逐一查找,由此可以想见两者在性能上的差距。

受限于单一 RowKey 在复杂查询上的局限性,基于二级索引(Secondary Index)的解决方案成为最受关注的研究方向,并且开源社区已经在这方面已经取得了一定的成果,像 ITHBase、IHBase 以及华为的 hindex 项目,这些产品和框架都按照自己的方式实现了二级索引,各自具有不同的优势,同时也都有一定局限性。

关系型数据库中索引数据与原始数据之间的数据一致性是通过关系型数据库中的组件负责实现的,对于数据库数据的插入和删除都会在索引中展现出来,对于原始数据和索引的操作会是一个原子/事务操作,但是在 HBase 中没有框架自身提供的机制,只能靠开发人员自己去实现。

由于在 HBase 中的二级索引是通过建表的方式实现的,当需要更新时,就是两个表的数据原子更新,也就是跨表的事务功能,而 HBase 只提供行级事务,没有跨表和跨行的事务功能,这就需要开发者自己去实现,如果对数据一致性要求较高,那么就可能需要自己去实现一套分布式的事务机制,之所以是分布式的事务机制,是因为原始数据可能由一些HRegionserver 维护,而索引表由另外一些 HRegionserver 维护,这个事务机制就涉及到了多个 HRegionserver,也就是分布式的事务机制。因此,二级索引是 HBase 自身存在的一个短板

  • (1)二级索引设计
    二级索引的本质就是建立各列值与行键之间的映射关系,以列的值为键,以记录的RowKey 为值
    二级索引的建立
    其查询步骤如下:

1.根据 C1=C11 到索引数据中查找其对应的 RK,查询得到其对应的 RK=RK1;
2.得到 RK1 后就自然能根据 RK1 来查询 C2 的值了这是构建二级索引大概思路,其他组合查询的联合索引的建立也类似。

★★★

  • (2) 二级索引设计剖析
    ① “二级多列索引”是针对目标记录的某个或某些列建立的“键-值”数据,以列的值为键,以记录RowKey 为值,当以这些列为条件进行查询时,引擎可以通过检索相应的“键-值”数据快速找到目标记录。由于 HBase 本身并没有索引机制,为了确保非侵入性,引擎将索引视为普通数据存放在数据表中,所以如何解决索引与主数据的划分存储是引擎第一个需要处理的问题。

    ② 为了能获得最佳的性能表现,我们并没有将主数据和索引分表储存,而是将它们存放在了同一张表里,通过给索引和主数据的 RowKey 添加特别设计的 Hash 前缀,实现了在Region 切分时,索引能够跟随其主数据划归到同一 Region 上,即任意 Region 上的主数据其索引也必定驻留在同一 Region 上,这样我们就能把从索引抓取目标主数据的性能损失降低到最小

    ③ 与此同时,特别设计的 Hash 前缀还在逻辑上把索引与主数据进行了自动的分离,当全体数据按 RowKey 排序时,排在前面的都是索引,我们称之为索引区,排在后面的均为主数据,我们称之为主数据区。最后,通过给索引和主数据分配不同的 Column Family,又在物理存储上把它们隔离了起来。逻辑和物理上的双重隔离避免了将两类数据存放在同一张表里带来的副作用,防止了它们之间的相互干扰,降低了数据维护的复杂性,可以说这是在性能和可维护性上达到的最佳平衡。
    Hbase表格设计
    让我们通过一个示例来详细了解一下二级多列索引表的结构:
    ( a ). 假定有一张 Sample 表,使用四位数字构成 Hash 前缀,范围从 0000 到 9999,规划切分100 个 Region,则 100 个 Region 的 RowKey 区间分别为[0000,0099],[0100,0199],……[9900,9999]。以第一个 Region 为例,请看上图,所有数据按 RowKey 进行字典排序,自动分成了索引区和主数据区两段,主数据区的 Column Family 是 d,下辖 q1,q2,q3 等 Qualifier,为了简单起见,我们假定 q1,q2,q3 的值都是由两位数字组成的字符串,索引区的 Column Family 是 i,它不含任何 Qualifier,这是一个典型的“Dummy Column Family“,作为区别于 d 的另一个 Column Family,它的作用就是让索引独立于主数据单独存储

    ( b ). 接下来是最重要的部分,即索引和主数据的 RowKey,我们先看主数据的 RowKey,它由四位 Hash 前缀和原始 ID 两部分组成,其中 Hash 前缀是由引擎分配的一个范围在 0000 到9999 之间的随机值,通过这个随机的Hash 前缀可以让主数据均匀地散列到所有的Region 上,我们看上图,因为 Region 1 的 RowKey 区间是[0000,0099],所以没有任何例外,凡是且必须是前缀从 0000 到 0099 的主数据都被分配到了 Region 1 上。

    ( c ).接下来看索引的 RowKey,它的结构要相对复杂一些,格式为:RegionStartKey-索引名-索引键-索引值,与主数据不同,索引 RowKey 的前缀部分虽然也是由四位数字组成,但却不是随机分配的,而是固定为当前 Region 的 StartKey,这是非常重要而巧妙的设计,一方面,这个值处在 Region 的 RowKey 区间之内,它确保了索引必定跟随其主数据被划分到同一个 Region 里;另一方面,这个值是 RowKey 区间内的最小值,这保证了在同一 Region 里所有索引会集中排在主数据之前。接下来的部分是“索引名”,这是引擎给每类索引添加的一个标识,用于区分不同类型的索引,上图展示了两种索引:a 和 b,索引 a 是为字段 q1 和 q2 设计的两列联合索引,索引 b 是为字段 q2 和 q3 设计的两列联合索引,依次类推,我们可以根据需要设计任意多列的联合索引。再接下来就是索引的键和值了,索引键是由目标记录各对应字段的值组成,而索引值就是这条记录的 RowKey。

    ( Test ) :

    现在,假定需要查询满足条件 q1=01 and q2=02 的 Sample 记录,分析查询字段和索引匹配情况可知应使用索引a,也就是说我们首先确定了索引名,于是在 Region 1 上进行 scan 的区间将从主数据全集收窄至[0000-a,0000-b),接着拼接查询字段的值,我们得到了索引键:0102,scan 区间又进一步收窄为[0000-a-0102, 0000-a-0103),于是我们可以很快地找到 0000-a-0102-0000|25zh78f9 这条索引,进而得到了索引值,也就是目标数据的 RowKey: 0000|25zh78f9,通过在 Region 内执行 Get操作,最终得到了目标数据。需要特别说明的是这个 Get 操作是在本 Region 上执行的,这和通过 HTable 发出的 Get 有很大的不同,它专门用于获取Region 的本地数据,其执行效率是非常高的,这也是为什么我们一定要将索引和它的主数据放在同一张表的同一个 Region 上的原因。

★★★

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值