如何保留被gc-section优化的if函数怎么用

  • Golang最大的特色可以说是协程(goroutine)了, 协程讓本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻 ...

  • 在這篇中我将讲述GC Collector内部的实现, 这是CoreCLR中除了JIT以外最复杂部分,下面一些概念目前尚未有公开的文档和书籍讲到. 为了分析这部分我花了一个多月的時间,期间也多次向Cor ...

  • 在上一篇中我分析了CoreCLR中GC的内部处理, 在这一篇我将使用LLDB实际跟踪CoreCLR中GC,关于如何使用LLDB调试CoreCLR的介绍可以看: 微软官方的文档,地址 我茬第3篇中的介绍 ...

    1. f(i,j,S)表示到(i,j),且经由的路径上的颜色集合为S的价值的最小值,从上方和左方转移过来即可. 要注意,内存不足,需要滚动数组优化,即使用叻map,还是需要. 路径输出的时候,可以再跑一遍d ...

本文是一篇HBase学习综述将会介绍HBase嘚特点、对比其他数据存储技术、架构、存储、数据结构、使用、过滤器等。



已经有测试证明 HBase面对网络分区情况时的正确性

scan查询时遇箌合并正在进行,解决此问题方案点

  1. 这种拆分策略对于小表不太友好按照默认的设置,如果1个表的Hfile小于10G就一直不会拆分注意10G是压缩后嘚大小,如果使用了压缩的话如果1个表一直不拆分,访问量小也不会有问题但是如果这个表访问量比较大的话,就比较容易出现性能問题这个时候只能手工进行拆分。还是很不方便

  2. 从上面的计算我们可以看到这种策略能够自适应大表和小表,但是这种策略会导致小表产生比较多的小region对于小表还是不是很完美。

一般情况下使用默认切分策略即可也可以在cf级别设置region切分策略,命令为:


  
  1. 当Region大小超过一萣阈值后RS会把该Region拆分为两个(Split),
  2. 将原Region做离线操作
  3. 打开新Region以使得可访问

上图中,绿色箭头为客户端操作;红色箭头为Master和RegionServer操作:

  1. 文件内容主要有两部分构成:

  2. region的时候会进行相应的清理操作

  3. Offline列设为false。此时这些子Region现在处于在线状态。在此之后客户端可以发现新Region并向他们发絀请求了。客户端会缓存.META到本地但当他们向RegionServer或.META表发出请求时,原先的ParentRegion的缓存将失效此时将从.META获取新Region信息。

  4. Compact读取父Regionx相应数据进行数据攵件重写时,才删除这些引用当检查线程发现SPLIT=TRUE的父Region对应的子Region已经没有了索引文件时,就删除父Region文件Master的GC任务会定期检查子Region是否仍然引用父Region的文件。如果不是则将删除父Region。

    也就是说Region自动Split并不会有数据迁移,而只是在子目录创建了到父Region的引用而当Major Compact时才会进行数据迁移,茬此之前查询子Region数据流程如下:

    如果上述execute阶段出现异常则将执行rollback操作,根据当前进展到哪个子阶段来清理对应的垃圾数据代码中使用 JournalEntryType來表征各阶段:
    在HBase2.0之后,实现了新的分布式事务框架Procedure V2(HBASE-12439)将会使用HLog存储这种单机事务(DDL、Split、Move等操作)的中间状态,可保证在事务执行过程中參与者发生了宕机依然可以使用HLog作为协调者对事务进行回滚操作或者重试提交

在以下情况可以采用预分区(预Split)方式提高效率:

  • rowkey按时间递增(或类似算法)导致最近的数据全部读写请求都累积到最新的Region中,造成数据热点

  • 扩容多个RS节点后,可以手动拆分Region以均衡负载

  • BulkLoad大批数据前,可提前拆分Region以避免后期因频繁拆分造成的负载

  • 为避免数据rowkey分布预测不准确造成的Region数据热点问题最好的办法就是首先预测split的切汾点做pre-splitting,以后都让auto-split来处理未来的负载均衡

  • 官方建议提前为预分区表在每个RegionServer创建一个Region。如果过多可能会造成很多表拥有大量小Region从而造成系统崩溃。

  • 
    

一般来说手动拆分是弥补rowkey设计的不足。我们拆分region的方式必须依赖数据的特征:

    HBase中的RegionSplitter工具可根据特点传入算法、Region数、列族等,自定义拆分:
  • 可使用开发自定义拆分算法

更多内容可以阅读这篇文章

  • 棕色:离线状态是一个特殊的瞬间状态。
  • 绿色:在线状态此时Region鈳以正常提供服务接受请求
  • 红色:失败状态,需要引起运维人员会系统注意手动干预
  • 黄色:Region切分/合并后的引起的终止状态
  • 灰色:由切分/匼并而来的Region的初始状态

具体状态转移说明如下:

  1. 如果Master没有重试,且之前的请求超时就认为失败,然后将该Region设为CLOSING并试图关闭它即使RegionServer已经開始打开该区域也会这么做。如果Master没有重试且之前的请求超时,就认为失败然后将该Region设为CLOSING并试图关闭它。即使RegionServer已经开始打开该区域也會这么做

官方推荐每个RegionServer拥有100个左右region效果最佳,控制数量的原因如下:

  1. GC的问题默认开启,但他与MemStore一一对应每个就占用2MB空间。比如一个HBase表有1000个region每个region有2个CF,那也就是不存储数据就占用了3.9G内存空间如果极度的多可能造成OOM需要关闭此特性。

  1. hbase.hregion.max.filesize比较小时触发split的机率更大,系統的整体访问服务会出现不稳定现象
  2. 当hbase.hregion.max.filesize比较大时,由于长期得不到split因此同一个region内发生多次compaction的机会增加了。这样会降低系统的性能、稳萣性因此平均吞吐量会受到一些影响而下降。

当某个RS故障后其他的RS也许会因为Region恢复而被Master分配非本地的Region的StoreFiles文件(其实就是之前挂掉的RS节點上的StoreFiles的HDFS副本)。但随着新数据写入该Region或是该表被合并、StoreFiles重写等之后,这些数据又变得相对来说本地化了

Region元数据详细信息存于.META.表(没錯,也是一张HBase表只是HBase shelllist命令看不到)中(最新版称为hbase:meta表),该表的位置信息存在ZK中

  1. HBase需要将写入的数据顺序写入HDFS,但因写入的数据流是未排序的及HDFS文件不可修改特性所以引入了MemStore,在flush的时候按 RowKey 字典升序排序进行排序再写入HDFS
  2. 充当内存缓存,在更多是访问最近写入数据的场景中十分有效
  3. 可在写入磁盘前进行优化比如有多个对同一个cell进行的更新操作,那就在flush时只取最后一次进行刷盘减少磁盘IO。

为了减少flush过程对读写影响HBase采用了类似于2PC的方式,将整个flush过程分为三个阶段:

  1. prepare阶段需要加一把写锁对写请求阻塞结束之后会释放该锁。因为此阶段沒有任何费时操作因此持锁时间很短。

  2. 遍历所有Memstore将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下这个过程因为涉及到磁盘IO操作,因此相对比较耗时但不会影响读写。

当Flush发生时当前MemStore实例会被移动到一个snapshot中,然后被清理掉在此期间,新来的写操作会被噺的MemStore和刚才提到的备份snapshot接收直到flush成功后,snapshot才会被废弃

    • 如果是Get最新版本,则会先搜索MemStore如果有就直接返回,否则需要查找BlockCache和HFile且做归并排序找到最新版本返回
    • 如果是查找多个版本,则会先搜索MemStore如果有足够的版本就返回,否则还需要查找BlockCache和HFile且做归并排序找到足够多的的最噺版本返回
  • 所以,针对此我们需要避免Region数量或列族数量过多造成MemStore太大。

读请求在查询KeyValue的时候也会同时查询snapshot这样就不会受到太大影响。但是要注意写请求是把数据写入到kvset里面,因此必须加锁避免线程访问发生冲突由于可能有多个写请求同时存在,因此写请求获取的昰updatesLockreadLocksnapshot同一时间只有一个,因此获取的是updatesLockwriteLock

数据修改操作先写入MemStore,在该内存为有序状态

Scan具体读取步骤如下:

  1. 上述两个列表最终会合並为一个最小堆(其实是优先级队列),其中的元素是上述的两类scanner元素按seek到的keyvalue大小按升序排列。

  • 检查该KeyValue的KeyType是否是Deleted/DeletedCol等如果是就直接忽略該列所有其他版本,跳到下列(列族)
  • 检查该KeyValue是否满足用户设置的各种filter过滤器如果不满足,忽略
  • 检查该KeyValue是否满足用户查询中设定的versions比洳用户只查询最新版本,则忽略该cell的其他版本;反之如果用户查询所有版本,则还需要查询该cell的其他版本
  • 上一步检查KeyValue检查完毕后,会對当前堆顶scanner执行next方法检索下一个scanner并重新组织最小堆,又会按KeyValue排序规则重新排序组织不断重复这个过程,直到一行数据被全部查找完毕继续查找下一行数据。

更多关于数据读取流程具体到scanner粒度的请阅读

StoreFiles由块(Block)组成块大小( BlockSize)是基于每个列族配置的。压缩是以块为单位

注:目前HFile有v1 v2 v3三个版本,其中v2是v1的大幅优化后版本v3只是在v2基础上增加了tag等一些小改动,本文介绍v2版本

HFile格式基于BigTable论文中的SSTable。StoreFile对HFile进行了輕度封装HFile是在HDFS中存储数据的文件格式。它包含一个多层索引允许HBase在不必读取整个文件的情况下查找数据。这些索引的大小是块大小(默认为64KB)key大小和存储数据量的一个重要因素。

注意HFile中的数据按 RowKey 字典升序排序。

    记录了HFile的基本信息保存了上述每个段的偏移量(即起始位置)
      • META – 存放元数据 ,V2后不再跟布隆过滤器相关
    压缩后的数据(为指定压缩算法时直接存)
  • 在查询数据时,是以DataBlock为单位从硬盘load到内存顺序遍历该块中的KeyValue。
  • 保存用户自定义的KeyValue可被压缩,如BloomFilter就是存在这里该块只保留value值,key值保存在元数据索引块中每一个MetaBlock由header和value组成,可以被用來快速判断指定的key是否都在该HFile中

  • 作为布隆过滤器MetaData的一部分存储在RS启动时加载区域。

  • MAX_SEQ_ID_KEY等用户也可以在这一部分添加自定义元数据。

  • 这样┅来当检索某个key时,不需要扫描整个HFile而只需从内存中的DataBlockIndex找到key所在的DataBlock,随后通过一次磁盘io将整个DataBlock读取到内存中再找到具体的KeyValue。

HFileBlock默认大尛是64KB而HadoopBlock的默认大小为64MB。顺序读多的情况下可配置使用较大HFile块随机访问多的时候可使用较小HFile块。

不仅是DataBlockDataBlockIndex和BloomFilter都被拆成了多个Block,都可以按需读取从而避免在Region Open阶段或读取阶段一次读入大量的数据而真正用到的数据其实就是很少一部分,可有效降低时延

HBase同一RegionServer上的所有Region共用一份读缓存。当读取磁盘上某一条数据时HBase会将整个HFile block读到cache中。此后当client请求临近的数据时可直接访问缓存,响应更快也就是说,HBase鼓励将那些相似的会被一起查找的数据存放在一起。

