列式存储Nosql——HBase
一、Hbase介绍
HBase是BigTable的开源java版本。是建立在HDFS之上,提供高可靠性、高性能、列存储、可伸缩、实时读写NoSQL的数据库系统。其主要特点如下:
- HBase仅能通过主键(row key)和主键的range来检索数据,和全文检索。
- 仅支持单行事务。
- Hbase中支持的数据类型:byte[]
- 主要用来存储结构化和半结构化的松散数据
- 支持横向扩展
HBase仅能通过主键(row key)和主键的range来检索数据,仅支持单行事务。主要用来存储结构化和半结构化的松散数据。Hbase中支持的数据类型:byte[]。
1.1 Hbase与RDBMS比较
1.2 Hbase与HDFS比较
HDFS:HDFS是分布式存储系统,不支持快速查询。
Hbase:构建在HDFS之上,能够为大型表提供快速查询和更新功能。
1.3 Hbase与Hive比较
二、Hbase数据模型
2.1 表(table)
- Hbase中数据都是以表形式来组织的
- Hbase中的表由多个行组成
2.2 行(row)
- HBASE中的行由一个rowkey(行键)和一个或多个列组成,列的值与rowkey、列相关联。
- 行在存储时是按row key 按字典顺序排序。
2.3 列簇(Column Family)
1、列蔟将一组列及其值组织在一起
2、每个列蔟都有一组存储属性,例如:
3、是否应该缓存在内存中
4、数据如何被压缩或行键如何编码等
5、表中的每一行都有相同的列蔟,但在列蔟中不存储任何内容
6、所有的列蔟的数据全部都存储在一块(文件系统HDFS)
7、HBase官方建议所有的列蔟保持一样的列,并且将同一类的列放在一个列蔟中
2.4 列标识符(Column Qualifier)
1、列蔟中包含一个个的列限定符,这样可以为存储的数据提供索引
2、列蔟在创建表的时候是固定的,但列限定符是不作限制的
3、不同的行可能会存在不同的列标识符
2.5 单元格(Cell)
由{row key, column, version} 唯一确定的单元。cell中的数据是没有类型的,全部是以字节的形式进行存储的。
三、Hbase架构
下图是同自己画的,加了HMaster的高可用,凑活看吧。
3.1 Hbase架构解析
根据上面的图,下面对HBase的架构一一解释吧。
3.1.1 Client
客户端,例如:发出HBase操作的请求。例如:我们编写的Java API代码、以及HBase shell,都是CLient。
Client 访问用户数据前需要首先访问 ZooKeeper,找到.META.表位置,最后才能找到用户数据的位置去访问,中间需要多次网络操作,不过 client 端会做 cache 缓存。
3.1.2 Zookeeper
作用:
1、选举 Master,避免单点 Master 单点故障问题。
2、存储所有Region的寻址入口。用户通过查找zk(zookeeper)的/hbase/meta-region-server节点查询哪台RegionServer上有hbase:meta表。
由上图可知:meta表在node1上,登录http://node1:16010/master-status查看,
3、实时监控 RegionServer 的状态,将 RegionServer 的上线和下线信息实时通知给 Master。由Master去处理RegionServer故障转移。
4、存储Hbase的Schema,包括有哪些table,namespace。
3.1.3 HMaster
HMaster进程主要职责:
1、为RegionServer分配Region
2、处理RegionServer的故障转移
3、通过zk发现失效的 RegionServer 并重新分配其上的 Region
4、处理元数据的变更。
3.1.4 HRegionServer
1、RegionServer 维护 Master 分配给它的 Region,处理对这些 Region 的 IO 请求。
2、RegionServer 负责 Split 在运行过程中变得过大的 Region,负责 Compact 操作。
3、可以看到,client 访问 HBase 上数据的过程并不需要 master 参与(寻址访问 zookeeper 和 RegioneServer,数据读写访问 RegioneServer),Master 仅仅维护者 Table 和 Region 的元数据信息,负载很低。
.META. 存的是所有的 Region 的位置信息,那么 RegioneServer 当中 Region 在进行分裂之后 的新产生的 Region,是由 Master 来决定发到哪个 RegioneServer,这就意味着,只有 Master 知道 new Region 的位置信息,所以,由 Master 来管理.META.这个表当中的数据的 CRUD
所以结合以上两点表明,在没有 Region 分裂的情况,Master 宕机一段时间是可以忍受的。
3.1.5 HRegion
table在行的方向上分隔为多个Region。Region是HBase中分布式存储和负载均衡的最小单元,即不同的region可以分别在不同的Region Server上。一个RegionServer上可以有多个HRegion。Region按大小分隔,每个表一般是只有一个region。当region中的数据逐渐变大之后,达到某一个阈值,会进行裂变一个region等分为两个region,并分配到不同的RegionServer。原本的Region会下线,新Split出来的两个Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。
3.1.6 Store
每一个region由一个或多个store组成,至少是一个store,hbase会把一起访问的数据放在一个store里面,即为每个 ColumnFamily建一个store,如果有几个ColumnFamily,也就有几个Store。,HBase以store的大小来判断是否需要切分region。即一个region中最大store的大小大于设置阈值之后才会触发切分。这里所说的store大小是压缩后的文件总大小还是未压缩文件总大小,实际实现中store大小为压缩后的文件大小(采用压缩的场景)。
3.1.7 MemStore
MemStore是写缓存。当往HBase中写入数据时,首先是写入到MemStore。当memStore的大小达到一个阀值(默认128MB)时,memStore会被flush到文 件,即生成一个快照。目前hbase 会有一个线程来负责memStore的flush操作。
注:在后面会在 Hbase中的合并 中细讲 MemStore的in memory compaction
3.1.8 StoreFile
memStore内存中的数据写到文件后就是StoreFile,StoreFile底层是以HFile的格式保存。当storefile文件的数量增长到一定阈值后,系统会进行合并(minor、major compaction),在合并过程中会进行版本合并和删除工作(majar),形成更大的storefile。
3.1.9 HFile
HBase中KeyValue数据的存储格式,HFile是Hadoop的 二进制格式文件,实际上StoreFile就是对Hfile做了轻量级包装,即StoreFile底层就是HFile。
3.1.10 HLog
HLog(WAL log):WAL意为write ahead log,用来做灾难恢复使用。WAL是HBase中提供的一种高并发、持久化的日志保存与回放机制。每个业务数据的写入操作(PUT/DELETE/INCR),都会保存在WAL中。一旦服务器崩溃,通过回放WAL,就可以实现恢复崩溃之前的数据。物理上存储是Hadoop的Sequence File。
3.2 Hbase读写过程
3.2.1 HBase读过程
1、客户端在zk找到.META表的Region数据
2、根据namespace、表名、rowkey从.META表中获取region信息
3、找到对应的regionServer,查找对应的region
4、先从memStore读取数据、如果没有,再去BlockCache中读,如果还没有,再在StoreFile上读。
3.2.2 HBase写过程
1、从ZK中获取.META表的region数据
2、读取meta表中的数据,根据namespace、表名、rowkey获取对应的Region信息。并将该table的region信息以及meta表的位置信息缓存在客户端的metacache,方便下次访问。
3、根据获取的的地址访问对应的region。
4、先将数据顺序写入(追加)到WAL。
5、将数据写入对应的MemStore,数据会在MemStore进行排序
6、等达到MemStore的刷写时机后,将数据刷写到HFile。
3.3 Hbase中的数据结构
3.3.1 跳表(skip list)
跳表是一种类似链表的数据结构。其查询/插入/删除的时间复杂度都是O(logn)。跳表有如下特点:
1、跳表由很多层组成
2、每一层都是一个有序链表
3、对于每一层的任意结点,不仅有指向下一个结点的指针,也有指向其下一层的指针。
注: 跳表的层数是根据一种随机算法得到的,为了不让层数过大,还会有一个最大层数MAX_LEVEL限制,随机算法生成的层数不会大于这个值。
结构如下图所示:
特点:查询空间换时间。
3.3.2 LSM树
传统关系型数据库,一般都选择使用B+树作为索引结构,而在大数据场景下,HBase、Kudu这些存储引擎选择的是LSM树。LSM,也就是日志结构合并树。LSM树主要目标是快速建立索引,LSM树通过磁盘的顺序写,来实现最好的写性能。
LSM结构图如下:
结构特征如下:
1、LSM的主要思想是划分不同等级的结构,换句话来理解,就是 LSM中不止一个数据结构,而是存在多种结构。
2、一个结构在内存、其他结构在磁盘(HBase存储结构中,有内存——MemStore、也有磁盘——storefile)
3、内存的结构可以是B树、红黑树、跳表等结构(Hbase中是跳表),磁盘中的树就是一棵B+树
4、C0层保存了最近写入的数据,数据就是有序的,而且是随机更新、随机查询。
5、C1到CK层的数据都是存在磁盘中,每一层中key都是有存储的。
LSM的数据写入操作:
1、首先将数据写入到WAL(Write Ahead log),写日志是顺序写,效率相对较高(PUT、DELETE都是顺序写)。
2、数据项写入到内存中的C0结构中。
3、只有内存中的C0结构超过一定阈值的时候,将内存中的C0、和C1进行合并。这个过程就是Compaction(合并)。
4、合并后的新的C1顺序写磁盘,替换之前的C1。
5、但C1层达到一定的大小,会继续和下层合并,合并后旧的文件都可以删除,只保留最新的。
6、整个写入的过程只用到了内存结构,Compaction由后台异步完成,不阻塞写入。
LSM的数据查询操作:
1、先在内存中查C0层
2、如果C0层中不存在数据,则查询C1层
3、不断逐层查询,最早的数据在CK层
4、C0层因为是在内存中的结构中查询,所以效率较高。因为数据都是分布在不同的层结构中,所以一次查询,可能需要多次跨层次结构查询,所以读取的速度会慢一些。
5、根据以上,LSM树结构的程序适合于写密集、少量查询的场景
HBase为了提升LSM结构下的随机读性能,还引入了布隆过滤器(建表语句中可以指定)。通过布隆过滤器,HBase就能以少量的空间代价,换来在读取数据时非常快速地确定是否存在某条数据,效率进一步提升。
参考博客:从B+树到LSM树,及LSM树在HBase中的应用
3.4 Hbase中的合并
3.4.1 In memory compaction
In-memory合并是HBase 2.0之后添加的。它与默认的MemStore的区别:实现了在内存中进行compaction(合并)。在CompactingMemStore中,数据是以段(Segment)为单位存储数据的。MemStore包含了多个segment。
1、当数据写入时,首先写入到的是Active segment中(也就是当前可以写入的segment段)
2、在2.0之前,如果MemStore中的数据量达到指定的阈值时,就会将数据flush到磁盘中的一个StoreFile。
3、2.0的In-memory compaction,active segment满了后,将数据移动到pipeline中。这个过程跟以前不一样,以前是flush到磁盘,而这次是将Active segment的数据,移到称为pipeline的内存当中。一个pipeline中可以有多个segment。而In-memory compaction会将pipeline的多个segment合并为更大的、更紧凑的segment,这就是compaction。
相信很多同学看到这里了的时候肯定很懵。
为什么要放在多个segment?
为什么合并多个segment就更紧凑了呢?
首先,我们要明确一点,MemStore的数据结构是跳表,跳表的特点就是拿空间换时间。JDK实现的跳表在内存使用方面有些粗糙,导致内存中产生了大量意义不大的Java对象,这些Java对象的频繁产生一方面导致内存效率使用比较低,另一方面会引起比较严重的Java GC。所以我们可以将跳表转换为对内存更加友好的数据结构呢?于是就有了 In memory compaction。其工作流程如下:
为什么要将一个大的MemStore切分成这么多小的Segment?这么设计的初衷是为In-memory Compaction做准备,只有将MemStore分为MutableSegment和ImmutableSegment,才可能基于ImmutableSeg
ment进行内存优化。
因为ImmutableSegment本身已经不再接收任何更新删除写入操作,只允许读操作,这样的话跳表就可以转换为对内存更加友好的Array或者其他的数据结构。这个转换就是In-memory Compaction。
跳表转换到数组的过程:
HBase会尽量延长CompactingMemStore的生命周期,以达到减少总的IO开销。当需要把CompactingMemStore flush到磁盘时,pipeline中所有的segment会被移动到一个snapshot中,然后进行合并后写入到HFile。
3.4.2 compaction策略
但Active segment flush到pipeline中后,后台会触发一个任务来合并pipeline中的数据。合并任务会扫描pipeline中所有的segment,将segment的索引合并为一个索引。有三种合并策略:
3.4.3 StoreFile合并
当MemStore超过阀值的时候,就要flush到HDFS上生成一个StoreFile。因此随着不断写入,HFile的数量将会越来越多,根据前面所述,StoreFile数量过多会降低读性能。为了避免对读性能的影响,需要对这些StoreFile进行compact操作,把多个HFile合并成一个HFile。compact操作需要对HBase的数据进行多次的重新读写,因此这个过程会产生大量的IO。可以看到compact操作的本质就是以IO操作换取后续的读性能的提高
3.4.3.1 minor compaction
Minor Compaction操作只用来做部分文件的合并操作,包括minVersion=0并且设置ttl的过期版本清理,不做任何删除数据、多版本数据的清理工作。小范围合并,默认是3-10个文件进行合并,不会删除其他版本的数据。Minor Compaction则只会选择数个StoreFile文件compact为一个StoreFile。Minor Compaction的过程一般较快,而且IO相对较低。
触发条件:
- 在打开Region或者MemStore时会自动检测是否需要进行Compact(包括Minor、Major)。
- minFilesToCompact由hbase.hstore.compaction.min控制,默认值为3。
- 即Store下面的StoreFile数量减去正在compaction的数量 >=3时,需要做compaction。
3.4.4 major compaction
Major Compaction操作是对Region下的Store下的所有StoreFile执行合并操作,最终的结果是整理合并出一个文件。一般手动触发,会删除其他版本的数据(不同时间戳的)。
触发条件:
- 如果无需进行Minor compaction,HBase会继续判断是否需要执行Major Compaction。
- 如果所有的StoreFile中,最老(时间戳最小)的那个StoreFile的时间间隔大于Major Compaction的时间间隔(hbase.hregion.majorcompaction——默认7天。
想进一步了解的,查阅博客:HBase原理 | HBase内存管理之MemStore进化论
四、HBase批量装载——Bulk load
Hbase支持 bulk load 的入库方式,它是利用hbase的数据信息按照特定格式存储在hdfs内这一原理,直接在hdfs中生成持久化HFile数据格式文件,然后上传至合适位置,即完成巨量数据快速入库的办法。配合mapreduce完成(文件:TextOutputFormat、Mysql:DBOutputFormat、用于读取Hbase:TableOutputFormat),高效敏捷,而且不占用region资源,增添负载,在大数据量写入时能极大提高写入效率,并降低对Hbase结点的写入压力。
可以看看这篇博客:HBase快速导入数据–BulkLoad
五、Hbase二级索引
注意,HBase只支持rowkey查询、rowkey range查询、全表扫描查询。rowkey为一级索引,二级索引的本质就是建立各列值与行键之间的映射关系。
5.1 全文索引
- 全局索引适用于读多写少业务。
- 全局索引绝大多数负载都发生在写入时,当构建了全局索引时,Phoenix会拦截写入(DELETE、UPSERT值和UPSERT SELECT)上的数据表更新,构建索引更新,同时更新所有相关的索引表,开销较大。
- 读取时,Phoenix将选择最快能够查询出数据的索引表。默认情况下,除非使用Hint,如果SELECT查询中引用了其他非索引列,该索引是不会生效的。
- 全局索引一般和覆盖索引搭配使用,读的效率很高,但写入效率会受影响。
创建表:
create index pay_index on PAYINFO(USER_ID,CATEGORY);
查看索引表内容:
select * from PAY_INDEX;
表中出现了二级索引的字段, 打开hbase shell查看:
查询索引表 scan ‘PAY_INDEX’,{FORMATTER => ‘toString’}
由上可知,全局索引会在hbase中生成一个新的索引表,索引表的rowkey=新的索引列+原来rowkey。
5.2 本地索引
1、本地索引适合写操作频繁,读相对少的业务。
2、当使用SQL查询数据时,Phoenix会自动选择是否使用本地索引查询数据。
3、在本地索引中,索引数据和业务表数据存储在同一个服务器上,避免写入期间的其他网络开销。
4、在Phoenix 4.8.0之前,本地索引保存在一个单独的表中,在Phoenix 4.8.1中,本地索引的数据是保存在一个影子列蔟中。
5、本地索引查询即使SELECT引用了非索引中的字段,也会自动应用索引的。
创建本地索引
create local index local_pay_index on PAYINFO(USER_ID,CATEGORY);
在phoenix中查看,出现一个本地索引表。
查看phoenix中索引表内容:
查看hbase中并未生成名为local_pay_index 的索引表。
查看pay_info表中数据
发现在原表数据基础上,增加了数据,rowkey=新的索引列+原来rowkey。
5.3 覆盖索引
Phoenix提供了覆盖的索引,可以不需要在找到索引条目后返回到主表。Phoenix可以将关心的数据捆绑在索引行中,从而节省了读取时间的开销。
创建覆盖索引
create index PAY_INDEX_2 on PAYINFO(USER_ID,CATEGORY) include(STATUS) ;
Phoenix查看索引表内容:
hbase shell 查看索引表内容
索引表的rowkey = 全局索引rowkey + 原来的rowkey; 里面是没有覆盖索引的字段的。
5.4 函数索引
函数索引(4.3和更高版本)可以支持在列上创建索引,还可以基于任意表达式上创建索引。然后,当查询使用该表达式时,可以使用索引来检索结果,而不是数据表。例如,可以在UPPER(FIRST_NAME||‘ ’||LAST_NAME)上创建一个索引,这样将来搜索两个名字拼接在一起时,索引依然可以生效。
六、Hbase的过滤器类型
七、Hbase优化
7.1 减少调整
7.1.1 Region的调整
如果没有预建分区的话,随着region条数的增加,region会分裂,这将增加开销,所以解决办法就是根据rowkey设计来进行预分区,减少region的分裂。
7.1.2 HFile的调整
HFlie是数据底层存储文件,在每个memstore进行刷新时会生成一个HFile,当HFile增加到一定程度,会将属于一个region的HFile进行合并,这个步骤会带开销但不可避免,但是合并后HFile大小如果大于设定的值,那么HFile会重新分裂。为了减少这样的无谓的IO开销,建议估计项目数据量的大小,给HFile设定一个合适的值。
7.2 减少启停
7.2.1 批量数据写入时采用BulkLoad
如果通过Hbase-shell或者API的put来实现大量数据的写入,性能差,BulkLoad可以避开 memstore 合并过程,直接在hdfs中生成持久化HFile数据格式文件。所以当需要写入大量离线数据时建议使用BulkLoad。
7.3 减少数据量
7.3.1 开启过滤
开启bloomFilter。bloomFilter是列簇级别的过滤,在生成一个storefile同时会生成一个MetaBolck,用于查询时过滤数据。
7.3.2 使用压缩
一般推荐使用Snappy和LZO压缩。
7.4 合理设计
7.4.1 Row key的设计
散列性: 散列性能够保证相同相似的rowkey聚合,相异的rowkey分散,有利于查询。
简短性: rowkey作为key的一部分存储在HFile中,如果为了可读性将rowKey设计得过长,那
么将会增加存储压力。
唯一性: rowKey必须具备明显的区别性。
业务性: 假如我的查询条件比较多,而且不是针对列的条件,那么rowKey的设计就应该支持多条件查询。如果我的查询要求是最近插入的数据优先,那么rowKey则可以采用叫上Long.Max-时间戳的方式,这样rowKey就是递减排列。
7.5.1 列簇的设计
列族的设计需要看应用场景
多列族设计的优劣:
优势:HBase中数据时按列进行存储的,那么查询某一列族的某一列时就不需要全盘扫描,只需要扫描某一列族,减少了读I/O;其实多列族设计对减少的作用不是很明显,适用于读多写少的场景
劣势:降低了写的I/O性能。原因如下:数据写到store以后是先缓存在memstore中,同一个region中存在多个列族则存在多个store,每个store都一个memstore,当其实memstore进行flush时,属于同一个region的store中的memstore都会进行flush,增加I/O开销。
八、常见问题
8.1 为什么要按列存储?按列存储有什么好处?
1、只访问查询涉及的列–大量降低系统IO。
2、数据类型一致,数据特征相似–高效压缩。
3、数据按列存储–每一列单独存放。
4、可以存储的结构稀疏的数据
8.2 Hbase为什么读写快?
因为HBase采用了LSM树型结构,HBase先到内存中去寻找,再到磁盘中查找。并且磁盘中的数据是有序的。
8.3 Client 会缓存.META的数据,该数据更新了怎么办?
Client的元数据缓存不不更更新,当.META.的数据发⽣生更更新。⽐比如因为region重新均衡,某个Region的位置发⽣生了了变
化,Client再次根据缓存去访问的时候,会出现错误,当出现异常达到最⼤大重试次数后,client就会重新去.META.所在的RegionServer获取最新的Region信息,如果.META.所在的RegionServer也变了了,Client就会重新去ZK上获取.META.所在的RegionServer的最新地址。
8.4 HBase宕机怎么处理?
8.4.1 HMaster宕机
如果是 HMaster 宕机, HMaster 没有单点问题, HBase 中可以启动多个 HMaster,通过Zookeeper 的Master Election 机制保证总有⼀一个 Master 运⾏行行。即 ZooKeeper 会保证总会有⼀一个 HMaster 在对外提供服务。
8.4.2 HRegionServer宕机
HMaster 会将其所管理理的 region 重新分布到其他活动的 RegionServer 上,由于数据和⽇日志都持久在 HDFS中,该操作不不会导致数据丢失。所以数据的⼀一致性和安全性是有保障的。
8.5 WAL的工作流程?
Write Ahead Log(WAL)将HBase中数据的所有更改记录到基于文件的存储中。在正常操作下,不需要WAL,因为数据更改从MemStore移动到StoreFiles。但是,如果在刷新MemStore之前RegionServer崩溃或变得不可用,则WAL确保可以重播对数据所做的更改。如果写入WAL失败,则修改数据的整个操作将失败。
8.6 怎么避免热点问题?
避免热点问题就是要让rowkey散列。
8.6.1 反转
把rowkey中经常变化的部分放在前面。如反转时间戳。
8.6.2 加盐
在rowkey的前⾯增加随机数,使得它和之前的rowkey的开头不同。分配的前缀种类数量应该和你想使⽤数据分散到不同的region的数量⼀致。加盐之后的rowkey就会根据随机⽣成的前缀分散到各个region上,以避免热点。但是随机数加盐对读性能影响较大。
8.6.3 hash
哈希可以使负载分散到整个集群,但是读却是可以预测的。