您的位置:首页 > 汽车 > 时评 > HBase 进阶

HBase 进阶

2024/9/8 12:21:29 来源:https://blog.csdn.net/m0_64261982/article/details/140538785  浏览:    关键词:HBase 进阶

前言

1、HBase 进阶

1.1、Master 架构

        HBase 的主要进程,具体实现类为 HMaster,通常部署在 NameNode(它俩都是管理元数据的);

        RegionServer 并不能够直接和 Master 直接通信,因为 Master 必须和 RegionServer 满足分布一致性,而让 HBase 自己来实现就比较复杂了,所以 HBase 的分布一致性是由 Zookeeper 来保证的:每个 RegionServer 把自己的信息注册到 Zookeeper 上;同理,Master 也会注册一些信息到 Zookeeper 中;

        Master 除了和 zk 交互之外,还会与 HDFS 进行交互,因为 Hbase 的元数据存储在 HDFS 的 "/hbase/data/hbase/meta" 路径下,该路径下存储着 HBase 的元数据信息(每个 RegionServer 存储着那些 Region 信息);Master 的负载均衡器组件会通过读取 meta 表来了解 Region 的一个分布情况,通过 zk 了解 RegionServer 的启动情况。每五分钟进行一次分配平衡;

        Master 的组件元数据表管理器(相当于 HMaster 进程中的一个线程),它会定期整理元数据表里的数据(比如哪些版本过期了就清理掉);

        WAL 预写日志管理器,比如当 Master 要创建一张表的时候,会走一个比较复杂的流程,为了防止因为节点故障造成任务失败丢失,它会把这个任务先写入到日志当中才会开始执行(一般预写日志存储在 hdfs 的 "/hbase/MasterData/WALs",为了避免产生小文件问题,一般每小时写入一次或者达到32M写入一次);

1.1.1、Meta 表

全称 hbase:meta,只是在 list 命令中被过滤掉了,本质上和 HBase 的其他表格一样,也是每个 rowkey 标识一行,一行存在多列;

 meta 表的 rowKey:
 meta 表的列:
  • info:regioninfo 为 region 信息,存储一个 HRegionInfo 对象。
  • info:server 当前 region 所处的 RegionServer 信息,包含端口号。
  • info:serverstartcode 当前 region 被分到 RegionServer 的起始时间。

如果一个表处于切分的过程中,即 region 切分,还会多出两列 info:splitA 和 info:splitB, 存储值也是 HRegionInfo 对象,拆分结束后,删除这两列。

注意:在客户端对元数据进行操作的时候才会连接 master(Admin 连接的是 Master,Table 连接的是 RegionServer),如果对元数据进行读写,直接连接 zookeeper 读取目录/hbase/meta-region-server 节点信息,会记录 meta 表格的位置。直接读取即可,不需要访问 master,这样可以减轻 master 的压力,相当于 master 专注 meta 表的 写操作,客户端可直接读取 meta 表。

         在 HBase 的 2.3 版本更新了一种新模式:Master Registry。客户端可以访问 master 来读取 meta 表信息。加大了 master 的压力,减轻了 zookeeper 的压力。

1.2、RegionServer 架构

        RegionServer 同样会有一个 WAL  的组件,一方面是为了防止节点挂了导致数据丢失,第二方面是为了使文件有序(rowKey),毕竟一次 put 命令只能写入一个单元格;

        RegionServer 还有一个读缓存的组件,为的是加速读取(把读取过的数据写进缓存,方便下次读取),数量只有一个;此外,还有一个写缓存的组件,每个 store 都会有一个对应的写缓存,同样为的是写入有序;

除了主要的组件之外,RegionServer 还会启动多个线程去监控一些必要的服务:

  • Region 拆分:当一个 Region 体积过大,RegionServer 需要把它拆分成两个 Region
  • Region 合并:一些 Region 中的数据可能比较少(由于部分数据删除等原因),需要进行合并
  • MemStore 刷写:写缓存,由于 HFile 中的数据要求是有序的,所以数据是先存储在 MemStore 中,排好序后,等到达刷写时机才会刷写到 HFile,每次刷写都会形成一个新的 HFile,写入到对应的 文件夹 store 中。
  • WAL 预写日志滚动(和 Master 差不多)因为数据要经 MemStore 排序后才能刷写到 HFile,但把数据保存在内存中会有很高的 概率导致数据丢失,为了解决这个问题,数据会先写到 WAL 文件中,然后再写入 MemStore 中。所以在系统出现故障的时候,数据可以通过这个日志文件重建。