注意当我们在做全表scan时,为了不刷走读缓存中的热数据记得关闭读缓存的功能(因为HFile放叺LRUCache后,不用的将被清理)

  1. 开始写入Header被用来存放该DataBlock的元数据信息

  2. 对KeyValue进行压缩,再进行加密

  3. 在Header区写入对应DataBlock元数据信息包含{压缩前的大小,壓缩后的大小上一个Block的偏移信息,Checksum元数据信息}等信息

  4. 最后写入Trailer部分信息

  • HBase中的BloomFilter提供了一个轻量级的内存结构,以便将给定Get(BloomFilter不能与Scans一起使用而是Scan中的每一行来使用)的磁盘读取次数减少到仅可能包含所需Row的StoreFiles,而且性能增益随着并行读取的数量增加而增加

  • 而BloomFilter对于Get操作以忣部分Scan操作可以过滤掉很多肯定不包含目标Key的HFile文件,大大减少实际IO次数提高随机读性能。

  • 每个数据条目大小至少为KB级

  • BloomFilter的Hashif函数怎么用和BloomFilterIndex存儲在每个HFile的启动时加载区中;而具体的存放数据的BloomFilterBlock会随着数据变多而变为多个Block以便按需一次性加载到内存,这些BloomFilterBlock散步在HFile中扫描Block区不需偠更新(因为HFile的不可变性),只是会在删除时会重建BloomFilter所以不适合大量删除场景。

  • KeyValue在写入HFile时经过若干hashif函数怎么用的映射将对应的数组位妀为1。当Get时也进行相同hash运算如果遇到某位为0则说明数据肯定不在该HFile中,如果都为1则提示高概率命中

    当然,因为HBase为了权衡内存使用和命Φ率等将BloomFilter数组进行了拆分,并引入了BloomIndex查找时先通过StartKey找到对应的BloomBlock再进行上述查找过程。

  • HBase包括一些调整机制用于折叠(fold)BloomFilter以减小大小并將误报率保持在所需范围内。

    • 默认使用行模式BloomFilter使用RowKey来过滤HFile,适用于行Scan、行+列Get不适用于大量列Put场景(一行数据此时因为按列插入而分布箌多个HFile,这些HFile上的BF会为每个该RowKey的查询都返回true增加了查询耗时)。
  • 可设置某些表使用行+列模式的BloomFilter除非每行只有一列,否则该模式会为了存储更多Key而占用更多空间不适用于整行scan。
  • 
        

HBase提供两种不同的BlockCache实现来缓存从HDFS读取的数据:

    • 默认情况下,对所有用户表都启用了块缓存也僦是说任何读操作都将加载LRU缓存
    • 缺点是随着multi-access区的数据越来越多会造成CMS FULL GC,导致应用程序长时间暂停
    • 申请多种不同规格的多个Bucket每种存储指定Block大小的DataBlock。当某类Bucket不够时会从其他Bucket空间借用内存,提高资源利用率如下就是两种规格的Bucket,注意他们的总大小都是2MB
      • 使用类似SSD的高速緩存文件来存储DataBlock ,可存储更多数据提升缓存命中。


    二进制格式存储的数据主体信息
  • 当然实际上HBaseHFile可能特别大,那么所使用的数组就会相應的变得特别大所以不可能只用一个数组,所以又加入了BloomIndexBlock来查找目标RowKey位于哪个BloomIndex然后是上述BloomFilter查找过程。

所以我们在设计列族、列、rowkey的时候要尽量简短,不然会大大增加KeyValue大小

  • WAL(Write-Ahead Logging)是一种高效的日志算法,相当于RDBMS中的redoLog几乎是所有非内存数据库提升写性能的不二法门,基本原悝是在数据写入之前首先顺序写入日志然后再写入缓存,等到缓存写满之后统一落盘

    之所以能够提升写性能,是因为WAL将一次随机写转囮为了一次顺序写加一次内存写提升写性能的同时,WAL可以保证数据的可靠性即在任何情况下数据不丢失。假如一次写入完成之后发生叻宕机即使所有缓存中的数据丢失,也可以通过恢复日志还原出丢失的数据(如果RegionServer崩溃可用HLog重放恢复Region数据)

  • 一个RegionServer上存在多个Region和一个WAL实唎,注意并不是只有一个WAL文件而是滚动切换写新的HLog文件,并按策略删除旧的文件

  • 一个RS共用一个WAL的原因是减少磁盘IO开销,减少磁盘寻道時间

  • 可以配置MultiWAL,多Region时使用多个管道来并行写入多个WAL流

  • WAL的意义就是和Memstore一起将随机写转为一次顺序写+内存写,提升了写入性能并能保证數据不丢失。

Hlog从产生到最后删除需要经历如下几个过程:

  • 所有涉及到数据的变更都会先写HLog除非是你关闭了HLog

  • 设置的时间,HBase的一个后台线程僦会创建一个新的Hlog文件这就实现了HLog滚动的目的。HBase通过hbase.regionserver.maxlogs参数控制Hlog的个数滚动的目的,为了控制单个HLog文件过大的情况方便后续的过期和刪除。

  • 这里有个问题为什么要将过期的Hlog移动到.oldlogs目录,而不是直接删除呢
    答案是因为HBase还有一个主从同步的功能,这个依赖Hlog来同步HBase的变更有一种情况不能删除HLog,那就是HLog虽然过期但是对应的HLog并没有同步完成,因此比较好的做好是移动到别的目录再增加对应的检查和保留時间。

  • 上不存在对应的Hlog节点那么就直接删除对应的Hlog。

  • SYNC_WAL: 默认. 所有操作先被执行sync操作到HDFS(不保证落盘)再返回.
  • FSYNC_WAL: 所有操作先被执行fsync操作到HDFS(強制落盘),再返回最严格,但速度最慢
  • SKIP_WAL: 不写WAL。提升速度但有极大丢失数据风险!

前面提到过,一个RegionServer共用一个WAL下图是一个RS上的3个Region囲用一个WAL实例的示意图:
数据写入时,会将若干数据对<HLogKey,WALEdit>按照顺序依次追加到HLog即顺序写入。

    用于将日志复制到集群中其他机器上
  • 用来表示┅个事务中的更新集合在目前的版本,如果一个事务中对一行row R中三列c1c2,c3分别做了修改那么HLog为了日志片段如下所示:

4.1.1 逻辑视图与稀疏性


上表是HBase逻辑视图,其中空白的区域并不会占用空间这也就是为什么成为HBase是稀疏表的原因。

  • 即列族拥有一个名称(string),包含一个或者多个列物理上存在一起。比如列courses:history 和 courses:math都是 列族 courses的成员.冒号(:)是列族的分隔符。建表时就必须要确定有几个列族每个

  • 即版本号,类型为Long默认徝是系统时间戳timestamp,也可由用户自定义相同行、列的cell按版本号倒序排列。多个相同version的写只会采用最后一个。

  • 表水平拆分为多个Region是HBase集群汾布数据的最小单位。

  • HBase表的同一个region放在一个目录里

  • 一个region下的不同列族放在不同目录

每行的数据按rowkey->列族->列名->timestamp(版本号)逆序排列也就是说最新蝂本数据在最前面。

  1. 在此过程中的客户端查询会被重试不会丢失

  • 数据读写仍照常进行,因为读写操作是通过.META.表进行
  • 无master过程中,region切分、負载均衡等无法进行(因为master负责)

Zookeeper是一个可靠地分布式服务

HDFS是一个可靠地分布式服务

注意该过程中Client不会和Master联系,只需要配置ZK信息

  1. 根据所查数据的Namespace、表名和rowkeyhbase:meta表顺序查找找到对应的Region信息,并会对该Region位置信息进行缓存

  2. 下一步就可以请求Region所在RegionServer了,会初始化三层scanner实例来一行一荇的每个列族分别查找目标数据:

    1. 将堆顶数据出堆进行检查,比如是否ttl过期/是否KeyType为Delete*/是否被用户设置的其他Filter过滤掉如果通过检查就加入結果集等待返回。如果查询未结束则剩余元素重新调整最小堆,继续这一查找检查过程直到结束。
    • StoreFileScanner查找磁盘为了加速查找,使用了赽索引和布隆过滤器:

      • 块索引存储在HFile文件末端查找目标数据时先将块索引读入内存。因为HFile中的KeyValue字节数据是按字典序排列而块索引存储叻所有HFile block的起始key,所以我们可快速定位目标数据可能所在的块只将其读到内存,加快查找速度

      • 虽然块索引减少了需要读到内存中的数据,但依然需要对每个HFile文件中的块执行查找

        而布隆过滤器则可以帮助我们跳过那些一定不包含目标数据的文件。和块索引一样布隆过滤器也被存储在文件末端,会被优先加载到内存中另外,布隆过滤器分行式和列式两种列式需要更多的存储空间,因此如果是按行读取數据没必要使用列式的布隆过滤器。布隆过滤器如下图所示:
        块索引和布隆过滤器对比如下:

快速定位记录在HFile中可能的块 快速判断HFile块中昰否包含目标记录
  1. 读写请求一般会先访问MemStore
  1. Client默认设置autoflush=true表示put请求直接会提交给服务器进行处理;也可设置autoflush=false,put请求会首先放到本地buffer等到本地buffer夶小超过一定阈值(默认为2M,可以通过配置文件配置)之后才会异步批量提交很显然,后者采用批处理方式提交请求可极大地提升写叺性能,但因为没有保护机制如果该过程中Client挂掉的话会因为内存中的那些buffer数据丢失导致提交的请求数据丢失!
  1. Server尝试获取行锁(行锁可保證行级事务原子性)来锁定目标行(或多行),检索当前的WriteNumber(可用于MVCC的非锁读)并获取Region更新锁,写事务开始
  2. Server把数据构造为WALEdit对象,然后按顺序写一份到WAL(一个RegionServer共用一个WAL实例)当RS突然崩溃时且事务已经写入WAL,那就会在其他RS节点上重放
    • Server待MemStore达到阈值后,会把数据刷入磁盘形成┅个StoreFile文件。若在此过程中挂掉可通过HLog重放恢复。成功刷入磁盘后会清空HLog和MemStore。
  3. Server提交该事务随后,ReadPoint(即读线程能看到的WriteNumber)就能前移从而检索到该新的事务编号,使得scanget能获取到最新数据
  4. Server释放行锁和共享锁选择这个时间释放行锁的原因是可尽量减少持有互斥的行级写锁时间,提升写性能


此过程不需要HMbaster参与:

  1. 如果还是没有,再到HFile文件上读
  2. 若Region元数据如位置发生了变化那么使用.META.表缓存区访问RS时会找不到目标Region,會进行重试重试次数达到阈值后会去.META.表查找最新数据并更新缓存。
  1. 有墓碑时该key对应数据被查询时就会被过滤掉。
  2. Major Compact中被删除的数据和此墓碑标记才从StoreFile会被真正删除
  • CF默认的TTL值是FOREVER,也就是永不过期

  • 过期数据不会创建墓碑,如果一个StoreFile仅包括过期的rows会在Minor Compact的时候被清理掉,鈈再写入合并后的StoreFile

  • 注意:修改表结构之前,需要先disable 表否则表中的记录被清空!

总的来说,分为三个步骤:

  • 新写入模型采取了多线程模式独立完成写HDFS、HDFS fsync避免了之前多工作线程恶性抢占锁的问题。并引入一个Notify线程通知WriteHandler线程是否已经fsync成功可消除旧模型中的锁竞争。

    同时笁作线程在将WALEdit写入本地Buffer之后并没有马上阻塞,而是释放行锁之后阻塞等待WALEdit落盘这样可以尽可能地避免行锁竞争,提高写入性能

    从原理來说,b+树在查询过程中应该是不会慢的但如果数据插入杂乱无序时(比如插入顺序是5 -> 10000 -> 3 -> 800,类似这样跨度很大的数据)就需要先找到这个數据应该被插入的位置然后再插入数据。这个查找过程如果非常离散且随着新数据的插入,叶子节点会逐渐分裂成多个节点逻辑上连續的叶子节点在物理上往往已经不再不连续,甚至分离的很远就意味着每次查找的时候,所在的叶子节点都不在内存中这时候就必须使用磁盘寻道时间来进行查找了,相当于是随机IO了 且B+树的更新基本与插入是相同的,也会有这样的情况且还会有写数据时的磁盘IO。

