🔖Day 15HBase 的深度讲解

理解内容

8.6.2 读取数据流程

15-Hbase深入理解数据读写、表设计原则-LMLPHP

15-Hbase深入理解数据读写、表设计原则-LMLPHP

  1. Client访问Zookeeper,获取hbase:meta的元数据,

  2. 元数据获取后加载到内存中

  3. 通过Rowkey信息,从元数据中查找Region的相关信息

  4. RegionServer构建RegionScanner,一个RowKey的信息对应着一个RegionScanner(它是一个主键的总信息)

  5. 一个RegionScanner会生成多个storeScanner,这个主要和store的个数有关,而store的个数与列族的个数有关,一个列族对应着一个store

  6. storeScanner会排序,生成最小堆StoreHeap:PriorityQueue< StoreScanner >

  7. 一个storeScanner会生成一个缓冲区memStore和多个StoreFileScanner,这个

  8. Client访问Zookeeper,获取hbase:meta所在的RegionServer的节点信息

  9. Client访问hbase:meta所在的RegionServer,获取hbase:meta记录的元数据后先加载到内存中,然后再从内存中根据需要查询的RowKey来查询出RowKey所在的Region的相关信息(Region在RegionServer中)

  10. Client访问RowKey所在的RegionServer,发起数据读取请求

  11. RegionServer构建RegionScanner,用于对该Region的数据检索

    • 需要查询的RowKey分布在多少个Region中就需要构建多少个RegionScanner
  12. RegionScanner构建StoreScanner,用于对该列族的数据检索

    • Region中有多少个Store就需要构建多少个StoreScanner,Store的数量取决于Table的ColumnFamily的数量
  13. 多个StoreScanner合并构建最小堆(已排序的完全二叉树)StoreHeap:PriorityQueue

  14. StoreScanner构建一个MemStoreScanner和一个或多个StoreFileScanner(数量取决于StoreFile的数量)

  15. 过滤掉某些能够确定要查询的RowKey一定不在StoreFile内对于的StoreFileScanner或MEMStoreScanner

  16. 经过筛选后留下的Scanner开始做读取数据的准备,将对应的StoreFile定位到满足RowKey的起始位置

  17. 将所有的StoreFileScanner和MemStoreScanner合并构建最小堆

    • KeyValueHeap:PriorityQueue,排序的规则按照KeyValue从小到大排序
  18. 从KeyValueHeap:PriorityQueue中经过一系列筛选后一行行的得到需要查询的KeyValue。

8.6.3 写入数据流程

  1. 首先客户端和RegionServer建立连接
  2. 然后将DML要做的操作写入到日志wa-log中
  3. 将数据的修改更新到memstore中,本次的操作介绍
    • 一个region由多个store组成,一个store对应一个CF(列族),store包括位于内存中的memstore和位于磁盘中的StoreFile,写操作先写入memstore
  4. 当memstore数据写到阈值之后,创建一个新的memstore
  5. 旧的memstore写成一个独立的StoreFile,RegionServer会启动flashcache进程写入StoreFile,每次写入形成单独的一个StoreFile,存放到HDFS上
  6. 当StoreFile文件的数量增长到一定阈值后,系统会进行合
    • minor compaction
    • major compaction
  7. 在合并的过程中会进行版本合并和删除工作,形成更大的StoreFile
  8. 当一个region所有StoreFile的大小和数量超过一定阈值后,会把当前的region分割成两个,并由HMaster分配到相应的RegionServer服务器,实现负载均衡
  9. Store负载管理当前列族的数据
    • Store=1 memstore+n storefile
  10. 当我们进行数据DML的时候,以插入数据为例
    • 我们会将数据先存到memstore中,当memstore达到阈值(128M)时
    • 首先我们会创建一个新的memstore
    • 然后会将memstore中的数据写成一个StoreFile,然后StoreFile会存储到HDFS上
  11. 随着时间的推移,数据会进行合并
    • HFile中会存放大量的失效数据(删除、修改)
    • 会产生多个HFile
    • 等达到阈值(时间、数量)会进行合并
      • 多个HFile会合并成一个大的HFile
      • 合并会触发连锁反应,相邻的store也会进行合并
  12. 在Hbase中,表被分割成多个更小的块然后分散的存储在不同的服务器上,这些小块叫做
    Regions,存放Regions的地方叫做RegionServer。Master进程负责处理不同的RegionServe之间的Region的分发。在Hbase实现中HRegionServer和HRegion类代表RegionServer和Region。HRegionServer除了包含一些HRegions之外,还处理两种类型的文件用于数据存储
    • HLog 预写日志文件,也叫做WAL(write-ahead log)
    • HFile 是HDFS中真实存在的数据存储文件

