针对结构化数据的分布式、列存储数据系统来源于Google的Big Table论文 Hbase的特点
海量存储 + 快速的查询
PB级别数据能在毫秒-秒级别的数据返回
列式存储
区别于传统关系型数据库的行组织方式的一种表组织方式
行存储和列存储各自优缺点?2、insert/update更方便1、只查询想要的列
2、列投影(projection)很高效
3、任何列都可以作为主键/建索引缺点1、读取数据时整列数据都会被读取1、insert/update不方便
2、涉及查询后的组装
行存储的优势是OLTP事务型处理,如查询id为3的人员信息,可以一次性取出整行记录
列存储则是同一列的数据保存在一起,对OLAP分析型处理支持较好,也是Hbase的存储模式,如果是查平均值,则只需要取整列数据,I/O一次即可,查ID则要多次I/O,也就是列式存储的缺点所在
极易拓展
上层的RegionServer和存储的HDFS都是极易拓展的
高并发
稀疏
稀疏主要是针对Hbase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。
2、Hbase 的架构 + 工作流程(读&写) Hbase的架构Hbase有主要三个模块,Zookeeper,Master,RegionServer
Zookeeper
1.Hbase内置Zookeeper,但一般会用外部的Zookeeper集群来进行管理
2.监控各个模块的注册,监控上/下线信息,主Master的选举
3.存储 Hbase 的 schema 和 table 元数据
总结: 元数据入口 + 高可用实现
Master
管理:监控 RegionServer, 处理 RegionServer的故障转移(fair over)
处理元数据变更
region的分配、转移,空闲时做数据的负载均衡
client的DML操作实际是和ZK交互的,只有DDL(元数据操作)会与Master交互,所以Master短暂挂掉不影响数据读写的DML
RegionServer
负责实际数据存储,缓存刷新,HLog维护,处理region分片等
关键组件:
WAL (HLog + Memstore)(Write-Ahead logs)
写数据到内存前先写HLlog,好处:防止机器宕机,内存的数据丢失
Region --> Store --> HFile
实际的物理存储
Region:分片,一个RegionServer可以有多个Region
Store : HFile 存储在 Store 中,一个 Store 对应 Hbase 表中的一个列族。
HFile:Hadoop的二进制格式文件
Memstore + BlockCache:内存,分别存储最近写入和最近读取的数据
LSM存储设计思想
两层思想,上层是日志+内存+硬盘(对内存的持久化),下层是合并
内存数据超过阈值,会溢写到硬盘,最后再对溢写的小文件进行合并
Client:访问接口 + .meta.元数据的cache
Hbase 的读写流程
读流程
Client --》 ZooKeeper --》 .meta.表 --》 RegionServer --》 Region --》memstore(命中则返回) --》 StoreFile(HFile)
最后取storefiles文件时候,用到了BloomFilter,布隆过滤器,有一定的误判率
写流程
Client --》 RowKey --》 RegionServer --》 region --》 region做数据检查 --》写WAL Log --》 写内存(超阈值就溢写)
在做海量数据的插入操作时,避免出现递增 rowkey 的 put 操作 ,否则可能导致数据热点
读友好还是写友好?
读比写慢,所以是写友好
原因
1.客户端写数据时,只需要保证数据到内存,由hbase后续去刷写到hdfs,而读必须等数据从hdfs读出来才算结束
2.客户端读,不仅要读RegionServer的缓存(缓存block cache,区别于内存mem store),还要读磁盘,然后取时间戳最大的作为数据,所以要慢一些
总而言之,原因是因为一个数据可能有多个版本,存在于内存和磁盘中,所以要读磁盘
Hbase的写流程为什么能高效运行?
本质上,对Hbase的修改和删除都是增加,因为Hbase对数据的删改是做先增加到内存,等溢写了很多的小文件后,才会做合并,再针对同一份数据的删改记录,进行删除和修改,所以用户的写操作只到内存,保证了I/O的效率,所以
Hbase 的寻址机制老版本的Hbase,采用三层:ZK --> ROOT表 --> meta表 --> region
新版本采用两层,ZK --> meta表 --> region,因为两层就够用了,用两层性能提高
Hbase的高级特性
过滤器(Filter)
更高级的查询方法,根据簇、列、版本等更多的条件来对数据进行过滤,作用:海量数据查询更快
在 Hbase 服务器端上执行的判断操作,还降低了网络传输压力
完成过滤需要两个参数:抽象操作符和比较器
操作符就是LESS 、LESS_OR_EQUAL 、 GREATER 、等比较符号
有自带的比较器 值过滤器ValueFilter 列过滤器RowFilter 等…
也可以自定义Filter需要实现Filterbase接口
协处理器
总的来说,就是一些用户自定义执行的逻辑,实现一些Hbase本来不支持或者支持得不是很好的功能,并且这些功能是放在RegionServer端执行,不执行MR的,是Hbase的高级应用功能,如:
1、访问权限控制2、引用完整性,基于外键检验数据,3、给hbase设计二级索引,从而提高基于列过滤时的查询性能,4、像监控MySQL的binlog一样,监控hbase的wal预写log5、服务端自定义实现一些聚合函数的功能......
参考链接
参考链接2
3、Hbase 的优缺点?有哪些使用场景?优点 海量存储列存储模式:无模式,每一行都允许有不同的列列存储模式:稀疏存储,节省存储空间快速的读写速度 :LSM树 + HFile易于拓展,弹性伸缩资源数据多版本
缺点 数据类型单一,只有字符串不支持SQL和复杂条件过滤数据的聚合需要业务实现
应用场景
1 交通方面
船舶GPS信息,全长江的船舶GPS信息,每天有1千万左右的数据存储。
2 金融方面
消费信息、贷款信息、信用卡还款信息等
3 电商方面
电商网站的交易信息、物流信息、游览信息等
4 电信方面
通话信息、语音详单等
总结:海量明细数据的存储,并且后期需要有很好的查询性能
参考文章1
参考文章2
4、Hbase 的基本使用/快速入门hbase shell ## 查看所有表list## DDLcreate ‘default:test’,’cf’desc ‘default:test’,’cf’truncate 'default:test'drop 'default:test'## 基于scan的数据筛选scan 'default:test'#常用过滤语句## 1、rowkey查询 ## 筛选rowkey为 vip 开头的,只去 col1 和col2 两列 scan 'default:test',{ROWPREFIXFILTER => 'vip',COLUMN => {'cf:col1','cf:col2'}} ## 筛选rowkey 中包含 vip 的 scan 'default:test',{FILTER => "RowFilter(=,'substring:vip')",COLUMN => {'cf:col1'}}## 2、值查询 ## 查询值等于sku88的 scan 'default:test',{FILTER => "ValueFilter(=,'binary:sku88')"} ## 查询值包含sku88的 scan 'default:test',{FILTER => "ValueFilter(=,'substring:sku88')"} ## 查询值小于等于2000的 scan 'default:test',{FILTER => "ValueFilter(<=,'binary:sku88')"} ## 注意:1、值查询为二进制查询;2、substring 不能用于小于等于等符号 ## 3、列查询 # column前缀为c2,且值包含88或122的 scan 'default:test',{FILTER => "ColumnPrefixFilter('c2') AND (ValueFilter(=,'substring:88') OR ValueFilter(=,'substring:122'))"}## 4、列族scan 'default:test',{FILTER => "FamilyFitler(=,'substring:cf3')"}## 5、时间戳scan 'default:test',{FILTER => "TimestampFitler(164328320,164328399)"}......
参考
5、Hbase 的优化? Rowkey的设计主要需要考虑两点:1.避免数据热点;2.方便下游使用查询
Hbase的RowKey设计原则和一些常用的设计方式
唯一性
长度原则
散列性:和预分区结合起来,要求数据能规范的落在所有预分区里面,就叫散列,避免数据热点
具体的方法:字符串反转、字符串拼接、时间戳反转 , 通过计算然后分区键,然后拼接…
数据预分区 通过命令(关键字 ‘SPLITS’) 或者 API 进行预分区
三个分middleKey分四个区
从三个方面入手: 内存刷写+storeFile合并+Region切分
内存flush
hbase.regionserver.global.memstore.size
比例,内存达到该比例就会阻塞写入,直到内存刷写到小于这个比例,默认0.4
如果大规模写入发生时,有可能写入速度比flush速度还快的话,就会导致内存溢出,因此这个参数
storeFile合并
major compact 和minor compact
minor compact : 小合并,只做文件的合并(删除标记)
major compact : 大合并,一个列族(HStore)的合并,真正做删除操作,非常消耗资源,默认周期删除,一般都禁用,手动合并
Hbase之MinorCompact全程解析 (解释的非常清楚明白!)
相关重要参数:
hbase.hregion.majorcompaction:major:
compact的周期,默认7天,生产环境建议禁用,非常消耗资源,可能影响业务,一般用手动合并
hbase.hstore.compaction.max/min :
每次执行minor conpact 的最大/最小文件个数,除了个数维度外,还有文件大小维度,太大的就不参与minor compact
Hregion切分
hbase.hregion.max.filesize: 控制多大Hregion进行切分,一个列族Region越多,切分大小就越大,但是最大不会超过该参数
列族的设计 6、Hbase 相关的面试问题 Hbase 为什么快?
架构上:Region的拆分和列式的存储
数据量很大的时候,Hbase会拆分成多个Region分配到多台RegionServer.客户端通过meta信息定位到某台RegionServer(也可能是多台),从而提高查询速度
列式的存储保证了每一列的查询都可以应用到索引,并且不需要的列不需要查询,减少了I/O
设计上:LSM树的设计
先说B+树;B+树主要用在传统的行数据库中,因为查询速度快。但是如有有大量的数据需要查询时就暴露出其弊端。
LSM树的应用场景:Hbase就是使用了LSM树。
主要的实现方式:
第一步,写数据时,写到预写日志中,目的是防止 数据在写入时丢失; 第二步,将数据放入到内存中。 第三步,当内存的大小超过指定值,会把内存中的 数据写入到磁盘上。
需要注意一个关键点:磁盘的数据是有序的,这是利用预写日志和内存把随机写数据进行排序后写入,因此也能保证稳定的数据插入速率。
LSM的优点:能快速进行数据的合并和拆分。
知道hbase的存储形式,接下来讲下hbase为什么能快速的读写删除。
如何快速的实现读功能:
读取内容的顺序是先到内存中去寻找,再到磁盘中查找。我们都清楚的一点是,磁盘的查询速度是非常慢的。
而在Hbase进行磁盘读取时,实际上是进行的顺序读写,Hbase会将整个Region涉及的磁盘文件整体读取,从而加快读取速度,这点和Kafka有类似的设计
如何快速的实现删除功能:
删除数据不是进行实质上的删除,也就是磁盘上仍然存在此条数据。只不过是对删除的数据打上了墓碑标记。利用墓碑标记,读数据会忽略此条数据。
当进行小文件合并时,才会进行实质上删除。
参考文章
设计上:独特的数据结构HFile和+ 过滤器的过滤机制 + 布隆过滤器(Bloom Filter)
参考文章
数据flush流程,数据合并的过程,Region切分的流程 什么时候触发 MemStore Flush 有很多情况会触发 MemStore 的 Flush 操作,
所以我们最好需要了解每种情况在什么时候触发 Memstore Flush。
总的来说,主要有以下几种情况会触发 Memstore Flush:
Region 中所有 MemStore 占用的内存超过相关阈值整个 RegionServer 的 MemStore 占用内存总和大于相关阈值WAL数量大于相关阈值定期自动刷写数据更新超过一定阈值手动触发刷写
下面对这几种刷写进行简要说明。
Region 中所有 MemStore 占用的内存超过相关阈值
当一个 Region 中所有 MemStore 占用的内存(包括 onHeap + OffHeap)大小
超过刷写阈值的时候会触发一次刷写,
这个阈值由 hbase.hregion.memstore.flush.size 参数控制,默认为128MB。
我们每次调用 put、delete 等操作都会检查的这个条件的。
但是如果我们的数据增加得很快,
达到了 hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier 的大小,hbase.hregion.memstore.block.multiplier 默认值为4,
也就是128*4=512MB的时候,
那么除了触发 MemStore 刷写之外,
Hbase 还会在刷写的时候同时阻塞所有写入该 Store 的写请求!
这时候如果你往对应的 Store 写数据,会出现 RegionTooBusyException 异常。
整个 RegionServer 的 MemStore 占用内存总和大于相关阈值
Hbase 为 RegionServer 的 MemStore 分配了一定的写缓存,
大小等于 hbase_heapsize(RegionServer 占用的堆内存大小)* hbase.regionserver.global.memstore.size。hbase.regionserver.global.memstore.size 的默认值是 0.4,
也就是说写缓存大概占用 RegionServer 整个 JVM 内存使用量的 40%。
如果整个 RegionServer 的 MemStore 占用内存总和大于 hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize 的时候,
将会触发 MemStore 的刷写。
其中 hbase.regionserver.global.memstore.size.lower.limit 的默认值为 0.95。
举个例子,如果我们 Hbase 堆内存总共是 32G,按照默认的比例,
那么触发 RegionServer 级别的 Flush 是
RS 中所有的 MemStore 占用内存为:32 * 0.4 * 0.95 = 12.16G。
PS:
从这里可以看出
MemoryStore 是 Store级别的,
但是其占用的内存却是RegionServer级别的
注意:
0.99.0 前 hbase.regionserver.global.memstore.size 是
hbase.regionserver.global.memstore.upperLimit 参数;
hbase.regionserver.global.memstore.size.lower.limit 是
hbase.regionserver.global.memstore.lowerLimit,
参见 Hbase-5349
RegionServer 级别的 Flush 策略是每次找到 RS 中占用内存最大的 Region 对他进行刷写,
这个操作是循环进行的,
直到总体内存的占用低于全局 MemStore 刷写下限
(hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize)
才会停止。
需要注意的是,如果达到了 RegionServer 级别的 Flush,
那么当前 RegionServer 的所有写操作将会被阻塞,
而且这个阻塞可能会持续到分钟级别。
WAL数量大于相关阈值
WAL(Write-ahead log,预写日志)用来解决宕机之后的操作恢复问题的。
数据到达 Region 的时候是先写入 WAL,
然后再被写到 Memstore 的。
如果 WAL 的数量越来越大,
这就意味着 MemStore 中未持久化到磁盘的数据越来越多。
当 RS 挂掉的时候,恢复时间将会变得很长,
所以有必要在 WAL 到达一定的数量时进行一次刷写操作。
这个阈值(maxLogs)的计算公式如下:
this.blocksize = WALUtil.getWALBlockSize(this.conf, this.fs, this.walDir);float multiplier = conf.getFloat("hbase.regionserver.logroll.multiplier", 0.5f);this.logrollsize = (long)(this.blocksize * multiplier);this.maxLogs = conf.getInt("hbase.regionserver.maxlogs", Math.max(32, calculateMaxLogFiles(conf, logrollsize)));
public static long getWALBlockSize(Configuration conf, FileSystem fs, Path dir)
throws IOException {
return conf.getLong("hbase.regionserver.hlog.blocksize",
CommonFSUtils.getDefaultBlockSize(fs, dir) * 2);
}
private int calculateMaxLogFiles(Configuration conf, long logRollSize) {
Pair
return (int) ((globalMemstoreSize.getFirst() * 2) / logRollSize);
}
也就是说,如果设置了 hbase.regionserver.maxlogs,那就是这个参数的值;
否则是 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize)。
如果某个 RegionServer 的 WAL 数量大于 maxLogs 就会触发 MemStore 的刷写。
WAL 数量触发的刷写策略是,
找到最旧的 un-archived WAL 文件,
并找到这个 WAL 文件对应的 Regions,
然后对这些 Regions 进行刷写。
定期自动刷写
如果我们很久没有对 Hbase 的数据进行更新,
这时候就可以依赖定期刷写策略了。
RegionServer 在启动的时候会启动一个线程 PeriodicMemStoreFlusher
每隔 hbase.server.thread.wakefrequency 时间
去检查属于这个 RegionServer 的 Region
有没有超过一定时间都没有刷写,
这个时间是由 hbase.regionserver.optionalcacheflushinterval 参数控制的,默认是 3600000,
也就是1小时会进行一次刷写。如果设定为0,则意味着关闭定时自动刷写。
为了防止一次性有过多的 MemStore 刷写,
定期自动刷写会有 0 ~ 5 分钟的延迟,
具体参见 PeriodicMemStoreFlusher 类的实现。
数据更新超过一定阈值
如果 Hbase 的某个 Region 更新的很频繁,
而且既没有达到自动刷写阀值,
也没有达到内存的使用限制,
但是内存中的更新数量已经足够多,
比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为30000000,
那么也是会触发刷写的。
手动触发刷写
除了 Hbase 内部一些条件触发的刷写之外,
我们还可以通过执行相关命令或 API 来触发 MemStore 的刷写操作。
比如调用可以调用 Admin 接口提供的方法:
void flush(TableName tableName) throws IOException;void flushRegion(byte[] regionName) throws IOException;void flushRegionServer(ServerName serverName) throws IOException;
分别对某张表、某个 Region 或者某个 RegionServer 进行刷写操作。
也可以在 Shell 中通过执行 flush 命令:
hbase> flush 'TABLENAME'hbase> flush 'REGIONNAME'hbase> flush 'ENCODED_REGIONNAME'hbase> flush 'REGION_SERVER_NAME'
需要注意的是,
以上所有条件触发的刷写操作最后都会检查对应的 HStore 包含的 StoreFiles 文件
超过 hbase.hstore.blockingStoreFiles 参数配置的个数,默认值是16。
如果满足这个条件,那么当前刷写会被推迟到
hbase.hstore.blockingWaitTime 参数设置的时间后再刷写。
在阻塞刷写的同时,Hbase 还会请求 Split 或 Compaction 操作。
我们常见的 put、delete、append、increment、
调用 flush 命令、Region 分裂、Region Merge、bulkLoad HFiles
以及给表做快照操作都会对上面的相关条件做检查,
以便判断要不要做刷写操作。
在 Hbase 1.1 之前,MemStore 刷写是 Region 级别的。
就是说,如果要刷写某个 MemStore ,
MemStore 所在的 Region 中其他 MemStore 也是会被一起刷写的!
这会造成一定的问题,比如小文件问题,
具体参见 《为什么不建议在 Hbase 中使用过多的列族》。
针对这个问题,Hbase-10201/Hbase-3149引入列族级别的刷写。
我们可以通过 hbase.regionserver.flush.policy 参数选择不同的刷写策略。
目前 Hbase 2.0.2 的刷写策略全部都是实现 FlushPolicy 抽象类的。
并且自带三种刷写策略:
FlushAllLargeStoresPolicy、
FlushNonSloppyStoresFirstPolicy
以及 FlushAllStoresPolicy。
FlushAllStoresPolicy
这种刷写策略实现最简单,
直接返回当前 Region 对应的所有 MemStore。
也就是每次刷写都是对 Region 里面所有的 MemStore 进行的,
这个行为和 Hbase 1.1 之前是一样的。
FlushAllLargeStoresPolicy
在 Hbase 2.0 之前版本是 FlushLargeStoresPolicy,
后面被拆分成分 FlushAllLargeStoresPolicy 和FlushNonSloppyStoresFirstPolicy,
参见 Hbase-14920。
这种策略会先判断 Region 中每个 MemStore 的使用内存(OnHeap + OffHeap)是否大于某个阀值,
大于这个阀值的 MemStore 将会被刷写。
阀值的计算是由 hbase.hregion.percolumnfamilyflush.size.lower.bound 、hbase.hregion.percolumnfamilyflush.size.lower.bound.min
以及 hbase.hregion.memstore.flush.size 参数决定的。
计算逻辑如下:
//region.getMemStoreFlushSize() / familyNumber
//就是 hbase.hregion.memstore.flush.size 参数的值除以相关表列族的个数
flushSizeLowerBound = max(region.getMemStoreFlushSize() / familyNumber, hbase.hregion.percolumnfamilyflush.size.lower.bound.min)
//如果设置了 hbase.hregion.percolumnfamilyflush.size.lower.bound
flushSizeLowerBound = hbase.hregion.percolumnfamilyflush.size.lower.bound
计算逻辑上面已经很清晰的描述了。
hbase.hregion.percolumnfamilyflush.size.lower.bound.min 默认值为 16MB,而 hbase.hregion.percolumnfamilyflush.size.lower.bound 没有设置。
比如当前表有3个列族,其他用默认的值,
那么 flushSizeLowerBound = max((long)128 / 3, 16) = 42。
如果当前 Region 中没有 MemStore 的使用内存大于上面的阀值,
FlushAllLargeStoresPolicy 策略就退化成 FlushAllStoresPolicy 策略了,
也就是会对 Region 里面所有的 MemStore 进行 Flush。
FlushNonSloppyStoresFirstPolicy
Hbase 2.0 引入了 in-memory compaction,
参见 Hbase-13408。
如果我们对相关列族 hbase.hregion.compacting.memstore.type 参数的值不是 NONE,
那么这个 MemStore 的 isSloppyMemStore 值就是 true,否则就是 false。
FlushNonSloppyStoresFirstPolicy 策略
将 Region 中的 MemStore 按照 isSloppyMemStore
分到两个 HashSet 里面(sloppyStores 和 regularStores)。
然后
判断 regularStores 里面是否有 MemStore 内存占用大于相关阀值的 MemStore ,
有的话就会对这些 MemStore 进行刷写,
其他的不做处理,
这个阀值计算和 FlushAllLargeStoresPolicy 的阀值计算逻辑一致。如果 regularStores 里面没有 MemStore 内存占用大于相关阀值的 MemStore,
这时候就开始在 sloppyStores 里面寻找
是否有 MemStore 内存占用大于相关阀值的 MemStore,
有的话就会对这些 MemStore 进行刷写,其他的不做处理。如果上面 sloppyStores 和 regularStores 都没有满足条件的 MemStore 需要刷写,
这时候就 FlushNonSloppyStoresFirstPolicy 策略就
退化成 FlushAllStoresPolicy 策略了。 刷写的过程
MemStore 的刷写过程很复杂,很多操作都可能触发,
但是这些条件触发的刷写最终都是调用 HRegion 类中的 internalFlushcache 方法。
protected FlushResultImpl internalFlushcache(WAL wal, long myseqid, Collection
从上面的实现可以看出,Flush 操作主要分以下几步做的
prepareFlush 阶段:
刷写的第一步是对 MemStore 做 snapshot,
为了防止刷写过程中更新的数据同时在 snapshot 和 MemStore 中而造成后续处理的困难,
所以在刷写期间需要持有 updateLock 。
持有了 updateLock 之后,
这将阻塞客户端的写操作。
所以只在创建 snapshot 期间持有 updateLock,
而且 snapshot 的创建非常快,
所以此锁期间对客户的影响一般非常小。
对 MemStore 做 snapshot 是 internalPrepareFlushCache 里面进行的。
flushCache 阶段:
如果创建快照没问题,
那么返回的 result.result 将为 null。
这时候我们就可以进行下一步 internalFlushCacheAndCommit。
其实 internalFlushCacheAndCommit 里面包含两个步骤:
flushCache 和 commit 阶段。
flushCache 阶段:
其实就是将 prepareFlush 阶段创建好的快照写到临时文件里面,
临时文件是存放在对应 Region 文件夹下面的 .tmp 目录里面。
commit 阶段:
将 flushCache 阶段生产的临时文件移到(rename)对应的列族目录下面,
并做一些清理工作,比如删除第一步生成的 snapshot。
参考文章:Hbase之 数据刷写 (Memstore Flush) 详细说明