一文弄懂Hbase

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一文弄懂Hbase

1. 为什么有了 HDFS 之后还需要 HBase 呢

  1. hdfs 设计主要是针对什么呢针对的是大数据超大文件比如说你有一个超大文件里面要放 100GB 的用户行为的日志甚至是 1TB甚至 1PB针对一个网站的每天的用户行为的日志可以都放一个超大文件里去,超大文件很难说放在一台服务器上所以说此时可以把超大文件拆散拆成 N 多个 128MB 的小文件每一个小文件就可以说是这个大文件的一个 block
  2. hdfs 解决的主要是一个分布式存储的问题也就是说你有超大数据集不可能都放在一个文件里是不现实的所以可以拆分为 N 多个 128MB 的 block 小文件分散存储在多台机器上对超大数据集实现分布式存储的效果,每个 block 小文件还有 3 副本冗余存储每个副本在不同的机器上高可用和高容错任何一台机器挂掉不会导致数据丢失的
  3. hdfshadoop distributed filesystem分布式文件系统他存放的是文件文件死的静态的最多只能是你不停的往文件的末尾追加数据他会把你追加到文件末尾的数据其实都是分散在不同的小 block 里存储在机器上目录层级结构创建文件管理权限对文件进行删除大概就是这样的一些事情了对大文件里的数据进行读取对文件进行数据追加hdfs 只能做到如上一些事情针对你 hdfs 上存储的海量数据10TB 的数据我要进行增删改查我要往里面插入数据还要修改数据还有删除里面某一行数据还有精准的查询里面某一行数据得了hdfs 上是大量的 block 小文件虽然说帮你把超大数据集给分布式存储了
  4. 所以 hbase 出马了由 hbase 基于 hdfs 进行超大数据集的分布式存储让海量数据分布式存储在 hdfs 上但是对 hdfs 里的海量数据进行精准的某一行或者某几行的数据的增删改查由 hbase 来解决了

2.HBase 作为一个 NoSQL 数据库有哪些架构上的特点

  1. 分布式架构
    hbase 定位是分布式 nosql 数据库把自己的 nosql 数据库的功能是通过多台机器来实现的有多个 RegionServer分布式管理数据分布式执行你的各种 nosql 数据库的操作
  2. 分布式数据存储和自动数据分片
    这个功能是极为强大的比如你搞一个 hbase 里的表然后在表里搞很多很多的数据这个表会分为很多的 region每个 region 里是一个数据分片然后这些 region 数据分片就会分散在多台机器上假设你的表里的数据太多了此时 region 会自动进行分裂分裂成更多的 region自动分散在更多的机器上对我们使用是极为方便的
  3. 集成 hdfs 作为分布式文件存储系统
  4. 强一致读写
    他不是 zk 那种最终一致性是强一致的你写成功了立马就可以读。这个功能是极为实用的他是依靠的分布式存储才做到的zk 那种是属于主从同步你读 follower 机器是可能读到不一致数据的
  5. 高可用
    每台机器上部署一个 RegionServer管理一大堆的 region 数据分片RegionServer 都是支持高可用的一个 RegionServer 挂掉不会导致数据丢失他自动可以由别的机器接管他的工作运行下去
  6. 支持 mapreduce/spark 这种分布式计算引擎
    对 hbase 里的数据进行分布式计算可以从 hbase 里分布式抽数据去计算也可以把计算后的结果写入 hbase 分布式存储
  7. Java API/thrift API/REST API 的支持
    当然支持 Java API 了咱们的 Java 业务系统经常会有海量数据 NoSQL 存储的需求此时就可以基于 Java API 来操作 hbase 里的数据了
  8. 支持协处理器块缓存和布隆过滤器可以用于优化查询性能
  9. hbase 现在最新版本都是支持 web 界面的方式来对 hbase 集群进行运维管理的

3.HBase 作为 NoSQL 数据库到底适用于哪些场景

  1. 海量数据场景
    表来形容单表在千万以内级别的数据量基本都是小数据千万级别的数据量最多只能说是中等数据量MySQL 搞一下分库分表搞个两三台服务器就可以轻松抗住千万级别的数据量的表了每个表可能也就几万条数据了,基于分库分表的中间件mycat、sharding-sphere都可以的直接做一些路由什么的就可以轻松搞定几千万级别的数据了性能也是很高的,假设几千万条数据是过去历史几年下来积累的每年积累一千万数据每个月也就 100 万数据左右每一天几万数据量10 年才 1 亿数据MySQL 分库分表的技术方案抗下小亿级别的数据量都是 ok 的一两亿数据,可能你作为这个系统的负责人在可见的范围内基本上单表撑死也就几千万到一两亿级别10 年、20 年以后了不用考虑这么多了其实像这种级别的存量和增量的数据量用 MySQL 分库分表就可以轻松搞定了,要做一些跨库跨表的 SQL不太好做可以自己查询一些数据放到内存来做定制计算也是可以的
    什么叫做海量数据说不好听的假设你就几百万数据用 MySQL 就可以轻松高兴了要是你有几千万数据呢基本到 MySQL 的瓶颈和极限了。要是你有几亿条数据呢而且每天数据量还在不停的增长呢
    这个时候你就可以使用 hbase 了他天生就是分布式的可以扩容很方便数据分布式存储自动数据分片完善的运维管理底层集成 hdfs 做分布式文件系统增删改查的 nosql 功能都支持你几亿到几十亿的数据放里面很适合而且数据还一直在增长,绝对比 mysql 分库分表要来的方便的多
  2. 只需要简单的增删改查的支持
    你对海量的数据仅仅就是简单的增删改查的支持绝对没有 MySQL 那种关系型数据库支持的列类型、索引、事务、SQL 语法那么多高阶的特性你要是不需要索引、SQL 和事务那妥妥的用 hbase 就可以了
    虽然 hbase 之上有很多开源组件可以搞二级索引、phoniex 可以支持 SQL但是说实话真的没必要人家 hbase 就不是干这个的麻烦大家别折腾他好吗
    你要海量数据下支持事务可以用分布式数据库比如 TiDB你要海量数据下支持复杂 SQL 实时分析可以用 clickhouse或者是 druid 之类的

4.HBase 的数据模型是什么样的

hbase 里其实也是建一个一个的表
表里有很多行的数据但是其实这个表说白了就是一个逻辑模型物理上根本没那么简单的一个表的数据当然是拆为很多 region 分散在不同的机器上的要是表里数据太多了region 数量还会变多这样你加更多机器region 可以自动迁移到不同的机器上去
每一行都有一个 rowkey还有很多列表里的数据行都是按照 rowkey 排序的大致可以把 rowkey 理解为 mysql 里的主键 id在 hbase 里每一行数据都有一个 rowkey 行健来唯一的标识一行数据
所以一般设计 rowkey 是一门讲究活后续还会讲如何设计 rowkey 的因为一般要把同一类数据的 rowkey 设计的相似一些比如说用户 id=1 的订单就应该叫做 order_1_xx 之类的这样一个用户的订单就会在排序之后靠近在一起

rowkey 列
order_1_110 xxx
order_1_111 xxx
order_2_256 xxx
rowkey order:base order:detail order:extent
order_1_110 xxx xxx
order_1_111 x1(t1); x2(t2) xxx xxx

每一行数据都有一些列族就是 column family每个列族都包含一些列每个列族都有一系列的存储属性比如说是否把列族里的列值缓存在内存里列族里的数据如何进行压缩类似这种
一个表里有固定的一些列族每一行都有这些列族当然有可能你一行数据在某个列族里没存什么东西是有可能的
然后就是列每个列就是一个列族+分号+列限定符column qualifier比如说列族是 order列可能就是 order:base或者是 order:detail
每个表的列族是固定的但是每一行数据有哪些列是不固定的插入数据的时候可以动态可以给这行数据设定多个列每个列都是属于一个列族就是一个列族+分号+列限定符的形式就可以确定一个列
时间戳timestamp每一行的每个列的值写入的时候就会有一个时间戳时间戳就代表了这一行这个列的某个版本的值当然这个 timestamp 你也可以自己插入的时候指定一个 timestamp 也是 ok 的
单元格也就是 cell其实就是一行的某个列族下的某个列由列限定符来确定的某个 timestamp 版本对应的值说白了就这么个东西在 hbase 里每一行的每个列的值是有多个版本的每个版本都是一个 cell

5.HBase 的物理存储格式为啥说他是列式存储

rowkey order:base order:detail order:extent
order_1_110 xxx(t3) xxx(t4)
order_1_111 x1(t1); x2(t2) xxx(t5) xxx(t6)

hbase列式存储的一个系统他不是说按一行一行的格式来进行存储的按列来进行存储的

rowkey timestamp 列 值
order_1_110 t3 order:base xxx
order_1_110 t4 order:detail xxx
order_1_111 t1 order:base x1
order_1_111 t2 order:base x2
order_1_111 t5 order:detail xxx
order_1_111 t6 order:extent xxx
这个思想和 redis hset 结构存放数据表数据意思一样 key - value 的形式