8.7 数据刷写(Memstore Flush)

8.7.1触发时机

  1. Region中所有的MemStore占用的内存超过相关阈值

    • hbase.hregion.memstore.flush.size 参数控制,默认为128MB
    • 如果我们的数据增加得很快,达到了 hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier的大小hbase.hregion.memstore.block.multiplier 默认值为4,也就是128*4=512MB的时候,那么除了触发 MemStore 刷写之外,HBase 还会在刷写的时候同时阻塞所有写入该 Store的写请求
  2. 整个RegionServer的MemStore占用内存总和大于相关阈值

    • HBase 为 RegionServer 所有的 MemStore 分配了一定的写缓存(),大小等于
      • hbase_heapsize(RegionServer 占用的堆内存大小)* hbase.regionserver.global.memstore.size (默认值是 0.4)。
    • 如果整个 RegionServer 的 MemStore 占用内存总和大于阈值将会触发 MemStore 的刷写。
      • hbase.regionserver.global.memstore.size.lower.limit (默认值为 0.95)* MAX_SIZE
    • 例如:HBase 堆内存总共是 32G ,MemStore 占用内存为:32 * 0.4 * 0.95 = 12.16G将触发刷写
    • 如果达到了 RegionServer 级别的 Flush,当前 RegionServer 的所有写操作将会被阻塞,这个阻塞可能会持续到分钟级别。
  3. WAL数量大于相关阈值

    • 数据到达 Region 的时候是先写入 WAL,然后再被写到 Memstore 。
    • 如果 WAL 的数量越来越大,这就意味着 MemStore 中未持久化到磁盘的数据越来越多。
    • 当 RS 挂掉的时候,恢复时间将会变得很长,所以有必要在 WAL 到达一定的数量时进行一次刷写操作
  4. 定期自动刷写

    • 默认值 3600000(即 1 小时),HBase 定期 Flush 所有 MemStore 的时间间隔。
    • 一般建议调大,比如 10 小时,因为很多场景下 1 小时 Flush 一次会产生很多小文件,一方面导致 Flush 比较频繁,另一方面导致小文件很多,影响随机读性能
  5. 数据更新超过一定阈值

    • 如果 HBase 的某个 Region 更新的很频繁,而且既没有达到自动刷写阀值,也没有达到内存的使用限制,但是内存中的更新数量已经足够多,也会触发刷写的
    • 比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为30000000,那么也是会触发刷写的。
  6. 手动触发刷写

    • Shell 中通过执行 flush 命令
    hbase> flush 'TABLENAME'
    hbase> flush 'REGIONNAME'
    hbase> flush 'ENCODED_REGIONNAME'
    hbase> flush 'REGION_SERVER_NAME'
    
  7. 特别注意:

    • 以上所有条件触发的刷写操作最后都会检查对应的 HStore 包含的 StoreFiles 文件数是否超过hbase.hstore.blockingStoreFiles 参数配置的个数,默认值是16。
    • 如果满足这个条件,那么当前刷写会被推迟到hbase.hstore.blockingWaitTime 参数设置的时间后再刷写。
    • 在阻塞刷写的同时,HBase 还会请求 Compaction 或者Split 操作。

8.7.2 刷写策略

  1. HBASE1.1之前:
    • MemStore 刷写是 Region 级别的。就是说,如果要刷写某个 MemStore ,MemStore 所在的 Region 中其他 MemStore 也是会被一起刷写的
  2. HBASE2.x之后
    • FlushAllStoresPolicy
      • 每次刷写都是对 Region 里面所有的 MemStore 进行的
    • FlushAllLargeStoresPolicy
      • 判断 Region 中每个 MemStore 的使用内存是否大于某个阀值,大于这个阀值的MemStore 将会被刷写。
      • flushSizeLowerBound = max((long)128 / 3, 16) = 42
    • FlushNonSloppyStoresFirstPolicy