1.2.1、写流程

HBase 的写流程从客户端创建连接开始,刷写到 HDFS 结束;

1、创建连接
  • 客户端向 zk 发送请求创建连接(连接hbase就得连接zk)
  • 向 zk 询问 meta 表的存储位置(zk 只存储 meta 表的位置,不存储 meta 表的数据,毕竟这个表是个 hbase 类型的表)
  • 访问 meta 表
  • 将 meta 表中的数据缓存到连接中(重量级操作,如果元数据发生变化,还需要再次更新元数据)
2、写入数据
  • 发送 put 命令给目标 RegionServer(通过 rowKey 并对照 meta 表,得知写入到哪个 RegionServer)
  • 将写请求持久化到 WAL 日志中(保存在 hdfs 的 /hbase/MasterData/WALs,防止丢失)
  • 将写请求写入到对应 MemStore (根据 rowKey 和 columnFamily 确定写入的 Store,从而确定写入到哪个写缓存)
  • 等待触发写缓存刷写,写入到对应的 Store(只能保证每次刷写的文件是有序的,store 下的多个文件之间并不有序)
3、MemStore Flush

MemStore 刷写由多个线程控制,条件互相独立:

主要的刷写规则是控制刷写文件的大小,在每一个刷写线程中都会进行监控

(1)当某个 memstroe 的大小达到了 hbase.hregion.memstore.flush.size(默认值 128M, 其所在 region 的所有 memstore 都会刷写(一个 region 包含多个 store,一个 store 对应一个 memstore),因为不同的 store 存储着不同的列,而一般一行数据是均匀分布的(每列基本都有数据),所以即使刷写多个 memstore 也不用太担心小文件问题。memstore 内的数据都差不多大

当 memstore 的大小达到了 hbase.hregion.memstore.flush.size(默认值 128M) * hbase.hregion.memstore.block.multiplier(默认值 4) 时,会刷写同时阻止继续往该 memstore 写数据(由于线程监控是周期性的,所以有可能面对数据洪峰,尽管可能性比较小;返防止内存溢出,数据丢失)

(2)由 HRegionServer 中的属性 MemStoreFlusher 内部线程 FlushHandler 控制。标准为 LOWER_MARK(低水位线)和 HIGH_MARK(高水位线),意义在于避免写缓存使用过多的内 存造成 OOM 当 region server 中 memstore 的总大小达到低水位线 java_heapsize * hbase.regionserver.global.memstore.size(默认值 0.4)*hbase.regionserver.global.memstore.size.lower.limit(默认值 0.95)

        region 会按照其所有 memstore 的大小顺序(由大到小)依次进行刷写。直到 region server 中所有 memstore 的总大小减小到上述值以下。 当 region server 中 memstore 的总大小达到高水位线 java_heapsize *hbase.regionserver.global.memstore.size(默认值 0.4) 时,会同时阻止继续往所有的 memstore 写数据。

(3)为了避免数据过长时间处于内存之中,到达自动刷写的时间,也会触发 memstore flush。由 HRegionServer 的属性 PeriodicMemStoreFlusher 控制进行,由于重要性比较低,5min才会执行一次。 自动刷新的时间间隔由该属性进行配置 hbase.regionserver.optionalcacheflushinterval(默认 1 小时)

(4)当 WAL 文件的数量超过 hbase.regionserver.max.logs,region 会按照时间顺序依次 进行刷写,直到 WAL 文件数量减小到 hbase.regionserver.max.logs 以下(该属性名已经废弃, 现无需手动设置,最大值为 32)

1.2.2、读流程

1、HFile 结构

        HFile 是存储在 HDFS 上面每一个 store 文件夹下实际存储数据的文件。

        里面存储多种内 容。包括数据本身(kv 键值对)、元数据记录(记录当前存储的数据在哪个表,哪个 Region)、文件信息、数据索引(数据的索引)、元数据索引和 一个固定长度的尾部信息用于优化读取性能,记录文件的版本)。

        键值对按照块大小(HFile 的块大小,默认 64K)保存在文件中,数据索引按照块创建,块越多,索引越大。每一个 HFile 还会维护一个布隆过滤器(就像是一个很大的地图,文件中每有一种 key, 就在对应的位置标记,读取时可以大致判断(哈希判断,存在哈希碰撞,所以说是大致判断)要 get 的 key 是否存在 HFile 中)。