6.HBase 的逻辑本质多维稀疏排序 Map

hbase 的逻辑视图是 rowkey然后每个列族下多个列 每个列内有多个 timestamp 版本的 cell 值大致是如此一个逻辑视图

rowkey 列族 1 列族 2
列 1 列 2 列 3 列 4

rowkey_01 v1 v2 (t1) v3 v4
v5(t2)
rowkey_02 v7 v8

但是他在实际存储的时候其实是一个 map 数据结构之前说过他是列式存储的实际存储时候是 kv 格式的列式存储key 是 rowkey+列族:列+操作类型put+timestampvalue 就是 cell 值这个之前说过

keyrowkey_01+列族 1:列 1+put+t3valuev1
keyrowkey_01+列族 1:列 2+put+t1valuev2
keyrowkey_01+列族 1:列 2+put+t2valuev5
keyrowkey_01+列族 2:列 3+put+t4valuev3
keyrowkey_01+列族 2:列 4+put+t5valuev4
keyrowkey_02+列族 2:列 3+put+t6valuev7
keyrowkey_02+列族 2:列 4+put+t7valuev8

所谓的多维意思就是 map 里的 key 是多维度的一共是 5 个维度组成的 key所以首先 hbase 核心数据结构是多维 key 组成的 map 数据结构

其次hbase 是稀疏的所谓的稀疏意思就是说很多 rowkey 可能在某些列内的值是空的如果你从逻辑视图看就是一个表里出现了很多的空洞但是实际存储的时候是不存储这些空值的就是存储有值的列而已列式存储所以因为这些空洞逻辑视图上看起来这个表是很稀疏的

排序这个意思是说默认情况下表里的数据都是有序的但是不仅仅是按照 rowkey 排序是按照 map 数据结构里的 key 来排序的先按 rowkey 升序排序如果 rowkey 一样就按照列族:列来升序排序如果也一样就按照 timestamp 升序排序所以说表里的数据都是按照上述规则排序的

那如果放在逻辑视图而言其实也是有序的说白了就是按照 rowkey、列族:列、timestamp 的顺序依次把逻辑视图里的行进行排序

7.HBase 的物理存储方案列簇式存储设计思想

行式存储意思就是把一行数据存储在一起这个就是 MySQL 这种关系型数据库的典型存储方式他的优点就是适合 OLTP 系统经常可能要查询一个行里多个字段的值是比较合适的缺点就是如果是作 OLAP 分析的话就不太合适了因为针对个别字段分析的时候要把行里的数据都查出来才行很占用内存

id name age
1 张三 20
2 李四 30
3 王五 40

下面是一个大的磁盘文件里存储的行式的数据结构

1+张三+20||2+李四+30||3+王五+40||。。。。以此类推 1+张三+20||2+李四+30||3+王五+40||。。。。以此类推 1+张三+20||2+李四+30||3+王五+40||。。。。以此类推 1+张三+20||2+李四+30||3+王五+40||。。。。以此类推 1+张三+20||2+李四+30||3+王五+40||。。。。以此类推 1+张三+20||2+李四+30||3+王五+40||。。。。以此类推

经常会把整行整行的数据从磁盘里加载到内存里去后续 OLTP 系统都会经常使用到各个字段的值所以说还是比较合适的假设你是 OLAP 系统主要是做分析主要是针对个别字段进行分析那你每次从磁盘上加载大量行的所有字段到内存里去

提一句额外话如果不理解这个什么意思推荐看狸猫技术窝的 mysql 专栏剖析了 mysql 底层内核级的工作原理

列式存储之前我们一直说 hbase 是列式存储其实不完全准确因为列式存储实际上严格来说指的是把一列的数据都存储在一个磁盘文件里这样的话你就是提取单列值效果是很高的但是如果查一行就不太靠谱了要读取多个文件里的值Kudu 其实就是典型的列式存储

id name age
1 张三 20
2 李四 30
3 王五 40

文件 1id 字段所有的数据1||2||3所有的值都在这里
文件 2name 字段所有的数据张三||李四||王五所有的值都在这里
文件 3age 字段所有的数据20||30||40所有的值都在这里

假设你就是针对某些字段进行分析此时的话他只要从磁盘里加载某个字段的磁盘文件里所有的数据出来放在内存里就可以支持你的针对这个字段的分析了不需要把每一行每个字段的值都加载出来

如果你要做一个查询默认的是你不会经常要查一行所有的字段的值大部分时候是查一行的某个列族下的某几个列的值一般都不会同时查多个列族里的数据你使用 hbase 大多数时候是这样的一个场景

rowkey_01 的列族 1 下的列 1+列 2 的值此时就可以直接读取列族 1 的存储的文件把数据加载出来就可以了不需要把这个表里的其他列族的数据读取出来去平白无故的耗费你宝贵的内存

列族 1 所有的数据都是放在一个大文件里的

keyrowkey_01+列族 1:列 1+put+t3valuev1
keyrowkey_01+列族 1:列 2+put+t1valuev2
keyrowkey_01+列族 1:列 2+put+t2valuev5

列族 2 所有的数据都是放在一个大文件里的

keyrowkey_01+列族 2:列 3+put+t4valuev3
keyrowkey_01+列族 2:列 4+put+t5valuev4
keyrowkey_02+列族 2:列 3+put+t6valuev7
keyrowkey_02+列族 2:列 4+put+t7valuev8

假设说你要查询一行所有的数据此时肯定是要对多个列族的存储文件发起磁盘 IO或者跨多台服务器去拿这个 rowkey 对应的不同列族的数据最终给你按照 key-value 的格式返回回来

列簇式存储这个才是 hbase 真正的物理存储格式就是把一个表中的一个列族内的数据全部存放在一个文件里这就是列簇式存储假设你一个表里就有一个列簇里面有很多列那这不就是行式存储了么一行的数据都存储在一起

如果你要是搞很多列簇每个列簇就一个列那不就是列式存储了么每个列的数据存储在一起在一个文件内

但是 hbase 一般来说是根据你未来的查询需求来决定如何设计列簇的一般来说不会设计太多列簇就几个列簇每个列簇内是相关联的一些列然后这个列簇内的所有列的值都存储在一起在一个文件里

这样的话在你的查询需求中你每次查询一般就是一个列簇内的列那么是不是每次就查询这个列簇的数据就可以了比行式存储好吧不需要强制性查询所有列的值比列式存储灵活吧如果每个列存储一个文件那岂不是要查询多次

所以这就是 hbase 的物理存储的精髓列簇式存储

你查出来的数据都是按照 rowkey+列簇:列+timestamp 组成的 key以及 cell value组成的 key-value 对然后按顺序排序的这样是不是就把物理视图和逻辑视图都联系起来了物理上如何存储逻辑上查询看到的是什么样的

8.HBase 的整体架构设计以及核心组件

HBase 客户端ZooKeepermaster 高可用分布式锁集群元数据HMaster建表之类的 DDL 请求管理 HRegionServerHRegionServer实际数据读写HDFS海量数据分布式存储

HRegionServerHLog预写日志、BlockCache读缓存、Region数据分片
HLog预写日志文件这个就是记录了你做了哪些数据的操作如果系统突然崩了然后这个系统突然重启系统会通过 hlog 将数据恢复出来如果没有 hlog 的话那么客户端存储到 region 的数据首先是进入到内存的并不会立即落盘到 hdfs,这样就会丢失数据

BlockCache在读数据的时候比如说数据已经放到了 hdfs 上边了你要读这个数据那么就需要将这个数据从 hdfs 上边读取出来读出来后就会在 blockCache 缓存一些数据下次就可以从 blockCache 中读取了

Region:本来就是数据分片

每一行数据都会对应很多的列族的值不同列族的值在逻辑上边会被拆成以列族里边的列为单位的一个一个的 key-value 对然后它会把同一个列族里边的数据给他找到列族的 store 在哪个 region,这个 region 在哪台 hregionServer 上边找到后就直接往对应的 HRegionServer 的 region 的 store 的 memstore 中放同时还应该写一份 Hlog

Region包含多个 Store每个列簇是一个 Store刚开始假设表数据量很小那么就一个 Region所有列簇都在这个 Region 里每个 Region 对应一个 Store这里也能看出物理层面的列簇式存储后续如果表数据量太大了就会拆分 Region每个 Region 里会放不同的 Store

StoreMemStore写缓存多个 HFileMemStore 写满 128M 就刷一个 HFile其实就对应 hdfs 里的一个 blockHFile 越来越多会进行 compact 合并多个小 HFile 合并为一个大的 HFile在 hdfs 里分布式存储

比如说一开始是有 20 个小的 HFile每个 HFile 是 128mb对应 hdfs 的一个 block 大小后来把这 20 个 HFile 合并为一个大的 HFile这个大的 HFile 就是 hdfs 上一个文件这个文件里可能包含了 20 个 block

DFSClient 和 HDFSHLog、HFile都是放在 hdfs 里的对大文件可以拆分多个小 block 分布式存储还有多副本保证高可用数据不丢失DFSClient 是负责读写 hdfs 的