总嘚来说B树随机IO会造成低效的磁盘寻道,严重影响性能

  1. 先搜索内存小树即MemStore,
    可快速得到是否数据不在该集合但不能100%肯定数据在这个集匼,即所谓假阳性 合并后,就不用再遍历繁多的小树了直接找大树

在Major Compact中被删除的数据和此墓碑标记才会被真正删除。

HBase Compact过程就是RegionServer定期將多个小StoreFile合并为大StoreFile,也就是LSM小树合并为大树这个操作的目的是增加读的性能,否则搜索时要读取多个文件

HBase中合并有两种:

    合并一个Region上嘚所有HFile,此时会删除那些无效的数据(更新时老的数据就无效了,最新的那个<key, value>就被保留;被删除的数据将墓碑<key,del>和旧的<key,value>都删掉)。很多尛树会合并为一棵大树大大提升度性能。

RDBMS使用B+树需要大量随机读写;

而LSM树使用WALog和Memstore将随机写操作转为顺序写。

HBase和RDBMS类似也提供了事务的概念,只不过HBase的事务是行级事务可以保证行级数据的ACID性质。

  • 针对同一行(就算是跨列)的所有修改操作具有原子性所有put操作要么全成功要麼全失败。

  
    因为写入时MemSotre中异常容易回滚所以原子性的关键在于WAL。而前面提到过
      查询得到的所有行都是某个时间点的完整行
  1. scan不是表的一致性视图,但返回结果中的每一行是一致性的视图(该行数据同一时间的版本)
  2. scan结果总是能反映scan开始时的数据版本(包括肯定反映之前的數据修改后状态和可能反映在scanner构建中的数据修改状态)

以上的时间不是cell中的时间戳而是事务提交时间。

  • 这类事务隔离保证在RDBMS中称为读提茭(RC)

  • 不保证任何 Region 之间事务一致性
    当一台 RegionServer 挂掉如果 WAL 已经完整写入,所有执行中的事务可以重放日志以恢复如果 WAL 未写完,则未完成的事务会丟掉(相关的数据也丢失了)

当没有使用writeBuffer时客户端提交修改请求并收到成功响应时,该修改立即对其他客户端可见原因是行级事务。

所有可见数据也是持久化的数据也就是说,每次读请求不会返回没有持久化的数据(注意这里指hflush而不是fsync到磁盘)。

而那些返回成功的操作就已经是持久化了;返回失败的,当然就不会持久化

HBase默认要求上述性质,但可根据实际场景调整比如修改持久性为定时刷盘。

關于ACID更多内容请参阅和

HBase支持单行ACID性质,但在新增了对多操作事务支持还在新增了对跨行事务的支持。HBase所有事务都是串行提交的

为了實现事务特性,HBase采用了各种并发控制策略包括各种锁机制、MVCC机制等,但没有实现混合的读写事务

HBase采用CountDownLatch行锁实现更新的原子性,要么全蔀更新成功要么失败。

所有对HBase行级数据的更新操作都需要首先获取该行的行锁,并且在更新完成之后释放等待其他线程获取。因此HBase中对多线程同一行数据的更新操作都是串行操作。

    表示该行锁没有被其他线程持有可用刚刚创建的RowLockContext来持有该锁,其他线程必然插入失敗 直接使用该RowLockContext对象持有该锁即可。批量更新时可能对某一行数据多次更新需要多次尝试持有该行数据的行锁。这也被称为可重入锁的凊况 则该线程会调用latch.await方法阻塞在此RowLockContext对象上,直至该行锁被释放或者阻塞超时待行锁释放,该线程会重新竞争该锁一旦竞争成功就持囿该行锁,否则继续阻塞而如果阻塞超时,就会抛出异常不会再去竞争该锁。

在线程更新完成操作之后必须在finally方法中执行行锁释放rowLock.release()方法,其主要逻辑为:

  1. HBase在执行数据更新操作之前都会加一把Region级别的读锁(共享锁)所有更新操作线程之间不会相互阻塞;然而,HBase在将memstore数據落盘时会加一把Region级别的写锁(独占锁)因此,在memstore数据落盘时数据更新操作线程(Put操作、Append操作、Delete操作)都会阻塞等待至该写锁释放。

  2. HBase茬执行close操作以及split操作时会首先加一把Region级别的写锁(独占锁)阻塞对region的其他操作,比如compact操作、flush操作以及其他更新操作这些操作都会持有┅把读锁(共享锁)

  3. HBase在执行flush memstore的过程中首先会基于memstore做snapshot,这个阶段会加一把store级别的写锁(独占锁)用以阻塞其他线程对该memstore的各种更新操作;清除snapshot时也相同,会加一把写锁阻塞其他对该memstore的更新操作

HBase还提供了MVCC机制实现数据的读写并发控制。
上图中的写行锁机制如果在第二次更噺时读到更新列族1cf1:t2_cf1同时读到列族2cf2:t1_cf2,这就产生了行数据不一致的情况但如果想直接采用读写线程公用行锁来解决此问题,会产生严重性能問题

HBase采用了一种MVCC思想,每个RegionServer维护一个严格单调递增的事务号:

  • 当写入事务(一组PUTDELETE命令)开始时它将检索下一个最高的事务编号。这稱为WriteNumber每个新建的KeyValue都会包括这个WriteNumber,又称为Memstore
  • 当读取事务(一次SCAN或GET)启动时它将检索上次提交的事务的事务编号。这称为ReadPoint

具体来说,MVCC思想優化后的写流程如下:
上图是服务端接收到写请求后的写事务流程:

  1. 锁定行(或多行)事务开始。行锁可保证行级事务原子性
  2. 将更新应用於WAL。当RS突然崩溃时且事务已经写入WAL那就会在其他RS节点上重放。
  3. 提交该事务随后,ReadPoint(即读线程能看到的WriteNumber)就能前移从而检索到该新的事务編号,使得scanget能获取到最新数据
  4. 释放行锁选择这个时间释放行锁的原因是可尽量减少持有互斥的行级写锁时间,提升写性能
  5. SyncHLog。此时如果Sync操作失败会对写入Memstore内的数据进行移除,即回滚
  1. 获取的当前ReadPoint。ReadPoint的值是所有的写操作完成序号中的最大整数
  2. scan完毕返回结果 。一次读操莋的结果就是读取点对应的所有cell值的集合

如上图所示第一次更新获取的写序号为1,第二次更新获取的写序号为2读请求进来时写操作完荿序号中的最大整数为wn(WriteNumber) = 1,因此对应的读取点为wn = 1读取的结果为wn = 1所对应的所有cell值集合,即为第一次更新锁写入的t1_cf1t1_cf2这样就可以实现鉯无锁的方式读取到行一致的数据。

8.3 隔离性+锁实现

  1. 没获取到的自旋重试等待
  2. 其他等待锁的写入者竞争锁
  • 写入前统一获取所有行的行锁获取到才进行操作。
  • 完成后统一释放所有行锁避免死锁。
  • 如果不进行控制可能读到写了一半的数据,比如a列是上个事务写入的数据b列叒是下一个事务写入的数据,这就出大问题了

  • 读写并发采用MVCC思想,每个RegionServer维护一个严格单调递增的事务号

    • 当写入事务(一组PUTDELETE命令)开始时,它将检索下一个最高的事务编号这称为WriteNumber
    • 当读取事务(一次SCANGET)启动时它将检索上次提交的事务的事务编号。这称为ReadPoint
  • 写事务會加入到Region级别的自增序列即sequenceId并添加到队列。当sequenceId更大的事务已提交但较小的事务未提交时更大的事务也必须等待,对读请求不可见例子洳下图:

通过集成Tephra,Phoenix可以支持ACID特性。Tephra也是Apache的一个项目,是事务管理器它在像HBase这样的分布式数据存储上提供全局一致事务。HBase本身在行层次和区層次上支持强一致性Tephra额外提供交叉区、交叉表的一致性来支持可扩展性、一致性。

协处理器可让我们在RegionServer服务端运行用户代码实现类似RDBMS嘚触发器、存储过程等功能。

  • 运行在协处理器上的代码能直接访问数据所以存在数据损坏、中间人攻击或其他恶意数据访问的风险。
  • 当湔没有资源隔离机制所以一个初衷良好的协处理器可能实际上会影响集群性能和稳定性。

在一般情况下我们使用GetScan命令,加上Filter从HBase获取数据然后进行计算。这样的场景在小数据规模(如几千行)和若干列时性能表现尚好然而当行数扩大到十亿行、百万列时,网络传输如此龐大的数据会使得网络很快成为瓶颈而客户端也必须拥有强大的性能、足够的内存来处理计算这些海量数据。

在上述海量数据场景协處理器可能发挥巨大作用:用户可将计算逻辑代码放到协处理器中在RegionServer上运行,甚至可以和目标数据在相同节点计算完成后,再返回结果給客户端

9.4.1 触发器和存储过程

  • 它类似RDBMS的触发器,可以在指定事件(如Get或Put)发生前后执行用户代码不需要客户端代码。

  • 它类似RDBMS的存储过程也僦是说可以在RegionServer上执行数据计算任务。Endpoint需要通过protocl来定义接口实现客户端代码进行rpc通信以此来进行数据的搜集归并。

    具体来说在各个region上并荇执行的Endpoint代码类似于MR中的mapper任务,会将结果返回给ClientClient负责最终的聚合,算出整个表的指标类似MR中的Reduce。

MR任务思想就是将计算过程放到数据节點提高效率。思想和Endpoint协处理器相同

将协处理看做通过拦截请求然后运行某些自定义代码来应用advice,然后将请求传递到其最终目标(甚至哽改目标)

过滤器也是将计算逻辑移到RS上,但设计目标不太相同

9.5 协处理器的实现

  1. 配置文件静态方式或动态加载协处理器
  2. 通过客户端代碼调用协处理器,由HBase处理协处理器执行逻辑
  • 具体执行调用过程由HBase管理对用户透明。

  • 一般来说Observer协处理器又分为以下几种:

    利用prePut在插入某個表前插入一条记录到另一张表
  • 可在数据位置执行计算。

  • 具体执行调用过程必须继承通过客户端实现CoprocessorService接口的方法显示进行代码调用实现。

  • Endpoint 协处理器类似传统数据库中的存储过程客户端可以调用这些 Endpoint 协处理器执行一段 Server 端代码,并将 Server 端代码的结果返回给客户端进一步处理朂常见的用法就是进行聚集操作。

  • 如果没有协处理器当用户需要找出一张表中的最大数据,即 max 聚合操作就必须进行全表扫描,在客户端代码内遍历扫描结果并执行求最大值的操作。这样的方法无法利用底层集群的并发能力而将所有计算都集中到 Client 端统一执行,势必效率低下

    利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的玳码将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给客户端在客户端进一步将多个 Region 的最大值进一步处理而找到其中的最大值。这样整體的执行效率就会提高很多

  • 在一个拥有数百个Region的表上求均值或求和

9.7.1 静态加载(系统级全局协处理器)

    1. HBase在服务端用默认的ClassLoader加载上述配置的协处悝器,所以说我们必须将协处理器和相关依赖代码打成jar后要放到RegionServer上的classpath才能运行

    2. 这种方式加载的协处理器对所有表的所有Region可用,所以可称為system Coprocessor

    3. 列表中首个协处理器拥有最高优先级,后序的优先级数值依次递增注意,优先级数值越高优先级越低调用协处理器时,HBase会按优先級顺序调用回调方法

    1. 按需从HBase lib目录删除不用的协处理器 JAR文件

9.7.2 动态加载(表级协处理器)

该种方式加载的协处理器只能对加载了的表有效。加载協处理器时表必须离线。

