2013年马上就要过去了,总结下这一年HBase在这么一年中发生的主要变化。影响最大的事件就是HBase 0.96的发布,代码结构已经按照模块化发布了,而且提供了许多大家迫切需求的特点。这些特点大多在Yahoo!/Facebook/淘宝/小米等公司内部的集群中跑了挺长时间了,可以算是比较稳定可用了。
1. Compaction优化
HBase的Compaction是长期以来广受诟病的一个特性,很多人吐槽HBase也是因为这个特征。不过我们不能因为HBase有这样一个缺点就把它一棒子打死,更多的还是希望能够驯服它,能够使得它适应自己的应用场景。根据业务负载类型调整Compaction的类型和参数,一般在业务高峰时候禁掉Major Compaction。在0.96中HBase社区为了提供更多的Compaction的策略适用于不同的应用场景,采用了插件式的架构。同时改进了HBase在RegionServer端的存储管理,原来是直接Region->Store->StoreFile,现在为了支持更加灵活多样的管理StoreFile和Compact的策略,RS端采用了StoreEngine的结构。一个StoreEngine涉及到StoreFlusher、CompactionPolicy、Compactor、StoreFileManager。不指定的话默认是DefaultStoreEngine,四个组件分别是DefaultStoreFlusher, ExploringCompactionPolicy、DefaultCompactor、DefaultStoreFileManager。可以看出在0.96版之后,默认的Compaction算法从RatioBasedCompactionPolicy改为了ExploringCompactionPolicy。为什么要这么改,首先从Compaction的优化目标来看:compaction is about trading some disk IO now for fewer seeks later,也就是Compaction的优化目标是执行Compaction操作能合并越多的文件越好,如果合并同样多的文件产生的IO越小越好,这样select出来的列表才是最优的。
主要不同在于:
RatioBasedCompactionPolicy是简单的从头到尾遍历StoreFile列表,遇到一个符合Ratio条件的序列就选定执行Compaction。对于典型的不断flush memstore形成 StoreFile的场景是合适的,但是对于bulk-loaded是不合适的,会陷入局部最优。
而ExploringCompactionPolicy则是从头到尾遍历的同时记录下当前最优,然后从中选择一个全局最优列表。
关于这两个算法的逻辑可以在代码中参考对应的applyCompactionPolicy()函数。其他CompactionPolicy的研究和开发也非常活跃,例如Tier-based compaction(HBASE-6371,来自Facebook)和stripe compaction(HBASE-7667)
吐槽:HBase Compaction为什么会问题这么多,我感觉缺少了一个整体的IO负载的反馈和调度机制。因为Compaction是从HDFS读数据,然后再写到HDFS中,和其他HDFS上的负载一样在抢占IO资源。如果能有个IO资源管理和调度的机制,在HDFS负载轻的时候执行Compaction,在负载重的时候不要执行。而且这个问题在Hadoop/HDFS里同样存在,Hadoop的资源管理目前只针对CPU/Memory的资源管理,没有对IO的资源管理,会导致有些Job受自己程序bug的影响可能会写大量的数据到HDFS,会严重影响其他正常Job的读写性能。
更多内容:HBase Compaction、HBase 2013 Compaction 提升。
2. Mean Time To Recovery/MTTR优化
目前HBase对外提供服务,Region Server是单点。如果某台RS挂掉,那么直到该RS上的所有Region被重新分配到其他RS上之前,这些Region是的数据是无法访问的。对这个过程的改进主要包括:
HBASE-5844 和 HBASE-5926:删除zookeeper上Region Server/Master对应的znode,这样就省的等到znode 30s超时才发现对应的RS/Master挂了。
HBASE-7006: Distributed Log Replay,就是直接从HDFS上读取宕机的WAL日志,直接向新分配的RS进行Log Replay,而不是创建临时文件recovered.edits然后再进行Log Replay
HBASE-7213/8631: HBase的META表中所有的Region所在的Region Server将会有两个WAL,一个是普通的,一个专门给META表对应的Region用。这样在进行recovery的时候可以先恢复META表。
3. Bucket Cache (L2 cache on HBase)
HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。Block的cache命中率对HBase的读性能影响十分大。目前默认的是LruBlockCache,直接使用JVM的HashMap来管理BlockCache,会有Heap碎片和Full GC的问题。
HBASE-7404引入Bucket Cache的概念可以放在内存中,也可以放在像SSD这样的适合高速随机读的外存储设备上,这样使得缓存的空间可以非常大,可以显着提高HBase读性能。Bucket Cache的本质是让HBase自己来管理内存资源而不是让Java的GC来管理,这个特点也是HBase自从诞生以来一直在激烈讨论的问题。
4. Java GC改进
MemStore-Local Allocation Buffers通过预先分配内存块的方式解决了因为内存碎片造成的Full GC问题,但是对于频繁更新操作的时候,MemStore被flush到文件系统时没有reference的chunk还是会触发很多的Young GC。所以HBase-8163提出了MemStoreChunkPool的概念,也就是由HBase来管理一个ChunkPool用来存放chunk,不再依赖JVM的GC。这个ticket的本质也是由HBase进程来管理内存分配和重分配,不再依赖于Java GC。
5. HBase的企业级数据库特性(Secondary Index、Join和Transaction)
谈到HBase的企业级数据库特性,首先想到的就是Secondary Index、Join、Transaction。不过目前这些功能的实现都是通过外围项目的形式提供的。
华为的hindex是目前看到的最好的Secondary Index的实现方式。主要思想是建立Index Table,而且这个Index Table的Region分布跟主表是一致的,也就是说主表中某一Region对应的Index Table对应的Region是在同一个RS上的。而且这个索引表是禁止自动或者手动出发split的,只有主表发生了split才会触发索引表的split。
这个是怎么做到的呢?本质上Index Table也是一个HBase的表,那么也只有一个RowKey是可以索引的。这个索引表的RowKey设计就比较重要了,索引表的RowKey=主表Region开始的RowKey+索引名(因为一个主表可能有多个索引,都放在同一个索引表中)+需要索引的列值+主表RowKey。这样的索引表的RowKey设计就可以保证索引表和主表对应的Region是在同一台RS上,可以省查询过程中的RPC。每次Insert数据的时候,通过Coprocessor顺便插入到索引表中。每次按照二级索引列Scan数据的时候,先通过Coprocessor从索引表中获取对应的主表的RowKey然后就行Scan。在性能上看,查询的性能获得了极大提升,插入性能下降了10%左右。
Phoenix也通过另外建一张表的方式实现二级索引
Phoenix也实现了一大一小两个表的Join操作。还是老办法把小表broadcast到所有的RS,然后通过coprocessor来做hash join,最后汇总。感觉有点画蛇添足,毕竟HBase设计的初衷就是用大表数据冗余来尽量避免Join操作的。现在又来支持Join,不知道Salesfore的什么业务需求这个场景。
关于Transaction的支持,目前最受关注的还就是Yahoo!的Omid。不过貌似大家对这个特性的热情还不是特别高。
6. PrefixTreeCompression
由于HBase的KeyValue存储是按照Row/Family/Qualifier/TimeStamp/Value的形式存储的,Row/Family/Qualifier这些相当于前缀,如果每一行都按照原始数据进行存储会导致占据存储空间比较大。HBase 0.94版本就已经引入了DataBlock Encode的概念(HBASE-4218),将重复的Row/Family/Qualifier按照顺序进行压缩存储,提高内存利用率,支持四种压缩方式FAST_DIFFPREFIXPREFIX_TRIEDIFF。但是这个特性也仅仅是通过delta encoding/compression降低了内存占用率,对数据查询效率没有提升,甚至会带来压缩/解压缩对CPU资源占用的情况。
HBASE-4676:PrefixTreeCompression是把重复的Row/Family/Qualifier按照Prefix Tree的形式进行压缩存储的,可以在解析时生成前缀树,并且树节点的儿子是排序的,所以从DataBlock中查询数据的效率可以超过二分查找。(PREFIX_TREE压缩的初步探究及测试)
7. 其他变化
HBASE-5305:为了更好的跨版本的兼容性,引进了Protocol Buffer作为序列化/反序列化引擎使用到RPC中(此前Hadoop的RPC也全部用PB重写了)。因为随着HBase Server的不断升级,有些Client的版本可能还比较旧,所以需要RPC在新旧版本之间兼容。
HBASE-6055 HBASE-7290:HBase Table Snapshot。创建snaphost对HBase集群没有性能影响,只是生成了snaphost对应的metadata而不会去拷贝数据。用户可以通过创建snaphost实现backup和disaster recovery,例如用户在创建一个snaphost之后可能会误操作导致一些表出现了问题,这样我们可以选择回滚到创建snaphost的那个阶段而不会导致数据全都不可用。也可以定期创建snapshot然后拷贝到其他集群用于定时的离线处理。
HBASE-8015: 在0.96中,ROOT表已经改名为hbase:namespace,META则是hbase:meta。而且hbase:namespace是存在Zookeeper上的。这个namespace类似于RDBMS里的database的概念,可以更好的做权限管理和安全控制。HBase中table的META信息也是作为一种Region存放在Region Server上的,那么META表的Region和其他普通Region就会产生明显的资源竞争。为了改善META Region的性能,360的HBase中提出了专属MetaServer,在这个Region Server上只存放META Region
HBASE-5229:同一个Region内的跨行事务。一次操作中涉及到同一个Region中的所有写操作在获取到的相关Row的所有行锁(按照RowKey的顺序依次取行锁,防止死锁)之后事务执行。
HBASE-4811:Reverse Scan。过去被问到说如何反向查找HBase中的数据,常常被答道再建一张反向存储的表,而且LevelDB和Cassandra都支持反向扫描。HBase反向扫描比正向扫描性能下降30%,这个和LevelDB是差不多的。
Hoya — HBase on YARN。可以在一个YARN集群上部署多个不同版本、不同配置的HBase实例,可以参考GitHub。
展望2014年,HBase即将release 1.0版本,更好的支持multi-tenancy, 支持Cell级别的ACL控制。
8. 总结
Cloudera/Hortonworks/Yahoo!/Facebook的人从系统和性能等多方面关注
Salesfore/huawei的人貌似更关注企业级特性,毕竟他们面对的客户都是电信、金融、证券等高帅富行业
来自国内的阿里巴巴/小米/360等公司更加关注系统性能、稳定性和运维相关的话题。国内互联网行业用HBase更加关注的是如何解决业务问题。
越来越多的公司把它们的HBase集群构建在云上,例如Pinterest所有的HBase集群都是在AWS上,国外的start up环境太好了,有了AWS自己根本不用花费太多的资源在基础设施上。
传统的HBase应用是在线存储,实时数据读取服务。例如支付宝用HBase存放用户的历史交易信息服务用户查询,中国联通也使用HBase存储用户的上网历史记录信息用于用户的实时查询需求。现在HBase也向实时数据挖掘的应用场景中发展,例如wibidata公司开源的kiji能够在HBase上轻松构建实时推荐引擎、实时用户分层和实时欺诈监控。