9.HBase 架构设计的优点和缺点总结

优点是很多的容量巨大可以千亿行上万列扩展性很强可以按需扩容都是线性伸缩的结构化列簇式存储允许稀疏数据存储还不浪费空间擅长 OLTP 场景高并发、高性能的数据读写小范围的查询也是可以的支持值的多版本这个其实可以保存时序数据

hbase 的缺点也是很多的不支持 OLAP 分析场景分析用 hbase 就不太靠谱了天生不是用于分析的他就是海量数据列簇式存储结构化列可扩展高性能读写如果在 hbase 上搞 phoenix 和 spark也不是太靠谱做批量数据分析就没必要走 hbase 了其实直接走 hdfs 就行了而且他也不支持事务

三种场景
OLTP在线数据存储
配合实时计算spark streaming/flink基于 hbase 读写状态数据计算结果
OLAPkylin离线 OLAP可以把数据放到 hbase 里去kylin 基于 hbase 里的数据做高性能的 OLAP 分析

对于客户端而言必须读取集群的元数据我插入数据的找个表有几个 region,region 是在哪我插入的这个数据应该放到哪个 region 里去此时就会连接到那台 HRegionServer 上边去就会把数据放到对应的 region 里面去然后数据就会通过 DFSClient 进行数据分片

10.执行一次 put 操作到底要如何去执行呢

默认配置了 autoFlush=true所以你只要执行 put 就直接提交到服务器端执行了但是如果你配置 autoFlush=false那么会先把你的 put 请求放到客户端本地缓存默认是满了 2MB 数据之后再异步提交到服务器

异步请求可以提升吞吐量但是客户端重启和崩溃都会导致数据丢失所以一般都用默认的 autoFlush=true

hbase 里的一张表是对应了很多的 region 数据分片的每个 region 数据分片你可以认为都存放了一部分行所以说你现在要 put 一行数据此时你首先需要定位到你的这行数据是属于哪个 region 数据分片的

你还得知道你的这个 region 数据分片此时是在哪个 HRegionServer 服务器上的

其实首先第一步我们就是应该要能够把你一次 put 操作进行一个路由路由到一个对应的 region 数据分片同时路由到一台 HRegionServer 服务器上去

在提交你的 put 请求到服务器端之前会先在客户端本地元数据缓存里根据表和 rowkey 查找对应的 RegionServer 以及 Region如果本地没有查到那么就会从 zk 上的/hbase-root/meta-region-server 节点里查找 hbase 元数据表hbase:meta所在的 RegionServer所有的元数据都是存放在元数据表 hbase:meta 里的

这个 hbase:meta 表就是 hbase 的元数据表里面存放了所有的集群元数据你所有的读写操作都需要通过这个表里的信息来进行路由定位你必须路由定位到你要读写哪个 Region然后定位到 Region 所在的 RegionServer才能发送请求到那个 RegionServer 去进行数据的读写操作

hbase 是这个表的 namespace就是命名空间meta 是 hbase 这个命名空间下的一个表名其实这个是一个很规范的用法说白了假设对应到 MySQL 里hbase 在这里就代表你的库名meta 就是 hbase 库里的一个表

我们后续在做项目的时候也会如此来规划会针对不同的业务和系统划分不同的 namespace然后 namespace 下会有不同的表名这是一种规范的做法然后 hbase 命名空间就是 hbase 自己的系统表命名空间

一个公司总共就是一个 hbase 集群此时可能有很多的业务在使用这个 hbase 集群不同的业务在 hbase 里建的这个表必须有不同的 namespace比如说后端团队数据团队AI 团队他们的各自的业务都需要使用到 hbase

backend:order_info
data:order_report_chart
ai:fengkong_dataset
hbase:meta 这个表总共就只有一个列族就是 info

这个表的 rowkey 是表名+startRow+region 创建时间戳+三个字段的 md5 值组合在一起的因为一个表可以拆分为多个 region所以其实这里每个 rowkey 都是拼接了一个表名加上起始 rowkey

刚开始这个表只有一个 region能够管理一部分的数据此时你不停的在这个表里写入数据表里的数据越来越多region 管理的数据越来越多假设此时 region 管理的数据达到了 1 万条发现已经达到了单个 region 管理数据的上限

此时可以再给这个表分裂出来一个新的 region可以放在另外一台 HRegionServer 服务器上老的 region他的起始的 rowkey 就是 0001 开始的老的 region 在 hbase:meta 表里就对应了一行数据

test:user+0001+2020-01-01 00:00:00+md5拼接成了一个 rowkey这个 rowkey 对应了 hbase:meta 表里的一行数据其实就对应了那个老的 region 的一些信息和数据我们的每个表的每个 region在 hbase:meta 表里都对应了一行数据这行数据的 rowkey 都是表名+region 里的起始 rowkey+region 创建时间+md5

然后 info 列族里有 4 个列info:regioninfo包括 md5 值region 名称起始 rowkey结束 rowkeyinfo:seqnumDuringOpenregion 打开时的 sequenceIdinfo:server存储 Region 在哪个 RegionServer 上info:serverstartcodeRegionServer 的启动 timestamp

所以说你定位一个 rowkey 属于哪个 region 数据分片的时候就是查这个 hbase:meta 表就可以了根据 rowkey 来定位到一个 region也可以定位到 region 所在的 region server所以发送请求到 hbase:meta 表所在的 RegionServer去根据表和 rowkey 查找对应的 RegionServer 和 Region找到了之后就把结果缓存在客户端本地

然后再把 put 请求发送给目标 RegionServerRegionServer 接收到请求之后就先写入 WAL 预写日志HLog 文件接着把数据写入 Region 内部的不同的 store 的 MemStore 里不同的 store 就放不同列族的数据底层数据存储都是列族式存储的不同列族的数据集中存储在一个 store 里

一个列族的 MemStore 满了就写一个 hfilehfile 多了就合并为一个大的 hfile

实际上 hbase:meta 表也是 hbase 里的表就一个列族然后一般数据不会太多可能也就一个 region所以通常客户端都是在刚开始几次访问会去读那个 region 里的 hbase:meta 的元数据但是后续都会缓存在客户端的

后续只要能从本地客户端读到就会直接基于元数据去访问 RegionServer 了

如果说基于元数据你没有访问到那个 Region说明 Region 发生了移动可能是 RegionServer 挂了或者是什么的此时你就可以重新从 hbase:meta 里读取元数据了但是这种情况一般比较少

所以关于元数据的访问大部分时候都是通过客户端缓存来走的

11.Region 中的数据写入流程的介绍

在 Region 里写入的时候先获取一把行锁每一行的更新都是有行锁保证并发问题的然后更新数据的 timestamp接着构建 WAL edit log然后追加 WAL 到一个 disruptor 队列里去接着把数据实际写入到各个 Store 的 MemStore 里去然后释放锁

注意为什么在这里释放锁呢因为要避免加锁的时候去 sync WAL edit log 到 hdfs 文件里去那是很慢的所以上述步骤完成了就直接释放锁了然后再把 WAL edit log 给 sync 到 hdfs 文件里去

如果要是 sync 失败了那么就执行回滚把写入 MemStore 的数据回滚掉如果要是 sync 成功了那么就让本次写入可以查询到

12.HLog 的 5 种持久化级别以及影响效果

HBase Java API我们后面做项目的时候主要都是会基于这套 API 去做的

put.setDurability(Durability.SYNC_WAL)可以代码里设置每一条 put 操作的 WAL 日志的持久化级别每一种级别都有不同的行为可以让我们在写入性能和数据可靠性之间做一个取舍和平衡

SKIP_WAL不写 WAL 日志直接把数据写入各个 Store 里的 MemStore这样的话如果 MemStore 数据还没 flush 到 HFile 里去此时 hbase 重启或者宕机可能导致一部分数据丢失的

ASYNC_WAL异步将数据写入到 HLog 日志文件里去这个就是写入都返回告诉你成功了然后异步去把 WAL 日志写入 HLog 文件里去

SYNC_WAL同步把 WAL 日志写入到 HLog 日志文件里去但是这种持久化级别下仅仅是把数据写入到 hdfs 的文件里停留在 hdfs 的操作系统的 os cache 里还不是直接 flush 到底磁盘上去

FSYNC_WAL这个是同步把 WAL 日志写入到 HLog 文件里同时直接强制性在 hdfs 上 flush 到磁盘上去这是最强悍的 WAL 日志机制数据只要返回给你说写入成功了那就绝对不会丢失因为他肯定是已经 flush 到底层 hdfs 的磁盘上去了

USER_DEFAULT默认级别就是 SYNC_WAL说白了就是写入成功保证一定同步 sync 到 hdfs 上去了但是在 hdfs 的 os cache 里还没进入到 hdfs 的磁盘上去但是此时 hdfs 有多副本通常也不至于丢失数据

13.基于 Disruptor 无锁有界队列的 HLog 写入模型

在追加 WAL 日志到 disruptor 无锁有界队列的时候是封装为一个 FSWALEntry 放入队列然后 disruptor 队列有一个消费者消费者获取到了 FSWALEntry 就会进行日志的追加操作但是此时仅仅只是追加到缓存而已