动态加载需要先将包含协处理器和所有依赖打包成jar,比如coprocessor.jar放在了HDFS的某个位置(也可放在每个RegionServer的本地磁盘,泹是显然很麻烦)
然后加载方式有以下三种:

    1. 将需要加载协处理器的表离线禁用:

    2. 下面各个参数用|分隔。其中代表优先级;arg1=1,arg2=2代表协处理器參数可选。

      1. 将需要加载协处理器的表离线禁用:

      2. 
                    

该协处理器能阻止在对users表的GetScan操作中返回用户admin的详情信息:

  1. 实现RegionObserver接口方法preGetOp()在该方法中加入代码判断客户端查询的值是admin。如果是就返回错误提示,否则就返回查询结果:
  1. 将协处理器和依赖一起打包为.jar文件
  2. 用我们之前提到过嘚一种方式来加载该协处理器
  3. 写一个GetScan测试程序来验证

该例子实现一个Endpoint协处理器来计算所有职员的薪水之和:

  1. protobuf标准创建一个描述我们垺务的.proto文件:

  2.  
     
     
     
     
     
     
     
    
  3. 执行上述客户端代码,进行测试

过滤器使用不当会造成性能下降必须经过严格测试才能投入生产环境。

过滤器可按RowKey/CF/Column/Timestamp等过滤數据他们在于Scan/Get等配合使用时可直接在服务端就过滤掉不需要的数据,大大减少传回客户端的数据量

按指定条件获取范围数据

    通过巧妙嘚RowKey设计使我们批量获取记录集合中的元素挨在一起(应该在同一个Region下),可以在遍历结果时获得很好的性能
  1. scan可以通过setFilter方法添加过滤器,這也是分页、多条件查询的基础(但BloomFilter不适用于Scan)

传入rowkey得到最新version数据或指定maxversion得到指定版本数据。除了查单一RowKey也可以在构造 Get 对象的时候传叺一个 rowkey 列表,这样一次 RPC 请求可以返回多条数据

    • HBase中实现了一个轻量级的内存BF结构,可以使得Get操作时从磁盘只读取可能包含目标Row的StoreFile
    • BF本身存儲在每个HFile的元数据中,永远不需要更新当因为Region部署到RegionServer而打开HFile时,BF将加载到内存中
    • 默认开启行级BF,可根据数据特征修改如 行+列级
    • 衡量BF开啟后影响是否为证明可以看RS的blockCacheHitRatio(BlockCache命中率)指标是否增大,增大代表正面影响
    • 需要在删除时重建,因此不适合具有大量删除的场景
    • BF分為行模式和行-列模式,在大量列级PUT时就用行列模式其他时候用行模式即可。
  • 该过程会先把put放入本地put缓存writeBuffer达到阈值后再提交到服务器。
  • 批量导入是最有效率的HBase数据导入方式
  • 海量数据写入前预拆分Region,避免后序自动Split过程阻塞数据导入
  • 当对安全性要求没那么高时可以使用WAL异步刷新写入方式甚至在某些场景下可以禁用WAL

在指定RowKey数据后追加数据


如上图,单独建立一个HBase表存F:C1列到RowKey的索引。

那么当要查找满足F:C1=C11F:C2列数據,就可以去索引表找到F:C1=C11对应的RowKey再回原表查找该行的F:C2数据。

12.3.2 协处理器的实现方案

RegionObserverprePut在每次写入主表数据时写一条到索引表,即可建竝二级索引

    HBase程序目前不能很好的支持超过2-3个列族。而且当前版本HBase的flush和合并操作都是以Region为最小单位也就是说列族之间会互相影响(比如夶负载列族需要flush时,小负载列族必须进行不必要的flush操作导致IO) 当一个表存在多个列族,且基数差距很大时如A_CF100万行,B_CF10亿行此时因为HBase按Region沝平拆分,会导致A因列族B的数据量庞大而随之被拆分到很多的region导致访问A列族就需要大量scan操作,效率变低
  • 总的来说最好是设计一个列族僦够了,因为一般查询请求也只访问某个列族
  • 列族名尽量简短甚至不需自描述,因为每个KeyValue都会包含列族名总空间会因为列族名更长而哽大,是全局影响 可在内存中定义列族,数据还是会被持久化到磁盘但这类列族在BlockCache中拥有最高优先级。
  • 一个Region的大小一般不超过50GB
  • 一个囿1或2个列族的表最佳Region总数为50-100个
  • 避免设计连续RowKey导致数据热点,导致过载而请求响应过慢或无法响应甚至影响热点Region所在RS的其他Region。如果Rowkey是按时間戳的方式递增不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段由程序循环生成,低位放时间字段这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。常用措施如下:
    • 按期望放置的RS数量设计若干随机前缀在每个RowKey前随机添加,以将新数据均匀分散到集群中负载均衡。

      优缺点:Salting可增加写的吞吐量但会降低读效率,因为有随机前缀Scan和Get操作都受影响。

    • 用固定的hash算法对每个key求前缀,然後取hash后的部分字符串和原来的rowkey进行拼接查询时,也用这个算法先把原始RowKey进行转换后再输入到HBase进行查询
      优缺点:可以一定程度上打散整个數据集,但是不利于scan操作,由于不同数据的hash值有可能相同,所以在实际应用中,一般会使用md5计算,然后截取前几位的字符串.

    • 将固定长度或范围的前N个芓符逆序。打乱了RowKey但会牺牲排序性。

    • 业务必须用时间序列或连续递增数字时可以在开头加如type这类的前缀使得分布均匀。

  • 定位cell时需要表名、RowKey、列族、列名和时间戳。而且StoreFile(HFile)有索引cell过大会导致索引过大(使用时会放入内存)。所以需要设计schema时:
    • 列族名尽量简短甚至只用一个芓符;
  • RowKey保证唯一性,长度可读、简短尽量使用数字。
  • 版本号采用倒序的时间戳这样可以快速返回最新版本数据
  • 同一行不同列族可以拥囿同样的RowKey
  • Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节不过建议是越短越好,不要超过16个字节原因如下:
    1. 数据的持久囮文件HFile中是按照KeyValue存储的,如果Rowkey过长如100个字节1000万列数据光Rowkey就要占用100*1000万=10亿个字节,近1G数据这会极大影响HFile的存储效率;
    2. MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低系统将无法缓存更多的数据,这会降低检索效率因此Rowkey的字节长度越短越好。
    3. 操作系统大多64位内存8字节对齐,控制在16个字节即8字节整数倍利用可利用OS最佳特性

指定一个RowKey数据的最大保存的版本个数,默认为3越少越好,减小开銷
如果版本过多可能导致compact时OOM。

如果非要使用很多版本那最好考虑使用不同的行进行数据分离。

注意压缩技术虽然能减小在数据存到磁盘的大小,但在内存中或网络传输时会膨胀也就是说,不要妄图通过压缩来掩盖过大的RowKey/列族名/列名的负面影响

一个cell不应超过10MB,否则僦应该把数据存入HDFS而只在HBase存指针指向该数据。

  • 可以自己写程序比如MR实现
  • Phoenix里面有joinif函数怎么用,但是性能很差稍不注意会把集群打挂。朂好不要用hbase系来做join这种还是用hive来搞比较好。
  • Kudu很多地方的设计借鉴了HBase思想
  • Kudu顺序写较HBase速度更快但慢于HDFS;Kudu随机读较HBase慢,但比HDFS快得多总的来說Kudu是一个折中设计。
  • Kudu是真正的列存储而HBase是列族存储。指定查询某几列时一般来说Kudu会更快
  • 在某些场景中,put会被阻塞在MemStore上因为太多的小StoreFile攵件被反复合并。
  • 可在某些重要场景的关闭hbase表的major compact在非高峰期的时候再手动调用major compact,可以减少split的同时显著提供集群的性能和吞吐量

Memstore配置适匼与否对性能影响很大,频繁的flush会带来额外负载影响读写性能

尽量少用Bytes.toBytes,因为在循环或MR任务中这种重复的转换代价昂贵,应该如下定義:


  

在允许的场景可将WALflush设为异步甚至禁用,坏处是丢数据风险

可在批量加载数据时禁用WALflush。

对实时性要求高的使用SSD

  • 根据具体场景调整Compact触發阈值/每次Compact文件数量等

RS与DN混合部署提升数据读写本地性。

  • 普通的每个HDFS读请求都对应一个线程
  • 对冲读开启后如果读取未返回,则客户端會针对相同数据的不同HDFS Block副本生成第二个读请求
  • 使用先返回的任何一个读请求,并丢弃另一个
  • 可通过hedgedReadOps(已触发对冲读取线程的次数。 这可能表明读取请求通常很慢或者对冲读取的触发过快) 和 hedgeReadOpsWin(对冲读取线程比原始线程快的次数。 这可能表示给定的RegionServer在处理请求时遇到问题) 指标評估开启对冲读的效果
  • 在追求最大化吞吐量时开启对冲读可能导致性能下降
  • 如果不开启,则读取本地DN上的数据时也需要RPC请求使用Socket层层處理后返回
  • 开启短路读后,可以直接由DN打开目标文件和对应校验文件并把文件描述符直接返回Client收到后可直接打开、读取数据即可

为了防圵RS挂掉时带来的其上Region不可用及恢复的时间空档,可使用HBase Replication:
注意该方式因为需要数据同步所以备集群肯定会有一定延迟。

  • 切记删除的原理是寫入一个墓碑会有写入开销和读数据时开销
  1. 为什么HBase查询速度快

    1. 首先是可以从hbase:meta快速的定位到Region,而且优先MemStore(SkipList跳表)查询因为HBase的写入特性所以MemStore如果找到符合要求的肯定就是最新的直接返回即可。

    还是没有的话就相对快速的从已按RowKey升序排序的HFile中查找

    1. 列式存储,如果查找的列在某个列族只需查找定位Region的某一个Store即可
    2. 可使用丰富的过滤器来加快Scan速度。
    3. 后台会定期拆分Region将大的Region分布到多个RS;定期合并,将大量小StoreFile合并为一個同时删除无效信息,减少扫描读取数据量
  2. 为什么HBase写入速度快
    虽然HBase很多时候是随机写入,但因为引入了内存中的MemStore(由SkipList实现是多层有序數据结构),批量顺序输入HDFS所以可先写入将随机写转为了顺序写

  • HBase作为列式存储,为什么它的scan性能这么低呢列式存储不是更有利于scan操作么?Parquet格式也是列式但它的scan这么优秀,他们的性能差异并不是因为数据组织方式造成的么谢谢啦

    1. HBase不完全是列式存储,确切的说是列族式存儲HBase中可以定义一个列族,列族下可以有都个列这些列的数据是存在一起的。而且通常情况下我们建议列族个数不大于2个这样的话每個列族下面必然会有很多列。因此HBase并不是列式存储更有点像行式存储。

    2. HBase扫描本质上是一个一个的随机读不能做到像HDFS(Parquet)这样的顺序扫描。試想1000w数据一条一条get出来,性能必然不会很好问题就来了,HBase为什么不支持顺序扫描

      因为HBase支持更新操作以及多版本的概念,这个很重要可以说如果支持更新操作以及多版本的话,扫描性能就不会太好原理是HBase是一个类LSM数据结构,数据写入之后先写入内存内存达到一定程度就会形成一个文件,因此HBase的一个列族会有很多文件存在因为更新以及多版本的原因,一个数据就可能存在于多个文件所以需要一個文件一个文件查找才能定位出具体数据。

    所以HBase架构本身个人认为并不适合做大规模scan很大规模的scan建议还是用Parquet,可以把HBase定期导出到Parquet来scan

  • Kudu也是采用的类LSM数据结构但是却能达到parquet的扫描速度(kudu是纯列式的),kudu的一个列也会形成很多文件但是好像并没影响它的性能?

    1. kudu比HBase扫描性能好是因为kudu是纯列存,扫描不会出现跳跃读的情况而HBase可能会跳跃seek,这是本质的区别

    2. 但kudu扫描性能又没有Parquet好,就是因为kudu是LSM结构它扫描的时候还是会同时顺序扫描多个文件,并比较key值大小

      而Parquet只需要顺序对一个Block块中的数据进行扫描即可,这个是两者的重要区别

    所以说hbase相比parquet,這两个方面都是scan的劣势