8.7.3 刷写流程

  1. prepareFlush 阶段:
    • 刷写的第一步是对 MemStore 做 snapshot(快照)
    • 为了防止刷写过程中更新的数据同时在 snapshot 和 MemStore 中而造成后续处理的困难
    • 所以在刷写期间需要持有 updateLock 。持有了 updateLock 之后,这将阻塞客户端的写操作。
    • 所以只在创建 snapshot 期间持有 updateLock
    • 而且 snapshot 的创建非常快,所以此锁期间对客户的影响一般非常小。
    • 对 MemStore 做 snapshot 是 internalPrepareFlushCache 里面进行的。
  2. flushCache 阶段:
    • 如果创建快照没问题,那么返回的 result.result 将为 null。
    • 这时候我们就可以进行下一步 internalFlushCacheAndCommit。
    • 其实 internalFlushCacheAndCommit 里面包含两个步骤:flushCache 和 commit 阶段。
      • flushCache 阶段:
        其实就是将 prepareFlush 阶段创建好的快照写到临时文件里面,临时文件是存放在对应 Region 文件夹下面的 .tmp 目录里面。
      • commit 阶段:
        将 flushCache 阶段生产的临时文件移到(rename)对应的列族目录下面,并做一些清理工作,比如删除第一步生成的 snapshot。

8.8 数据合并(Compaction)

15-Hbase深入理解数据读写、表设计原则-LMLPHP

8.8.1 合并分类

HBase 根据合并规模将 Compaction 分为了两类:MinorCompaction 和 MajorCompaction

  1. Minor Compaction

    • 是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell,但是会处理超过TTL的数据
    • 一次Minor Compaction的结果是让小的storefile变的更少并且产生更大的StoreFile。
  2. Major Compaction

    • 是指将所有的StoreFile合并成一个StoreFile
    • 清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。
    • 一般情况下,Major Compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发Major Compaction功能,改为手动在业务低峰期触发。

8.8.3 合并时机

触发compaction的方式有三种:Memstore刷盘、后台线程周期性检查、手动触发。

  1. Memstore刷盘

    • memstore flush会产生HFile文件,文件越来越多就需要compact。
    • 每次执行完Flush操作之后,都会对当前Store中的文件数进行判断,一旦文件数大于配置,就会触发compaction。
    • compaction都是以Store为单位进行的,而在Flush触发条件下,整个Region的所有Store都会执行compact
  2. 后台线程周期性检查

    • 后台线程定期触发检查是否需要执行compaction,检查周期可配置。
      • hbase.server.thread.wakefrequency(默认10000毫秒)*hbase.server.compactchecker.interval.multiplier(默认1000)
      • CompactionChecker大概是2hrs 46mins 40sec 执行一次
    • 小文件周期性合并成大文件
      • 线程先检查小文件数是否大于配置3,一旦大于就会触发compaction。
    • 大文件周期性合并成Major Compaction
      • 如果不满足,它会接着检查是否满足major compaction条件,
        • 如果当前store中hfile的最早更新时间早于某个值mcTime,
        • 就会触发major compaction(默认7天触发一次,可配置手动触发)。
  3. 手动触发

    • 一般来讲,手动触发compaction通常是为了执行major compaction,一般有这些情况需要手动触发合并
      • 是因为很多业务担心自动major compaction影响读写性能,因此会选择低峰期手动触发;
      • 也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发major
        compaction;
      • 是HBase管理员发现硬盘容量不够的情况下手动触发major compaction删除大量过期数据;

8.8.4 合并策略

承载了大量IO请求但是文件很小的HFile,compaction本身不会消耗太多IO,而且合并完成之后对读的性能会有显著提升。