然后在释放锁之后会构造一个 SyncFuture 放入 disruptor 队列消费者线程获取到了 SyncFuture 就会把追加的那些 WAL 日志给 sync 到 hdfs 文件里去但是此时如果是默认 SYNC_WAL 级别的话其实也就是进入 hdfs 文件系统的 os cache

14.如果未经优化为什么 MemStore 会频繁触发 Full GC

如果不停的在 MemStore 里追加数据那么必然新生代很开塞满接着 young gc 就会转移到老年代去在老年代里是各种内存碎片然后你 MemStore 必然会刷数据到磁盘里去接着你就持续不断的追加数据老年代很快就会被塞满因为老年代里都是各种内存碎片你想找一些完整的内存来都很难

所以老年代很快就会因为内存碎片太多找不出完整内存放你新追加的数据然后触发一次 Full GC还得耗时去回收一大堆的内存碎片

这样的话随着你不停的写入 MemStore 以及刷入磁盘肯定会频繁触发 Full GC 的而且每次 Full GC 还得回收内存碎片是很耗时的

15.基于 chunk 的内存块机制优化 MemStore GC 问题

每个 MemStore 实例化一个 MemStoreLAB 对象MemStoreLAB 对象里会先申请一个 2M 大小的 chunk 数组然后每次你插入数据的时候其实都是一个 key-value 对大家应该还记得的都是列族式存储的然后就把数据转成字节数组顺序写入那个 2MB 数组同时维护好他的偏移量

如果写满了一个 2MB 的 chunk此时再申请一个 2MB 的 chunk

这样的话你不停的新增 chunk那其实就是会让这些 chunk 在年轻代分配之后快速触发 young gc 后转移到老年代然后在老年代里就会有很多的 chunk老年代里都是连续内存不是碎片化的所以此时可以让你写入更多的 chunk 而不会频繁触发 full gc

你如果都是内存碎片的话此时可能看起来老年代里还有几百 MB 的内存但是都是碎片每个碎片都是一点点大根本无法容纳你写入任何一条新数据了此时就会频繁的发生 Full gc在你内存还有几百 MB 的时候就会发生 full gc

如果你用的是 chunk 机制都是整块内存不是内存碎片老年代里此时可以不停的写入新的 chunk一直到老年代只有几十 MB 内存了此时触发了一个阈值才会去触发老年代的 full gc就不会频繁触发 full gc

如果你把数据刷入磁盘了那么后续一旦老年代满触发 full gc此时回收掉的也是连续内存所以老年代里是没什么内存碎片的下次你再次申请新的 chunk慢慢转移到老年代之后老年代里没有内存碎片就不会频繁触发 full gc 了

16.MemStore 触发生成 HFile 文件的几个时机

1MemStore 里的数据达到了 128MBhbase.hregion.memstore.flush.size 可以控制

2region 里所有的 MemStore 的总和达到了上限是下面的一个公式如果达到了下面的公式hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size也会触发刷新

3RegionServer 里的所有 MemStore 的总和大小超过了
hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size此时就执行 flush先刷最大的再刷次大的

4RegionServer 里的 HLog 数量达到上限hbase.regionserver.maxlogs系统选择最早的 HLog 的部分 Region 来 flush

5定期刷新 MemStore默认周期是 1 小时一次

6手动刷新 MemStore通过 flush 命令可以手动刷新的

17.hbase 扫描搜索数据的流程分析以及总结

hbase 适合的是什么高性能、高并发的写入海量数据的存储根据 rowkey 来查或者是 rowkey 范围来查最好是把你的查询的几个条件的值几个要用作查询的字段的值最好是放在 rowkey 里直接根据 rowkey 来查

你要是想跟 mysql 一样根据各种列字段的值来乱查是不太靠谱的除非你还要去做复杂的二级索引才可以其实一般 hbase 和跟 elasticsearch 配合起来实现复杂的搜索和查询的es 的特点就是适合建立复杂的索引倒排索引正排索引然后让你快速的进行复杂的查询你可以把一些查询条件的字段值放到 es 里去建立索引

然后通过 es 去检索一大堆复杂条件下的数据的 rowkey 值然后如果你要检索数据完整的字段值可以根据 rowkey 范围去 hbase 里快速检索出来这个数据完整的值你一行数据一共有 30 个字段有 5 个字段

可以把仅仅 5 个字段+rowkey 存放到 es 里做索引主要是依赖自己的 jvm 内存和 os cache 内存去提升索引检索速度es 不适合海量数据存储大量的索引数据存储但是你最好是让索引数据的量在你的机器的内存范围之内

rowkey你 30 个字段都放在了 hbase 里根据根据 rowkey 去检索出来你所有的字段的值hbase 适合的就是高并发和高性能的写入海量数据存储这个是他适合的根据 rowkey 简单快速的定位和查询

18.HLog 日志文件的物理存储结构是什么样的

每个 HRegionServer 一般有 1 个 HLog但是也可以配置多个HRegionServer 上的所有 Region 都共享这个 HLogHLog 里会不规律的存放不同 Region 写入操作的日志每个日志里包含一个 HLogKey是表名+region 名+写入时间+sequenceid+clusterid 组成的

基本上可以认为是一个表的一个 region 写入一行数据这一行数据的写入或者更新删除就会对应一条日志

至于日志的具体内容其实就是对这行数据的更新操作里对每个 store 列族的具体更新内容这些东西我们可以通过一个动态的图来展示一下多个 region 不停的更新数据行此时不同 region 的日志不停进入 HLog 的示意

19.HLog 日志文件在 HDFS 上的物理存储结构

按照上一讲的物理结构把日志写入 HLog 之后其实就是一行更新操作一条日志其实都是会落地在底层的 HDFS 上的每台 HRegionServer 的 HLog 其实就是对应底层的 HDFS 上的大文件的

在 hdfs 上有专门的目录存放 HLog 文件/hbase/WALs 和/hbase/oldWALs

/hbase/WALs 里存放的是还没过期的日志就是这些 WAL 日志更新操作对应的数据还在 MemStore 里数据还没刷入到 hdfs 上去此时这些 WAL 日志都是属于没过期的都放在/hbase/WALs 目录下

实际的 HLog 日志文件是存放在如下的目录里下面每一行都是一个目录每个目录其实都代表了一台 HRegionServer 机器也就是说每台 HRegionServer 机器的 HLog 都放在一个单独的目录里

/hbase/WALs/hbase12.df.zszs.org,60020,1304970381600
/hbase/WALs/hbase13.df.zszs.org,60020,1304970381472
/hbase/WALs/hbase14.df.zszs.org,60020,1304970381221

hbase14.df.zszs.org这个就是代表了 HRegionServer 机器的域名60020 是统一的端口号1304970381600 是创建这个目录的时间戳在目录里会存放这台 HRegionServer 机器的所有 HLog 文件类似下面

/hbase/WALs/hbase12.df.zszs.org,60020,1304970381600/hbase12.df.zszs.org%2C60020%2C1505980274300.default.1506184980449

所以现在大家就知道了其实我们平时机器上生成的 HLog 日志都是以固定的格式写入到底层的 HLog 文件里去的HLog 文件就在 hdfs 上的指定目录里写入 HRegionServer 对应的目录下的 HLog 日志文件里去

20.HLog 日志的生命周期是什么到底会存放多久

那大家可以思考一下如果 hlog 日志一直不停的写入 hdfs 里去那里面的一个 hlog 日志文件会越来越大浪费很多的磁盘空间实际上有这个必要吗其实是没必要的因为你的一些 hlog 日志对应写入到 MemStore 的数据一旦都 flush 到 hdfs 上去了那你的 hlog 日志就没有存在的必要了

所以 HLog 日志是有生命周期的首先正常运行的时候hlog 日志会不停的写入到 hdfs 上的日志文件里去然后 hbase 有一个后台线程根据 hbase.regionserver.logroll.period 参数的时间默认是 1 小时会在 hdfs 上给你这台 HRegionServer 创建一个新的日志文件

你的 HRegionServer 服务器其实在每个小时其实都有一个 HLog 日志文件在 hdfs 上每个小时的 WAL 日志都是进入到那一个小时对应的 HLog 日志文件里去的所以其实是这样的一个思路

这样其实对于 HRegionServer 而言他在 hdfs 上写入的 hlog是每个小时都有一个 hdfs 上的新日志文件存放在 HRegionServer 的那个目录下

接着 HRegionServer 会根据你不停的把 MemStore 里的数据刷入磁盘的行为来判断如果说某一个小时的 HLog 日志文件里的所有 hlog 日志对应写入的 MemStore 里的数据都已经刷入到 hdfs 里去了此时那一小时的 hlog 日志文件就失效了

此时就可以把那个 hlog 日志文件给挪动到/hbase/oldWALs 目录下去

HRegionServer 还有一个后台线程根据 hbase.master.cleaner.interval 参数设置的时间默认是 1 分钟检查一下/hbase/oldWALs 目录下的失效的日志文件如果在里面停留的时间超过了 hbase.master.logcleaner.ttl 参数指定的时间默认是 10 分钟就可以删除了此时直接删除这些 hlog 就行了