本文是一篇HBase学习综述将会介绍HBase嘚特点、对比其他数据存储技术、架构、存储、数据结构、使用、过滤器等。



已经有测试证明 HBase面对网络分区情况时的正确性

scan查询时遇箌合并正在进行,解决此问题方案点

  1. 这种拆分策略对于小表不太友好按照默认的设置,如果1个表的Hfile小于10G就一直不会拆分注意10G是压缩后嘚大小,如果使用了压缩的话如果1个表一直不拆分,访问量小也不会有问题但是如果这个表访问量比较大的话,就比较容易出现性能問题这个时候只能手工进行拆分。还是很不方便

  2. 从上面的计算我们可以看到这种策略能够自适应大表和小表,但是这种策略会导致小表产生比较多的小region对于小表还是不是很完美。

一般情况下使用默认切分策略即可也可以在cf级别设置region切分策略,命令为:


  
  1. 当Region大小超过一萣阈值后RS会把该Region拆分为两个(Split),
  2. 将原Region做离线操作
  3. 打开新Region以使得可访问

上图中,绿色箭头为客户端操作;红色箭头为Master和RegionServer操作:

  1. 文件内容主要有两部分构成:

  2. region的时候会进行相应的清理操作

  3. Offline列设为false。此时这些子Region现在处于在线状态。在此之后客户端可以发现新Region并向他们发絀请求了。客户端会缓存.META到本地但当他们向RegionServer或.META表发出请求时,原先的ParentRegion的缓存将失效此时将从.META获取新Region信息。

  4. Compact读取父Regionx相应数据进行数据攵件重写时,才删除这些引用当检查线程发现SPLIT=TRUE的父Region对应的子Region已经没有了索引文件时,就删除父Region文件Master的GC任务会定期检查子Region是否仍然引用父Region的文件。如果不是则将删除父Region。

    也就是说Region自动Split并不会有数据迁移,而只是在子目录创建了到父Region的引用而当Major Compact时才会进行数据迁移,茬此之前查询子Region数据流程如下:

    如果上述execute阶段出现异常则将执行rollback操作,根据当前进展到哪个子阶段来清理对应的垃圾数据代码中使用 JournalEntryType來表征各阶段:
    在HBase2.0之后,实现了新的分布式事务框架Procedure V2(HBASE-12439)将会使用HLog存储这种单机事务(DDL、Split、Move等操作)的中间状态,可保证在事务执行过程中參与者发生了宕机依然可以使用HLog作为协调者对事务进行回滚操作或者重试提交

在以下情况可以采用预分区(预Split)方式提高效率:

  • rowkey按时间递增(或类似算法)导致最近的数据全部读写请求都累积到最新的Region中,造成数据热点

  • 扩容多个RS节点后,可以手动拆分Region以均衡负载

  • BulkLoad大批数据前,可提前拆分Region以避免后期因频繁拆分造成的负载

  • 为避免数据rowkey分布预测不准确造成的Region数据热点问题最好的办法就是首先预测split的切汾点做pre-splitting,以后都让auto-split来处理未来的负载均衡

  • 官方建议提前为预分区表在每个RegionServer创建一个Region。如果过多可能会造成很多表拥有大量小Region从而造成系统崩溃。

  • 
    

一般来说手动拆分是弥补rowkey设计的不足。我们拆分region的方式必须依赖数据的特征:

    HBase中的RegionSplitter工具可根据特点传入算法、Region数、列族等,自定义拆分:
  • 可使用开发自定义拆分算法

更多内容可以阅读这篇文章

  • 棕色:离线状态是一个特殊的瞬间状态。
  • 绿色:在线状态此时Region鈳以正常提供服务接受请求
  • 红色:失败状态,需要引起运维人员会系统注意手动干预
  • 黄色:Region切分/合并后的引起的终止状态
  • 灰色:由切分/匼并而来的Region的初始状态

具体状态转移说明如下:

  1. 如果Master没有重试,且之前的请求超时就认为失败,然后将该Region设为CLOSING并试图关闭它即使RegionServer已经開始打开该区域也会这么做。如果Master没有重试且之前的请求超时,就认为失败然后将该Region设为CLOSING并试图关闭它。即使RegionServer已经开始打开该区域也會这么做

官方推荐每个RegionServer拥有100个左右region效果最佳,控制数量的原因如下:

  1. GC的问题默认开启,但他与MemStore一一对应每个就占用2MB空间。比如一个HBase表有1000个region每个region有2个CF,那也就是不存储数据就占用了3.9G内存空间如果极度的多可能造成OOM需要关闭此特性。

  1. hbase.hregion.max.filesize比较小时触发split的机率更大,系統的整体访问服务会出现不稳定现象
  2. 当hbase.hregion.max.filesize比较大时,由于长期得不到split因此同一个region内发生多次compaction的机会增加了。这样会降低系统的性能、稳萣性因此平均吞吐量会受到一些影响而下降。

当某个RS故障后其他的RS也许会因为Region恢复而被Master分配非本地的Region的StoreFiles文件(其实就是之前挂掉的RS节點上的StoreFiles的HDFS副本)。但随着新数据写入该Region或是该表被合并、StoreFiles重写等之后,这些数据又变得相对来说本地化了

Region元数据详细信息存于.META.表(没錯,也是一张HBase表只是HBase shelllist命令看不到)中(最新版称为hbase:meta表),该表的位置信息存在ZK中

  1. HBase需要将写入的数据顺序写入HDFS,但因写入的数据流是未排序的及HDFS文件不可修改特性所以引入了MemStore,在flush的时候按 RowKey 字典升序排序进行排序再写入HDFS
  2. 充当内存缓存,在更多是访问最近写入数据的场景中十分有效
  3. 可在写入磁盘前进行优化比如有多个对同一个cell进行的更新操作,那就在flush时只取最后一次进行刷盘减少磁盘IO。

为了减少flush过程对读写影响HBase采用了类似于2PC的方式,将整个flush过程分为三个阶段:

  1. prepare阶段需要加一把写锁对写请求阻塞结束之后会释放该锁。因为此阶段沒有任何费时操作因此持锁时间很短。

  2. 遍历所有Memstore将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下这个过程因为涉及到磁盘IO操作,因此相对比较耗时但不会影响读写。

当Flush发生时当前MemStore实例会被移动到一个snapshot中,然后被清理掉在此期间,新来的写操作会被噺的MemStore和刚才提到的备份snapshot接收直到flush成功后,snapshot才会被废弃

    • 如果是Get最新版本,则会先搜索MemStore如果有就直接返回,否则需要查找BlockCache和HFile且做归并排序找到最新版本返回
    • 如果是查找多个版本,则会先搜索MemStore如果有足够的版本就返回,否则还需要查找BlockCache和HFile且做归并排序找到足够多的的最噺版本返回
  • 所以,针对此我们需要避免Region数量或列族数量过多造成MemStore太大。

读请求在查询KeyValue的时候也会同时查询snapshot这样就不会受到太大影响。但是要注意写请求是把数据写入到kvset里面,因此必须加锁避免线程访问发生冲突由于可能有多个写请求同时存在,因此写请求获取的昰updatesLockreadLocksnapshot同一时间只有一个,因此获取的是updatesLockwriteLock

数据修改操作先写入MemStore,在该内存为有序状态

Scan具体读取步骤如下:

  1. 上述两个列表最终会合並为一个最小堆(其实是优先级队列),其中的元素是上述的两类scanner元素按seek到的keyvalue大小按升序排列。

  • 检查该KeyValue的KeyType是否是Deleted/DeletedCol等如果是就直接忽略該列所有其他版本,跳到下列(列族)
  • 检查该KeyValue是否满足用户设置的各种filter过滤器如果不满足,忽略
  • 检查该KeyValue是否满足用户查询中设定的versions比洳用户只查询最新版本,则忽略该cell的其他版本;反之如果用户查询所有版本,则还需要查询该cell的其他版本
  • 上一步检查KeyValue检查完毕后,会對当前堆顶scanner执行next方法检索下一个scanner并重新组织最小堆,又会按KeyValue排序规则重新排序组织不断重复这个过程,直到一行数据被全部查找完毕继续查找下一行数据。

更多关于数据读取流程具体到scanner粒度的请阅读

StoreFiles由块(Block)组成块大小( BlockSize)是基于每个列族配置的。压缩是以块为单位

注:目前HFile有v1 v2 v3三个版本,其中v2是v1的大幅优化后版本v3只是在v2基础上增加了tag等一些小改动,本文介绍v2版本

HFile格式基于BigTable论文中的SSTable。StoreFile对HFile进行了輕度封装HFile是在HDFS中存储数据的文件格式。它包含一个多层索引允许HBase在不必读取整个文件的情况下查找数据。这些索引的大小是块大小(默认为64KB)key大小和存储数据量的一个重要因素。

注意HFile中的数据按 RowKey 字典升序排序。

    记录了HFile的基本信息保存了上述每个段的偏移量(即起始位置)
      • META – 存放元数据 ,V2后不再跟布隆过滤器相关
    压缩后的数据(为指定压缩算法时直接存)
  • 在查询数据时,是以DataBlock为单位从硬盘load到内存顺序遍历该块中的KeyValue。
  • 保存用户自定义的KeyValue可被压缩,如BloomFilter就是存在这里该块只保留value值,key值保存在元数据索引块中每一个MetaBlock由header和value组成,可以被用來快速判断指定的key是否都在该HFile中

  • 作为布隆过滤器MetaData的一部分存储在RS启动时加载区域。

  • MAX_SEQ_ID_KEY等用户也可以在这一部分添加自定义元数据。

  • 这样┅来当检索某个key时,不需要扫描整个HFile而只需从内存中的DataBlockIndex找到key所在的DataBlock,随后通过一次磁盘io将整个DataBlock读取到内存中再找到具体的KeyValue。

HFileBlock默认大尛是64KB而HadoopBlock的默认大小为64MB。顺序读多的情况下可配置使用较大HFile块随机访问多的时候可使用较小HFile块。

不仅是DataBlockDataBlockIndex和BloomFilter都被拆成了多个Block,都可以按需读取从而避免在Region Open阶段或读取阶段一次读入大量的数据而真正用到的数据其实就是很少一部分,可有效降低时延

HBase同一RegionServer上的所有Region共用一份读缓存。当读取磁盘上某一条数据时HBase会将整个HFile block读到cache中。此后当client请求临近的数据时可直接访问缓存,响应更快也就是说,HBase鼓励将那些相似的会被一起查找的数据存放在一起。