线程池选择

  • HBase CompacSplitThread类内部对于Split、Compaction等操作专门维护了各自所使用的线程池和Compaction相关的是如下的longCompactions和shortCompactions
  • 前者用来处理大规模compaction,后者处理小规模compaction
    • 默认值为2 * maxFlilesToCompact * hbase.hregion.memstore.flush.size
    • 如果flush size 大小是128M,该参数默认值就是2 * 10 * 128M = 2.5G

合并策略选择

HBase 主要有两种 minor 策略: RatioBasedCompactionPolicy (0.96.x之前)和ExploringCompactionPolicy(当前默认)

  1. RatioBasedCompactionPolicy(基于比列的合并策略)

    • 从老到新逐一扫描HFile文件,满足以下条件之一停止扫描
      • 当前文件大小<比当前文件新的所有文件大小总和*ratio(高峰期1.2,非高峰期5)
      • 当前所剩候选文件数<=阈值(默认为3)
  2. ExploringCompactionPolicy策略(默认策略)

    • 基于Ratio策略,不同之处在于Ratio策略找到一个合适文件集合就停止扫描,而Exploring策略会记录所有合适的文件集合,然后寻找最优解,待合并文件数最多或者待合并文件数相同的情况下文件较小的进行合并

    15-Hbase深入理解数据读写、表设计原则-LMLPHP

  3. FIFO Compaction策略

    • 收集过期文件并删除,对应业务的列簇必须设置有TTL
  4. Tier-Based Compaction策略(分层策略)

    • 针对数据热点情况设计的策略,根据候选文件的新老程度将其划分为不同的等级,每个等级都有对应的Ratio,表示该等级文件比选择为参与Compation的概率
  5. Stripe Compation策略(条纹策略)

    • 将整个Store中的文件按照key划分为多个range,此处称为stripe,一个Stripe内部就类似于一个小Region,可以执行Minon Compation和major Compation

执行文件合并

  1. 分别读出待合并hfile文件的KV,并顺序写到位于./tmp目录下的临时文件中
  2. 将临时文件移动到对应region的数据目录
  3. 将compaction的输入文件路径和输出文件路径封装为KV写入WAL日志,并打上compaction标记,最后强制执行sync
  4. 将对应region数据目录下的compaction输入文件全部删除

8.9 数据切分(Region Split)

通过切分,一个region变为两个近似相同大小的子region,再通过balance机制均衡到不同 region server上,使系统资源使用更加均衡。

8.9.1 切分原因

  1. 数据分布不均匀
    • 同一 region server 上数据文件越来越大,读请求也会越来越多。一旦所有的请求都落在同一个 region server 上,尤其是很多热点数据,必然会导致很严重的性能问题。
  2. compaction性能损耗严重
    • compaction本质上是一个排序归并的操作,合并操作需要占用大量的内存,因此文件越大,占用内存越多
    • compaction有可能需要迁移远程数据到本地进行处理(balance之后的compaction就会存在这样的场景),如果需要迁移的数据是大文件的话,带宽资源就会损耗严重
  3. 资源耗费严重
    • HBase的数据写入量也是很很惊人的,每天都可能有上亿条的数据写入
    • 不做切分的话一个热点region的新增数据量就有可能几十G,用不了多长时间大量读请求就会把单台region server的资源耗光。

8.9.2 触发时机

  1. 触发时机
    • 每次数据合并之后都会针对相应region生成一个requestSplit请求,requestSplit首先会执行checkSplit,检测file size是否达到阈值,如果超过阈值,就进行切分。
  2. 检查阈值算法
    • 主要有两种:ConstantSizeRegionSplitPolicy( 0.94版本)和IncreasingToUpperBoundRegionSplitPolicy(当前)
    • ConstantSizeRegionSplitPolicy
      • 系统会遍历Region所有的store的文件大小,如果文件大小>hbase.hregion.max.filesize(默认10G),就会触发切分操作。
    • IncreasingToUpperBoundRegionSplitPolicy
      • 如果store大小大于一个变化的阀值就允许split。
      • 默认只有1个region,那么逻辑这个region的store大小超过 1 * 1 * 1 * flushsize * 2 =128M * 2 =256M 时,才会允许split
      • 切分之后会有两个region,其中一个region中的某个store大小大于 2 * 2 * 2 * flushsize * 2= 2048M 时,则允许split
      • 后续超过hbase.hregion.max.filesize + hbase.hregion.max.filesize * 随机小数 *
        hbase.hregion.max.filesize.jitter才允许split
      • 基本也就固定了,如果粗劣的计算可以把这个hbase.hregion.max.filesize的大小作为最后的阀值,默认是10G

