背景
用户/内容画像的对存储的要求其实是比较高的:
- 能批量更新(比如更新所有用户某个属性)
- 大量随机读取(甚至可能没有热点数据)
- 随机属性更新/添加
- 可持久化
- 易于横向扩展解决性能问题
上一次重度使用HBase已经是两年前了。HBase能够满足上面五个要求,所以用HBase作为画像体系的主要存储引擎便水到渠成。
问题
因为有批量更新,随机属性的更新/添加,那么必然会缓存失效,从而触发磁盘IO导致读取响应时间受到影响。在画像体系里,随机读取量大,比如召回了1000个id,然后你需要取这1000个id的属性集合,并且要求响应时间能够控制在100ms。基本只要碰到磁盘IO就歇菜了。所以现在我们希望HBase能够把所有数据都缓存住。
HBase读缓存特色
HBase的缓存目前我所了解的是Block Cache. Block Cache是什么概念的呢,我们知道HBase的最小文件单元是HFile, HFile是有结构的,主要包含:
- 索引,可以是多层
- 数据
- 元数据
当然还有一个布隆过滤器,方便确定一个元素是不是在HFile里。
并且只会有三个动作:
- 新增HFile
- HFile 合并
- HFile的分裂
这里需要注意HFile一旦生成里面的元素就不会被改变。
一个HFile的数据会被切分成多个Block,每个Block一般而言都会有一些元信息。当然这些切分其实是逻辑上的。Block Cache就是前面三部分的Cache. 在HBase里,当打开一个HFile时,默认会cache住一些索引信息,文件信息,读取时则会连数据都会Cache起来。当然你也可以通过参数让HBase在打开时就把数据Cache住。
如果是传统意义上的缓存,如果有更新,那么必然会导致一个问题:缓存失效。但是HBase其实并没有这个问题。一个简单的Get请求,HBase的读取方式是读MemStore 和HFile,读HFile的时候会看数据是不是已经在BlockCache里。
- MemStore,这个是写缓存,但是是有结构的,可以直接查。
- BlockCache, 这个是读缓存,也是有结构的,可以直接查。
- HFile,这个就是HDFS文件,会touch到IO。
假设我在读取的时候,MemStore没有进行flush,那么可能不会触碰到磁盘,虽然BlockCache的数据已经是老版本的了,MemStore里却有最新版本的数据,所以HBase简单的到MemStore/BlockCache都拿一遍。第二次再来拿,假设正好碰到MemStore flush生成新的HFile,这个时候就触发磁盘IO了。当然,其他如Compaction也会导致触发磁盘。
解决方案
下面解决方案的前提是,可用于BlockCache的内存大于你的数据。在画像体系里,这点可以做到的,因为内容和用户都是有限的。
根据前面的描述,为了能够保证随机读不触发磁盘IO操作,那么我们在生成新HFile的同时,也需要让它写进BlockCache,HBase也提供了相关参数让你完成这个功能:
hfile.block.index.cacheonwrite 写HFile时把索引加入到Cache
hfile.block.bloom.cacheonwrite 写HFile时把布隆过滤器加入到Cache
hbase.rs.cacheblocksonwrite 写HFile时把数据也写入到Cache里
这里我们基本知道写Cache的几个时机点:
- 打开HFile
- 读HFile
- 写HFile