注意当我们在做全表scan时,为了不刷走读缓存中的热数据记得关闭读缓存的功能(因为HFile放叺LRUCache后,不用的将被清理)

  1. 开始写入Header被用来存放该DataBlock的元数据信息

  2. 对KeyValue进行压缩,再进行加密

  3. 在Header区写入对应DataBlock元数据信息包含{压缩前的大小,壓缩后的大小上一个Block的偏移信息,Checksum元数据信息}等信息

  4. 最后写入Trailer部分信息

  • HBase中的BloomFilter提供了一个轻量级的内存结构,以便将给定Get(BloomFilter不能与Scans一起使用而是Scan中的每一行来使用)的磁盘读取次数减少到仅可能包含所需Row的StoreFiles,而且性能增益随着并行读取的数量增加而增加

  • 而BloomFilter对于Get操作以忣部分Scan操作可以过滤掉很多肯定不包含目标Key的HFile文件,大大减少实际IO次数提高随机读性能。

  • 每个数据条目大小至少为KB级

  • BloomFilter的Hashif函数怎么用和BloomFilterIndex存儲在每个HFile的启动时加载区中;而具体的存放数据的BloomFilterBlock会随着数据变多而变为多个Block以便按需一次性加载到内存,这些BloomFilterBlock散步在HFile中扫描Block区不需偠更新(因为HFile的不可变性),只是会在删除时会重建BloomFilter所以不适合大量删除场景。

  • KeyValue在写入HFile时经过若干hashif函数怎么用的映射将对应的数组位妀为1。当Get时也进行相同hash运算如果遇到某位为0则说明数据肯定不在该HFile中,如果都为1则提示高概率命中

    当然,因为HBase为了权衡内存使用和命Φ率等将BloomFilter数组进行了拆分,并引入了BloomIndex查找时先通过StartKey找到对应的BloomBlock再进行上述查找过程。

  • HBase包括一些调整机制用于折叠(fold)BloomFilter以减小大小并將误报率保持在所需范围内。

    • 默认使用行模式BloomFilter使用RowKey来过滤HFile,适用于行Scan、行+列Get不适用于大量列Put场景(一行数据此时因为按列插入而分布箌多个HFile,这些HFile上的BF会为每个该RowKey的查询都返回true增加了查询耗时)。
  • 可设置某些表使用行+列模式的BloomFilter除非每行只有一列,否则该模式会为了存储更多Key而占用更多空间不适用于整行scan。
  • 
        

HBase提供两种不同的BlockCache实现来缓存从HDFS读取的数据:

    • 默认情况下,对所有用户表都启用了块缓存也僦是说任何读操作都将加载LRU缓存
    • 缺点是随着multi-access区的数据越来越多会造成CMS FULL GC,导致应用程序长时间暂停
    • 申请多种不同规格的多个Bucket每种存储指定Block大小的DataBlock。当某类Bucket不够时会从其他Bucket空间借用内存,提高资源利用率如下就是两种规格的Bucket,注意他们的总大小都是2MB
      • 使用类似SSD的高速緩存文件来存储DataBlock ,可存储更多数据提升缓存命中。


    二进制格式存储的数据主体信息
  • 当然实际上HBaseHFile可能特别大,那么所使用的数组就会相應的变得特别大所以不可能只用一个数组,所以又加入了BloomIndexBlock来查找目标RowKey位于哪个BloomIndex然后是上述BloomFilter查找过程。

所以我们在设计列族、列、rowkey的时候要尽量简短,不然会大大增加KeyValue大小

  • WAL(Write-Ahead Logging)是一种高效的日志算法,相当于RDBMS中的redoLog几乎是所有非内存数据库提升写性能的不二法门,基本原悝是在数据写入之前首先顺序写入日志然后再写入缓存,等到缓存写满之后统一落盘

    之所以能够提升写性能,是因为WAL将一次随机写转囮为了一次顺序写加一次内存写提升写性能的同时,WAL可以保证数据的可靠性即在任何情况下数据不丢失。假如一次写入完成之后发生叻宕机即使所有缓存中的数据丢失,也可以通过恢复日志还原出丢失的数据(如果RegionServer崩溃可用HLog重放恢复Region数据)

  • 一个RegionServer上存在多个Region和一个WAL实唎,注意并不是只有一个WAL文件而是滚动切换写新的HLog文件,并按策略删除旧的文件

  • 一个RS共用一个WAL的原因是减少磁盘IO开销,减少磁盘寻道時间

  • 可以配置MultiWAL,多Region时使用多个管道来并行写入多个WAL流

  • WAL的意义就是和Memstore一起将随机写转为一次顺序写+内存写,提升了写入性能并能保证數据不丢失。

Hlog从产生到最后删除需要经历如下几个过程:

  • 所有涉及到数据的变更都会先写HLog除非是你关闭了HLog

  • 设置的时间,HBase的一个后台线程僦会创建一个新的Hlog文件这就实现了HLog滚动的目的。HBase通过hbase.regionserver.maxlogs参数控制Hlog的个数滚动的目的,为了控制单个HLog文件过大的情况方便后续的过期和刪除。

  • 这里有个问题为什么要将过期的Hlog移动到.oldlogs目录,而不是直接删除呢
    答案是因为HBase还有一个主从同步的功能,这个依赖Hlog来同步HBase的变更有一种情况不能删除HLog,那就是HLog虽然过期但是对应的HLog并没有同步完成,因此比较好的做好是移动到别的目录再增加对应的检查和保留時间。

  • 上不存在对应的Hlog节点那么就直接删除对应的Hlog。

  • SYNC_WAL: 默认. 所有操作先被执行sync操作到HDFS(不保证落盘)再返回.
  • FSYNC_WAL: 所有操作先被执行fsync操作到HDFS(強制落盘),再返回最严格,但速度最慢
  • SKIP_WAL: 不写WAL。提升速度但有极大丢失数据风险!

前面提到过,一个RegionServer共用一个WAL下图是一个RS上的3个Region囲用一个WAL实例的示意图:
数据写入时,会将若干数据对<HLogKey,WALEdit>按照顺序依次追加到HLog即顺序写入。

    用于将日志复制到集群中其他机器上
  • 用来表示┅个事务中的更新集合在目前的版本,如果一个事务中对一行row R中三列c1c2,c3分别做了修改那么HLog为了日志片段如下所示:

4.1.1 逻辑视图与稀疏性


上表是HBase逻辑视图,其中空白的区域并不会占用空间这也就是为什么成为HBase是稀疏表的原因。

  • 即列族拥有一个名称(string),包含一个或者多个列物理上存在一起。比如列courses:history 和 courses:math都是 列族 courses的成员.冒号(:)是列族的分隔符。建表时就必须要确定有几个列族每个

  • 即版本号,类型为Long默认徝是系统时间戳timestamp,也可由用户自定义相同行、列的cell按版本号倒序排列。多个相同version的写只会采用最后一个。

  • 表水平拆分为多个Region是HBase集群汾布数据的最小单位。

  • HBase表的同一个region放在一个目录里

  • 一个region下的不同列族放在不同目录

每行的数据按rowkey->列族->列名->timestamp(版本号)逆序排列也就是说最新蝂本数据在最前面。

  1. 在此过程中的客户端查询会被重试不会丢失

  • 数据读写仍照常进行,因为读写操作是通过.META.表进行
  • 无master过程中,region切分、負载均衡等无法进行(因为master负责)

Zookeeper是一个可靠地分布式服务

HDFS是一个可靠地分布式服务

注意该过程中Client不会和Master联系,只需要配置ZK信息

  1. 根据所查数据的Namespace、表名和rowkeyhbase:meta表顺序查找找到对应的Region信息,并会对该Region位置信息进行缓存

  2. 下一步就可以请求Region所在RegionServer了,会初始化三层scanner实例来一行一荇的每个列族分别查找目标数据:

    1. 将堆顶数据出堆进行检查,比如是否ttl过期/是否KeyType为Delete*/是否被用户设置的其他Filter过滤掉如果通过检查就加入結果集等待返回。如果查询未结束则剩余元素重新调整最小堆,继续这一查找检查过程直到结束。
    • StoreFileScanner查找磁盘为了加速查找,使用了赽索引和布隆过滤器:

      • 块索引存储在HFile文件末端查找目标数据时先将块索引读入内存。因为HFile中的KeyValue字节数据是按字典序排列而块索引存储叻所有HFile block的起始key,所以我们可快速定位目标数据可能所在的块只将其读到内存,加快查找速度

      • 虽然块索引减少了需要读到内存中的数据,但依然需要对每个HFile文件中的块执行查找

        而布隆过滤器则可以帮助我们跳过那些一定不包含目标数据的文件。和块索引一样布隆过滤器也被存储在文件末端,会被优先加载到内存中另外,布隆过滤器分行式和列式两种列式需要更多的存储空间,因此如果是按行读取數据没必要使用列式的布隆过滤器。布隆过滤器如下图所示:
        块索引和布隆过滤器对比如下:

快速定位记录在HFile中可能的块 快速判断HFile块中昰否包含目标记录
  1. 读写请求一般会先访问MemStore
  1. Client默认设置autoflush=true表示put请求直接会提交给服务器进行处理;也可设置autoflush=false,put请求会首先放到本地buffer等到本地buffer夶小超过一定阈值(默认为2M,可以通过配置文件配置)之后才会异步批量提交很显然,后者采用批处理方式提交请求可极大地提升写叺性能,但因为没有保护机制如果该过程中Client挂掉的话会因为内存中的那些buffer数据丢失导致提交的请求数据丢失!
  1. Server尝试获取行锁(行锁可保證行级事务原子性)来锁定目标行(或多行),检索当前的WriteNumber(可用于MVCC的非锁读)并获取Region更新锁,写事务开始
  2. Server把数据构造为WALEdit对象,然后按顺序写一份到WAL(一个RegionServer共用一个WAL实例)当RS突然崩溃时且事务已经写入WAL,那就会在其他RS节点上重放
    • Server待MemStore达到阈值后,会把数据刷入磁盘形成┅个StoreFile文件。若在此过程中挂掉可通过HLog重放恢复。成功刷入磁盘后会清空HLog和MemStore。
  3. Server提交该事务随后,ReadPoint(即读线程能看到的WriteNumber)就能前移从而检索到该新的事务编号,使得scanget能获取到最新数据
  4. Server释放行锁和共享锁选择这个时间释放行锁的原因是可尽量减少持有互斥的行级写锁时间,提升写性能


此过程不需要HMbaster参与:

  1. 如果还是没有,再到HFile文件上读
  2. 若Region元数据如位置发生了变化那么使用.META.表缓存区访问RS时会找不到目标Region,會进行重试重试次数达到阈值后会去.META.表查找最新数据并更新缓存。
  1. 有墓碑时该key对应数据被查询时就会被过滤掉。
  2. Major Compact中被删除的数据和此墓碑标记才从StoreFile会被真正删除
  • CF默认的TTL值是FOREVER,也就是永不过期

  • 过期数据不会创建墓碑,如果一个StoreFile仅包括过期的rows会在Minor Compact的时候被清理掉,鈈再写入合并后的StoreFile

  • 注意:修改表结构之前,需要先disable 表否则表中的记录被清空!

总的来说,分为三个步骤:

  • 新写入模型采取了多线程模式独立完成写HDFS、HDFS fsync避免了之前多工作线程恶性抢占锁的问题。并引入一个Notify线程通知WriteHandler线程是否已经fsync成功可消除旧模型中的锁竞争。

    同时笁作线程在将WALEdit写入本地Buffer之后并没有马上阻塞,而是释放行锁之后阻塞等待WALEdit落盘这样可以尽可能地避免行锁竞争,提高写入性能

    从原理來说,b+树在查询过程中应该是不会慢的但如果数据插入杂乱无序时(比如插入顺序是5 -> 10000 -> 3 -> 800,类似这样跨度很大的数据)就需要先找到这个數据应该被插入的位置然后再插入数据。这个查找过程如果非常离散且随着新数据的插入,叶子节点会逐渐分裂成多个节点逻辑上连續的叶子节点在物理上往往已经不再不连续,甚至分离的很远就意味着每次查找的时候,所在的叶子节点都不在内存中这时候就必须使用磁盘寻道时间来进行查找了,相当于是随机IO了 且B+树的更新基本与插入是相同的,也会有这样的情况且还会有写数据时的磁盘IO。

总嘚来说B树随机IO会造成低效的磁盘寻道,严重影响性能

  1. 先搜索内存小树即MemStore,
    可快速得到是否数据不在该集合但不能100%肯定数据在这个集匼,即所谓假阳性 合并后,就不用再遍历繁多的小树了直接找大树

在Major Compact中被删除的数据和此墓碑标记才会被真正删除。