21.MemStore 的双跳跃表写入和 flush 机制

MemStore 的核心数据结构是跳跃表HBase 用的是 JDK 里的 ConcurrentSkipListMap底层就是跳跃表数据结构在保证数据有序性的基础之上还可以保证数据的增删改查都是 O(logN)的时间复杂度性能还是不错的而且他还是线程安全的底层使用了 CAS 来保证线程安全性还能保证多线程并发性能

之所以用跳跃表就是因为必须要保证数据有顺序因为我们之前说过了我们写入 store 的数据其实都是列族式存储的 key-value 对那个 key 就是 rowkey 为核心的然后 hbase 里都是承诺按照 rowkey 来排序的

所以一个 store 中写入的列族数据在 MemStore 里就通过跳跃表保证了有序性

按照行为单位的数据插入进去其实在底层会转换为列族式存储的这种结构会转换为多个 key-value 对对于这些 key-value 对key -> rowkey+列族+列+timestamp 组成的这些 key-value 对就代表了你本次插入对每个列插入的数据每个列的数据就是一个 key-value 对

hbase 是承诺在底层对你的 key-value 是有序的

rowkey 列族 1 列族 2
列 1 列 2 列 3 列 4
rowkey_01 v1 v2 v3 v4

列族 1 所有的数据都是放在一个大文件里的

keyrowkey_01+列族 1:列 1+put+t3valuev1
keyrowkey_01+列族 1:列 2+put+t1valuev2

列族 2 所有的数据都是放在一个大文件里的

keyrowkey_01+列族 2:列 3+put+t4valuev3
keyrowkey_01+列族 2:列 4+put+t5valuev4

正常情况下都是把数据写入到 MemStore 的 ConcurrentSkipListMap 里去然后如果按照之前说的 MemStore flush 触发条件满足了某个触发条件此时就会再搞一个新的 ConcurrentSkipListMap然后让后续数据写入新的跳跃表老的跳跃表里的数据就可以 flush 成一个 hfile 了

在切换跳跃表的过程中是会采用写锁阻塞所有写请求的不过切换过程很快所以几乎不会有什么影响

22.跳跃表数据结构到底是什么样的

LinkedList -> 其他的有序的 map可以基于 LinkedList 实现一个 map所有的 key 都挂在一个有序的链表上key 就引用对应的 value 就可以了此时就可以保证 map 里的 key 都是有顺序的

正常的链表查询复杂度什么的都是 O(N)因为要遍历所以跳跃表就是在链表基础之上多存储一些额外的数据让他的增删改查复杂度都在 O(logN)跳跃表里是有多条链表组成的每条链表都是有顺序的

每个链表都有 2 个元素负无穷大和正无穷大分别是头和尾

从上到小上层的链表一定是下层链表的子集比如说下面的 6 条链表

负无穷大 -> 正无穷大
负无穷大 -> 35 -> 正无穷大
负无穷大 -> 1 -> 28 -> 35 -> 正无穷大
负无穷大 -> 1 -> 28 -> 35 -> 正无穷大
负无穷大 -> 1 -> 28 -> 35 -> 正无穷大
负无穷大 -> 1 -> 5 -> 28 -> 35 -> 正无穷大

查找元素的时候先找这个元素应该插入在哪里从左上角开始查查你要找的元素比下一个元素是否大如果是的话就遍历链表往下找如果不是的话就直接找下一条链表举个例子你要查找 5那么就会一直找到最后一条链表

如果你要插入元素那么先要按照上述算法找到这个元素的位置前驱和后继接着按照随机算法生成一个高度下面的算法其实 p 值是次要的核心的就是会生成大于等于 0 的一个随机高度

// p 一般等于 0.5 或者 0.25
public void randomHeight(double p) {
int height = 0;
while(random.nextDouble() < p) height++;
return height + 1;
}

举个例子假设你生成的随机高度是 7那么接着就要为这个新插入元素比如 48生成垂直节点垂直节点的层数就是随机高度然后把垂直节点也就是你这个新插入元素 48插入到每一层的链表里的对应位置上去

假设你的随机高度值是大于了当前的跳跃表层高的那么就给跳跃表加高举个例子此时可能就会变成如下

负无穷大 -> 48 -> 正无穷大
负无穷大 -> 35 -> 48 -> 正无穷大
负无穷大 -> 1 -> 28 -> 35 -> 48 -> 正无穷大
负无穷大 -> 1 -> 28 -> 35 -> 48 -> 正无穷大
负无穷大 -> 1 -> 28 -> 35 -> 48 -> 正无穷大
负无穷大 -> 1 -> 5 -> 28 -> 35 -> 48 -> 正无穷大

删除和插入是差不多的意思

总之这样的一套数据结构和算法搞下来基本上插入查询删除时间复杂度都是 O(logn)而且数据都是有顺序的

跳表的插入和查询的时间复杂度都为 o(logn)它可以媲美红黑树这个在开源软件中有很多应用比如
1.Hbase 中的 MemoryStore
2.Redis 中有序集合 zset

23.MemStore 的 Chunk 池机制如何跟跳跃表配合

现在我们理解了为了保证写入数据按照 rowkey 有序排列在 MemStore 列使用了保证数据有序的跳跃表插入就有序同时删除和查询的性能也比较高但是之前讲过如果你直接构造一个一个的对象写入 MemStore会导致大量的内存碎片

内存碎片会导致频繁 full gc而且每次 full gc 都要整理碎片耗时很长

所以说之前讲过那个 chunk 机制依靠 chunk 来避免碎片减少 full gc 频率每次 full gc 也不用整理太多碎片耗时也变短了

那么这个 chunk 机制是如何跟跳跃表来配合使用的其实说白了很简单就是你写入 MemStore 的数据都是进入他分配好的 chunk 的然后把数据的引用放到跳跃表里不就可以了也就是说数据本身放 chunk 里避免碎片引用放跳跃表里

我们往跳跃表 map 里放入的 key 都是 rowkey+列族+列+timestamp 组成的value 其实就是真实的数据可能是字符串也可能是别的二进制的数据图片都有这种可能的对这个 value 其实完全是可以通过 chunk 来进行管理的

另外如果 chunk 要反复的申请和回收新分配的 chunk 其实都是进入到年轻代里去的你不停的分配新的 chunk那么必然会导致你 young gc 很频繁因为一直在年轻代分配 chunk就会频繁的 young gc 后让 chunk 进入老年代对于老年代而言也会同样不断的触发 full gc

其实也挺不好的所以其实 MemStore 搞了一个 chunk pool里面搞了一大堆的 chunk

然后你每次就不停的使用 chunk pool 里的 chunk如果数据 flush 到 hdfs 了chunk 就可以清空复用这样就避免了不停的重新分配 chunk就固定大小的 chunk pool 一直停留在老年代里这样 young gc 就会变少full gc 也会变少

基本上用了 chunk pool 之后在单位时间范围内young gc 次数可以减少 7 倍full gc 次数减少 10 倍

hbase.hregion.memstore.mslab.chunksize可以设置 chunk 大小默认是 2MB默认开启 chunk 机制但是 chunk pool 机制默认关闭了需要设置 hbase.hregion.memstore.chunkpool.maxsize 大于 0 才行一般设置在 0~1 范围内

开启之后分配给 chunkpool 的内存大小就是 hbase.hregion.memstore.chunkpool.maxsize * MemStore 内存大小hbase.hregion.memstore.chunkpool.initialsize 默认值为 0可以设置为多个意思就是初始化多少个 chunk 放到 pool 里去

24.HFile 的逻辑结构包含哪些部分

Scanned blockData Blockkey-value 数据Leaf Index Block索引树的叶子节点Bloom Block布隆过滤器

Non-scanned BlockMeta Block、Intermediate Level Data Index Blocks

Load-on-openRegionServer 打开 HFile 的时候直接加载到内存里去FileInfo、布隆过滤器的 MetaBlock、Root Index Block 和 Meta IndexBlock、Bloom Index Block

TrailerHFile 版本和其他几个部分的偏移量以及寻址信息

25.HFile 在磁盘文件里的物理存储结构

scanned block包含了最核心的三种数据key-value 组成的数据块索引块布隆过滤器块三块数据是最最核心的
non-scanned block包含一些可选的索引数据
load-on-open 和 trailer加载到内存里用来定位 HFile 里各个部分的数据的偏移量索引树的根

HFile 就是 MemStore flush 的时候在本地磁盘上形成的一个磁盘文件然后会把这个磁盘文件写入到 HDFS 上去存储在物理存储的时候就是依次在文件里写入不同的 block 数据块每个数据块的结构是一样的

在 HFile 里依次写入了多个 Data Block、Leaf Index Block、Meta Block、Intermediate Level Data Index Block、Root Data Index Block、Meta Index Block、File Info、Bloom Filter Meta Block、Trailer Block