8.9.3 切分流程

  1. 寻找切分点

    • 将一个region切分为两个近似大小的子region,首先要确定切分点。切分操作是基于region执行的,每个region有多个store(对应多个column famliy)。系统首先会遍历所有store,找到其中最大的一个,再在这个store中找出最大的HFile,定位这个文件中心位置对应的rowkey,作为region的切分点。
  2. 开启切分事物

    切分线程会初始化一个SplitTransaction对象,从字面上就可以看出来Split流程是一个类似于’事物’的过程,整个过程分为三个阶段:prepare-excute-rollback

    • prepare阶段
      • 在内存中初始化两个子Region,具体是生成两个HRegionInfo对象,包含tableName、RegionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录切分的进展
    • execute 阶段
      • region server 更改ZK节点 /region-in-transition 中该region的状态为SPLITING。
      • master检测到region状态改变。
      • region在存储目录下新建临时文件夹.split保存split后的daughter region信息。
      • parent region关闭数据写入并触发flush操作,将写入region的数据全部持久化到磁盘。
      • 在.split文件夹下新建两个子文件夹,称之为daughter A、daughter B,并在文件夹中生成引用文件,分别指向父region中对应文件。
      • 将daughter A、daughter B拷贝到HBase根目录下,形成两个新的region。
      • parent region通知修改 hbase.meta 表后下线,不再提供服务。
      • 开启daughter A、daughter B两个子region。
      • 通知修改 hbase.meta 表,正式对外提供服务。
    • rollback阶段
      • 如果execute阶段出现异常,则执行rollback操作。
      • 为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。

8.9.4 切分优化(预分配Region)

  1. 切分优化
    • 对于预估数据量较大的表,需要在创建表的时候根据RowKey执行Region的预分配
  2. 预分配解决的问题
    • 通过Region预分配,数据会被均衡到多态机器上,这样可以一定程度上解决热点应用数据剧增导致的性能问题

8.10 Hbase表设计要点

8.10.1 行键的设计

行健不能改变,唯一可以改变的方式是先删除后插入

1. 长度原则

  1. 长度原则

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

    • 数据的持久化文件HFile中是按照KeyValue存储的,如果RowKey过长,比如超过100字节,1000w行数据,光RowKey就业占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率
    • MemStore将缓存部分数据到内存,如果RowKey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样就会降低检索效率
    • 目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性

2. 散列原则

  1. 散列原则理解
    • 尽量将连续数据存放到更多的RegionServer上
    • 我们设计的RowKey应该均匀的分布在各个HBase节点上,避免单个RegionServer机器负载过高,引起性能下降甚至Region不可用
  2. 散列原则能够避免的问题
    • 假如RowKey是按照时间戳的方式递增,RowKey的第一部分如果是时间戳信息的话将会造成所有的新数据都在一个RegionServer上堆积的热点现象,也就是通常说的Region热点问题。
    • 热点发生在大量的client直接访问集中在个别RegionServer上(访问可能是读,写或者其他操作),导致单个RegionServer机器自身负载过高,引起性能下降甚至Region不可用,常见的是发生jvm full gc或者显示region too busy异常情况,当然这也会影响同一个RegionServer上的其他Region

3. 唯一原则

  1. 如何保证唯一原则

    • RowKey字典序排序
  2. 保证唯一原则的原因

    • 由于HBase中数据存储是Key-Value形式,若HBase中同一表插入相同Rowkey,则原先的数据会被覆盖掉(如果表的version设置为1的话),所以务必保证Rowkey的唯一性

4.数据热点

8.10.2 列族设计

8.11 HBase经典设计案例

8.12 HBase常用优化

8.13 Hive和HBase的整合

06-29 09:58