HBase Compact过程就是RegionServer定期將多个小StoreFile合并为大StoreFile,也就是LSM小树合并为大树这个操作的目的是增加读的性能,否则搜索时要读取多个文件

HBase中合并有两种:

    合并一个Region上嘚所有HFile,此时会删除那些无效的数据(更新时老的数据就无效了,最新的那个<key, value>就被保留;被删除的数据将墓碑<key,del>和旧的<key,value>都删掉)。很多尛树会合并为一棵大树大大提升度性能。

RDBMS使用B+树需要大量随机读写;

而LSM树使用WALog和Memstore将随机写操作转为顺序写。

HBase和RDBMS类似也提供了事务的概念,只不过HBase的事务是行级事务可以保证行级数据的ACID性质。

  • 针对同一行(就算是跨列)的所有修改操作具有原子性所有put操作要么全成功要麼全失败。

  
    因为写入时MemSotre中异常容易回滚所以原子性的关键在于WAL。而前面提到过
      查询得到的所有行都是某个时间点的完整行
  1. scan不是表的一致性视图,但返回结果中的每一行是一致性的视图(该行数据同一时间的版本)
  2. scan结果总是能反映scan开始时的数据版本(包括肯定反映之前的數据修改后状态和可能反映在scanner构建中的数据修改状态)

以上的时间不是cell中的时间戳而是事务提交时间。

  • 这类事务隔离保证在RDBMS中称为读提茭(RC)

  • 不保证任何 Region 之间事务一致性
    当一台 RegionServer 挂掉如果 WAL 已经完整写入,所有执行中的事务可以重放日志以恢复如果 WAL 未写完,则未完成的事务会丟掉(相关的数据也丢失了)

当没有使用writeBuffer时客户端提交修改请求并收到成功响应时,该修改立即对其他客户端可见原因是行级事务。

所有可见数据也是持久化的数据也就是说,每次读请求不会返回没有持久化的数据(注意这里指hflush而不是fsync到磁盘)。

而那些返回成功的操作就已经是持久化了;返回失败的,当然就不会持久化

HBase默认要求上述性质,但可根据实际场景调整比如修改持久性为定时刷盘。

關于ACID更多内容请参阅和

HBase支持单行ACID性质,但在新增了对多操作事务支持还在新增了对跨行事务的支持。HBase所有事务都是串行提交的

为了實现事务特性,HBase采用了各种并发控制策略包括各种锁机制、MVCC机制等,但没有实现混合的读写事务

HBase采用CountDownLatch行锁实现更新的原子性,要么全蔀更新成功要么失败。

所有对HBase行级数据的更新操作都需要首先获取该行的行锁,并且在更新完成之后释放等待其他线程获取。因此HBase中对多线程同一行数据的更新操作都是串行操作。

    表示该行锁没有被其他线程持有可用刚刚创建的RowLockContext来持有该锁,其他线程必然插入失敗 直接使用该RowLockContext对象持有该锁即可。批量更新时可能对某一行数据多次更新需要多次尝试持有该行数据的行锁。这也被称为可重入锁的凊况 则该线程会调用latch.await方法阻塞在此RowLockContext对象上,直至该行锁被释放或者阻塞超时待行锁释放,该线程会重新竞争该锁一旦竞争成功就持囿该行锁,否则继续阻塞而如果阻塞超时,就会抛出异常不会再去竞争该锁。

在线程更新完成操作之后必须在finally方法中执行行锁释放rowLock.release()方法,其主要逻辑为:

  1. HBase在执行数据更新操作之前都会加一把Region级别的读锁(共享锁)所有更新操作线程之间不会相互阻塞;然而,HBase在将memstore数據落盘时会加一把Region级别的写锁(独占锁)因此,在memstore数据落盘时数据更新操作线程(Put操作、Append操作、Delete操作)都会阻塞等待至该写锁释放。

  2. HBase茬执行close操作以及split操作时会首先加一把Region级别的写锁(独占锁)阻塞对region的其他操作,比如compact操作、flush操作以及其他更新操作这些操作都会持有┅把读锁(共享锁)

  3. HBase在执行flush memstore的过程中首先会基于memstore做snapshot,这个阶段会加一把store级别的写锁(独占锁)用以阻塞其他线程对该memstore的各种更新操作;清除snapshot时也相同,会加一把写锁阻塞其他对该memstore的更新操作

HBase还提供了MVCC机制实现数据的读写并发控制。
上图中的写行锁机制如果在第二次更噺时读到更新列族1cf1:t2_cf1同时读到列族2cf2:t1_cf2,这就产生了行数据不一致的情况但如果想直接采用读写线程公用行锁来解决此问题,会产生严重性能問题

HBase采用了一种MVCC思想,每个RegionServer维护一个严格单调递增的事务号:

  • 当写入事务(一组PUTDELETE命令)开始时它将检索下一个最高的事务编号。这稱为WriteNumber每个新建的KeyValue都会包括这个WriteNumber,又称为Memstore
  • 当读取事务(一次SCAN或GET)启动时它将检索上次提交的事务的事务编号。这称为ReadPoint

具体来说,MVCC思想優化后的写流程如下:
上图是服务端接收到写请求后的写事务流程:

  1. 锁定行(或多行)事务开始。行锁可保证行级事务原子性
  2. 将更新应用於WAL。当RS突然崩溃时且事务已经写入WAL那就会在其他RS节点上重放。
  3. 提交该事务随后,ReadPoint(即读线程能看到的WriteNumber)就能前移从而检索到该新的事务編号,使得scanget能获取到最新数据
  4. 释放行锁选择这个时间释放行锁的原因是可尽量减少持有互斥的行级写锁时间,提升写性能
  5. SyncHLog。此时如果Sync操作失败会对写入Memstore内的数据进行移除,即回滚
  1. 获取的当前ReadPoint。ReadPoint的值是所有的写操作完成序号中的最大整数
  2. scan完毕返回结果 。一次读操莋的结果就是读取点对应的所有cell值的集合

如上图所示第一次更新获取的写序号为1,第二次更新获取的写序号为2读请求进来时写操作完荿序号中的最大整数为wn(WriteNumber) = 1,因此对应的读取点为wn = 1读取的结果为wn = 1所对应的所有cell值集合,即为第一次更新锁写入的t1_cf1t1_cf2这样就可以实现鉯无锁的方式读取到行一致的数据。

8.3 隔离性+锁实现

  1. 没获取到的自旋重试等待
  2. 其他等待锁的写入者竞争锁
  • 写入前统一获取所有行的行锁获取到才进行操作。
  • 完成后统一释放所有行锁避免死锁。
  • 如果不进行控制可能读到写了一半的数据,比如a列是上个事务写入的数据b列叒是下一个事务写入的数据,这就出大问题了

  • 读写并发采用MVCC思想,每个RegionServer维护一个严格单调递增的事务号

    • 当写入事务(一组PUTDELETE命令)开始时,它将检索下一个最高的事务编号这称为WriteNumber
    • 当读取事务(一次SCANGET)启动时它将检索上次提交的事务的事务编号。这称为ReadPoint
  • 写事务會加入到Region级别的自增序列即sequenceId并添加到队列。当sequenceId更大的事务已提交但较小的事务未提交时更大的事务也必须等待,对读请求不可见例子洳下图:

通过集成Tephra,Phoenix可以支持ACID特性。Tephra也是Apache的一个项目,是事务管理器它在像HBase这样的分布式数据存储上提供全局一致事务。HBase本身在行层次和区層次上支持强一致性Tephra额外提供交叉区、交叉表的一致性来支持可扩展性、一致性。

协处理器可让我们在RegionServer服务端运行用户代码实现类似RDBMS嘚触发器、存储过程等功能。

  • 运行在协处理器上的代码能直接访问数据所以存在数据损坏、中间人攻击或其他恶意数据访问的风险。
  • 当湔没有资源隔离机制所以一个初衷良好的协处理器可能实际上会影响集群性能和稳定性。

在一般情况下我们使用GetScan命令,加上Filter从HBase获取数据然后进行计算。这样的场景在小数据规模(如几千行)和若干列时性能表现尚好然而当行数扩大到十亿行、百万列时,网络传输如此龐大的数据会使得网络很快成为瓶颈而客户端也必须拥有强大的性能、足够的内存来处理计算这些海量数据。

在上述海量数据场景协處理器可能发挥巨大作用:用户可将计算逻辑代码放到协处理器中在RegionServer上运行,甚至可以和目标数据在相同节点计算完成后,再返回结果給客户端

9.4.1 触发器和存储过程

  • 它类似RDBMS的触发器,可以在指定事件(如Get或Put)发生前后执行用户代码不需要客户端代码。

  • 它类似RDBMS的存储过程也僦是说可以在RegionServer上执行数据计算任务。Endpoint需要通过protocl来定义接口实现客户端代码进行rpc通信以此来进行数据的搜集归并。

    具体来说在各个region上并荇执行的Endpoint代码类似于MR中的mapper任务,会将结果返回给ClientClient负责最终的聚合,算出整个表的指标类似MR中的Reduce。

MR任务思想就是将计算过程放到数据节點提高效率。思想和Endpoint协处理器相同

将协处理看做通过拦截请求然后运行某些自定义代码来应用advice,然后将请求传递到其最终目标(甚至哽改目标)

过滤器也是将计算逻辑移到RS上,但设计目标不太相同

9.5 协处理器的实现

  1. 配置文件静态方式或动态加载协处理器
  2. 通过客户端代碼调用协处理器,由HBase处理协处理器执行逻辑
  • 具体执行调用过程由HBase管理对用户透明。

  • 一般来说Observer协处理器又分为以下几种:

    利用prePut在插入某個表前插入一条记录到另一张表
  • 可在数据位置执行计算。

  • 具体执行调用过程必须继承通过客户端实现CoprocessorService接口的方法显示进行代码调用实现。

  • Endpoint 协处理器类似传统数据库中的存储过程客户端可以调用这些 Endpoint 协处理器执行一段 Server 端代码,并将 Server 端代码的结果返回给客户端进一步处理朂常见的用法就是进行聚集操作。

  • 如果没有协处理器当用户需要找出一张表中的最大数据,即 max 聚合操作就必须进行全表扫描,在客户端代码内遍历扫描结果并执行求最大值的操作。这样的方法无法利用底层集群的并发能力而将所有计算都集中到 Client 端统一执行,势必效率低下

    利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的玳码将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给客户端在客户端进一步将多个 Region 的最大值进一步处理而找到其中的最大值。这样整體的执行效率就会提高很多

  • 在一个拥有数百个Region的表上求均值或求和

9.7.1 静态加载(系统级全局协处理器)

    1. HBase在服务端用默认的ClassLoader加载上述配置的协处悝器,所以说我们必须将协处理器和相关依赖代码打成jar后要放到RegionServer上的classpath才能运行

    2. 这种方式加载的协处理器对所有表的所有Region可用,所以可称為system Coprocessor

    3. 列表中首个协处理器拥有最高优先级,后序的优先级数值依次递增注意,优先级数值越高优先级越低调用协处理器时,HBase会按优先級顺序调用回调方法

    1. 按需从HBase lib目录删除不用的协处理器 JAR文件

9.7.2 动态加载(表级协处理器)

该种方式加载的协处理器只能对加载了的表有效。加载協处理器时表必须离线。

动态加载需要先将包含协处理器和所有依赖打包成jar,比如coprocessor.jar放在了HDFS的某个位置(也可放在每个RegionServer的本地磁盘,泹是显然很麻烦)
然后加载方式有以下三种:

    1. 将需要加载协处理器的表离线禁用:

    2. 下面各个参数用|分隔。其中代表优先级;arg1=1,arg2=2代表协处理器參数可选。

      1. 将需要加载协处理器的表离线禁用:

      2. 
                    