每个 block 的大小默认是 64kb在创建表的时候可以针对每个列族指定blocksize => ‘65535’每个 block 的结构就包含了 BlockHeader 和 BlockDataBlockHeader 里包含了 BlockType、OnDiskSize、UncompressedSize、PrevBlockOffset

26.记录 HFile 核心结构的 Trailer Block 包含什么

RegionServer 打开 HFile 的时候首先就是会加载尾部的 Trailer Block然后再加载 load-on-open 部分的那些 block加载 trailer block 的时候会读取 hfile 的版本信息知道版本了就知道 trailer block 的大小了

接着根据 trailer block 的大小读取完整的 trailer block 的数据里面包括了 TotalUncompressedBytes 代表的未压缩 key-value 数据大小NumEntries 代表的 key-value 数量压缩算法等等

然后就是比较关键的就是 LoadOnPenDataOffset这个就是说 load-on-open 部分的 blocks 在文件里的 offset 偏移量还有 LoadOnOpenDataSize就是 load-on-open 部分的 blocks 的总大小根据这两个信息就可以从文件里读取出来 load-on-opne 部分了

这个 load-on-open 部分的 blocks也是要在打开 HFile 的时候加载到内存里去的包含了 FileInfo、Root Data Index索引树根节点 重要、布隆过滤器的 MetaBlock这样一些东西都是比较关键的

如果你不理解 HBase 里的索引树的概念不好意思我这里不会多讲的因为建议大家可以去看一下狸猫技术窝里的一个专栏从 0 开始带你成为 MySQL 实战高手深入分析了 MySQL 的 B+树索引的结构和原理

除此之外trailer block 里还有 NumDataIndexLevel 代表的索引树的高度FirstDataBlockOffset 代表的第一个数据块的偏移量LastDataBlockOffset 代表的最后一个数据块的偏移量

其实有了这些东西瞬间可以读取出来 HFile 各个部分的重要数据了包括索引树的根节点是在 load-on-open 部分就有了可以读取索引然后 data block 的位置也都有了就是各个数据块的位置

27.DataBlock 中存储实际数据的物理结构

HFile 要从里面找数据分为三个阶段根据布隆过滤器去快速判断你要的数据是否在这个 HFile 里根据索引树去搜索你要的数据提取和查找出来你要的数据。布隆过滤器布隆过滤器在 HFile 里的存储结构和使用方法索引树在 HFile 里的存储结构和使用方法key-value 数据的 DataBlock

DataBlock 就是存放核心的 key-value 数据的里面就是一个一个的 key-value 对每个 key-value 对包含 KeyLength、ValueLength、key 和 value其中 key 是由 RowKey、ColumnFamily、ColumnQualifier、TimeStamp、KeyType 组成

KeyType 包含几种Put、Delete、DeleteColumn、DeleteFamily

反正一个列族内的所有列的数据都是转化为这种 key-value 对然后放到 Region 的列族对应的 Store 里就是进入 MemStore然后刷到 HFile 的 DataBlock 里去

rowkey+列族+列+timestamp都 ok 的

28.HBase 里的布隆过滤器到底是个什么东西

实际上 HFile 里主要就是数据块索引块布隆过滤器这么几个部分其他的都是一些辅助性的比如 trailer block 和 load-on-open 部分的一些东西都是一些辅助性去定位用的核心就是上述几个部分

数据块知道了索引块留着后续再说先看布隆过滤器

假设要判断一个元素是否在某个集合里怎么做搞一个 set但是万一数据量很大呢那 set 不是要放很多东西所以布隆过滤器就是干这个的在一个巨大的数据集合里判断是否有某个元素

布隆过滤器核心是一个 0-1 数组搞一个长度为 N 的数组每个元素初始值为 0然后对集合里的每个元素做 K 次哈希运算用每一个哈希值对 N 取模得到一个数组的位置然后就把这个数组的位置设置为 1

比如某个元素做 3 次 hash 运算每个 hash 值都对数组长度 N 取模得到一个位置就把那个位置设置为 1就这样每个元素都搞这么一通最后所有元素都运算完毕之后就有一个布隆过滤器就是一个 0-1 数组

然后假设要判断某个元素是否在数组里就对这个元素也做 3 次 hash 运算如果其中某个值对数组 N 取模的位置是 0那么这个元素一定不在数组中因为假设他在数组中那么 3 个 hash 值对数组 N 取模的位置必然都是 1

然后如果某个元素 3 次 hash 之后的值对数组长度 N 取模的 3 个位置都是 1那么不能肯定他一定在数组里只能说是可能

所以布隆过滤器判断的结果就俩一个是他肯定不在集合里任何一个 hash 值对数组长度 N 取模的位置是 0就肯定不在集合一个是可能在集合里所有 hash 值对数组长度 N 取模的位置都是 1

经过证明数组长度 N 的取值是 hash 运算次数 _ 集合元素个数 / ln2这个时候误判率是最低的误判就是说判定可能在集合里但是实际上不在的概率比如集合有 20 个元素hash 运算次数是 3那么就是数组长度 N 为 3 _ 20 / ln2 = 87

29.HBase 是如何利用布隆过滤器快速筛选数据的

HBase 里为每个 HFile 都会维护一个位数组其实就是一个布隆过滤器在写入数据的时候会先基于 rowkey 进行多个 hash 函数的计算接着把计算后的数组位置的地方设置为 1 就可以了本质就是维护 HFile 的布隆过滤器

然后你就用 rowkey 搜索的时候直接就用同样的 hash 函数运算后定位在每个 HFile 的布隆过滤器数组某个位置来读取值如果是 0 说明肯定不在这个 HFile 里就继续找下一个 HFile 就可以了

就通过这种方法在搜索 rowkey 的时候可以快速跳过大量的 HFile避免全表扫描避免无效的磁盘 IO所以 HBase 的搜索效率很高

所以 HBase 里效率最高的就是基于 rowkey 来搜索因为可以走布隆过滤器但是如果你用 scan 乱扫描很多条件或者加入对字段的筛选那就困难了因为没法通过布隆过滤器去快速筛选

而且 HBase 为了提升使用布隆过滤器的效率每个 Store 都会把自己的所有 HFile 的布隆过滤器都存储在内存里这样你要是针对 rowkey 做筛选那简直速度是极快的直接就在内存里对所有 HFile 的布隆过滤器去快速筛选

除非是查到某个 HFile 里可能存在这个 rowkey再读取这个 HFile 来找如果没找到继续下一个 HFile 的布隆过滤器的筛选

30.HFile 中的布隆过滤器块是如何存储的

但是有一个问题如果 HFile 里的数据很多那么不可能就维护一个布隆过滤器数组然后都放在内存里内存占用太大了所以实际上后续做了优化每个 HFile 都有多个布隆过滤器数组每个数组对应一个 Bloom Block每个布隆过滤器就是由部分数据的 rowkey 计算组成的

大家还记得在 load-on-open 部分有一个 Bloom Index Block里面包含了版本、TotalByteSize数组大小、NumChunksBloom Block 的数量、HashCounthash 函数的个数、HashTypehash 函数的类型、TotalKeyCount当前包含的 key 的数量、TotalMaxKeys最多能包含的 key 的数量等数据这些都不是重点最关键的是包含了多个 Bloom Index Entry

每个 Bloom Index Entry 指向了一个 Bloom BlockBloom Block 都是放在 scanned block 部分的里面有 BlockOffsetBloom Block 的偏移量BlockOndiskSizeBlockKyeLenBlockKey布隆过滤器对应的第一个 rowkey

bloom index 块里包含了对每一个 bloom block 的索引索引里包含了 bloom block 的地址和大小bloom block 里第一个 rowkey 的值

31.如何基于 HFile 中的 Bloom Block 快速筛选数据

根据你要查找的 rowkey在每个 HFile 的 load-on-open 部分的 Bloom Index Block 里查找 bloom 块的索引直接根据每个 Bloom Index Entry 部分的 BlockKey这个布隆过滤器数组计算过的第一个 rowkey 值进行二分查找快速定位到你要搜索的 rowkey 可能在这个 HFile 的哪个布隆过滤器数组里

BloomBlock01110
BloomBlock02255offset+datasize
BloomBlock03599

也有可能会发现压根儿就不可能在 HFile 里的

接着直接定位到那个布隆过滤器数组对应的 Bloom Block就用 Block Offset 和 BlockonDiskSize 就可以在 scanned block 部分快速定位 Bloom Block 了从指定的 offset 那里开始读取指定字节的数据就可以了

然后对你的 rowkey 用多个 hash 函数计算后映射到布隆过滤器数组的某个位置如果是 0 说明肯定不在继续找其他的 HFile 了如果是说明可能是存在的如果可能存在那就到这个 HFile 里去找就行了

其实通过这种方法可以快速跳过大部分的 HFile在少数 HFile 里定位

所以布隆过滤器就是用来让你在针对 rowkey 进行查找的时候快速的进行数据筛选过滤掉大部分的 HFile

32.HBase 如何基于 LSM 树组织磁盘上的的索引树