KeyValue 内容如下:

  • rowlength -----------→ key 的长度
  • row -----------------→ key 的值
  • columnfamilylength --→ 列族长度
  • columnfamily --------→ 列族
  • columnqualifier -----→ 列名
  • timestamp -----------→ 时间戳(默认系统时间)
  • keytype -------------→ Put

        由于 HFile 存储经过序列化,所以无法直接查看。可以通过 HBase 提供的命令来查看存 储在 HDFS 上面的 HFile 元数据内容。

bin/hbase hfile -m -f /hbase/data/命名空间/表名/regionID/列族/HFile 名

2、读流程

1)创建连接
  • 客户端向 zk 发送请求创建连接(连接hbase就得连接zk)
  • 向 zk 询问 meta 表的存储位置(zk 只存储 meta 表的位置,不存储 meta 表的数据,毕竟这个表是个 hbase 类型的表)
  • 访问 meta 表
  • 将 meta 表中的数据缓存到连接中(重量级操作,如果元数据发生变化,还需要再次更新元数据)
2)读取数据

读取数据一共要去读三个地方:读缓存、写缓存、store 文件(即使从读缓存中读到了,也还会去读写缓存磁盘(即使读取写缓存也不能保证数据是最新的,因为数据的 timestamp 字段是可以被指定的),因为不能保证数据是最新版本的);

  • 发送 get 请求(通过 rowKey 并对照 meta 表,得知从哪个 RegionServer 读取)
  • 将读请求持久化到 WAL (hdfs 的 WAL 目录)
  • 先从读缓存读取,读缓存中存储的是元数据(索引文件,布隆过滤器和key值),其余内存按照 64K 缓存原始数据(kv键值对),清理读缓存时(根据活跃度),清理的是原始数据而不是元数据(版本可能发生变化,所以并不一定正确)
  • 读取写缓存,因为可能读取的是刚刚写进来的数据,还在 memstore 没有来得及落盘的,
  • 读取 store 下的 hfile 文件(非常慢,所以通过元数据中的索引、布隆过滤器(布隆过滤器可以简单判断文件里有没有我们要找的那个 rowKey)进行优化),读取到之后还会将读取到的数据缓存到读缓存
  • 合并所有数据返回(高版本覆盖低版本)
3、合并读取数据优化

        每次读取数据都需要读取三个位置,最后进行版本的合并(即使是读写缓存中的数据也不能保证数据是最新的,因为 timestamp 是可以被手动指定的;所以不管什么时候都是读取三个文件)。效率会非常低,所有系统需 要对此优化(优化的主要是读缓存和读磁盘文件的优化)。

(1)HFile 带有索引文件,读取对应 RowKey 数据会比较快。

(2)Block Cache 会缓存之前读取的内容和元数据信息,如果 HFile 没有发生变化(记录在 HFile 尾信息中),则不需要再次读取。

(3)使用布隆过滤器能够快速过滤当前 HFile 不存在需要读取的 RowKey,从而避免读取文件。(布隆过滤器使用 HASH 算法,不是绝对准确的,出错会造成多扫描一个文件,对读取数据结果没有影响)

  • 布隆过滤器很长(常见的布隆过滤器基本上是 10 亿个长度,但是它的一个长度才占用 1 bit,大约 100 多MB;HBase 的布隆过滤器要小一点,大约千百万个长度)

1.2.3、StoreFile Compaction(HFile 文件的合并)

        由于 memstore 每次刷写都会生成一个新的 HFile(就像我们 MapReduce 的 map spill 阶段),文件过多读取不方便,所以会进行文 件的合并,清理掉过期和删除的数据,会进行 StoreFile Compaction。

         Compaction 分为两种,分别是 Minor Compaction(小合并)Major Compaction(大合并)。Minor Compaction 会将临近的若干个较小的 HFile 合并成一个较大的 HFile,并清理掉部分过期和删除的数据, 有系统使用一组参数自动控制,Major Compaction 会将一个 Store 下的所有的 HFile 合并成 一个大 HFile,并且会清理掉所有过期和删除的数据,由参数 hbase.hregion.majorcompaction 控制,默认 7 天。

  • 小合并把相邻的文件进行合并,即使是 deleteAll 也不会把前面老的数据删除掉,因为现在是小合并,只有在大合并才能波及到所有数据(就像 MapReduce 的 Conbiner)
  • 大合并会合并所有的文件,遇到 deleteAll 会直接把前面老的数据都删除
小文件合并机制

