本文是一篇HBase学习综述,将会介绍HBase的特点、对比其他数据存储技术、架构、存储、数据结构、使用、过滤器等。
关于Phoenix on HBase,即Sql化的HBase服务,可以参考Phoenix学习
未完成
起源
HBase源于Google 2005年的论文Bigtable。由Powerset公司在2007年发布第一个版本,2008年成为Apache Hadoop子项目,2010年单独升级为Apache顶级项目。
设计目标
HBase的设计目标就是为了那些巨大的表,如数十亿行、数百万列。
一句话概括
HBase是一个开源的、分布式的、版本化、列式存储的非关系型数据库。
面向列
准确的说是面向列族。每行数据列可以不同。
扩展性 | 表设计 | 负载均衡 | failover | 事务 | 适用数据量 | |
---|---|---|---|---|---|---|
RDBMS | 差 | 灵活性较弱 | 差 | 同步实现 | 支持 | 万级 |
HBase | 强 | 十亿级行,百万级列;动态列,每行列可不同。且引入列族和数据多版本概念。 | 强 | 各组件都支持HA | MVCC, Produce LOCK;行级事务 | 亿级 |
作为曾经Hadoop项目的子项目,HBase还是与Hadoop生态关系密切。HBase底层存储用了HDFS
,并可直接用MapReduce
操作HBase
CAP定理指出,分布式系统可以保持以下三个特征中的两个:
HBase选择的是一致性和分区容忍即CP。
这篇文章给出了为什么分区容忍重要的原因you-cant-sacrifice-partition-tolerance。
已经有测试证明 HBase面对网络分区情况时的正确性。
可参考HBase——强一致性详解
订单流水、交易记录、需要记录历史版本的数据等
几千、几百万那种还不如使用RDBMS
需要类型列(不过已经可以用Phoniex on HBase解决这个问题)
需要跨行事务,目前HBase只支持单行事务,需要跨行必须依赖第三方服务
SQL查询(不过可以用Phoniex on HBase解决这个问题)
硬件太少,因为HBase依赖服务挺多,比如至少5个HDFS DataNode,1个HDFS NameNode(为了安全还需要个备节点),一个Zookeeper集群,然后还需要HBase自身的各节点
需要表间Join。
HBase只适合Scan和Get,虽然Phoenix支持了SQL化使用HBase,但Join性能依然很差。如果非要用HBase做Join,只能再客户端代码做.
需要快速顺序读写(scan)
LSM树的原因可能读取时需要查找Memstore和多个分片和多个数据文件;
HBase更新和删除都有个标记文件,所以虽然简化了更新和删除流程,但读取数据时处理比较麻烦,需要根据版本号过滤等操作。
读取流程复杂
scan
流程:next
请求到HBase Serverget
时,可以传入多个rowkey批量查询,并按目标region分组并行执行读取,这一点和scan不同(scan不能并行操作)。面向列族存储
还有一点,HBase是面向列族存储而不是面向列,且推荐列族不超过2个,也就是说每个列族下列相当多,在扫描数据时效率有影响。
写流程复杂
而在写的时候,需要涉及到WAL、锁、事务等机制,也是比较麻烦。
综上,HBase不适合快速顺序读写。
HBase是基于列存储的。本节对比下行列两种存储格式。
从上图可以看到,行列存储最大的不同就是表的组织方式不同。
列式存储,意味着该列数据往往类型相同,可以采用某种压缩算法进行统一压缩存储。
比如下面这个例子,用字典表的方式压缩存储字符串:
查询Customers
列为Miller
且Material
列为Regrigerator
的流程如下:
行 | 列 | |
---|---|---|
优点 | 1.便于按行查询数据,OLTP往往是此场景 2.便于行级插入、删除、修改 3.易保证行级一致性 |
1.便于按列使用数据,如对列分组、排序、聚合等,OLAP很多是这样 2.列数据同类型,便于压缩 3.表设计灵活,易扩展列 |
缺点 | 1.当只需查询某几个列时,还是会读整行数据 2.扩展列代价往往较高 |
1.不便于按行使用数据 2.很难保证行级一致性 |
优化思想 | 读取过程尽量减少不需要的数据 | 提高读写效率 |
优化措施 | 1.设计表时尽量减少冗余列 2.内存中累积写入到阈值再批量写入 |
1.多线程方式并行读取不同列文件 2.行级一致性,可通过加入RDBMS中回滚机制、校验码等 3.内存中累积写入到阈值再批量写入 |
应用场景 | OLTP | OLAP |
Client有访问Hbase的接口,会去meta表查询目标region所在位置(此信息会放入缓存),并连接对应RegionServer进行数据读写。
当master rebalance region时,Client会重新进行查找。
.META.
表所在RegionServer位置一个RegionServer上存在多个Region和一个HLog读写实例。
一个RegionServer上只有一个HLog实例,共用一个HLog的原因是减少磁盘IO开销,减少磁盘寻道时间。
HLog的就是WAL(Write-Ahead-Log),相当于RDBMS中的redoLog,写数据时会先写一份到HLog。可以配置MultiWAL
,多Region时使用多个管道来并行写入多个WAL流。
一个表中所有行按RowKey字典序排序,而每个Region是某个表按行水平拆分的结果(一般每个表初始只有一个Region),每个表的Region分部到多个RegionServer。
Region上按列族划分为多个Store
每个Store有一个MemStore,当有读写请求时先请求MemStore
每个Store又有多个StoreFile
HFiles是数据的实际存储格式,他是二进制文件。StoreFile对HFile进行了封装。HBase的数据在底层文件中时以KeyValue键值对的形式存储的,HBase没有数据类型,HFile中存储的是字节,这些字节按字典序排列。
BlockCache
为HBase提供最终的底层数据存储服务,多副本保证高可用性 .
/hbase
/data
/<Namespace> (集群里的Namespaces)
/<Table> (该集群的Tables)
/<Region> (该table的Regions)
/<ColumnFamily> (该Region的列族)
/<StoreFile> (该列族的StoreFiles)
/hbase
/WALs
/<RegionServer> (RegionServers)
/<WAL> (WAL files for the RegionServer)
每个表在行的方向上分割为N个Region。
一个Region水平切分的示例:
Region知识要点:
一个RegionServer上存在多个Region和一个Hlog实例。
Region属于某个表水平拆分的结果(初始一个Region),每个表的Region分部到多个RegionServer。
需要注意的是,Region是逻辑存储划分的最小单元,也就是说任何一个Region不会拆分存储到不同RegionServer:
每个Store有一个MemStore,当有读写请求时先请求MemStore。MemStore内部是根据RowKey
, Column
, Version
排序
每个Store又有多个StoreFile
AssignmentManager
,他会通过.META.
表检测Region分配的合法性,当发现不合法(如RegionServer挂掉)时,调用LoadBalancerFactory
来重分配Region到其他RS。.META.
表。可参考:
注意,这里说的是Region级别的合并,一旦手动触发,HBase会不做很多自动化检查,直接执行合并。
major compaction
影响读写性能,因此选择低峰期手动触发;alter
操作之后希望立刻生效;该过程对Client来说是异步的,是Master和RegionServer共同参与,步骤如下:
这里指的是StoreFile级别的合并。
原因
当MemStore不断flush到磁盘,StoreFile会越来越多,从而导致查询时IO次数增加,效率降低。如下图
时机
合并根据许多因素,可能有益于形同表现也有可能是负面影响。
hbase.hstore.blockingStoreFiles
,则会阻塞该Region的更新写入操作,直到有Compact发生减少了StoreFile数量或等待until hbase.hstore.blockingWaitTime
超时,然后继续正常Flush。Compact过程
HBase Compact
过程,就是RegionServer定期将多个小StoreFile合并为大StoreFile,具体如下:
./tmp
目录下的临时文件。此时就会忽略TTL过期数据。Compact影响
读放大
合并操作的目的是增加读的性能,否则搜索时要读取多个文件,当然合并过程会有短时间的IO消耗所以影响读响应时间造成所谓读放大,但可以是的后续查询延迟降低,如下图:
写放大
如果在合并时不限制写请求,当HFile生成速度大于合并速度时可能使得HFile越来越多,读性能不断下降,所以必须对此时写请求进行限制。具体来说,如果任何一个Region的Store中存在超过hbase.hstore.blockingStoreFiles
的StoreFiles,则会阻塞此Region的更新,直到Compact使得文件数低于该值或阻塞时间超出hbase.hstore.blockingWaitTime
。这种阻塞行为可在RS的日志中查看到。
影响小结
Compact会使得数据读取延迟一直比较平稳,但付出的代价是大量的读延迟毛刺和一定的写阻塞。
自动合并分为Minor Compact和Major Compact:
Minor Compact
合并概述:
仅会挑选Store内少量小的、临近的StoreFile进行合并,最理想是选到IO负载高但size较小的文件,合并后就能读取较少的文件。Minor Compact结果是更少、更大的StoreFile。
Minor Compact会合并TTL过期的数据:合并时会删除这些TTL过期数据,不再写入合并后的StoreFile。(注意TTL删除的数据无墓碑)
Minor Compact一般速度很快,对业务的影响也比较小,就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。
合并过程
输出
每个Store合并完成的输出是少量较大的StoreFile。
Major Compact
默认Major Compact 7天执行一次,可能会导致异常开销,影响系统表现,所以可以进行手动调优。
当前版本(1.2.0版)采用的合并策略为ExploringCompactionPolicy,挑选最佳的StoreFiles集合以使用最少量的工作进行压缩,可以减少合并带来的消耗。使用ExploringCompactionPolicy,主要Major Compact的频率要低得多,因为Minor Compaction效率更高。
流程如下:
列出目标Store中所有现有的StoreFiles,待过滤此列表以提出将被选择用于Compact的HFile子集。
如果这是手动执行的Compact,则尝试执行手动请求的压缩类型。
请注意,即使用户请求Major Compact,也可能无法执行。原因可能是列族中某些StoreFiles不可Compact,或者因为列族中的StoreFiles太多。
某些StoreFiles会被自动排除掉:
hbase.hstore.compaction.max.size
的StoreFilehbase.mapreduce.hfileoutputformat.compaction.exclude
排除的批量加载操作的StoreFile遍历步骤1中的列表,并列出所有可合并的StoreFiles Set,将他们合并成一个StoreFile。这里的Set是指那些列表中的按hbase.hstore.compaction.min
大小连续分组的StoreFile。对于每个备选Set执行一些检查来确定最佳合并Set:
hbase.hstore.compaction.min
或大于hbase.hstore.compaction.max
的Set算法卡住
时的fall back
。hbase.hstore.compaction.max.size
的StorFilehbase.hstore.compaction.min.size
,则根据基于文件的比率进行健全性检查,以查看它是否太大而排除掉。 如果符合以下条件,则完整性检查成功:
hbase.hstore.compaction.ratio
(如果配置非高峰时间且检查时恰好是非高峰时间,则为hbase.hstore.compaction.ratio.offpeak
)的结果。(比如FileX = 5MB, FileY = 2MB, FileZ = 3MB,此时5 <= 1.2 x (2 + 3)=6,所以FileX筛选通过;FileX大小大于6MB就不通过Compact筛选。)如果该Set通过筛选,请将其与先前选择的最佳Compact Set进行比较。如果更好就替换。
当处理完整个潜在CompactFileList后,执行我们已经找到的最佳Compact。
如果没有成功选择出Compact Set,但存在多个StoreFiles,则认为此事属于算法卡住
,就执行步骤4.2中找到的最小Set进行Compact。
RatioBasedCompactionPolicy想找到第一个”可行解“即可,而ExploringCompactionPolicy却在尽可能的去寻求一种自定义评价标准中的”最优解“。
HBase 0.96.x之前使用RatioBasedCompactionPolicy,从老到新扫描潜在可选CompactStoreFile,选择那些没有在CompactQueue且比比正在Compact的StoreFile更新的StoreFile组成List且按SequenceId排序。此时如果算法卡住,则强制执行开销巨大的Major Compact。作为对比,更优的ExploringCompactionPolicy
则是Minor Compact最小Set。
接下来的步骤和ExploringCompactionPolicy基本相同。
但如果剩余未遍历的待合并文件list数量少于hbase.hstore.compaction.min
,则放弃Minor Compact。一旦找到满足条件的第一个StoreFile Set就停止扫描并开始Compact。Minor Compact的peak规则如下:
hbase.hstore.compaction.ratio
。也可使用hbase.hstore.compaction.ratio.offpeak
, hbase.offpeak.start.hour
hbase.offpeak.end.hour
配置非高峰期选项。最后,如果检查到最近一次Major Compact是很久以前的并且目前需要合并多个StoreFile,则会运行一个Major Compact,即使本应是Minor Compact。
以上合并策略选取文件未考虑最近写入数据往往也更容易被读取这一特点
,所以还是有缺陷。大量合并那些读的很少的老文件是没有必要的,因为他们合并后也不会对读性能有很多提升。
DateTieredCompaction(日期分层合并)是一种日期感知的StoreFile合并策略,将StoreFile按日期分为多个不同分区,并加入时间窗口概念,有利于的时序数据的time-range scan
。
性能提升:
适合场景:
不适合的场景:
性能提升:
要为表或列族启用DateTieredCompaction:
hbase.hstore.engine.class
设置为org.apache.hadoop.hbase.regionserver.DateTieredStoreEngine
。hbase.hstore.blockingStoreFiles
设置为较大数字,例如60,而不是默认值12)。hbase.hstore.compaction.max
设置为与hbase.hstore.blockingStoreFiles
相同的值,避免Major Compat时发生写入阻塞。DateTieredCompaction主要参数如下:
参数 | 含义 | 默认值 |
---|---|---|
hbase.hstore.compaction.date.tiered.max.storefile.age.millis | Storefile的最大Timestamp值比该参数还小的永不会被合并 | Long.MAX_VALUE |
hbase.hstore.compaction.date.tiered.base.window.millis | 毫秒级的基础时间窗口大小,后面会越来越大 | 6小时 |
hbase.hstore.compaction.date.tiered.windows.per.tier | 每个层级的增加的窗口倍数,比如为2,则窗口大小变动为6小时->12小时->24小时 | 4 |
hbase.hstore.compaction.date.tiered.incoming.window.min | 在incoming窗口中Compact的最小文件数。 将其设置为窗口中预期的文件数,以避免浪费资源进行极少文件Compact | 6 |
hbase.hstore.compaction.date.tiered.window.policy.class | ||
在同一时间窗口内挑选Storefile的策略,该策略不适用于incoming窗口。 | ExploringCompactionPolicy | |
hbase.regionserver.throughput.controller | 推荐将Compact节流阀设为org.apache.hadoop.hbase.regionserver.compactions.PressureAwareCompactionThroughputController,因为分层Compact中所有急群众的RS会同时提升窗口到高层级 | - |
下面是基础窗口为1小时,窗口成长倍数为2,最小合并文件数为3的一个例子:
可以看到 [0-1)->[1->3)->[3-7) 三个窗口依次寻找,只有[3-7)
这个窗口有3个文件满足了最小合并文件数,所以会被Compact。
如果HFile跨窗口,则会被计入时间更老的窗口。
StripeCompactionPolicy使用分层策略,分为L0和L1层:
MemStore数据Flush后的HFile属于L0,当L0的文件数达到可配的阈值后触发写入,即将L0数据读取后写入L1。
L1的数据按ROWKEY范围进行划分,划分结果是多个户不重叠的Stripe
,思想可类比将Region拆分多个子Region。
从L0写入L1的KeyValue数据就是根据Key来定位到具体的某个Stripe。
StripeCompactionPolicy提升如下:
StripeCompactionPolicy的适用场景:
HBase CompactSplitThead线程负责Compact和Split,内部又分为Split线程池和用于Compact的largeCompactions、smallCompactions线程池。分入哪个线程池的判断依据:
hbase.regionserver.thread.compaction.throttle
,则用largeCompactions。该阈值默认值为2
x hbase.hstore.compaction.max
(默认10) x hbase.hregion.memstore.flush.size
(默认128MB)hbase.regionserver.thread.compaction.large
和hbase.regionserver.thread.compaction.small
修改。当scan
查询时遇到合并正在进行,解决此问题方案点这里
Compact或多或少会影响HBase其他功能的表现,所以HBase在1.5之后有对Compact进行限流,2.x后默认会自动限流Compact,在压力大时降低合并吞吐量,压力小时增加。需要注意的是Compact限流是RegionServer级别,而非Compact级别。
具体来说:
lower + (higer – lower) * pressureRatio
的限制下,其中ratio
是一个取值范围在(0,1),它由当前store中待参与Compation的Hfile数量动态决定。文件数量越多,ratio越小,反之越大)。hbase.hstore.blockingStoreFiles
限制则阻塞MemStore Flush,直到Compact使得文件数低于该值或阻塞时间超出hbase.hstore.blockingWaitTime
。hbase.hregion.memstore.block.multiplier
乘以 hbase.hregion.memstore.flush.size
字节时,会阻塞写入,主要是为了防止在update高峰期间MemStore大小失控,造成其flush的文件需要很长时间来compact或split,甚至造成OOM服务直接down掉。内存足够大时,可调大该值。pressureRatio
默认为flushPressure
,throughput.controller
设为PressureAwareCompactionThroughputController
时为compactionPressure
。
minFilesToCompact
为hbase.hstore.compaction.min
。所以该值越大说明堆积的HFile越多,越可能达到阈值导致写入阻塞,需要加快合并,所以吞吐量限制阈值会变高。当pressureRatio大于1时,即当前HFile数大于blockingStoreFiles,发生写入阻塞,此时会直接不再限制合并吞吐量,疯狂Compact。如果当前HFile数小于minFilesToCompact则不会发生合并。主要配置如下:
参数 | 释义 | 默认值 |
---|---|---|
hbase.hstore.compaction.throughput.lower.bound | 吞吐量下界,默认50MB/s | 52428800 |
hbase.hstore.compaction.throughput.higher.bound | 吞吐量上界,默认100MB/s | 104857600 |
hbase.regionserver.throughput.controller | RS吞吐量控制器,若想无限制设为org.apache.hadoop.hbase.regionserver.throttle.NoLimitThroughputController;控制合并相关指标org.apache.hadoop.hbase.regionserver.compactions.PressureAwareCompactionThroughputController; | 控制刷写相关指标:PressureAwareFlushThroughputController |
hbase.hstore.blockingStoreFiles | 如果任何一个Region的Store中存在超过hbase.hstore.blockingStoreFiles 的StoreFiles,则会阻塞此Region的MemStore flush,直到Compact使得文件数低于该值或阻塞时间超出hbase.hstore.blockingWaitTime 。这种阻塞行为可在RS的日志中查看到。 |
16 |
hbase.hstore.compaction.ratio | 对于MinorCompac,此比率用于确定大于hbase.hstore.compaction.min.size 的StoreFile是否有资格进行Compact,目的是限制大型StoreFile Compact。 |
1.2F |
hbase.offpeak.start.hour | 非高峰期的起始小时,[0-23] | -1(禁用) |
hbase.offpeak.end.hour | 非高峰期的终止小时,[0-23] | -1(禁用) |
hbase.hstore.compaction.ratio.offpeak | 非高峰时段使用的Compact ratio,默认很激进的策略,用来决定非高峰期时段内大型StoreFile被涵盖在Compact内的策略。 表示为浮点小数。 这允许在设定的时间段内更积极(或更低级,如果您将其设置为低于hbase.hstore.compaction.ratio)的Compact。 如果禁用非高峰时则忽略本参数。 本参数与hbase.hstore.compaction.ratio的工作方式相同。具体可以参考ExploringCompactionPolicy | 5.0F |
可参考Region切分细节
默认情况下,HBase表初始创建时只有一个Region,放在一个RegionServer上。HBase有自动Split,也可以pre-split或手动触发split。
本段转自Hbase 技术细节笔记(下)
用到的参数主要是hbase.hregion.max.filesize
,即HFile大小超过此值就Split。
HBase Region的拆分策略有比较多,比如除了3种默认过的策略,还有DelimitedKeyPrefixRegionSplitPolicy、KeyPrefixRegionSplitPolicy、DisableSplitPolicy等策略,这里只介绍3种默认的策略。分别是ConstantSizeRegionSplitPolicy策略、IncreasingToUpperBoundRegionSplitPolicy策略和SteppingSplitPolicy策略。
ConstantSizeRegionSplitPolicy
是0.94版本之前的默认拆分策略,这个策略的拆分规则是:当region大小达到hbase.hregion.max.filesize
(默认10G)后拆分。
这种拆分策略对于小表不太友好,按照默认的设置,如果1个表的Hfile小于10G就一直不会拆分。注意10G是压缩后的大小,如果使用了压缩的话。如果1个表一直不拆分,访问量小也不会有问题,但是如果这个表访问量比较大的话,就比较容易出现性能问题。这个时候只能手工进行拆分。还是很不方便。
IncreasingToUpperBoundRegionSplitPolicy
是Hbase的0.94~2.0版本默认的拆分策略,这个策略相较于ConstantSizeRegionSplitPolicy策略做了一些优化,该策略的算法为:min(r^2*flushSize,maxFileSize ),最大为maxFileSize 。
从这个算是我们可以得出flushsize为128M、maxFileSize为10G的情况下,可以计算出Region的分裂情况如下:
第一次拆分大小为:min(10G,11128M)=128M
第二次拆分大小为:min(10G,33128M)=1152M
第三次拆分大小为:min(10G,55128M)=3200M
第四次拆分大小为:min(10G,77128M)=6272M
第五次拆分大小为:min(10G,99128M)=10G
第六次拆分大小为:min(10G,1111128M)=10G
从上面的计算我们可以看到这种策略能够自适应大表和小表,但是这种策略会导致小表产生比较多的小region,对于小表还是不是很完美。
SteppingSplitPolicy
SteppingSplitPolicy是在Hbase 2.0版本后的默认策略,拆分规则为:
if region=1
then: flush size * 2
else: MaxRegionFileSize
还是以flushsize为128M、maxFileSize为10场景为列,计算出Region的分裂情况如下:
第一次拆分大小为:2*128M=256M
第二次拆分大小为:10G
从上面的计算我们可以看出,这种策略兼顾了ConstantSizeRegionSplitPolicy策略和IncreasingToUpperBoundRegionSplitPolicy策略,对于小表也有比较好的适配。
一般情况下使用默认切分策略即可,也可以在cf级别设置region切分策略,命令为:
create ’table’, {NAME => ‘cf’, SPLIT_POLICY => ‘org.apache.hadoop.hbase.regionserver. ConstantSizeRegionSplitPolicy'}
可参考
注意:Split过程是RegionServer进行的,没有Master参与。
.META.
表。上图中,绿色箭头为客户端操作;红色箭头为Master和RegionServer操作:
RegionServer决定拆分Region,并准备拆分。此时,Split事务已经开始。RegionServer在表上获取共享读锁,以防止在他人在拆分过程中修改表的schema
;然后在ZK的/hbase/region-in-transition/region-name
下创建一个znode
,并将该节点状态设置为SPLITTING
。
Master在/hbase/region-in-transition
设置了Watcher
,所以会感知到这个znode变更,从而得知该split事件,在Master页面RIT
模块可以看到region执行split的状态信息。
RegionServer在HDFS的/hbase/region-in-transition/region-name
目录下创建一个名为.splits
的子目录。
RegionServer关闭该待split的Region,并在其本地数据结构中将该Region标记为离线状态。被分裂的Region现在处于离线状态。此时,如果客户端请求该Region将抛出NotServingRegionException
,客户端将自动重试。
RegionServer在父Region.splits
目录下为子Region A和B创建目录和必要的数据结构。然后它将拆分StoreFiles,在子Region目录中为每个父Region的StoreFile创建两个指针reference
文件来指向父Region的文件。
reference文件名的前半部分是父Region对应的HFile文件名,.
号后的部分是父Region名称。
文件内容主要有两部分构成:
RegionServer为子Region们在HDFS中创建实际的Region目录,并移动每个子Region的指针文件。
RegionServer向.META
表发送Put请求,将父Region设置为离线,并添加子Region的信息(此时客户端不可见)。在.META
表更新即将父Region Offline列设为true,Region拆分将由Master推进。
如果在 RPC 成功之前 region server 就失败了,master和下次打开parent region的region server 会清除关于这次split的脏状态。但是当RPC返回结果给到parent region ,即.META.
成功更新之后,region split的流程还会继续进行下去。相当于是个补偿机制,下次在打开这个parent region的时候会进行相应的清理操作。
RegionServer并行打开子Region A和B.
RegionServer将子Region A和B的信息添加到.META
表,具体来说在.META
表更新即将子Region Offline列设为false。此时,这些子Region现在处于在线状态。在此之后,客户端可以发现新Region并向他们发出请求了。客户端会缓存.META
到本地,但当他们向RegionServer或.META表发出请求时,原先的ParentRegion的缓存将失效,此时将从.META
获取新Region信息。
RegionServer将ZooKeeper中的znode/hbase/region-in-transition/region-name
更新为状态SPLIT,Master可以感知到该事件。如有必要,平衡器可以自由地将子Region重新分配给其他RegionServer。Split拆分事务现已完成。
拆分完成后,.META
和HDFS仍将包含对父Region的引用。当子Region进行Major Compact,读取父Regionx相应数据进行数据文件重写时,才删除这些引用。当检查线程发现SPLIT=TRUE
的父Region对应的子Region已经没有了索引文件时,就删除父Region文件。Master的GC任务会定期检查子Region是否仍然引用父Region的文件。如果不是,则将删除父Region。
也就是说,Region自动Split并不会有数据迁移,而只是在子目录创建了到父Region的引用。而当Major Compact
时才会进行数据迁移,在此之前查询子Region数据流程如下:
JournalEntryType
来表征各阶段:在以下情况可以采用预分区(预Split)方式提高效率:
rowkey
按时间递增(或类似算法),导致最近的数据全部读写请求都累积到最新的Region中,造成数据热点。
扩容多个RS节点后,可以手动拆分Region,以均衡负载
在BulkLoad
大批数据前,可提前拆分Region以避免后期因频繁拆分造成的负载
为避免数据rowkey分布预测不准确造成的Region数据热点问题,最好的办法就是首先预测split的切分点做pre-splitting
,以后都让auto-split
来处理未来的负载均衡。
官方建议提前为预分区表在每个RegionServer创建一个Region。如果过多可能会造成很多表拥有大量小Region,从而造成系统崩溃。
注意合理分区方式
比如采用Admin.createTable(byte[] startKey, byte[] endKey, 10)
构建的"0000000000000000"
到 "ffffffffffffffff"
的预分区如下:
这种分区方式就会因为数据的rowkey范围是[0-9]
和[a-f]
从而使得仅有1,2,10号Region有数据,而其他Region无数据。
例子
# 创建了t1表的f列族,有4个预分区
create 'test_0807_1','cf1',SPLITS => ['10','20','30']
# 指定切分点建预分区表
create 'test_0807_2','cf1',SPLITS => ['\x10\x00', '\x20\x00', '\x30\x00', '\x40\x00']
# 建表,使用随机字节数组来进行预分4个Region
create 'test_0807_3','cf1', { NUMREGIONS => 4 , SPLITALGO => 'UniformSplit' }
# 建表,假设RowKey都是十六进制字符串来进行拆分,预分5个Region
create 'test_0807_4','cf1', { NUMREGIONS => 5, SPLITALGO => 'HexStringSplit' }
一般来说,手动拆分是弥补rowkey设计的不足。我们拆分region的方式必须依赖数据的特征:
字母/数字类rowkey
可按范围划分。比如A-Z的26个字母开头的rowkey,可按[A, D]…[U,Z]这样的方式水平拆分Region。
自定义算法
HBase中的RegionSplitter
工具可根据特点,传入算法、Region数、列族等,自定义拆分:
只需传入要拆分的Region数量,会将数据从00000000
到FFFFFFFF
之间的数据长度按照N等分,并算出每一分段的startKey和endKey来作为拆分点。
HBase 允许客户端强制执行split,在hbase shell中执行以下命令:
//其中forced_table 为要split的table , ‘b’ 为split 点
split 'forced_table', 'b'
更多内容可以阅读这篇文章Apache HBase Region Splitting and Merging
HBase的HMaster负责为每个Region维护了状态并存在META表,持久化到Zookeeper。
上图颜色含义如下:
具体状态转移说明如下:
OFFLINE->OPENING
Master将Region从OFFLINE
状态移动到OPENING
状态,并尝试将该Region分配给RegionServer。 Master会重试发送请求直到响应通过或重试次数达到阈值。RegionServer收到该请求后开始打开该Region。
OPENING->CLOSING
如果Master没有重试,且之前的请求超时,就认为失败,然后将该Region设为CLOSING
并试图关闭它。即使RegionServer已经开始打开该区域也会这么做。如果Master没有重试,且之前的请求超时,就认为失败,然后将该Region设为CLOSING
并试图关闭它。即使RegionServer已经开始打开该区域也会这么做。
OPENING->OPEN
在RegionServer打开该Region后,通知Master直到Master将该Region状态变更为OPEN
状态并通知RegionServer。
OPENING->CLOSED
如果RegionServer无法打开该Region,则会通知Master将Region转移为CLOSED
,并尝试在其他的RegionServer上打开该Region。
OPENING->FAILED_OPEN
如果Master无法在任何RegionServer中打开该Region,则会将该Region设为到FAILED_OPEN
。HBase shell
进行手动干预或服务器停止之前不会再有其他操作。
OPEN->CLOSING
Master将Region从OPEN
状态转到CLOSING
状态。持有该Region的RegionServer可能已/未收到该关闭请求。Master重试发送关闭请求,直到RPC通过或重试次数达到阈值。
CLOSING->OFFLINE
如果RegionServer离线或抛出NotServingRegionException
,则Master将该Region移至OFFLINE
状态,并将其重新分配给其他RegionServer。
CLOSING->FAILED_CLOSE
如果RegionServer处于在线状态,但Master重试发送关闭请求达到阈值,则会将该Region设为FAILED_CLOSE
状态,并且在管理员从HBase shell
进行干预或服务器已停止之前不会采取进一步操作。
CLOSING->CLOSED
如果RegionServer获取到关闭Region请求,它将关闭该Region并通知Master将该Region设为CLOSED
状态,并将其重新分配给其他RegionServer。
CLOSED->OFFLINE
在分配Region之前处于CLOSED
状态,则Master会自动将Region移动到OFFLINE
状态。
OPEN->SPLITING
当RegionServer即将拆分Region时,它会通知Master将要拆分的Region从OPEN
切换到SPLITTING
状态,并将拆分后要创建的两个新Region添加到RegionServer。这两个Region最初处于SPLITTING_NEW
状态。
SPLITING->SPLIT
通知Master后,RegionServer开始拆分Region。一旦超过no return
点,RegionServer就会再次通知Master以便更新META
表。但在通知拆分完成之前,Master不会真正更新Region状态。如果拆分成功,则拆分区域将从SPLITTING
移至SPLIT
状态,并且两个新Region将从SPLITTING_NEW
移至OPEN
状态(13)。
SPLITTING->OPEN, SPLITTING_NEW->OFFLINE
如果拆分失败,则拆分Region将从SPLITTING
移回OPEN
状态,并且创建的两个新Region将从SPLITTING_NEW
移至OFFLINE
状态。
OPEN->MERGING
当RegionServer即将合并两个Region时会首先通知Master。Master将要合并的两个Region从OPEN
迁移到MERGING
状态,并将用来保存合并后内容的新Region添加到RegionServer。新Region最初处于MERGING_NEW
状态。
MERGING->MERGED, MERGING_NEW->OPEN
通知Master后,RegionServer开始合并这两个Region。一旦超过no return
点,RegionServer就会再次通知r,以便Master可以更新META
。但在通知合并已完成之前,Master不会更新区域状态。如果合并成功,则两个合并Region从MERGING
状态转移到MERGED
状态;新Region从MERGING_NEW
移动到OPEN
状态。
MERGING->OPEN, MEGING_NEW->OFFLINE
如果两个Region合并失败,则他们会从MERGING
移回到OPEN
状态;而那个用于存放合并后内容的新Region则从MERGING_NEW
转移到OFFLINE
状态。
FAILED_CLOSE->CLOSING, FAILED_OPEN->CLOSING
对于处于FAILED_OPEN
或FAILED_CLOSE
状态的Region,当Master通过HBase Shell
重新分配它们时会先尝试再次关闭它们。
由Master的LoadBalancer线程周期性的在各个RegionServer间移动region维护负载均衡。
请点击这里
可参考官网-Why should I keep my Region count low?
官方推荐每个RegionServer拥有100个左右region效果最佳,控制数量的原因如下:
MSLAB
HBase的一个特性MSLAB(MemStore-local allocation buffer
,Memstore内存本地分配缓冲,将JVM Heap分为很多Chunk) ,它有助于防止堆内存的碎片化,减轻Full GC的问题(CMS会因为是标记-清除算法而导致老年代内存碎片,碎片过小无法分配新对象导致FullGC整理内存),默认开启,但他与MemStore一一对应,每个就占用2MB空间。比如一个HBase表有1000个region,每个region有2个CF,那也就是不存储数据就占用了3.9G内存空间,如果极多可能造成OOM需要关闭此特性。
HBase不适用TLAB线程私有化分配的原因是一个线程管理了多个Region的多个MemStore,无法隔离各个MemStore内存。比如5个Region(A-E),然后分别写入ABCDEABCEDDAECBACEBCED,然后B的MemStore发生了Flush,内存情况现在是:A CDEA CEDDAEC ACE CED
,显然产生了内存碎片!如果后面的写入还是如之前一样的大小,不会有问题,但一旦超过就无法分配。也就是说,一个Region的MemStore内的内容其实在老年代内存物理地址上并不连续。于是HBase参考TLAB实现了一套以MemStore为最小分配单元的内存管理机制MSLAB。
RegionServer维护了一个全局MemStoreChunkPool实例,而每个MemStore实例又维护了一个MemStoreLAB实例,每个MemStore独占若干Chunk。所以MemStore收到KeyValue数据后先从MemStoreChunkPool中申请一个Chunk放入数据(放入curChunk并移动偏移量),放满了就重新申请Chunk来放数据。该过程是LockFree的,基于CAS。
如果MemStore因为flush而释放内存,则以chunk为单位来清理内存,避免内存碎片。注意,虽然能解决内存碎片,但会因为Chunk放小数据而降低内存利用率。
MSLAB还有一个好处是使得原本分开的MemStore内存分配变为老年代中连续的Chunk内分配。
最大的好处就是几乎完全避免了GC STW!
每个Region的每个列族就有一个Memstore,Region过多那么MemStore更多,总内存过大频繁触发Region Server级别阈值导致Region Server级别flush,会对用户请求产生较大的影响,可能阻塞服务响应或产生compaction storm
(因为Region过多导致StoreFile也过多,不断合并)。
HMaster要花大量的时间来分配和移动Region,且过多Region会增加ZooKeeper的负担。
默认MR任务一个region对应一个mapper,region太多会造成mapper任务过多。
推荐的每个RegionServer的Region数量公式如下:
((RS memory) * (total memstore fraction)) / ((memstore size)*(# column families))
在生产环境中如果Region过大会造成compaction尤其是major compaction严重影响性能,目前推荐的region最大10-20Gb,最优5-10Gb。参数hbase.hregion.max.filesize
控制单个region的HFile总大小最大值,再大就会触发split了:
hbase.hregion.max.filesize
比较小时,触发split的机率更大,系统的整体访问服务会出现不稳定现象。RegionServer本地性是通过HDFS Block副本实现。
当某个RS故障后,其他的RS也许会因为Region恢复而被Master分配非本地的Region的StoreFiles文件(其实就是之前挂掉的RS节点上的StoreFiles的HDFS副本)。但随着新数据写入该Region,或是该表被合并、StoreFiles重写等之后,这些数据又变得相对来说本地化了。
Region元数据详细信息存于.META.
表(没错,也是一张HBase表,只是HBase shell
的list
命令看不到)中(最新版称为hbase:meta
表),该表的位置信息存在ZK中。
该表的结构如下:
Key
该Region的key信息,格式:([table],[region start key],[region id])
Values
server:port
一个Store有一个MemStore
,保存数据修改。当flush后,当前MemStore就被清理了。
注意,MemStorez中的数据按 RowKey 字典升序排序。
Memstore Flush
最小单位是Region,而不是单个MemStore。为了减少flush过程对读写影响,HBase采用了类似于2PC的方式,将整个flush过程分为三个阶段:
prepare
遍历当前Region中的所有Memstore,将Memstore中当前数据集kvset做一个快照snapshot,对后来的读请求提供服务,读不到再去BlockCache/HFile中查找。
然后再新建一个新的kvset Memstore(SkipList
),服务于后来的写入。
prepare阶段需要加一把写锁对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
flush
遍历所有Memstore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp
下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时,但不会影响读写。
commit
遍历Region所有的Memstore,将flush阶段生成的临时文件移到指定的ColumnFamily
目录下,针对HFile生成对应的storefile
和Reader
,随后把storefile添加到HStore的storefiles列表中。最后清空prepare阶段生成的snapshot。
整个flush过程还可能涉及到compact和split
当Flush发生时,当前MemStore实例会被移动到一个snapshot
中,然后被清理掉。在此期间,新来的写操作会被新的MemStore和刚才提到的备份snapshot
接收,直到flush成功后,snapshot
才会被废弃。
Region级别-跨列族
Region内的其中一个MemStore大小达到阈值(hbase.hregion.memstore.flush.size
),该Region所有MemStore一起发生Flush,输入磁盘。
RegionServer级别
当一个RS内的全部MemStore使用内存总量所占比例达到了阈值(hbase.regionserver.global.memstore.upperLimit
),那么会一起按Region的MemStore用量降序排列flush,直到降低到阈值(hbase.regionserver.global.memstore.lowerLimit
)以下。
另有一个新的参数hbase.regionserver.global.memstore.size
,设定了一个RS内全部Memstore的总大小阈值,默认大小为Heap的40%,达到阈值以后就会阻塞更新请求,并开始RS级别的MemStore flush,和上述行为相同。
HLog-WAL文件
当region server的WAL的log数量达到hbase.regionserver.max.logs
,该server上多个region的MemStore会被刷写到磁盘(按照时间顺序),以降低WAL的大小。否则会导致故障恢复时间过长。
手动触发
通过HBase shell或Java Api手动触发MemStore flush
MemStore Flush不会更新BlockCache
不会造成从BlockCache读到脏数据:
Flush阻塞
当MemStore的数据达到hbase.hregion.memstore.block.multiplier
乘以 hbase.hregion.memstore.flush.size
字节时,会阻塞写入,主要是为了防止在update高峰期间MemStore大小失控,造成其flush的文件需要很长时间来compact或split,甚至造成OOM服务直接down掉。内存足够大时,可调大该值。
所以,针对此,我们需要避免Region数量或列族数量过多造成MemStore太大。
可参考hbase实践之flush and compaction
增加了内存中Compact逻辑。MemStore变为由一个可写的Segment,以及一个或多个不可写的Segments构成。
MemStore Flush时,为了避免对读请求的影响,MemStore会对当前内存数据kvset创建snapshot
,并清空kvset
的内容。
读请求在查询KeyValue的时候也会同时查询snapshot,这样就不会受到太大影响。但是要注意,写请求是把数据写入到kvset
里面,因此必须加锁避免线程访问发生冲突。由于可能有多个写请求同时存在,因此写请求获取的是updatesLock
的readLock
,而snapshot
同一时间只有一个,因此获取的是updatesLock
的writeLock
。
数据修改操作先写入MemStore,在该内存为有序状态。
先查MemStore,查不到再去查StoreFile。
Scan具体读取步骤如下:
客户端对table发起scan操作时,HBase的RegionServer会为每个region构建一个RegionScanner
RegionScanner包含一个StoreScanner
列表,每个列族创建了一个StoreScanner
StoreScanner又为每个StoreFile创建了一个StoreFileScanner
构成list,以及为MemStore传了一个KeyValueScanner
列表。
上述两个列表最终会合并为一个最小堆(其实是优先级队列),其中的元素是上述的两类scanner,元素按seek到的keyvalue大小按升序排列。
HBase中,Key大小首先比较RowKey,RowKey越小Key就越小;RowKey如果相同就看CF,CF越小Key越小;CF如果相同看Qualifier,Qualifier越小Key越小;Qualifier如果相同再看Timestamp,Timestamp越大表示时间越新,对应的Key越小;如果Timestamp还相同,就看KeyType,KeyType按照DeleteFamily -> DeleteColumn -> Delete -> Put 顺序依次对应的Key越来越大。
当构建StoreFileScanner
后,会自动关联一个MultiVersionConcurrencyControl Read Point
,他是当前的MemStore版本,scan操作只能读到这个点之前的数据。ReadPoint
之后的更改会被过滤掉,不能被搜索到。这也就是所谓的读提交(RC)。
查询的scanner会组成最小堆,每次pop出堆顶的那个scanner seek到的KeyValue,进行如下判定:
更多关于数据读取流程具体到scanner粒度的请阅读HBase原理-数据读取流程解析
一个Store有>=0个SotreFiles(HFiles)。
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 字典升序排序。
DataBlock
MagicHeader
和一些KeyValue组成,key的值是严格按照顺序存储的。DataIndex
DataBlock的索引,每条索引的key是被索引的block的第一条记录的key(StartKey),采用LRU机制淘汰,可以有多级索引。
格式为:(MagicHeader,(DataBlock在HFile的offset + DataBlockLength + DataBlockFirstKey),(DataBlock在HFile的offset + DataBlockLength + DataBlockFirstKey),……..)
。
MetaBlock (可选的)
保存用户自定义的KeyValue,可被压缩,如BloomFilter就是存在这里。该块只保留value值,key值保存在元数据索引块中。每一个MetaBlock由header和value组成,可以被用来快速判断指定的key是否都在该HFile中。
MetaIndex (可选的)
MetaBlock的索引,只有root级索引,保存在MetaBlock。
格式为:(MagicHeader,(MetaBlock在HFile的offset + MetaBlockLength + MetaBlockName),(MetaBlock在HFile的offset + MetaBlockLength + MetaBlockName),……..)
。
BloomIndex
作为布隆过滤器MetaData的一部分存储在RS启动时加载区域。
FileInfo
HFile的元数据,固定长度,不可压缩,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN
, AVG_VALUE_LEN
, LAST_KEY
, COMPARATOR
, MAX_SEQ_ID_KEY
等,用户也可以在这一部分添加自定义元数据。
Trailer
Trailer是定长的,保存了上述每个段的偏移量(即起始位置),所以读取一个HFile时会先读取Trailer,(段的MagicHeader被用来做安全check),随后读取DataBlockIndex到内存中。
这样一来,当检索某个key时,不需要扫描整个HFile,而只需从内存中的DataBlockIndex找到key所在的DataBlock,随后通过一次磁盘io将整个DataBlock读取到内存中,再找到具体的KeyValue。
Trailer部分格式为:
其中CompressionCodec
为int,压缩算法为enum类型,表示压缩算法:LZO-0,GZ-1,NONE-2。
HFileBlock默认大小是64KB,而HadoopBlock的默认大小为64MB。顺序读多的情况下可配置使用较大HFile块,随机访问多的时候可使用较小HFile块。
不仅是DataBlock,DataBlockIndex和BloomFilter都被拆成了多个Block,都可以按需读取,从而避免在Region Open阶段或读取阶段一次读入大量的数据而真正用到的数据其实就是很少一部分,可有效降低时延。
HBase同一RegionServer上的所有Region共用一份读缓存。当读取磁盘上某一条数据时,HBase会将整个HFile block
读到cache中。此后,当client请求临近的数据时可直接访问缓存,响应更快,也就是说,HBase鼓励将那些相似的,会被一起查找的数据存放在一起。
注意,当我们在做全表scan时,为了不刷走读缓存中的热数据,记得关闭读缓存的功能(因为HFile放入LRUCache后,不用的将被清理)
更多关于HFile BlockCache资料请查看HBase BlockCache 101
初始只有一层,数据多时分裂为多层索引(最多可支持三层索引,即最底层的Data Block Index称之为Leaf Index Block,可直接索引到Data Block;中间层称之为Intermediate Index Block,最上层称之为Root Data Index,Root Data index存放在一个称之为"Load-on-open Section"区域,Region Open时会被加载到内存中),使用LruBlockCache::
Root Data index
,Region打开时就会被加载到内存中初始HFile无Block,数据在MemStore
flush发生,HFileWriter初始,DataBlock生成,此时Header为空
开始写入,Header被用来存放该DataBlock的元数据信息
MemStore的KeyValue,写入DataBlock。如果设置了Data Block Encoding
,此时需要进行编码。
KeyValue达到Block大小,停止写入
对KeyValue进行压缩,再进行加密
在Header区写入对应DataBlock元数据信息,包含{压缩前的大小,压缩后的大小,上一个Block的偏移信息,Checksum元数据信息}等信息。
生成Checksum校验和信息
通过HFileWriter将DataBlock/Checksum写入HDFS
为DataBlock生成包含StartKey,Offset,Size等信息的索引,先写入内存中的Block Index Chunk
,累积到阈值后刷入HDFS,形成Leaf Index Block
。
DataBlock和DataBlockIndex(Leaf Index Block)在Scanned Block Section
交叉存在。
而HBase中还存在Root Index Chunk
用来记录每个DataBlockIndex的信息,即为DataBlock的索引的索引。
当MemStore Flush中最后一个KeyValue写入到最后一个DataBlock即最后一个DataBlockIndex时,随机开始flush Root Index Chunk。
注意,如果Root Index Chunk大小超出阈值,则会生成位于Non-Scanned Block Section
区域的Intermediate Index Block
,由Root Index生成索引来指向。
无论如何,都会输出位于的Load-On-Open Section
的Root Index Block
。
生成FileInfo,记录HFile的元数据
写入BloomFilter元数据与索引数据
最后写入Trailer部分信息
目的
HBase中的BloomFilter提供了一个轻量级的内存结构,以便将给定Get
(BloomFilter不能与Scans一起使用,而是Scan中的每一行来使用)的磁盘读取次数减少到仅可能包含所需Row的StoreFiles,而且性能增益随着并行读取的数量增加而增加。
和BlockIndex区别
一个Region有多个Store,一个Store需要扫描多个StoreFile(HFile),每个HFile有一个BlockIndex,粒度较粗,需要通过key range
扫描很多BlockIndex来判断目标key是否可能在该文件中,但仍需要加载该HFile中的若干Block并scan才能确定是否真的存在目标key。(比如一个1GB的HFile,就包含16384个64KB的Block,且BlockIndex只有StartKey信息。查询一个给点key则可能落在两个Block的StartKey范围之间,需要全部load-scan)。
而BloomFilter对于Get
操作以及部分Scan
操作可以过滤掉很多肯定不包含目标Key的HFile文件,大大减少实际IO次数,提高随机读性能。
使用场景
每个数据条目大小至少为KB级
存储位置
BloomFilter的Hash函数和BloomFilterIndex存储在每个HFile的启动时加载区
中;而具体的存放数据的BloomFilterBlock,会随着数据变多而变为多个Block以便按需一次性加载到内存,这些BloomFilterBlock散步在HFile中扫描Block区
,不需要更新(因为HFile的不可变性),只是会在删除时会重建BloomFilter,所以不适合大量删除场景。
加载时机
当因为Region部署到RegionServer而打开HFile时,BloomFilter将加载到内存。
HBase中的BloomFilter实现
KeyValue在写入HFile时,经过若干hash函数的映射将对应的数组位改为1。当Get时也进行相同hash运算,如果遇到某位为0则说明数据肯定不在该HFile中,如果都为1则提示高概率命中。
当然,因为HBase为了权衡内存使用和命中率等,将BloomFilter数组进行了拆分,并引入了BloomIndex,查找时先通过StartKey找到对应的BloomBlock再进行上述查找过程。
BloomFilterIndex
如上图,BloomFilterIndex中的BloomIndexEntry中有个BlockKey存有真实数据KeyValue的StartKey,所以每次需要据此来查找BloomFilterBlock进行使用。
弹性
HBase包括一些调整机制,用于折叠(fold)BloomFilter以减小大小并将误报率保持在所需范围内。
行-列模式
指标
blockCacheHitRatio
可观察到RegionServer上的BlockCache缓存命中率,如果开启BloomFilter来过滤那些不包含目标key的Block,则blockCacheHitRatio应该增加。
创建BloomFilter
创建HBase表时可用HColumnDescriptor.setBloomFilterType(NONE/ROW (default)/ ROWCOL)
或用以下命令创建BloomFilter:
create 'mytable',{NAME => 'colfam1', BLOOMFILTER => 'ROWCOL'}
可参考HBase BlockCache系列 - 探求BlockCache实现机制
HBase提供两种不同的BlockCache实现,来缓存从HDFS读取的数据:
MinMaxPriorityQueue
实现single-access
区(25%,存随机读入的Block块)、mutil-access
区(50%,single区中数据被多次读就移入本区)、in-memory
区(25%,存储访问频繁且量下的数据,如元数据)multi-access
区的数据越来越多,会造成CMS FULL GC,导致应用程序长时间暂停DataBlockIndex
和BloomFilter
,其他数据(比如最主要的DataBlock)放在BucketCache
KeyValue的构成
KeyValue
是HBase的最核心内容。他主要由keylength, valuelength, key, value 四部分组成:
Put
、Delete
、 DeleteColumn
和DeleteFamily
)等信息KeyValue与BloomFilter
KeyValue在写入HFile时会用到BloomFilter,经过若干Hash函数计算将某些位置设为1。当查询时也是针对目标RowKey,拿出要查询的HFile上的BloomFilter进行相同hash运算,如果遇到某位置的数为0说明肯定目标数据肯定不存在该HFile中。
当然,实际上HBaseHFile可能特别大,那么所使用的数组就会相应的变得特别大,所以不可能只用一个数组,所以又加入了BloomIndexBlock来查找目标RowKey位于哪个BloomIndex,然后是上述BloomFilter查找过程。
一个put操作如下:
Put #1: rowkey=row1, cf:attr1=value1
他的key组成如下:
rowlength -----------→ 4
row -----------------→ row1
columnfamilylength --→ 2
columnfamily --------→ cf
columnqualifier -----→ attr1
timestamp -----------→ server time of Put
keytype -------------→ Put
所以我们在设计列族、列、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文件数量达到最大个数的时候,就触发该RegionServer上的所有MemStore 按FIFO顺序进行Flush,直到WAL数量降到hbase.regionserver.max.logs
以下。此后,那些对应的HLog被视为过期,会被移动到.oldlogs
,随后被自动删除
WAL的意义就是和Memstore一起将随机写转为一次顺序写+内存写,提升了写入性能,并能保证数据不丢失。
注:本段转自Hbase 技术细节笔记(上)
Hlog从产生到最后删除需要经历如下几个过程:
产生
所有涉及到数据的变更都会先写HLog,除非是你关闭了HLog
滚动
HLog的大小通过参数hbase.regionserver.logroll.period
控制,默认是1个小时,时间达到hbase.regionserver.logroll.period
设置的时间,HBase的一个后台线程就会创建一个新的Hlog文件。这就实现了HLog滚动的目的。HBase通过hbase.regionserver.maxlogs
参数控制Hlog的个数。滚动的目的,为了控制单个HLog文件过大的情况,方便后续的过期和删除。
过期与sequenceid
Hlog的过期依赖于对sequenceid的判断。HBase会将HLog的sequenceid和HFile最大的sequenceid(刷新到的最新位置)进行比较,如果该Hlog文件中的sequenceid比flush的最新位置的sequenceid要小,那么这个HLog就过期了,对应HLog会被移动到.oldlogs
目录。
这里有个问题,为什么要将过期的Hlog移动到.oldlogs
目录,而不是直接删除呢?
答案是因为HBase还有一个主从同步的功能,这个依赖Hlog来同步HBase的变更,有一种情况不能删除HLog,那就是HLog虽然过期,但是对应的HLog并没有同步完成,因此比较好的做好是移动到别的目录。再增加对应的检查和保留时间。
删除
如果HLog开启了replication,当replication执行完一个Hlog的时候,会删除Zoopkeeper上的对应Hlog节点。在Hlog被移动到.oldlogs目录后,HBase每隔hbase.master.cleaner.interval
(默认60秒)时间会去检查.oldlogs目录下的所有Hlog,确认对应的Zookeeper的Hlog节点是否被删除,如果Zookeeper 上不存在对应的Hlog节点,那么就直接删除对应的Hlog。
hbase.master.logcleaner.ttl
(默认10分钟)这个参数设置Hlog在.oldlogs目录保留的最长时间。
前面提到过,一个RegionServer共用一个WAL。下图是一个RS上的3个Region共用一个WAL实例的示意图:
数据写入时,会将若干数据对
按照顺序依次追加到HLog,即顺序写入。
HLogKey主要包括:
WALEdit
用来表示一个事务中的更新集合,在目前的版本,如果一个事务中对一行row R中三列c1,c2,c3
分别做了修改,那么HLog为了行级事务原子性日志片段如下所示:
其中WALEdit
会被序列化为格式<-1, # of edits,
,比如<-1, 3,
,其中-1作为标示符表征这种新的日志结构。
见这里
上表是HBase逻辑视图,其中空白的区域并不会占用空间。这也就是为什么成为HBase是稀疏表的原因。
类似RDBMS的库。建表时指定,否则会被分配default
namespace。
类似RDBMS的表
底层实现是Byte Array(字节数组),是表中每条记录的“主键”,即唯一标识某行的元素,方便快速查找,RowKey的设计非常重要。
MemStore和HFile中按RowKey的字典三维升序排列,通过 RowKey,column key(column family和column qualifier)和 timestamp/version这个三个维度可以对 HBase 中的数据进行快速定位。。
且RowKey符合最左匹配原则,所以内含的各个值的位置顺序将深刻影响数据存储和检索效率。所以在设计RowKey时,需要对日后的业务应用有深入了解和前瞻性预测。
最左匹配原则例子:如设计RowKey为uid
+ phone
+ name
,那么可以匹配一下内容:
但是无法用RowKey支持以下搜索:
搜索时可用ROWKEY调用GET API
查询唯一行,也可以指定startRowkey、stopRowkey调用SCAN
查询范围, 还可以什么都不指定直接使用SCAN
进行全表扫描。
即列族,拥有一个名称(string),包含一个或者多个列,物理上存在一起。比如,列courses:history 和 courses:math都是 列族 courses的成员.冒号(:)是列族的分隔符。建表时就必须要确定有几个列族。每个
即列,属于某个columnfamily,familyName:columnName。列可动态添加
即版本号,类型为Long,默认值是系统时间戳timestamp,也可由用户自定义。相同行、列的cell按版本号倒序排列。多个相同version的写,只会采用最后一个。
{row, column, version} 组一个cell即单元格,其内容是byte数组。
表水平拆分为多个Region,是HBase集群分布数据的最小单位。
HBase表的同一个region放在一个目录里
一个region下的不同列族放在不同目录
每行的数据按rowkey->列族->列名->timestamp(版本号)逆序排列,也就是说最新版本数据在最前面。
请查看事务章节。
具体来说
RegionServer启动时,就会到ZK上创建自己的临时ZNode,并会定时向Zookeeper发送心跳
RegionServer挂掉后一段时间(SessionTimeout
),在ZK注册的临时节点会被删除,此时该RS上的Region立刻变得不可用
HMaster监听到ZK事件得知该RS挂掉,HMaster会认为该RS上的Region分配非法,开启Region重分配到其他RegionServer流程。
HMaster还会将该挂掉的RegionServer存在HDFS上的那个HLog文件按Region进行切分,待不可用的那些Region重分配到其他RegionServer后,对HLog按照Region和sequenceid
由小到大进行重放补足HLog中的数据,这样数据就迁移完毕。
在此过程中的客户端查询会被重试,不会丢失
详细流程可以参考:
Zookeeper选举机制选出一个新的Leader Master。但要注意在没有Master存活时:
.META.
表进行。Zookeeper是一个可靠地分布式服务
HDFS是一个可靠地分布式服务
注意,该过程中Client不会和Master联系,只需要配置ZK信息。
Client访问Zookeeper,找到hbase:meta
表所在RegionServer
根据所查数据的Namespace
、表名和rowkey
在hbase:meta
表顺序查找找到对应的Region信息,并会对该Region位置信息进行缓存。
如果Region由master负载均衡重新分配,或因为相关RegionServer挂掉,则Client将重新查询hbase:meta
表以确定Region的新位置然后放入缓存。
下一步就可以请求Region所在RegionServer了,会初始化三层scanner
实例来一行一行的每个列族分别查找目标数据:
PriorityQueue
优先级队列来构建最小堆,排序规则是RowKey分别比较RowKey(小优先),ColumnFamily
(小优先),Qualifier
(小优先),TimeStamp
(大优先),KeyType
(DeleteFamily
-> DeleteColumn
-> Delete
-> Put
),这样便于客户端取数据(比如需要获取最新版本数据时只需要再按版本号排次序即可)Delete*
/是否被用户设置的其他Filter过滤掉,如果通过检查就加入结果集等待返回。如果查询未结束,则剩余元素重新调整最小堆,继续这一查找检查过程,直到结束。RegionScanner将多个StoreScanner按列族小优先规则来合并构建最小堆
网易范欣欣-HBase-数据读取流程解析
网易范欣欣-HBase-迟到的‘数据读取流程’部分细节
StoreFileScanner查找磁盘。为了加速查找,使用了快索引和布隆过滤器:
块索引
块索引存储在HFile文件末端,查找目标数据时先将块索引读入内存。因为HFile中的KeyValue字节数据是按字典序排列,而块索引存储了所有HFile block
的起始key,所以我们可快速定位目标数据可能所在的块,只将其读到内存,加快查找速度。
布隆过滤器
虽然块索引减少了需要读到内存中的数据,但依然需要对每个HFile文件中的块执行查找。
而布隆过滤器则可以帮助我们跳过那些一定不包含目标数据的文件。和块索引一样,布隆过滤器也被存储在文件末端,会被优先加载到内存中。另外,布隆过滤器分行式和列式两种,列式需要更多的存储空间,因此如果是按行读取数据,没必要使用列式的布隆过滤器。布隆过滤器如下图所示:
块索引和布隆过滤器对比如下:
块索引 | 布隆过滤器 | |
---|---|---|
功能 | 快速定位记录在HFile中可能的块 | 快速判断HFile块中是否包含目标记录 |
Client默认设置autoflush=true
,表示put请求直接会提交给服务器进行处理;
也可设置autoflush=false
,put请求会首先放到本地buffer,等到本地buffer大小超过一定阈值(默认为2M,可以通过配置文件配置)之后才会异步批量提交。
很显然,后者采用批处理方式提交请求,可极大地提升写入性能,但因为没有保护机制,如果该过程中Client挂掉的话会因为内存中的那些buffer数据丢失导致提交的请求数据丢失!
Client根据前面提到的定位流程来定位到数据所在RegionServer。
若是批量请求,还会将rowkey按HRegionLocation
分组,每个分组可对应到目标Region的一次RPC请求MultiServerCallable
。
Client为通过rpcCallerFactory.
执行调用,忽略掉失败重新提交和错误处理。
代码在hbase-server module中的org.apache.hadoop.hbase.regionserver.HRegion#doMiniBatchMutate
内。
HBase1.2.9版的写入流程如下:
Server尝试获取该批次写入行的行锁(行锁可保证行级事务原子性)来锁定目标行,检索当前的WriteNumber
(可用于MVCC的非锁读)。还会获取Region更新共享锁,写事务开始。
调用Pre协处理器
更新所有写入操作的KeyValue时间戳为系统时间
Server把所有写入的数据构造为一个WALEdit,然后按顺序写一份到WAL(一个RegionServer共用一个WAL实例)。HBase这里用了disruptor
实现了高效的生产者消费者模式来实现WAL追加写。注意,此时WAL并没有sync到磁盘。
当RS突然崩溃时且事务已经写入WAL,那就会在其他RS节点上重放。
Server把数据再写一份到MemStore(每个Store一个MemStore实例),此时会把获取到的WriteNumber
附加到KeyValue。
注意此时还未更新ReadPoint,也就是说此时更新对读取线程不可见。
Server相关操作如下:
Flush
Server待MemStore达到阈值后,会把数据刷入磁盘,形成一个StoreFile文件。
若在此过程中挂掉,可通过HLog重放恢复。
成功刷入磁盘后,会清空HLog和MemStore。
Compact
Server待多个StoreFile文件达到一定的大小后,会触发StoreFileCompact合并操作,将多个小的StoreFile文件合并为一个大的StoreFile文件,同时清理无效数据。
Split
Server待Region大小超过一定阈值后,会触发Split拆分操作
Server释放共享锁和行锁。选择这个时间释放行锁的原因是可尽量减少持有互斥的行级写锁时间,提升写性能。
Server Sync HLog到磁盘。如果过程中sync操作失败,会对写入Memstore内的数据进行移除,即回滚。
调用Post协处理器
Server提交本次写事务。随后,ReadPoint
(即读线程能看到的WriteNumber)就能前移,从而检索到该新的事务编号,使得scan
和get
能获取到最新数据
如果memsotre达到64MB,则flush memstore
HBase2.2.2版的写入流程如下:
此过程不需要HMbaster参与:
.META.
表缓存区访问RS时会找不到目标Region,会进行重试,重试次数达到阈值后会去.META.
表查找最新数据并更新缓存。delete
命令时,hbase只是添加
标记,称为墓碑。此时并未真正删除Major Compact
中被删除的数据和此墓碑标记才从StoreFile会被真正删除。CF默认的TTL值是FOREVER,也就是永不过期。
过期数据不会创建墓碑,如果一个StoreFile仅包括过期的rows,会在Minor Compact的时候被清理掉,不再写入合并后的StoreFile。
TTL分为两类即Cell和CF:
注意:修改表结构之前,需要先disable 表,否则表中的记录被清空!
还可参考:
总的来说,分为三个步骤:
写入本地Buffer老写入模型
在老的写入模型中,每个写入线程的WriteHandler都需要分别竞争updateLock和flushLock,效率较低。
新写入模型
新写入模型采取了多线程模式独立完成写HDFS、HDFS fsync,避免了之前多工作线程恶性抢占锁的问题。并引入一个Notify线程通知WriteHandler线程是否已经fsync成功,可消除旧模型中的锁竞争。
同时,工作线程在将WALEdit写入本地Buffer之后并没有马上阻塞,而是释放行锁之后阻塞等待WALEdit落盘,这样可以尽可能地避免行锁竞争,提高写入性能。
关于此过程详细可参考网易范欣欣-HBase - 数据写入流程解析
B树存在的问题:
总的来说,B树随机IO会造成低效的磁盘寻道,严重影响性能。
可参考LSM树
HFile格式基于Bigtable论文中SSTable。
RowKey
, Version
, Column
排序,但多个StoreFile之间在合并前是无序的。添加
在Major Compact中被删除的数据和此墓碑标记才会被真正删除。
HBase Compact
过程,就是RegionServer定期将多个小StoreFile合并为大StoreFile,也就是LSM小树合并为大树。这个操作的目的是增加读的性能,否则搜索时要读取多个文件。
HBase中合并有两种:
RDBMS使用B+树,需要大量随机读写;
而LSM树使用WALog和Memstore将随机写操作转为顺序写。
可参考数据库事务系列-HBase行级事务模型
HBase和RDBMS类似,也提供了事务的概念,只不过HBase的事务是行级事务,可以保证行级数据的ACID性质。
boolean checkAndPut(byte[] row, byte[] family, byte[] qualifier,
byte[] value, Put put) throws IOException;
一致性概述
以上的时间不是cell中的时间戳,而是事务提交时间。
隔离性-读提交
当构建StoreFileScanner
后,会自动关联一个MultiVersionConcurrencyControl Read Point
,他是当前的MemStore版本,scan操作只能读到这个点之前的数据。ReadPoint
之后的更改会被过滤掉,不能被搜索到。
这类事务隔离保证在RDBMS中称为读提交(RC)
不保证任何 Region 之间事务一致性
注意:由于 HBase 不保证任何 Region 之间(每个 Region 只保存在一个 Region Server 上)的一致性,故 MVCC 的数据结果只需保存在每个 RegionServer 各自的内存中。
当一台 RegionServer 挂掉,如果 WAL 已经完整写入,所有执行中的事务可以重放日志以恢复,如果 WAL 未写完,则未完成的事务会丢掉(相关的数据也丢失了)
当没有使用writeBuffer时,客户端提交修改请求并收到成功响应时,该修改立即对其他客户端可见。原因是行级事务。
HBase读数据时的scanner有一个Readpoint,该取值是写数据线程写入WriteNumber到KeyValue并提交事务后更新的。scan结果中会滤掉所有大于该 ReadPoint 的 KeyValues。
当一个 KeyValue 的 memstore timestamp(WriteNumber) 比最老的scanner(实际是 scanner 持有的 ReadPoint)还要老时,会被清零(置为0),这样该 KeyValue会对所有的 scanner 可见,当然,此时比该 KeyValue 原 memstore timestamp 更早的 scanner 都已经结束了。
所有可见数据也是持久化的数据。也就是说,每次读请求不会返回没有持久化的数据(注意,这里指hflush
而不是fsync
到磁盘)。
而那些返回成功的操作,就已经是持久化了;返回失败的,当然就不会持久化。
HBase默认要求上述性质,但可根据实际场景调整,比如修改持久性为定时刷盘。
关于ACID更多内容,请参阅HBase-acid-semantics和ACID in HBase
可参考:
HBase支持单行ACID性质,但在HBASE-3584新增了对多操作事务支持,还在HBASE-5229新增了对跨行事务的支持。HBase所有事务都是串行提交的。
为了实现事务特性,HBase采用了各种并发控制策略,包括各种锁机制、MVCC机制等,但没有实现混合的读写事务。
HBase采用CountDownLatch行锁实现更新的原子性,要么全部更新成功,要么失败。
所有对HBase行级数据的更新操作,都需要首先获取该行的行锁,并且在更新完成之后释放,等待其他线程获取。因此,HBase中对多线程同一行数据的更新操作都是串行操作。
行锁主要相关类为RowLock
和RowLockContext
:
RowLockContext
存储行锁内容包括持锁线程、被锁对象以及可以实现互斥锁的CountDownLatch对象等putIfAbsert
到全局map lockedRows
中,会返回一个existingContext对象,有三种情况:latch.await
方法阻塞在此RowLockContext对象上,直至该行锁被释放或者阻塞超时。待行锁释放,该线程会重新竞争该锁,一旦竞争成功就持有该行锁,否则继续阻塞。而如果阻塞超时,就会抛出异常,不会再去竞争该锁。释放流程
在线程更新完成操作之后,必须在finally方法中执行行锁释放rowLock.release()
方法,其主要逻辑为:
CountDownLatch在加锁时的应用
另一种是基于ReentrantReadWriteLock实现的读写锁,该锁可以给临界资源加上read-lock或者write-lock。其中read-lock允许并发的读取操作,而write-lock是完全的互斥操作。HBase利用读写锁实现了Store级别、Region级别的数据一致性
Region更新读写锁
HBase在执行数据更新操作之前都会加一把Region级别的读锁(共享锁),所有更新操作线程之间不会相互阻塞;然而,HBase在将memstore数据落盘时会加一把Region级别的写锁(独占锁)。因此,在memstore数据落盘时,数据更新操作线程(Put操作、Append操作、Delete操作)都会阻塞等待至该写锁释放。
Region Close保护锁
HBase在执行close操作以及split操作时会首先加一把Region级别的写锁(独占锁),阻塞对region的其他操作,比如compact操作、flush操作以及其他更新操作,这些操作都会持有一把读锁(共享锁)
Store snapshot保护锁
HBase在执行flush memstore的过程中首先会基于memstore做snapshot,这个阶段会加一把store级别的写锁(独占锁),用以阻塞其他线程对该memstore的各种更新操作;清除snapshot时也相同,会加一把写锁阻塞其他对该memstore的更新操作。
HBase还提供了MVCC机制实现数据的读写并发控制。
上图中的写行锁机制,如果在第二次更新时读到更新列族1cf1:t2_cf1
同时读到列族2cf2:t1_cf2
,这就产生了行数据不一致的情况。但如果想直接采用读写线程公用行锁来解决此问题,会产生严重性能问题。
HBase采用了一种MVCC思想,每个RegionServer维护一个严格单调递增的事务号:
PUT
或DELETE
命令)开始时,它将检索下一个最高的事务编号。这称为WriteNumber
。每个新建的KeyValue都会包括这个WriteNumber
,又称为Memstore timestamp
,注意他和KeyValue的timestamp
属性不同。SCAN
或GET
)启动时,它将检索上次提交的事务的事务编号。这称为ReadPoint
。因为HBase事务不能跨Region,所以这些MVCC信息就分别保存在RegionServer内存中。
具体来说,MVCC思想优化后的写流程如下:
上图是服务端接收到写请求后的写事务流程:
WriteNumber
WriteNumber
附加到KeyValueReadPoint
(即读线程能看到的WriteNumber)就能前移,从而检索到该新的事务编号,使得scan
和get
能获取到最新数据scanner
ReadPoint
。ReadPoint的值是所有的写操作完成序号中的最大整数WriteNumber
(Memstore timestamp) 大于 ReadPoint
的 KeyValue例子:
如上图所示,第一次更新获取的写序号为1,第二次更新获取的写序号为2。读请求进来时写操作完成序号中的最大整数为wn(WriteNumber) = 1,因此对应的读取点为wn = 1,读取的结果为wn = 1所对应的所有cell值集合,即为第一次更新锁写入的t1_cf1
和t1_cf2
,这样就可以实现以无锁的方式读取到行一致的数据。
更多详细讨论可见HBase之七:事务和并发控制机制原理
CountDownLatch
读写并发控制原因
如果不进行控制,可能读到写了一半的数据,比如a列是上个事务写入的数据,b列又是下一个事务写入的数据,这就出大问题了。
实现思想
读写并发采用MVCC思想,每个RegionServer维护一个严格单调递增的事务号。
PUT
或DELETE
命令)开始时,它将检索下一个最高的事务编号。这称为WriteNumber
。SCAN
或GET
)启动时,它将检索上次提交的事务的事务编号。这称为ReadPoint
。原理
写事务会加入到Region级别的自增序列即sequenceId并添加到队列。当sequenceId更大的事务已提交但较小的事务未提交时,更大的事务也必须等待,对读请求不可见。例子如下图:
当scan
时遇到合并正在进行,HBase处理方案如下:
scanner
使用的最早的ReadPoint
,不返回Memstore timestamp
大于该ReadPoint的那些KeyValue。Memstore timestamp
删除的时机就是当它比最早的那个scanner还早时,因为这个时候所有scanner都能获取该数据。通过集成Tephra,Phoenix可以支持ACID特性。Tephra也是Apache的一个项目,是事务管理器,它在像HBase这样的分布式数据存储上提供全局一致事务。HBase本身在行层次和区层次上支持强一致性,Tephra额外提供交叉区、交叉表的一致性来支持可扩展性、一致性。
可参考官方博客-coprocessor_introduction
协处理器可让我们在RegionServer服务端运行用户代码,实现类似RDBMS的触发器、存储过程等功能。
在一般情况下,我们使用Get
或Scan
命令,加上Filter,从HBase获取数据然后进行计算。这样的场景在小数据规模(如几千行)和若干列时性能表现尚好。然而当行数扩大到十亿行、百万列时,网络传输如此庞大的数据会使得网络很快成为瓶颈,而客户端也必须拥有强大的性能、足够的内存来处理计算这些海量数据。
在上述海量数据场景,协处理器可能发挥巨大作用:用户可将计算逻辑代码放到协处理器中在RegionServer上运行,甚至可以和目标数据在相同节点。计算完成后,再返回结果给客户端。
Observer协处理器
它类似RDBMS的触发器,可以在指定事件(如Get或Put)发生前后执行用户代码,不需要客户端代码。
Endpoint协处理器
它类似RDBMS的存储过程,也就是说可以在RegionServer上执行数据计算任务。Endpoint需要通过protocl
来定义接口实现客户端代码进行rpc通信,以此来进行数据的搜集归并。
具体来说,在各个region上并行执行的Endpoint代码类似于MR中的mapper任务,会将结果返回给Client。Client负责最终的聚合,算出整个表的指标,类似MR中的Reduce。
MR任务思想就是将计算过程放到数据节点,提高效率。思想和Endpoint协处理器相同。
将协处理看做通过拦截请求然后运行某些自定义代码来应用advice
,然后将请求传递到其最终目标(甚至更改目标)。
过滤器也是将计算逻辑移到RS上,但设计目标不太相同。
Coprocessor
(协处理器祖先接口), RegionObserver
(Observer), CoprocessorService
(Endpoint)类似RDBMS的触发器,可以在指定事件(如Get或Put)发生前(preGet)后(postGet)执行用户代码。
具体执行调用过程由HBase管理,对用户透明。
一般来说Observer协处理器又分为以下几种:
preGet
或prePost
中执行权限验证。可在数据位置执行计算。
具体执行调用过程必须继承通过客户端实现CoprocessorService
接口的方法,显示进行代码调用实现。
Endpoint通过protobuf
实现
Endpoint 协处理器类似传统数据库中的存储过程,客户端可以调用这些 Endpoint 协处理器执行一段 Server 端代码,并将 Server 端代码的结果返回给客户端进一步处理,最常见的用法就是进行聚集操作。
如果没有协处理器,当用户需要找出一张表中的最大数据,即 max 聚合操作,就必须进行全表扫描,在客户端代码内遍历扫描结果,并执行求最大值的操作。这样的方法无法利用底层集群的并发能力,而将所有计算都集中到 Client 端统一执行,势必效率低下。
利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端,HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给客户端。在客户端进一步将多个 Region 的最大值进一步处理而找到其中的最大值。这样整体的执行效率就会提高很多。
hbase-site.xml
中配置一个SumEndPoint
:
<property>
<name>hbase.coprocessor.region.classesname>
<value>org.myname.hbase.coprocessor.endpoint.SumEndPointvalue>
property>
HBase在服务端用默认的ClassLoader
加载上述配置的协处理器,所以说我们必须将协处理器和相关依赖代码打成jar后要放到RegionServer上的classpath才能运行。
这种方式加载的协处理器对所有表的所有Region可用,所以可称为system Coprocessor
。
列表中首个协处理器拥有最高优先级,后序的优先级数值依次递增。注意,优先级数值越高优先级越低。调用协处理器时,HBase会按优先级顺序调用回调方法。
重启HBase即可
hbase-site.xml
中去掉协处理器配置该种方式加载的协处理器只能对加载了的表有效。加载协处理器时,表必须离线。
动态加载,需要先将包含协处理器和所有依赖打包成jar,比如coprocessor.jar
,放在了HDFS的某个位置(也可放在每个RegionServer的本地磁盘,但是显然很麻烦)。
然后加载方式有以下三种:
HBase Shell
将需要加载协处理器的表离线禁用:
hbase> disable 'users'
加载协处理器:
下面各个参数用|
分隔。其中1073741823
代表优先级;arg1=1,arg2=2
代表协处理器参数,可选。
hbase alter 'users',
METHOD => 'table_att',
'Coprocessor'=>'hdfs://:/user//coprocessor.jar| org.myname.hbase.Coprocessor.RegionObserverExample|1073741823|arg1=1,arg2=2'
恢复表可用
hbase(main):003:0> enable 'users'
验证协处理器可用性
hbase(main):04:0> describe 'users'
可以在user表的TABLE_ATTRIBUTES
属性中看到已加载的协处理器。
Java API
TableName tableName = TableName.valueOf("users");
Path path = new Path("hdfs://:/user//coprocessor.jar" );
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();
admin.disableTable(tableName);
HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);
HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet");
columnFamily1.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet");
columnFamily2.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily2);
hTableDescriptor.addCoprocessor(RegionObserverExample.class.getCanonicalName(), path,
Coprocessor.PRIORITY_USER, null);
admin.modifyTable(tableName, hTableDescriptor);
admin.enableTable(tableName);
动态卸载
HBase Shell
将需要加载协处理器的表离线禁用:
hbase> disable 'users'
移除协处理器:
alter 'users', METHOD => 'table_att_unset', NAME => 'coprocessor$1'
恢复表可用
hbase(main):003:0> enable 'users'
Java API
TableName tableName = TableName.valueOf("users");
String path = "hdfs://:/user//coprocessor.jar" ;
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();
admin.disableTable(tableName);
HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);
// columnFamily2.removeCoprocessor()
HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet");
columnFamily1.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet");
columnFamily2.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily2);
admin.modifyTable(tableName, hTableDescriptor);
admin.enableTable(tableName);
官方文档例子。
一个users
表,拥有两个列族personalDet
(用户详情) 和 salaryDet
(薪水详情)
该协处理器能阻止在对users
表的Get
或Scan
操作中返回用户admin
的详情信息:
RegionObserver
接口方法preGetOp()
,在该方法中加入代码判断客户端查询的值是admin
。如果是,就返回错误提示,否则就返回查询结果:public class RegionObserverExample implements RegionObserver {
private static final byte[] ADMIN = Bytes.toBytes("admin");
private static final byte[] COLUMN_FAMILY = Bytes.toBytes("details");
private static final byte[] COLUMN = Bytes.toBytes("Admin_det");
private static final byte[] VALUE = Bytes.toBytes("You can't see Admin details");
@Override
public void preGetOp(final ObserverContext<RegionCoprocessorEnvironment> e, final Get get, final List<Cell> results)
throws IOException {
if (Bytes.equals(get.getRow(),ADMIN)) {
Cell c = CellUtil.createCell(get.getRow(),COLUMN_FAMILY, COLUMN,
System.currentTimeMillis(), (byte)4, VALUE);
results.add(c);
e.bypass();
}
}
@Override
public RegionScanner preScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> e, final Scan scan,
final RegionScanner s) throws IOException {
// 使用filter从scan中排除ADMIN结果
// 这样的缺点是会覆盖原有的其他filter
Filter filter = new RowFilter(CompareOp.NOT_EQUAL, new BinaryComparator(ADMIN));
scan.setFilter(filter);
return s;
}
@Override
public boolean postScannerNext(final ObserverContext<RegionCoprocessorEnvironment> e, final InternalScanner s,
final List<Result> results, final int limit, final boolean hasMore) throws IOException {
Result result = null;
Iterator<Result> iterator = results.iterator();
while (iterator.hasNext()) {
result = iterator.next();
if (Bytes.equals(result.getRow(), ADMIN)) {
// 也可以通过postScanner方式从结果中移除ADMIN
iterator.remove();
break;
}
}
return hasMore;
}
}
.jar
文件Get
、Scan
测试程序来验证该例子实现一个Endpoint协处理器来计算所有职员的薪水之和:
以protobuf
标准,创建一个描述我们服务的.proto
文件:
option java_package = "org.myname.hbase.coprocessor.autogenerated";
option java_outer_classname = "Sum";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED;
message SumRequest {
required string family = 1;
required string column = 2;
}
message SumResponse {
required int64 sum = 1 [default = 0];
}
service SumService {
rpc getSum(SumRequest)
returns (SumResponse);
}
对以上.proto
文件执行protoc
命令来生成java
代码Sum.java
到src
目录:
$ mkdir src
$ protoc --java_out=src ./sum.proto
Endpoint协处理器代码编写
继承刚才生成的类,并实现Coprocessor
和CoprocessorService
接口的方法:
public class SumEndPoint extends Sum.SumService implements Coprocessor, CoprocessorService {
private RegionCoprocessorEnvironment env;
@Override
public Service getService() {
return this;
}
@Override
public void start(CoprocessorEnvironment env) throws IOException {
if (env instanceof RegionCoprocessorEnvironment) {
this.env = (RegionCoprocessorEnvironment)env;
} else {
throw new CoprocessorException("Must be loaded on a table region!");
}
}
@Override
public void stop(CoprocessorEnvironment env) throws IOException {
// do nothing
}
@Override
public void getSum(RpcController controller, Sum.SumRequest request, RpcCallback<Sum.SumResponse> done) {
Scan scan = new Scan();
// 列族
scan.addFamily(Bytes.toBytes(request.getFamily()));
// 列
scan.addColumn(Bytes.toBytes(request.getFamily()), Bytes.toBytes(request.getColumn()));
Sum.SumResponse response = null;
InternalScanner scanner = null;
try {
scanner = env.getRegion().getScanner(scan);
List<Cell> results = new ArrayList<>();
boolean hasMore = false;
long sum = 0L;
do {
hasMore = scanner.next(results);
// 按cell(rowkey/列/timestamp)遍历结果
for (Cell cell : results) {
// 累加结果
sum = sum + Bytes.toLong(CellUtil.cloneValue(cell));
}
results.clear();
} while (hasMore);
// 构建带结果的相应
response = Sum.SumResponse.newBuilder().setSum(sum).build();
} catch (IOException ioe) {
ResponseConverter.setControllerException(controller, ioe);
} finally {
if (scanner != null) {
try {
// 用完记得关闭scanner
scanner.close();
} catch (IOException ignored) {}
}
}
// 返回结果
done.run(response);
}
}
客户端调用代码:
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
TableName tableName = TableName.valueOf("users");
Table table = connection.getTable(tableName);
// 构建 对salaryDet列族 gross列 求和 的rpc请求
final Sum.SumRequest request = Sum.SumRequest.newBuilder().setFamily("salaryDet").setColumn("gross").build();
try {
// 调用Entpoint协处理器方法,得到结果
Map<byte[], Long> results = table.coprocessorService(
Sum.SumService.class,
null, /* start key */
null, /* end key */
// 回调方法
new Batch.Call<Sum.SumService, Long>() {
@Override
public Long call(Sum.SumService aggregate) throws IOException {
BlockingRpcCallback<Sum.SumResponse> rpcCallback = new BlockingRpcCallback<>();
// 得到结果
aggregate.getSum(null, request, rpcCallback);
Sum.SumResponse response = rpcCallback.get();
return response.hasSum() ? response.getSum() : 0L;
}
}
);
// 遍历打印结果
for (Long sum : results.values()) {
System.out.println("Sum = " + sum);
}
} catch (ServiceException e) {
e.printStackTrace();
} catch (Throwable e) {
e.printStackTrace();
}
加载Endpoint
协处理器
执行上述客户端代码,进行测试
可参考Apache HBase Filter
过滤器使用不当会造成性能下降,必须经过严格测试才能投入生产环境。
过滤器可按RowKey/CF/Column/Timestamp等过滤数据,他们在于Scan/Get等配合使用时可直接在服务端就过滤掉不需要的数据,大大减少传回客户端的数据量。
可参考
按指定条件获取范围数据
scan时会综合StoreFile和MemStore scanner扫描结果。当构建scanner时,会关联一个MultiVersionConcurrencyControl Read Point
,只能读到这个点之前的数据。ReadPoint
之后的更改会被过滤掉,不能被搜索到。
注意点
setCaching
, setBatch
, setMaxResultSize
等方法以空间换时间的思想来提高效率;
setCacheBlocks
开启块缓存传入rowkey得到最新version数据或指定maxversion得到指定版本数据。除了查单一RowKey,也可以在构造 Get 对象的时候传入一个 rowkey 列表,这样一次 RPC 请求可以返回多条数据。
blockCacheHitRatio
(BlockCache命中率)指标是否增大,增大代表正面影响。放入一行数据.
注意点:
删除指定数据
在指定RowKey数据后追加数据
可参考HBase Increment(计数器)简介及性能测试
在RegionServer端原子性的对某个Value数值加或减,而不是加锁的Read-Modify-Write。可作为计数器使用。
进入到HBase RS机器的$HBASE_HOME/bin
目录后,使用hbase shell
命令启动 shell客户端即可。
如上图,单独建立一个HBase表,存F:C1列到RowKey的索引。
那么,当要查找满足F:C1=C11
的F:C2
列数据,就可以去索引表找到F:C1=C11
对应的RowKey,再回原表查找该行的F:C2数据。
用RegionObserver
的prePut
在每次写入主表数据时,写一条到索引表,即可建立二级索引。
更多例子可以看http://hbase.apache.org/book.html#schema.casestudies
BlockSize
(默认64KB)。当cell较大时需加大此配置。且该值和StoreFile的索引文件大小成反比。避免设计连续RowKey导致数据热点,导致过载而请求响应过慢或无法响应,甚至影响热点Region所在RS的其他Region。
如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议:
这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。常用措施如下:
按期望放置的RS数量设计若干随机前缀,在每个RowKey前随机添加,以将新数据均匀分散到集群中,负载均衡。
处理前
foo0001
foo0002
foo0003
foo0004
处理后
a-foo0003
b-foo0001
c-foo0003
c-foo0004
d-foo0002
优点
Salting可增加写的吞吐量
缺点
会降低读效率,因为有随机前缀,Scan和Get操作都受影响。
处理方法
用固定的hash算法,对每个key求前缀,然后取hash后的部分字符串和原来的rowkey进行拼接。
查询方法
查询时,也用这个算法先把原始RowKey进行转换后再输入到HBase进行查询。
优缺点
可以一定程度上打散整个数据集,但是不利于scan操作
由于不同数据的hash值有可能相同,所以在实际应用中,一般会使用md5计算,然后截取前几位的字符串.
示例
substring(MD5(设备ID),0,x) + 设备的ID,其中(x一般会取5到6位.)
将固定长度或范围的前N个字符逆序。打乱了RowKey,但会牺牲排序性。
type
这类的前缀使得分布均匀。Long.MAX_VALUE
– timestamp 作为RowKey,这样能保证新写入的数据在读取时可以被快速命中。因为越小的越靠前。指定一个RowKey数据的最大保存的版本个数,默认为3。越少越好,减小开销。
如果版本过多可能导致compact
时OOM。
如果非要使用很多版本,那最好考虑使用不同的行进行数据分离。
详见http://hbase.apache.org/book.html#compression
注意,压缩技术虽然能减小在数据存到磁盘的大小,但在内存中或网络传输时会膨胀。也就是说,不要妄图通过压缩来掩盖过大的RowKey/列族名/列名的负面影响。
一个cell不应超过10MB,否则就应该把数据存入HDFS,而只在HBase存指针指向该数据。
Apache HBase Performance Tuning
Scan用法大观园
写性能优化策略
读性能优化策略
聊聊HBase核心配置参数
HBase隔离方案实战
有赞技术-HBase 读流程解析与优化的最佳实践
优化hbase的查询优化-大幅提升读写速率
可参考:
调优点:
vm.swappiness = 0
可参考:
HBase最佳实践-CMS GC调优
调优点:
普通内存时用-XX:+UseConcMarkSweepGC
,即老年代CMS并发搜集,年轻带使用ParNew并行搜集;
大内存时启用G1 GC,针对大内存(>=32GB)优化。
Concurrent Mode Failure
调低-XX:CMSInitiatingOccupancyFraction
(比如60-70%),否则可能导致老年代占满并发搜集失败触发STW的FullGC。但不能过低,否则会导致过多的GC次数和CPU使用。
老年代内存碎片
默认开启了MSLAB来解决此问题。还可以在hbase-env.sh
中设置-XX:PretenureSizeThreshold
小于hbase.hregion.memstore.mslab.chunksize
以使得MSLAB直接在老年代分配内存,这样可以省去在年轻带Survivor区复制成本已经发生MinorGC时复制到老年代成本。
相关配置如下:
可参考Recommended Configurations
Memstore配置适合与否对性能影响很大,频繁的flush会带来额外负载影响读写性能。
可参考Configuring HBase Memstore: What You Should Know
还需要看这里
尽量少用Bytes.toBytes
,因为在循环或MR任务中,这种重复的转换代价昂贵,应该如下定义:
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(rowkey);
Result r = table.get(get);
byte[] b = r.getValue(CF, ATTR); // returns current version of value
使用基于MR的BulkLoad相较于HBaseAPI,使用更少的CPU和网络资源。
他的原理是使用MR来将数据转为HBase内部数据结构StoreFile,然后直接Load到运行中的HBase集群中,而不需要普通Client API写入的一系列复杂流程。主要流程如下:
通过MR的HFileOutputFormat2
格式来生成StoreFile
为了效率,HFileOutputFormat必须被特殊配置,以使得每个输出的HFile都能放入一个单独的Region中。
Load StoreFile到HBase集群
使用completebulkload
命令行遍历准备好的StoreFile数据文件,并为每个文件确定所属的Region,随后联系对应的RegionServer,将文件上传到他们的目录,最后让数据对客户端可见。
如果在BulkLoad过程中,Region边界发生变化,本命令行工具也可以自动处理,将数据文件按新的边界分片。
$ hadoop jar hbase-mapreduce-VERSION.jar completebulkload [-c /path/to/hbase/config/hbase-site.xml] /user/todd/myoutput mytable
-c
选项是可选的,是在未配置到CLASSPATH
时用来指定包含HBase参数的文件
在允许的场景,可将WALflush设为异步甚至禁用,坏处是丢数据风险。
可在批量加载数据时禁用WALflush。
对实时性要求高的使用SSD
RS与DN混合部署,提升数据读写本地性。
Hedged Reads(对冲读),是Hadoop 2.4.0 引入的一项HDFS特性。
hedgedReadOps
(已触发对冲读取线程的次数。 这可能表明读取请求通常很慢,或者对冲读取的触发过快) 和 hedgeReadOpsWin
(对冲读取线程比原始线程快的次数。 这可能表示给定的RegionServer在处理请求时遇到问题) 指标评估开启对冲读的效果hbase-site.xml
配置如下内容:<property>
<name>dfs.client.hedged.read.threadpool.sizename>
<value>50value>
property>
<property>
<name>dfs.client.hedged.read.threshold.millisname>
<value>100value>
property>
hbase-site.xml
文件:<property>
<name>dfs.client.read.shortcircuitname>
<value>truevalue>
<description>
This configuration parameter turns on short-circuit local reads.
description>
property>
<property>
<name>dfs.domain.socket.pathname>
<value>/home/stack/sockets/short_circuit_read_socket_PORTvalue>
<description>
Optional. This is a path to a UNIX domain socket that will be used for
communication between the DataNode and local HDFS clients.
If the string "_PORT" is present in this path, it will be replaced by the
TCP port of the DataNode.
description>
property>
为了防止RS挂掉时带来的其上Region不可用及恢复的时间空档,可使用HBase Replication
:
注意,该方式因为需要数据同步所以备集群肯定会有一定延迟。
Scan时建议指定需要的Column Family
,减少通信量,否则scan操作默认会返回整个row的所有CF/Column的数据.
Scan s = new Scan();
// 选择列族进行Scan
s.addFamily(Bytes.toBytes("cf1"));
// 选择列族/列进行scan
s.addColumn(Bytes.toBytes("cf1"), Bytes.toBytes("c1"));
// 仅检索列族cf下的c1列和c2列
Map<byte [], NavigableSet<byte []>> familyMap = new HashMap<>();
NavigableSet cs = new TreeSet();
cs.add("c1");
cs.add("c2");
familyMap.put(Bytes.toBytes("cf1"), cs);
s.setFamilyMap(familyMap);
使用完后,关闭Connection/Table/ResultScanner,特别是不关闭ResultScanner可能导致RegionServer出现性能问题,所以最好是把他放在try中:
Scan scan = new Scan();
// set attrs...
ResultScanner rs = table.getScanner(scan);
try {
for (Result r = rs.next(); r != null; r = rs.next()) {
// process result...
} finally {
rs.close(); // always close the ResultScanner!
}
table.close();
按需调整BloomFilter,默认是Row
模式
Table.delete(Delete)
就会有一个RPC请求发送到RS,而不能利用writerBuffer,所以大量删除时请使用Table.delete(List)
关于Phoenix on HBase,即Sql化的HBase服务,可以参考Phoenix学习
hbase:meta
快速的定位到Region,而且优先MemStore(SkipList跳表)查询,因为HBase的写入特性所以MemStore如果找到符合要求的肯定就是最新的直接返回即可。如果找不到还能通过BloomFilter/DataBlock索引等高效的从BlockCache查找,
还是没有的话就相对快速的从已按RowKey升序排序的HFile中查找。
2. 列式存储,如果查找的列在某个列族,只需查找定位Region的某一个Store即可
3. 可使用丰富的过滤器来加快Scan速度。
4. 后台会定期拆分Region,将大的Region分布到多个RS;定期合并,将大量小StoreFile合并为一个,同时删除无效信息,减少扫描读取数据量。
虽然HBase很多时候是随机写入,但因为引入了内存中的MemStore(由SkipList实现,是多层有序数据结构),批量顺序输入HDFS,所以可先写入将随机写转为了顺序写
HBase读数据流程复杂
其一是因为整个HBase存储引擎基于LSM-Tree实现,因此一次范围查询可能会涉及多个分片、多块缓存甚至多个数据存储文件;
其二是因为HBase中更新操作以及删除操作实现都很简单,更新操作并没有更新原有数据,而是使用时间戳属性实现了多版本;删除操作也并没有真正删除原有数据,只是插入了一条打上"deleted"标签的数据,而真正的数据删除发生在系统异步执行Major_Compact的时候。
很显然,这种实现套路大大简化了数据更新、删除流程,但是对于数据读取来说却意味着套上了层层枷锁,读取过程需要根据版本进行过滤,同时对已经标记删除的数据也要进行过滤。
Scan流程
这里是每次都会调用100行数据,客户端拿到之后,再扫描100条数据,直到数据被全部获取完毕。
上层业务不断一条一条获取扫描数据,在数据量大的情况下实际上HBase客户端会不断发送next请求到HBase服务器。有的朋友可能会问为什么scan需要设计为多次next请求的模式?个人认为这是基于多个层面的考虑:
get
的批处理操作是按照目标region进行分组,不同分组的get请求会并发执行读取。然而scan并没有这样的并发实现。
所以从客户端角度来看整个扫描时间=客户端处理数据时间+服务器端扫描数据时间,这能不能优化?
根据上面的分析,scan API的效率很大程度上取决于扫描的数据量。通常建议OLTP业务中少量数据量扫描的scan可以使用scan API。如果大量数据的扫描使用scan API,扫描性能有时很低,甚至造成服务器压力。
如果非要scan,请设定列族、列、start/end key、过滤器(可以通过setFilter方法添加过滤器,这也是分页、多条件查询的基础(但BloomFilter不适用于Scan)。过滤器使用不当会造成性能下降,必须经过严格测试才能投入生产环境)等。
检查RSMemStore内存设置
检查hbase.hstore.blockingStoreFiles
参数
Flush后如果该Store的StoreFile数量如果超过了hbase.hstore.blockingStoreFiles
,则会阻塞该Region的更新写入操作,直到有Compact发生减少了StoreFile数量或等待until hbase.hstore.blockingWaitTime
超时,然后继续正常Flush。
检查hbase.hregion.memstore.flush.size
hbase.hregion.memstore.block.multiplier
当MemStore的数据达到hbase.hregion.memstore.block.multiplier
乘以 hbase.hregion.memstore.flush.size
字节时,会阻塞写入,主要是为了防止在update高峰期间MemStore大小失控,造成其flush的文件需要很长时间来compact或split,甚至造成OOM服务直接down掉。内存足够大时,可调大该值。
检查hbase.regionserver.global.memstore.size
hbase.regionserver.global.memstore.size
设定了一个RS内全部Memstore的总大小阈值,默认大小为Heap的40%,达到阈值以后就会阻塞更新请求,并开始RS级别的MemStore flush。
RS因为长时间FullGC 导致STW,无法及时发送心跳到ZK,所以被ZK标为宕机。此时会Master会注意到该RS节点挂掉,将其上的Region迁移到其他RS节点。待故障RS恢复后,发现自己被标为宕机,所以只能自杀,否则会出现错乱。
HBase不完全是列式存储,确切的说是列族式存储,HBase中可以定义一个列族,列族下可以有都个列,这些列的数据是存在一起的。而且通常情况下我们建议列族个数不大于2个,这样的话每个列族下面必然会有很多列。因此HBase并不是列式存储,更有点像行式存储。
HBase扫描本质上是一个一个的随机读,不能做到像HDFS(Parquet)这样的顺序扫描。试想,1000w数据一条一条get出来,性能必然不会很好。
问题就来了,HBase为什么不支持顺序扫描?
因为HBase支持更新操作以及多版本的概念,这个很重要。可以说如果支持更新操作以及多版本的话,扫描性能就不会太好。原理是HBase是一个类LSM数据结构,数据写入之后先写入内存,内存达到一定程度就会形成一个文件,因此HBase的一个列族会有很多文件存在。因为更新以及多版本的原因,一个数据就可能存在于多个文件,所以需要一个文件一个文件查找才能定位出具体数据。
所以HBase架构本身个人认为并不适合做大规模scan,很大规模的scan建议还是用Parquet,可以把HBase定期导出到Parquet来scan
kudu性能并没有达到parquet的扫描速度,可以说介于HBase和HDFS(Parquet)之间
kudu比HBase扫描性能好,是因为kudu是纯列存,扫描不会出现跳跃读的情况,而HBase可能会跳跃seek,这是本质的区别。
但kudu扫描性能又没有Parquet好,就是因为kudu是LSM结构,它扫描的时候还是会同时顺序扫描多个文件,并比较key值大小。
而Parquet只需要顺序对一个Block块中的数据进行扫描即可,这个是两者的重要区别。
所以说hbase相比parquet,这两个方面都是scan的劣势。
Kudu写入流程
HBase写入流程
但HBase:
大概原因是:
具体看
Apache HBase
HBase 官方文档中文版
数据结构-常用树总结
HBase写请求分析
HBase Scan 中 setCaching setMaxResultSize setBatch 解惑
HBase二级索引实现方案
传统的行存储和(HBase)列存储的区别
大数据存取的选择:行存储还是列存储?
处理海量数据:列式存储综述(存储篇)
Hbase行级事务模型
HBase使用总结
Hbase split的三种方式和split的过程
Hbase 技术细节笔记(上)
Hbase 技术细节笔记(下)
HBase原理-数据读取流程解析
Apache HBase 全攻略
Hbase的Rowkey设计以及如何进行预分区
HBase之七:事务和并发控制机制原理
[翻译]HBase 中的 ACID
Hbase split的三种方式和split的过程
HBase数据存储格式
Hbase最佳实战:Region数量与大小的重要影响
网易范欣欣-HBase – Memstore Flush深度解析
网易范欣欣-HBase Compaction的前生今世-身世之旅
网易范欣欣-HBase - 数据写入流程解析
HBase篇(4)-你不知道的HFile
网易范欣欣-HBase-数据读取流程解析
网易范欣欣-HBase-迟到的‘数据读取流程’部分细节
Hbase 布隆过滤器BloomFilter介绍
HBase高性能随机查询之道 – HFile原理解析
HBase 读流程解析与优化的最佳实践
HBase-拆分合并和调优参考
hbase-hug-presentation
HBASE(Memstore的专属JVM策略MSLAB)
HBase MSLAB和MemStoreChunkPool源码
Hbase底层原理
HBase Rowkey四大特性