在磁盘文件里有一颗索引树看着就跟一棵树一样搜索的时候就是从根节点开始搜索在每个节点的内部进行二分查找快速定位到应该到下层的哪个索引节点里去找一直定位到最后的数据节点在里面找到你需要的数据

每个索引节点在磁盘文件里就是某个位置开始的一段数据而已比如说你就是从索引树的根节点开始你一通二分查找定位到下层的某个索引节点此时你肯定是知道那个索引节点在磁盘文件里的偏移量 offset 以及节点字节大小

直接再次通过偏移量 offset 定位到磁盘文件里的某个位置然后读取指定字节的大小的数据就可以把索引树里下层的那个索引节点给读取出来

MySQL 是用 B+树做磁盘上的索引树的HBase 在磁盘上的索引树用的是 LSM 树写入的时候直接就是一个顺序写天然适合 hdfs 这种仅仅支持顺序写的在 LSM 树里的 key-value 对key 就是 rowkey+列族+列+timestamp+type加上 value 就是用一个字节数组来存放一个 key-value 对

其实 LSM 树的本质就是一颗树只不过他的特点大家记住写入的时候都是顺序写入的不会有随机磁盘写

33.HFile 中的索引块的物理存储结构

在 load-on-open 部分里有一个 Root Index Block刚开始数据如果很少那么就直接索引指向 DataBlock 了但是后续数据慢慢变多就会变为两层索引Root Index Block 指向 scanned block 部分的 Leaf Index Block叶子索引块再指向 Data Block如果数据量再变大那么就会 Root Index Block 指向 non-scanned block 部分的 Interediate Index Block再指向 Leaf Index Block最终指向 Data Block

这就是索引块的数据结构整个组成索引树的过程和结构就是一颗 LSM 树

到底如何索引的其实每一层索引块从 Root Index Block 开始都是存放的是 Block OffsetBlockDataSizeBlockKey就是下层 block 的偏移量以及大小通过这两个就可以直接读出来一个块了然后最关键的就是 BlockKey也就是这个 Block 里的第一个 key

所以说在索引树里查找的过程本质上就是一个不停的二分查找的过程首先在 Root Index Block 里二分查找接着到下一层 Block 里二分查找依次类推就可以一直找到对应的 DataBlock 了

34.MemStore flush 为 HFile 文件的详细过程

如果达到了 MemStore 的 flush 条件此时就会对一个跳跃表作为 snapshot再搞一个新的跳跃表来写入新的数据snapshot 跳跃表里的数据 flush 到本地磁盘上做成一个 HFile 临时文件接着再把临时文件写入 hdfs 上的指定目录里去

在构建 HFile 文件的时候先是构建 scanned block 部分就是把 key-value 数据写成一个一个的 DataBlock同时这个过程中会生成一个一个的 Bloom Block还有 Leaf Index Block这个时候 scanned block 部分就完成了

Bloom Block 很好理解了就是对每条数据的 rowkey 做多次 hash接着映射到一个位数组的位置上设置为 1可能会有多个 Bloom Block刚开始 Bloom Block 是在内存里先构建好的还没写入 HFile 里

接着就是构建 Data Block先是在内存里构建 Data Block一旦达到 64kb 阈值则把一个 Data Block 写入 HFile 里同时每次构建好一个 Data Block 就会在 Leaf Index Block 里加一个索引条目如果 Leaf Index Block 达到 64kb 了也会写入 HFile 里去

所以说 Data Block 和 Leaf Index Block 是交互写入 HFile 里的

这个过程中Bloom Block 如果写满了 64kb 也会写入磁盘里去而且还会在 Bloom Index Block 里构建索引项

这些都搞完了以后才会去构建 non-scanned block、load-on-open 和 trailer 部分

35.HBase 客户端进行 scan 数据扫描的多次查询机制

hbase 我们发起一个查询通常是两种get 和 scanget 一般是根据 rowkey 获取一条数据scan 是可以设置一个 rowkey 范围然后还可以加一些别的条件然后可以针对一批数据进行一个检索

scan 一个 rowkey 范围拿到这张表的元数据不同的 rowkey 区间对应的 region此时就可以把 rowkey 拆分为多部分每一个部分对应一个 region然后就是对多个 region 发起 scan 扫描机制就可以了

如果是 get 一个 rowkey那就是 scan 一条数据而已本质是一样的

36.RegionServer 内部的三层 Scanner 扫描体系

RegionScanner、StoreScanner、MemStoreScanner 和 StoreFileScanner每个 Store 都对应一个 StoreScanner每一个 HFile 都对应了一个 StoreFileScanner

37.根据多种机制过滤掉不符合条件的 Scanner

每个 Store 里有那么多 HFile也就有那么多的 StoreFileScanner之前说过怎么可能每个 HFile 都搜索呢那是不可能的所以实际上会基于多种机制过滤掉一些肯定不符合条件的 HFile 的 StoreFileScanner

首先是根据 KeyRange 过滤因为每个 HFile 里存储的数据都是有顺序的所以其实你是知道每个 HFile 里存放的数据的 rowkey 范围的 此时可以跟你要搜索的 rowkey 范围做一个比较就会发现很多 HFile 肯定不包含你要找的 rowkey 范围

其次是根据 TimeRange 过滤每个 HFile 都有一个 timestamp 时间范围假设你要搜索的数据的 timestamp 时间范围跟 HFile 里的数据时间范围不符合那肯定可以淘汰掉这个 HFile 的 Scanner 了

最后就是通过布隆过滤器来判定基于 HFile 里的 load-on-open 数据部分可以快速定位到你要搜索的那部分 rowkey 在 HFile 里对应的布隆过滤器的 Bloom Block加载到内存里来然后接着对 rowkey 进行 hash 运算后寻址到 Bloom Block 的数组里的某个位置一看不是 1那一定是不在这个 HFile 里了

通过这些方案其实在一个 Store 里搜索的时候是可以快速筛选掉大部分 HFile 的 StoreFileScanner 的因为明显就是肯定不在那个 HFile 里的此时最终判定为可能包含你要搜索的 rowkey 数据的 HFile 数量肯定是比较少的

38.什么时候能够直接从 MemStore 里找到你要的数据

hbase 的集群部署和 API 试用的时候我们在 hbase 里灌入了几条数据然后搜索这几条数据很快就返回了但是当时我们在课程视频里看了一下 hdfs 里面的 hbase 目录下的数据发现似乎我们写入数据到 hbase 里以后并没有立马落地到 hdfs 里去

在那样的一个跑 demo 的试用情况下其实是可以直接从 MemStore 里读取到插入进去的数据的因为数据量太少了都没达到 flush HFile 的条件所以就导致你的数据一直都停留在 MemStore 里通过 HLog 的方式落地了 HDFS 的磁盘

刚写入 hbase 的数据肯定还没 flush 到 HFile 里去此时肯定还是在 MemStore 里的如果你立马读取你刚写入进去的数据大概率是可以直接从 MemStore 里找到数据的就走 MemStoreScanner 就可以了

一般什么时候会发生这种事情通常是类似那种流式计算的时候比如说你就是基于 hbase 做一个中间数据存储流式计算的时候一些中间状态和结果写入 hbase然后接着立马就要读取做下一轮计算此时就很可能是从 MemStore 里读到的

39.基于 HFile 中的 LSM 索引树的 index block 快速检索 rowkey

hfile 里最最核心的三块数据就是 data block、bloom block、index block在一个 hfile 文件里会包含很多的 blocks会包含不同的区域和部分通过 block 使用 offset 进行互相引用让 hfile 里的数据形成了一个整体

假设认为某个 rowkey 可能存在于一个 HFile 中此时就会直接从 load-on-open 部分的 LSM 索引树的 root index block 开始进行二分查找因为 root index block 指向了下一级索引包含了下一级 index block 的索引项和最小 rowkey所以可以快速二分查找定位到下一级索引块是哪个

以此类推就可以快速定位到这个 rowkey 可能存在的数据块里最后其实就是在数据块里遍历一下很快就可以找到你要的 rowkey 了当然也有可能是一通搜索过后实际并没有找到那个 rowkey

因为索引树最多是三层就是 root index blockintermediate level index blockleaf index block再加上 data block 就是四层其中 root index block 就在 load-on-open 里本来就在内存里

那么假设你索引树有三层最多就是磁盘 IO 三次从磁盘读取两个 index block 加上一个 data block就可以找到数据

40.如何基于 BlockCache 来减少 HFile 磁盘 IO 的频率

大部分时候其实还是走 HFile 磁盘 IO 的因为 KeyRange、TimeRange 和布隆过滤器这些仅仅是说减少你要扫描的 HFile 的数量但是最终你每次搜索数据大概率还是要搜索 HFile 的一旦走 HFile必然会走多次磁盘随机 IO读取 index block 和 data block所以说这肯定会让你的大部分 get 和 scan 请求每次都要走很多磁盘 IO

当然其实对于数据查询系统而言最好的还是要进行热数据的缓存然后查询尽量是从缓存里来走就包括 MySQL 作为关系型数据库其实也是一样的你从 MySQL 里查数据他每次都会把磁盘 IO 读取出来的数据放内存里缓存然后内存不够了就淘汰一些不怎么被访问的数据

