HBase的前世今生
这一切的一切,还要从谷歌的那篇论文说起。。。。
06年google对外发布了三大论文之一Bigtable: A Distributed Storage System for Structured Data(原文地址:链接)。从此以后,浩如烟海的各种所谓大数据存储类的产品或完全复用,或者部分借鉴,或者在此基础上微创新,基本上都能看到BigTable里面的设计影子。
这里面有开源的HBase,公司内部使用的Lindorm,商业化的OTS,包括最近大火的ClickHorse基本上都或多或少的有类似的设计。
而Hbase完全是基于BigTable开源的实现。
HBase八股介绍
OLAP(联机分析处理)场景的关键特征
- 大多数以读为主的。
- 数据以大的批次更新,很少单行更新(或者说没有单行);也可能没有更新。
- 已添加到数据库的数据不能修改。
- 读取的时候,从数据库总读取多行和列,但是只用其中几列数据。
- 一般表都是一张大宽表。
- 查询并发不高(QPS 100?)。
- 检查查询允许一定的延迟。百毫秒级返回。
- 列中的数据相对较小,数字和短字符。
- 处理单个查询时需要很高的吞吐量。(十亿行)
- 不需要事务
- 数据一致性要求不是非常高。
- 每个查询有一个大表,其他的都很小。
- 查询结果不大。数据经过过滤和聚合,可以存在单个服务器。
一句话总结:做饭就一个人做,吃就大家都来吃。说错了,写的时候尽量批量些,读的性能发挥到机制。
为什么分析类查询更适合列式存储数据库
- 针对分析类查询,通常只需要读取表的一小部分列。在列式数据库中你可以只读取你需要的数据。
- 由于数据总是批量读取的,同时数据按列分别存储这也更容易压缩。
- 同构数据压缩比高
- 有效利用cache IO
- 机械硬盘的特点也决定了顺序读消耗的时间更短。其实看下磁盘结构就容易理解了,如果被读取的数据相邻,那么盘片正常转动即可。如果不是顺序存储,还需要增加重新定位的时间。
总结:
- 同构数据便于压缩,利于磁盘顺序读取特性。
- 结合磁盘特性,顺序写入的时候,也会更加高效。
Hbase分布式策略
在学习Hbase之前,一定要带着一个问题,为什么Hbase比传统的关系型数据库性能要高很多?
说到这里就不得不提Hbase的数据结构,简而言之,Hbase维护的是一个Map数据,对于每一条数据,在Hbase上都是一个独立的Map,其中有一个RowKey,然后我们的查询都是基于这个RowKey进行的,在Hbase内部,维护了一个RowKey到具体分片数据的映射,类似于查字典,Hbase Master Server维护了一个字典的目录,这样查询的时候,你只需要提供RowKey,就能知道对应的数据存储的位置。但是关系型数据库,相当于从字典的第一页,查询直到查到数据位置,所以Hbase的时间复杂度理论上是接近O(1),而Mysql等关系型数据库尽管也通过B+树等数据结构做了优化,但是复杂度依然是O(Logn)。
当然,Hbase的缺点也很明显,无法根据条件进行查询,不支持事物,而且也不支持数据分析(如果是聚合,统计,最好使用ES)。
本质上Hbase是个列簇式数据库。
理解和感悟:我们在做技术选型的时候,不能一味的追求一些高大上的技术,要牢记一点,技术永远是服务于业务的,要找到合适的技术,而不是高大上的技术。
什么是列簇式存储?
先回顾下列式存储和行式存储。
- 列式存储:以列先关存储架构进行数据存储的数据库,主要适用于OLAP
- 行式存储:行相关存储体系架构进行空间分配,主要是和小批量的数据处理,常用语OLTP。
Hbase存储的特点:
- 不同的列簇存储在不同的FileStore(HFile).
- 数据按照Rowkey进行字典排序。
- 每一列的数据在HFile中都是以KV存储。
- 同一行的数据,如果列簇一样,是顺序放在一起的。
所以说HBase不是列存储也不是行存储,更像是一个列簇式存储,因为不同行相同的列簇是顺序存储的。同一行的不同列簇是存储的不同的位置的。
HBase实际上在HDFS上的存储形式:/hbase/data/default/<tbl_name>/<region_id>/<cf>/<hfile_id>
因此创建表的时候,必须指定列簇。
Hbase主要的组件
MasterServer,Zookeeper,Region Server.
其中:
- MaserService负责表,列创建,维护集群状态。
- RegionServer处理查询数据的操作,每个Region存储了Startkey-EndKey数量的数据,因为MasterServer维护了RowKey到Region的映射,因此可以很清楚的知道每一个RowKey存储在哪台RegionServer上。
那么问题来了,Hbase是如何管理这些映射呢?
通过下图就可以清晰的了解了,.META.表记录了Row和Region的映射关系,但是由于这个映射关系本身数据量可能很大,因此,又通过-ROOT-表来存储.META.表和Region的映射关系,而-ROOT-表要小得多,因此-ROOT-表是只有一个Region的,在Zookeeper中存储了-ROOT-表的地址
也就是说,一个Client访问HBase操作数据的时候,首先要经过Zookeeper
查询到-ROOT-的地址,然后,查询.META.表,最后查询RegionServer,进行相应的操作,但是HBase对这些数据做了Cache,所以不需要太担心性能问题。
HRegionServer架构图
每个HRegion对应了一个Table的一个Region(关键点,Region划分是和Table相关的)。
每个HStore对应一个Column Family的存储。因此需要将相同IO特性的数据放在一个CF里面。
几个关键点
- RegionServer中存储的是什么?Hbase如何和HDFS通信?
- 本质上Hbase的存储是HDFS。RegionServer上有多个Region,一个Region上有多个StoreFile(基于HFile)和memStore。一个RegionServer有一个HLog。
- 说白了,因为MemStore是为了减少小写入。定时将内存的数据刷到HDFS中。但是因为多了这个设计,如果遇到断电等极端情况,为了防止内存的数据丢失设计的。类似Mysql的Bin-Log文件。
- 写入MemStore的同时也会写入HLog。当MemStore写入文件的时候,HLog也会刷新,将写入文件的数据删除掉。
- 针对于一次Hbase数据查询,过程是怎么样的?
Client->Zookeeper->-Root- -> -META- ->用户表的RowKey对应的数据位置。
到Zookeeper及访问-ROOT- 甚至是-META-表都是有缓存的。具体的缓存逻辑要看下。
- HRegionServer写入数据之后的单region扩容是如何完成的?
- HStore是Hbase存储的核心,由MemStore和StoreFiles组成。MemStore是Sorted Memory Buffer。
- 用户写入的数据会先放入MemStore.MemStore满了之后,会Flush成一个StoreFile(底层还是HFile)。
- StoreFile文件数量到达一个阈值之后,触发Compact操作,多个StoreFiles合并成一个。合并过程中会进行版本合并和数据删除。
- Compact之后形成的StoreFile会越来越大,然后触发阈值,进行Split。
- 当前的Region也会Split成两个Region。父Region下线,新的Split region会被Hmaster分配到对应的RegionServer。原来一个Region的压力也进行分流。
- Hlog的功能介绍
- HRegionServer意外退出,memStore内存中的数据会丢失,因此引入Hlog.
- 一个HRegionServer一个Hlog对象,Hlog是实现 Write Ahead Log的类。每次用户操作写入MemStore的同时也会写入Hlog。HLog会滚动清除已经写入文件StoreFiles的数据.
- HRegionServer意外退出,Zk会感知到,HMaster首先处理遗留的Hlog.将不同Region的Log数据拆分,分别放到对应的Region目录下。
- 失效的Region重新分配,领取到新Region的HRegionServer会进行LoadRegion过程。
- 发现有历史HLog需要处理,会Replay HLog中的数据到MemStore.
- 然后Flush到StoreFiles中。
Hbase数据模型
从网上摘抄了一个图,非常清晰的阐述了HBase的数据模型。
关于Hbase源码:https://github.com/apache/hbase/tree/master/ (代码量过大,感兴趣的话,可以网上搜写源码解析的文章)
Hbase局限
不支持二级索引(引申二级索引方案)
核心是主表+索引表的设计方式,其实Hbase也有内置的一个方案(Coprocessor协处理器)
ES+Hbase。
Solr+Hbase.
HBase设计启发
如果只是为了学习原理而学习原理,那就真成八股文了,学以致用才是重点,接下来分享下基于Hbase的原理的设计注意事项
HBase热点问题
关键字:RowKey字典序存储
由于Rowkey是字典序存储,一个设计糟糕的Rowkey,会直接导致ReginServer热点问题,旱的旱死涝的涝死。
回顾下Hbase的架构图和数据模型,如果Rowkey没有很好的散列,就会导致:
- 数据量不均匀:某个RegionServer下存储了大量的数据,而其他RegionServer下数据却相对少很多。
- 数据访问频次不均匀:虽然数据量均匀分布,由于业务数据的特点(例如用户数据的RowKey按照 地域+UserID的方式设计),导致某个RegionServer下的数据更加活跃,被频繁访问。
为了避免热点问题,设计RowKey的时候需要注意尽可能的让Rowkey离散:
加盐
即在ID前缀加上一个随机字符串。以用户ID为例,加盐之后的RowKey:randomStr(length)+userID。
但由于前缀随机,业务查询的时候,如何获取RowKey是个很大的问题,所以并不推荐(除非前缀有固定的计算逻辑,例如经过哈希,但这方式放在了下面一类了)。
用哈希值
这个比较好理解,对ID进行哈希之后,作为Rowkey整体或者前缀的一部分。这样Rowkey本身足够散列,同时也可以按照固定的逻辑计算出Rowkey。是比较常用的方式。
反转ID
例如手机号,自增用户ID等,由于前缀相同,为了更好的散列,通常可以将ID反转之后进行存储。
HBase实践
案例:
在蚂蚁风控项目中,通过Redis+Hbase的方案(当然蚂蚁内部做了优化,TBASE对标Redis,Lindorm对标Hbase),承接了巅峰1400W/S的查询量级的数据访问。
如图:
在该项目中Lindorm(Hbase)中,通过RowKey${hash5}_${支付宝ID}_${支付明细ID} 的精心设计,让数据更加离散,防止出现热点数据。
在TBase(类比Redis),内存数据库中,直接将同一用户数据放在一起,让Value聚集,更方便数据压缩。
注:
- 实际业务中Rowkey的最后一段并不是支付明细ID,而是一个叫做velocityID的东东,为了降低理解成本,用支付明细ID代替,不影响案例描述。
- 为什么Lindorm中,明细要分开,本质上因为Hbase类的数据库,修改数据是极为不推荐的,一般都是增量写入的方式。
提问交流环节
问题1:支付宝以2088开头的支付宝账号ID,或者身份证号,作为RowKey是不是一个很好的设计?
问题2:您还有关于RowKey设计的一些最佳实践或者技巧分享么。
问题3:Hbase支持修改数据么?怎么支持?性能如何?
评论区留言讨论。