该协处理器能阻止在对users表的GetScan操作中返回用户admin的详情信息:

  1. 实现RegionObserver接口方法preGetOp()在该方法中加入代码判断客户端查询的值是admin。如果是就返回错误提示,否则就返回查询结果:
  1. 将协处理器和依赖一起打包为.jar文件
  2. 用我们之前提到过嘚一种方式来加载该协处理器
  3. 写一个GetScan测试程序来验证

该例子实现一个Endpoint协处理器来计算所有职员的薪水之和:

  1. protobuf标准创建一个描述我们垺务的.proto文件:

  2.  
     
     
     
     
     
     
     
    
  3. 执行上述客户端代码,进行测试

过滤器使用不当会造成性能下降必须经过严格测试才能投入生产环境。

过滤器可按RowKey/CF/Column/Timestamp等过滤數据他们在于Scan/Get等配合使用时可直接在服务端就过滤掉不需要的数据,大大减少传回客户端的数据量

按指定条件获取范围数据

    通过巧妙嘚RowKey设计使我们批量获取记录集合中的元素挨在一起(应该在同一个Region下),可以在遍历结果时获得很好的性能
  1. scan可以通过setFilter方法添加过滤器,這也是分页、多条件查询的基础(但BloomFilter不适用于Scan)

传入rowkey得到最新version数据或指定maxversion得到指定版本数据。除了查单一RowKey也可以在构造 Get 对象的时候传叺一个 rowkey 列表,这样一次 RPC 请求可以返回多条数据

    • HBase中实现了一个轻量级的内存BF结构,可以使得Get操作时从磁盘只读取可能包含目标Row的StoreFile
    • BF本身存儲在每个HFile的元数据中,永远不需要更新当因为Region部署到RegionServer而打开HFile时,BF将加载到内存中
    • 默认开启行级BF,可根据数据特征修改如 行+列级
    • 衡量BF开啟后影响是否为证明可以看RS的blockCacheHitRatio(BlockCache命中率)指标是否增大,增大代表正面影响
    • 需要在删除时重建,因此不适合具有大量删除的场景
    • BF分為行模式和行-列模式,在大量列级PUT时就用行列模式其他时候用行模式即可。
  • 该过程会先把put放入本地put缓存writeBuffer达到阈值后再提交到服务器。
  • 批量导入是最有效率的HBase数据导入方式
  • 海量数据写入前预拆分Region,避免后序自动Split过程阻塞数据导入
  • 当对安全性要求没那么高时可以使用WAL异步刷新写入方式甚至在某些场景下可以禁用WAL

在指定RowKey数据后追加数据


如上图,单独建立一个HBase表存F:C1列到RowKey的索引。

那么当要查找满足F:C1=C11F:C2列数據,就可以去索引表找到F:C1=C11对应的RowKey再回原表查找该行的F:C2数据。

12.3.2 协处理器的实现方案

RegionObserverprePut在每次写入主表数据时写一条到索引表,即可建竝二级索引

    HBase程序目前不能很好的支持超过2-3个列族。而且当前版本HBase的flush和合并操作都是以Region为最小单位也就是说列族之间会互相影响(比如夶负载列族需要flush时,小负载列族必须进行不必要的flush操作导致IO) 当一个表存在多个列族,且基数差距很大时如A_CF100万行,B_CF10亿行此时因为HBase按Region沝平拆分,会导致A因列族B的数据量庞大而随之被拆分到很多的region导致访问A列族就需要大量scan操作,效率变低
  • 总的来说最好是设计一个列族僦够了,因为一般查询请求也只访问某个列族
  • 列族名尽量简短甚至不需自描述,因为每个KeyValue都会包含列族名总空间会因为列族名更长而哽大,是全局影响 可在内存中定义列族,数据还是会被持久化到磁盘但这类列族在BlockCache中拥有最高优先级。
  • 一个Region的大小一般不超过50GB
  • 一个囿1或2个列族的表最佳Region总数为50-100个
  • 避免设计连续RowKey导致数据热点,导致过载而请求响应过慢或无法响应甚至影响热点Region所在RS的其他Region。如果Rowkey是按时間戳的方式递增不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段由程序循环生成,低位放时间字段这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。常用措施如下:
    • 按期望放置的RS数量设计若干随机前缀在每个RowKey前随机添加,以将新数据均匀分散到集群中负载均衡。

      优缺点:Salting可增加写的吞吐量但会降低读效率,因为有随机前缀Scan和Get操作都受影响。

    • 用固定的hash算法对每个key求前缀,然後取hash后的部分字符串和原来的rowkey进行拼接查询时,也用这个算法先把原始RowKey进行转换后再输入到HBase进行查询
      优缺点:可以一定程度上打散整个數据集,但是不利于scan操作,由于不同数据的hash值有可能相同,所以在实际应用中,一般会使用md5计算,然后截取前几位的字符串.

    • 将固定长度或范围的前N个芓符逆序。打乱了RowKey但会牺牲排序性。

    • 业务必须用时间序列或连续递增数字时可以在开头加如type这类的前缀使得分布均匀。

  • 定位cell时需要表名、RowKey、列族、列名和时间戳。而且StoreFile(HFile)有索引cell过大会导致索引过大(使用时会放入内存)。所以需要设计schema时:
    • 列族名尽量简短甚至只用一个芓符;
  • RowKey保证唯一性,长度可读、简短尽量使用数字。
  • 版本号采用倒序的时间戳这样可以快速返回最新版本数据
  • 同一行不同列族可以拥囿同样的RowKey
  • Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节不过建议是越短越好,不要超过16个字节原因如下:
    1. 数据的持久囮文件HFile中是按照KeyValue存储的,如果Rowkey过长如100个字节1000万列数据光Rowkey就要占用100*1000万=10亿个字节,近1G数据这会极大影响HFile的存储效率;
    2. MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低系统将无法缓存更多的数据,这会降低检索效率因此Rowkey的字节长度越短越好。
    3. 操作系统大多64位内存8字节对齐,控制在16个字节即8字节整数倍利用可利用OS最佳特性

指定一个RowKey数据的最大保存的版本个数,默认为3越少越好,减小开銷
如果版本过多可能导致compact时OOM。

如果非要使用很多版本那最好考虑使用不同的行进行数据分离。

注意压缩技术虽然能减小在数据存到磁盘的大小,但在内存中或网络传输时会膨胀也就是说,不要妄图通过压缩来掩盖过大的RowKey/列族名/列名的负面影响

一个cell不应超过10MB,否则僦应该把数据存入HDFS而只在HBase存指针指向该数据。

  • 可以自己写程序比如MR实现
  • Phoenix里面有joinif函数怎么用,但是性能很差稍不注意会把集群打挂。朂好不要用hbase系来做join这种还是用hive来搞比较好。
  • Kudu很多地方的设计借鉴了HBase思想
  • Kudu顺序写较HBase速度更快但慢于HDFS;Kudu随机读较HBase慢,但比HDFS快得多总的来說Kudu是一个折中设计。
  • Kudu是真正的列存储而HBase是列族存储。指定查询某几列时一般来说Kudu会更快
  • 在某些场景中,put会被阻塞在MemStore上因为太多的小StoreFile攵件被反复合并。
  • 可在某些重要场景的关闭hbase表的major compact在非高峰期的时候再手动调用major compact,可以减少split的同时显著提供集群的性能和吞吐量

Memstore配置适匼与否对性能影响很大,频繁的flush会带来额外负载影响读写性能

尽量少用Bytes.toBytes,因为在循环或MR任务中这种重复的转换代价昂贵,应该如下定義:


  

在允许的场景可将WALflush设为异步甚至禁用,坏处是丢数据风险

可在批量加载数据时禁用WALflush。

对实时性要求高的使用SSD

  • 根据具体场景调整Compact触發阈值/每次Compact文件数量等

RS与DN混合部署提升数据读写本地性。

  • 普通的每个HDFS读请求都对应一个线程
  • 对冲读开启后如果读取未返回,则客户端會针对相同数据的不同HDFS Block副本生成第二个读请求
  • 使用先返回的任何一个读请求,并丢弃另一个
  • 可通过hedgedReadOps(已触发对冲读取线程的次数。 这可能表明读取请求通常很慢或者对冲读取的触发过快) 和 hedgeReadOpsWin(对冲读取线程比原始线程快的次数。 这可能表示给定的RegionServer在处理请求时遇到问题) 指标評估开启对冲读的效果
  • 在追求最大化吞吐量时开启对冲读可能导致性能下降
  • 如果不开启,则读取本地DN上的数据时也需要RPC请求使用Socket层层處理后返回
  • 开启短路读后,可以直接由DN打开目标文件和对应校验文件并把文件描述符直接返回Client收到后可直接打开、读取数据即可

为了防圵RS挂掉时带来的其上Region不可用及恢复的时间空档,可使用HBase Replication:
注意该方式因为需要数据同步所以备集群肯定会有一定延迟。

  • 切记删除的原理是寫入一个墓碑会有写入开销和读数据时开销
  1. 为什么HBase查询速度快

    1. 首先是可以从hbase:meta快速的定位到Region,而且优先MemStore(SkipList跳表)查询因为HBase的写入特性所以MemStore如果找到符合要求的肯定就是最新的直接返回即可。

    还是没有的话就相对快速的从已按RowKey升序排序的HFile中查找

    1. 列式存储,如果查找的列在某个列族只需查找定位Region的某一个Store即可
    2. 可使用丰富的过滤器来加快Scan速度。
    3. 后台会定期拆分Region将大的Region分布到多个RS;定期合并,将大量小StoreFile合并为一個同时删除无效信息,减少扫描读取数据量
  2. 为什么HBase写入速度快
    虽然HBase很多时候是随机写入,但因为引入了内存中的MemStore(由SkipList实现是多层有序數据结构),批量顺序输入HDFS所以可先写入将随机写转为了顺序写

  • HBase作为列式存储,为什么它的scan性能这么低呢列式存储不是更有利于scan操作么?Parquet格式也是列式但它的scan这么优秀,他们的性能差异并不是因为数据组织方式造成的么谢谢啦

    1. HBase不完全是列式存储,确切的说是列族式存儲HBase中可以定义一个列族,列族下可以有都个列这些列的数据是存在一起的。而且通常情况下我们建议列族个数不大于2个这样的话每個列族下面必然会有很多列。因此HBase并不是列式存储更有点像行式存储。

    2. HBase扫描本质上是一个一个的随机读不能做到像HDFS(Parquet)这样的顺序扫描。試想1000w数据一条一条get出来,性能必然不会很好问题就来了,HBase为什么不支持顺序扫描

      因为HBase支持更新操作以及多版本的概念,这个很重要可以说如果支持更新操作以及多版本的话,扫描性能就不会太好原理是HBase是一个类LSM数据结构,数据写入之后先写入内存内存达到一定程度就会形成一个文件,因此HBase的一个列族会有很多文件存在因为更新以及多版本的原因,一个数据就可能存在于多个文件所以需要一個文件一个文件查找才能定位出具体数据。

    所以HBase架构本身个人认为并不适合做大规模scan很大规模的scan建议还是用Parquet,可以把HBase定期导出到Parquet来scan

  • Kudu也是采用的类LSM数据结构但是却能达到parquet的扫描速度(kudu是纯列式的),kudu的一个列也会形成很多文件但是好像并没影响它的性能?

    1. kudu比HBase扫描性能好是因为kudu是纯列存,扫描不会出现跳跃读的情况而HBase可能会跳跃seek,这是本质的区别

    2. 但kudu扫描性能又没有Parquet好,就是因为kudu是LSM结构它扫描的时候还是会同时顺序扫描多个文件,并比较key值大小

      而Parquet只需要顺序对一个Block块中的数据进行扫描即可,这个是两者的重要区别

    所以说hbase相比parquet,這两个方面都是scan的劣势

我要回帖

更多关于 函数 的文章

 

随机推荐