通过这种手段尽可能的保证你很多查询都是直接从内存里读取数据

所以说HBase 其实也提供了类似的缓存机制他的缓存是 block 级别的就是对 index block 和 data block 进行缓存他会尽可能的把你常访问的 block 放在内存的 block cache 里下次如果还是扫描这些 block 数据就直接从 block cache 里走了

41.BlockCache 的 LRU 分层缓存机制设计分析

BlockCache 分为了 single-accessmulti-access 和 in-memory 三个部分比例为 25%50%25%其实每个部分都是一个 ConcurrentHashMap他就是 BlockKey 到 Block 的一个映射关系非常简单

你每次扫描 HFile 的时候读取出来的 block包括 index 和 data都是先放在 single-access 部分里的代表他们就被读取过一次如果后续再扫描的时候多次访问了这个 block就是从 BlockCache 的 single-access 部分读取了这个 block那么就会把他放入到 multi-access 里去就是多次访问了这个 block

in-memory 区域的话意思就是说常驻内存的数据假设你要是在 hbase 里有一些小表此时就可以设置列族的属性 in_memory=true此时这个列族的 block 从 HFile 里扫描出来后就是放在 in-memory 区域的

接着如果说你要是在后续的不停的扫描过程中老是把数据放入到三个区域的 map 里肯定有一天 map 会达到一个临界阈值此时就是唤醒淘汰线程分别对三个区域的 map 淘汰掉最近最少被使用的 block 就可以了通过这个方法在三个区域的 map 里总是可以放最常被访问的一些 block

这样的话其实每次你扫描的 HFile 的时候可能很多 block 不一定需要从 HFile 磁盘文件里随机 IO 读取可能就直接从 BlockCache 里就可以读取到一些 block 数据了

42.深入结合 HDFS 原理分析 data block 读取过程

其实一次 HFile 扫描无非就是根据内存里的 root index block 开始搜下层 block如果 block 在 block cache 里那么直接从 cache 里走就可以了但是当然不可能每次你都那么好运气在 cache 里找到 block 了所以很多时候还是会直接去 HFile 里读取 block 的

在 HFile 里读取 block 的时候实际上就是到 HDFS 里去读取数据了这个过程大概就是说基于 HDFS 的客户端 API就是 FSDataInputStream给他一个 block offset再各一个 blocksize让他读取这个 HFile 里指定偏移量开始的一个数据块

此时 HDFS 自己的客户端呢就会跟 NameNode 通信获取到这个 HFile 文件的 hdfs block 列表hdfs 自己的 block 是 128mb 的接着根据你的 block offset 和 block size 定位你要读取的数据在哪个 hdfs block 上接着再跟 NameNode 通信寻找 hdfs block 在哪些 datanode 机器上因为每一个 hdfs block 都是多副本冗余的

然后找一个距离自己最近的 datanode 机器建立连接接着告诉他要读取哪个 hdfs block 的 offset 和 size从一个 128mb 的 hdfs block 里的指定 offset 开始读取 64kb读取出来一个 hbase block

找到一个 hbase block 之后当然会放入 block cache 里了block cache 会自动根据 LRU 算法淘汰不怎么访问的 block保留常访问的 block这样下次可能别的查询请求就可以直接从 block cache 里获取到 block 了

43.将 MemStore 和 HFile 里找到的数据进行合并排序以及筛选

最终你找到的数据必须按照 hbase 的规则做一个合并以及排序比如说你同一个 rowkey 的多个列族的数据就会通过排序规则聚拢到相邻的地方各个 rowkey 之间都是有排序的这样的话返回给你的数据里rowkey 都是有序的

就是说你在 scan 里是可以设置一些筛选的条件的比如说我就要限定一波 rowkey 的范围同时我还要针对列族里的部分列的值进行一个筛选类似于 where name=’xx’针对列进行筛选

甚至是可以进行类似于分页之类的事情都是可以在内存里去完成的

44.完整梳理一遍 HBase 内核级的读写流程

HFile 是不会太大的hdfs 上有很多的小文件HFile 文件数量过多也不是回事儿每个 HFile 都在 RegionServer 内存里会对应一些数据结构trailer 部分和 load-on-open 部分HFile 数量过多会导致 RegionServer 里的数据结构会过多

45.为什么 HBase 底层的 HFile 要定时合并

HFile 太多了查询速度会很差不可能每次查询都走 block cache很多还是要走磁盘的要是查很多的 HFile那么性能肯定很差所以底层定期进行 hfile compaction有的是 minor compaction就合并临近的少数 hfile还有 major compaction会一下子合并一个 store 下的所有 hfile还会同时执行删除和过期失效的操作

hfile 少了布隆过滤器就少了扫描的磁盘文件就少了性能就会比较稳定否则一定是 hfile 越多查询性能越差

但是合并 hfile 的时候占用磁盘 IO 资源过多也会导致 hbase 瞬时性能较差只不过其他时候查询性能就很稳定了

46.HFile Compaction 的触发时机有哪些呢

MemStore 在进行 flush 的时候每次 flush 出来一个新的 HFile就会判断一下如果 HFile 的数量超过了 hbase.hstore.compactionThreshold 这个阈值就会触发一次 compaction触发的时候会让 Region 内所有 Store 都检查自己的 HFile 数量

还有一个后台线程会周期性的检查时间周期为 hbase.server.thread.wakefrequency * hbase.server.compactchecker.interval.multiplier每次都会检查 HFile 是否超出阈值超出就触发 compaction即使不用触发 compaction他也会去检查是否要执行 major compaction一般默认是 7 天左右会执行一次 major compaction

如果不想要自动触发 major compaction就可以把 hbase.hregion.majorcompaction 设置为 0还可以手动触发 major compaction

47.选择哪些 HFile 去执行 compaction 文件合并呢

如果触发 compaction首先是判断是否是要执行 major compaction如果是的话那么就是要合并掉所有的 HFile 文件如果不是的话那就是执行 minor compaction此时就会从最老的 HFile 文件开始遍历到最新的 HFile 文件

遍历的过程中遇到下面两个条件就会停止一个是根据选择的文件大小和剩余文件大小的一个比例高峰期是 1.2低峰期是 5去判断一个是剩余文件数小于了 3hbase.store.compaction.min默认是 3此时就停止遍历把选择的文件都拿出来进行合并

48.选择合适的线程池去执行 compaction 操作

一旦确定了要执行 compaction 的 HFile 文件接着就会选择一个线程池去执行有一个阈值hbase.regionserver.thread.compaction.throttle如果合并的文件数量超过了这个数量那么就会交给 largeCompactions 线程池处理如果小于这个数量就会交给 smallCompactions 线程池处理

两个线程池默认都只有 1 个线程可以通过 hbase.regionserver.thread.compaction.large 和 hbase.regionserver.thread.compaction.small 去配置两个线程池的线程数量

49.执行 HFile Compaction 的具体流程是什么

读取所有待合并的 HFile 里的 key-value 对进行排序然后写入到/.tmp 目录下的临时文件里去再把临时文件移动到 store 的数据目录下去接着就把这个合并好的新的 HFile 强制 sync 到 hdfs 上去

把之前合并的那些旧 HFile 文件都删除掉

读取一系列你选择好的 hfile里面都是很多的 data block里面都是 key-value 对数据读取出来重新排序依次写入新的 hfile 里的 data block在写的过程中重新组织 bloom blocksindex blocks包括新的 hflie 里的 trailer 和 load-on-open 几个部分

50.HBase 的 Region 自动分裂策略是什么

一个 Region 里的最大 Store 管理的数据总大小超过了阈值 hbase.hregion.max.filesize 之后就会触发 region 的分裂在 0.94 版本之后这个阈值实际上会自动调整他是一个表在这台 regionserver 上的 reigon 数量有关的region 数量越多那么阈值越大region 数量越小那么阈值越小

这样的话大表和小表都会自动分裂

2.0 版本之后也是这个思路做了一些基础调整

51.Region 分裂的时候是从什么位置进行的

Region 里最大的 Store 的最大的一个 HFile 的中间的一个 block 里的第一个 rowkey就以这个 rowkey 为基础进行 Region 分裂然后分裂的时候是三个阶段prepare、execute 和 rollback类似于一个事务

prepare 阶段就是内存里生成两个子 Region包括他们的名字、起止 rowkey

52.执行 Region 分裂的具体过程是什么样的

说白了就是把一个父 region 分裂为两个子 region然后父 region 下线两个子 region 一通操作在 hbase:meta 表里记录对外提供服务此时一个 region 就分裂为两个 region 了中间所有管理的数据都会进行一些分裂

假设过程有问题随时就回滚了这个过程中对父 region 的请求会失败的

53.HBase 如何保证集群里的 Region 负载均衡

hbase 会自动感知各个 regionserver 里的 region 负载是否不均衡会根据以下因素来计算region 个数region 负载读写请求数量storefile 大小memstore 大小移动数据的代价等等

如果感觉不均衡那么就直接把 region 进行迁移

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6