参与到小合并的文件需要通过参数计算得到,有效的参数有 5 个

  1. hbase.hstore.compaction.ratio(默认 1.2F)合并文件选择算法中使用的比率。
  2. hbase.hstore.compaction.min(默认 3) 为 Minor Compaction 的最少文件个数。
  3. hbase.hstore.compaction.max(默认 10) 为 Minor Compaction 最大文件个数。
  4. hbase.hstore.compaction.min.size(默认 128M)为单个 Hfile 文件大小最小值,小于这个数会被合并。
  5. hbase.hstore.compaction.max.size(默认 Long.MAX_VALUE)为单个 Hfile 文件大小最大 值,高于这个数不会被合并(永远不会触发)。

小合并机制为拉取当前 store 中所有的文件,做成一个集合。之后按照从旧到新的顺序遍历。 判断条件为:

  • ① 过小合并,过大不合并(也就是小于 128MB 才合并,最大值因为默认是 Long.MAX_VALUE,所以永远不会触发,除非我们自己设置)
  • 文件大小 / hbase.hstore.compaction.ratio < (剩余文件大小和,当前选中的文件之外的其它文件) 则参与压缩。所以把比值设置过大,如 10 会最终合并为 1 个特别大的文件,相反设置为 0.4,会最终产生 4个 storeFile。 不建议修改默认值(1.2F 是大神调校过的)
  • ③ 满足压缩条件的文件个数达不到个数要求(3 <= count <= 10)则不压缩。

1.2.4、Region 自定义切分

        Region 切分分为两种,创建表格时候的预分区即自定义分区,同时系统默认还会启动一 个切分规则,避免单个 Region 中的数据量太大(经过刷写合并之后,所有文件不可避免会成为一个特别大的文件,如果不切分还会越来越大)。

        每一个 Region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 Region 维护的 rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。

手动设定预分区 
# 创建表格时指定 region 的切分规则
create 'staff1','info', SPLITS => ['1000','2000','3000','4000']

这样,当 rowKey < '1000' 的就会放到第一个分区,然后 [1000,2000),[2000,3000)... 以此类推 

生成 16 进制序列预分区(不太好用)
create 'staff2','info',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}
按照文件中设置的规则预分区

其实和第一种手动设定预分区是一样的,无非就是当分区特多的时候命令行写得难受,我们把它写到文件里:

# split.txt
1000
2000
3000
4000

然后执行:

create 'staff3', 'info',SPLITS_FILE => 'splits.txt'
使用 Java API 创建预分区
public class HBaseConnect { public static void main(String[] args) throws IOException { // 1.获取配置类 Configuration conf = HBaseConfiguration.create(); // 2.给配置类添加配置 conf.set("hbase.zookeeper.quorum","hadoop102,hadoop103,hadoop104"
); // 3.获取连接 Connection connection = 
ConnectionFactory.createConnection(conf); // 3.获取 admin Admin admin = connection.getAdmin(); // 5.获取 descriptor 的 builder TableDescriptorBuilder builder = 
TableDescriptorBuilder.newBuilder(TableName.valueOf("bigdata", 
"staff4")); // 6. 添加列族 builder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(
Bytes.toBytes("info")).build()); // 7.创建对应的切分 byte[][] splits = new byte[3][]; splits[0] = Bytes.toBytes("aaa"); splits[1] = Bytes.toBytes("bbb"); splits[2] = Bytes.toBytes("ccc"); // 8.创建表 admin.createTable(builder.build(),splits); // 9.关闭资源 admin.close(); connection.close(); } 
}

1.2.5、Region 系统切分

        Region 的拆分是由 HRegionServer 完成的,在操作之前需要通过 ZK 汇报 master,修改 对应的 Meta 表信息添加两列 info:splitA 和 info:splitB 信息。之后需要操作 HDFS 上面对 应的文件,按照拆分后的 Region 范围(rowKey 的范围)进行标记区分,实际操作为创建文件引用,不会挪动数据。刚完成拆分的时候,两个 Region 都由原先的 RegionServer 管理。之后汇报给 Master, 由Master将修改后的信息写入到Meta表中。等待下一次触发负载均衡机制,才会修改Region 的管理服务者,而数据要等到下一次合并时,才会实际进行移动

         不管是否使用预分区,系统都会默认启动一套 Region 拆分规则。不同版本的拆分规则 有差别。系统拆分策略的父类为 RegionSplitPolicy。

  • Hbase 2.0 引入了新的 split 策略:如果当前 RegionServer 上该表只有一个 Region, 按照 2 * hbase.hregion.memstore.flush.size (也就是 2*128=256MB)拆分,否则按照 hbase.hregion.max.filesize (10G) 分裂。 

也就是当第一次达到 256 MB 的时候一分为二,形成两个 Region,之后达到 10 G 才去切分;

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com