Hbase设计实战

Hbase设计实战

本文通过一个游戏公司客户实际案例的讲解,分析了 Hbase 表设计及开发在实际案例中的运用,对比了不同的 Hbase 设计考量对客户端访问模式及检索性能的差异。读者通过案例中 Hbase 表设计模式可以更深刻的理解 Hbase 原理及设计,并且熟悉 Hbase 客户端开发的思路及实现。

1、概述

由上期文章介绍的 Hbase 的整体架构及检索的时间复杂度分析我们可以看出,行键、列簇等的设计及数据存储决定了 Hbase 总体的性能及执行查询的效率,很多使用 Hbase 的项目及技术人员能熟练的使用 Hbase Shell 或 SDK API 访问 Hbase,进行表创建、删除等 DDL,以及 put/delete/scan 等 DML 操作,但并深入探究需要多少个列簇,一个列簇需要多少列,什么数据应该存入列名中,以及什么数据应该存入单元等开发设计中的关键问题。

基于 Hbase 的系统设计与开发中,需要考虑的因素不同于关系型数据库,Hbase 模式本身很简单,但赋予你更多调整的空间,有一些模式写性能很好,但读取数据时表现不好,或者正好相反,类似传统数据库基于范式的 OR 建模,在实际项目中考虑 Hbase 设计模式是,我们需要从以下几方面内容着手:

  1. 这个表应该有多少个列簇

  2. 列簇使用什么数据

  3. 每个列簇应有多少个列

  4. 列名应该是什么,尽管列名不必在建表时定义,但是读写数据时是需要的

  5. 单元应该存放什么数据

  6. 每个单元存储什么时间版本

  7. 行健结构是什么,应该包括什么信息

以下我们以一个使用 Hbase 技术的真实客户案例为例,说明 Hbase 设计模式在真实项目中的实践,并通过不同的表设计模式,可以看出在模式是如何影响到表结构和读写表的方式方法,以及对客户端检索查询的性能的影响。

2、客户场景介绍

客户简介:客户是一个互联网手机游戏平台,需要针对广大手游玩家进行手游产品的统计分析,需要存储每个手游玩家即客户对每个手游产品的关注度(游戏热度),且存储时间维度上的关注度信息,从而能针对客户的喜好进行挖掘并进行类似精准营销的手游定点推送,广告营销等业务,从而扩大该平台的用户量并提升用户粘着度。

该平台上手游产品分类众多,总共在 500 余以上,注册玩家(用户帐号)数量在 200 万左右,在线玩家数量 5 万多,每天使用手游频率峰值在 10 万/人次以上,年增量 10%以上。

根据以上需求,手游产品动态增长,无法确定哪些手游产品需要被存储,全部存储又会表超过 200 列,造成大量空间浪费,玩家每天使用手游的频率及分类不确定,客户注册用户超百万,按天的使用热度数据量超过 1000 万行,海量数据也使得表查询及业务分析需要的集群数量庞大及 SQL 优化,效率低下,因此传统关系型数据库不适合该类数据分析和处理的需求,在项目中我们决定采用 Hbase 来进行数据层的存储的分析。

3、高表设计

客户案例中表的设计,我们需要存储玩家信息,通常是微信号,QQ 号及在该手游平台上注册的帐号,同时需要存储该用户关注什么手游产品的信息,而用户每天会玩一个或者多个手游产品,每个产品玩一次或者多次,因此存储的应该是该用户对某一手游产品的关注度(使用次数),该使用次数在每天是一个动态的值,而用户对手游产品也是一个多对多的 keyvalue 键值的集合。该手游平台厂商关心的是诸如“XXX 客户玩家关注 YYY 手游了么?”,“YYY 手游被用户关注了么?”这类的业务维度分析。

假设每天每个手游玩家对每个产品的关注度都存在该表中,则一个可能的设计方案是每个用户每天对应一行,一用户 ID+当天的时间戳作为行健,建立一个保存手游产品使用信息的列簇,每列代表该天该用户对该产品的使用次数。

本案例中我们只设计一个列簇,一个特定的列簇在 HDFS 上会由一个 Region 负责,这个 region 下的物理存储可能有多个 HFile,一个列簇使得所有列在硬盘上存放在一起,使用这个特性可以使不同类型的列数据放在不同的列簇上,以便隔离,这也是 Hbase 被称为面向列存储的原因,在这张表里,因为所有手游产品并没有明显的分类,对表的访问模式也不需区分手游产品类型,因此并不需要多个列簇的划分,你需要意识到一点:一旦创建了表,任何对该表列簇的动作通常都需要先让表 offline。

我们可以使用 Hbase Shell 或者 Hbase SDK api 创建表,Hbase shell 脚本示例如下:

清单 1. Hbase shell 脚本示例
$hbase shell
Version 0.92.0, r1231986, Mon Nov 16 13:16:35 UTC 2015
$hbase(main):001:0 >create 'prodFocus' , 'degeeInfo'
0 row(s) in 0.1200 seconds
hbase(main):008:0> describe 'prodFocus'
DESCRIPTION ENABLED
'prodFocus', {NAME => 'cf', DATA_BLOCK_ENCODING => true
'NONE', BLOOMFILTER => 'ROW', REPLICATION_SCOPE =>
'0', VERSIONS => '1', COMPRESSION => 'NONE', MIN_VE
RSIONS => '0', TTL => '2147483647', KEEP_DELETED_CE
LLS => 'false', BLOCKSIZE => '65536', IN_MEMORY =>
'false', BLOCKCACHE => 'true'}
1 row(s) in 0.0740 seconds

现在的表如下图所示,一个存有示例数据的表。

表 1. prodFocus 表示例
       
rowkey:用户 ID$当天时间 degee(列簇,手游热度信息)
QQ121102645$20141216 degee:3DARPG:6 degeeInfo:DTLegend:1  
WeiXin_295y603765de8$12140928 degree:DTLegend:3    
ChaoChenYY$12141109 degree:3CountryBattle:1 degree:Forget Xian:1  
QQ5863976645$20141214 degree:Frus3D:2    
HeXaoYang$20140907 degree:Space Hunter:1 degree:3CountryBattle:2 degree:Frus3D:1
FengKe_Tony$20150216 degree:DTLegend:1    
junping_Jeff$20141204 degree:Frus3D:2    
XiaoFenXia$20150716 degree:Forget Xian:3    

表设计解释如下:

rowkey 为 QQ121102645$20141216 表示帐号为 QQ121102645 的手游玩家(以 QQ 号联邦认证的)在 2014 年 12 月 16 日当天的游戏记录;列簇 focuspro 记录该行账户当天对每种产品类型的点击热度 (游戏次数),比如 Space Hunter::1 表示玩 (或者点开)Space Hunter:(时空猎人) 的次数为 1 次

现在你需要检验这张表是否满足需求,为此最重要的事是定义访问模式,也就是应用系统如何访问 Hbase 表中的数据,在整个 Hbase 系统设计开发过程中应该尽早这么做。

我们现在来看,我们设计的该 Hbase 表是否能回答客户关心的问题:比如“帐号为 QQ121102645 的用户关注过哪些手游?”,沿着这个方向进一步思考,有相关业务分析的问题:“QQ121102645 用户是否玩过 3CountryBattle(三国 3)手游?”“哪些用户关注了 DTLegend(刀塔传奇)?”“3CountryBattle(三国 3)手游被关注过吗?”

基于现在的 prodFocus 表设计,要回答“帐号为 QQ121102645 的用户关注过哪些手游?”这个访问模式,可以在表上执行一个简单的 Scan 扫描操作,该调用会返回整个 QQ121102645 前缀的整个行,每一行的列簇进行列遍历就能找到用户关注的手游列表。

代码示例如下:

清单 2. 客户端查询用户关注手游列表
static {
Configuration HBASE_CONFIG = new Configuration();
HBASE_CONFIG.set(“hbase.zookeeper.quorum”,“192.168.2.6”);
HBASE_CONFIG.set(“hbase.zookeeper.property.clientPort”,“2181”);
cfg = new HBaseConfiguration(HBASE_CONFIG);
}
HTablePool pool = new HTablePool();
HTableInterface prodTable = pool.getTable(“prodFocus”);
Scan a = new Scan();
a.addFamily(Bytes.toBytes(“degreeInfo”));
a.setStartRow(Bytes.toBytes(“QQ121102645”));
ResultScanner results = prodTable.getScanner(a);
List<KeyValue> list = result.list();
List<String> followGamess = new ArrayList<String>();
for(Result r:results){
KeyValue kv = iter.next();;
String game =kv.get(1];
followGames.add(user);
}

代码解释:首先通过 Configuration 设置 Hbase Master 主机及客户端连接端口,然后使用 HtableInterface 接口示例连接上 prodFocus 表,因为 prodFocus 表 rowkey 设计为用户 ID+$+当天的时间戳,因此我们创建以用户“QQ121102645”为检索前缀的 Scan 扫描,扫描返回的 ResultScanner 即为该用户相关的所有行数据,遍历每行的“degreeInfo”列簇中的各个列即可获得该用户所有关注(玩过)的手游产品。

关于 Hbase API 操作的代码这里不再详述,感兴趣的读者可以查阅Hbase SDK,熟悉操作 HBase 表及 put、scan、delete 代码。

第二个问题“QQ121102645 用户是否玩过 3CountryBattle(三国 3)手游”的业务跟第一个类似,客户端代码可以用 Scan 找出行健为 QQ121102645 前缀的所有行,返回的 result 集合可以创建一个数组列表,遍历这个列表检查 3CountryBattles 手游是否作为列名存在,即可判断该用户是否关注某一手游,相应代码与上文问题 1 的代码类似:

清单 3. 客户端判断用户是否关注某一手游
HTablePool pool = new HTablePool();
HTableInterface prodTable = pool.getTable(“prodFocus”);
Scan a = new Scan();
a.addFamily(Bytes.toBytes(“degreeInfo”));
a.setStartRow(Bytes.toBytes(“QQ121102645”));
ResultScanner results = prodTable.getScanner(a);
List<Integer> degrees = new ArrayList<Integer>();
List<KeyValue> list = results.list();
Iterator<KeyValue> iter = list.iterator();
String gameNm =“3CountryBattle”;
while(iter.hasNext()){
KeyValue kv = iter.next();
if(gameNm.equals(Bytes.toString(kv.getKey()))){
return true;
}
}
prodTable.close();

代码解释:同样通过扫描前缀为“QQ121102645”的 Scan 执行表检索操作,返回的 List<keyValue>数组中每一 Key-value 是 degreeInfo 列簇中每一列的键值对,即用户关注(玩过)的手游产品信息,判断其 Key 值是否包含“3CountryBattle”的游戏名信息即可知道该用户是否关注该手游产品。

看起来这个表设计是简单实用的,但是如果我们接着看第三个和第四个业务问题“哪些用户关注了 DTLegend(刀塔传奇)?”“3CountryBattle(三国 3)手游被关注过吗?”

如你所看到的,现有的表设计对于多个手游产品是放在列簇的多个列字段中的,因此当某一用户对产品的喜好趋于多样化的时候(product key-value 键值对会很多,意味着某一 rowkey 的表列簇会变长,这本身也不是大问题,但它影响到了客户端读取的代码模式,会让客户端应用代码变得很复杂。

同时,对于第三和第四问题而言,每增加一种手游关注的 key-value 键值,客户端代码必须要先读出该用户的 row 行,再遍历所有行列簇中的每一个列字段。从上文 Hbase 索引的原理及内部检索的机制我们知道,行健是所有 Hbase 索引的决定性因素,如果不知道行健,就需要把扫描限定在若干 HFile 数据块中,更麻烦的是,如果数据还没有从 HDFS 读到数据块缓存,从硬盘读取 HFile 的开销更大,从上文 Hbase 检索的时间复杂度分析来看,现在的 Hbase 表设计模式下需要在 Region 中检索每一列,效率是列的个数*O(max(el b),从理论上已经是最复杂的数据检索过程。

对关注该平台业务的客户公司角度考虑,第三个第四个的业务问题更加关注客户端获取分析结果的实时分析的性能,因此从设计模式上应该设计更长的行健,更短的列簇字段,提高 Hbase 行健的检索效率并同时减少访问宽行的开销。

4、宽表设计

Hbase 设计模式的简单和灵活允许您做出各种优化,不需要做很多的工作就可以大大简化客户端代码,并且使检索的性能获得显著提升。我们现在来看看 prodFocus 表的另一种设计模式,之前的表设计是一种宽表(wide table)模式,即一行包括很多列。每一列代表某一手游的热度。同样的信息可以用高表(tall table)形式存储,新的高表形式设计的产品关注度表结构如表 2 所示。

表 2. prodFocusV2 表示例
   
  cf(列簇,按日期时间戳的关注度数据)
rowkey:被关注产品$某用户  
3DARPG$QQ121102645 20141224:6
DTLegend$QQ121102645 20141216:1
DTLegend$WeiXin_295y603765de8 20141212:3
3CountryBattle$ChaoChenYY 201412142
Frus3D$QQ5863976645 20150906:2
Space Hunter:&HeXaoYang 20140907:1
3CountryBattle$HeXiaoYang 20140907:2
Frus3D$HexiaoYang 20140907:1
DTLegend$FengKe_Tony 20150216:1
Frus3D$junping_Jeff 20141204:2
Forget Xian$XiaoFenXia 20150716:3

表解释:将产品在某一天被某用户关注的关联关系设计到 rowkey 中,而其关注度数据只用一个 key-value 来存储,行健 Daqier_weixin1398765386465 串联了两个值,产品名和用户的帐号,这样原来表设计中某一用户在某天的信息被转换为一个“产品-关注的用户”的关系,这是典型的高表设计。

HFile 中的 keyvalue 对象存储列簇名字。使用短的列簇名字在减少硬盘和网络 IO 方面很有帮助。这种优化方式也可以应用到行健,列名,甚至单元。紧凑的 rowkey 存储业务数据意味应用程序检索时,IO 负载方面会大大降低。这种新设计在回答之前业务关心的“哪些用户关注了 XXXX 产品?”或者“XXXX 产品被关注过吗?”这类问题时,就可以基于行健使用 get() 直接得到答案,列簇中只有一个单元,所以不会有第一种设计中多个 keyvalue 遍历的问题,在 Hbase 中访问呢驻留在 BlockCache 离得一个窄行是最快的读操作。从 IO 方面来看,扫描这些行在一个宽行上执行 get 命令然后遍历所有单元相比,从 RegionServer 读取的数据量是相同的,但索引访问效率明显大大提高了

例如要分析“3CountryBattles(三国群雄)手游是否被 QQ121102645 用户关注?”时,客户端代码示例如下:

清单 4. 客户端判断某一手游产品是否被关注
HTablePool pool = new HTablePool();
HTableInterface prodTable = pool.getTable(“prodFocusV2”);
String userNm =“QQ121102645”;
String gameNm =“3CountryBattles”;
Get g = new Get(Bytes.toBytes(userNm+”$”+gameNm));
g.addFamily(Bytes.toBytes(“degreeInfo”));
Result r = prodTable.get(g);
if(!r.isEmpty()){
return true;
}
table.close();

代码解释:由于 prodFocusV2 的 rowkey 设计改为被关注产品$用户 Id 的高表模式,手游产品及用户信息直接存放在行健中,因此代码以手游产品名“3CountryBattles”+“$”+用户帐号“QQ121102645”的 Byte 数据作为 Get 键值,在表上直接执行 Get 操作,判断返回的 Result 结果集是否为空即可知道该手游产品是否被用户关注。

我们使用压力测试来检验一下两种 Hbase 表设计模式下的并发访问性能的对比,在百万级及千万级行数据条件下,采用宽表和高表的两种设计模式下,在进行”关注 3CountryBattles 手游的用户”查询,取得 result 检索结果的相应时间如下表所示:

表 3. 高表 vs 宽表检索性能对比
  宽表设计模式(prodFocsV1) 高表设计模式 (prodFocsV2)
5 百万行数据 0.237s 0.079s
1 千万行数据 0.418s 0.112s
2 千万行数据 0.83s 0.283s

可以看到在客户关心的产品关注度维度上,高表的性能比宽表要高出 50%以上,这是 rowkey 和列簇的设计影响到 Hbase 索引检索在 Hbase 设计模式中成功运用的表现。掌握 Hbase 数据存储机制及内部检索工作机制之所以重要,很大一部分原因就在于运用该机制是提升性能的机会。

5其他调优考虑

当然还有一些其他优化技巧。你可以使用 MD5 值做为行健,这样可以得到定长的 rowkey。使用散列键还有其他好处,你可以在行健中使用 MD5 去掉“$”分隔符,这会带来两个好处:一是行键都是统一长度的,可以帮助你更好的预测读写性能。第二个好处是,不再需要分隔符后,scan 的操作代码更容易定义起始和停止键值。这样的话你使用基于用户+手游名的 MD5 散列值来设定 Scan 扫描紧邻的起始行(startRow 和 stopRow)就可以找到该手游受关注的最新的热度信息。

使用散列键也会有助于数据更均匀的分布在 region 上。如该案例中,如果客户的关注度是正常的(即每天都有不同的客户玩不同的游戏),那数据的分布不是问题,但有可能某些客户的关注度是天生倾斜的(即某用户就是喜欢某一两个产品,每天热度都在这一两个产品上),那就会是一个问题,你会遇到负载没有分摊在整个 Hbase 集群上而是集中在某一个热点的 region 上,这几个 region 会成为整体性能的瓶颈,而如果对 Daqier_weixin1398765386465 模式做 MD5 计算并把结果作为行键,你会在所有 region 上实现一个均匀的分布。

使用 MD5 散列 prodFocusV2 表后的表示例如下:

表 4. rowkey MD5 表示例
   
rowkey:MD5(被关注产品$某用户) cf(列簇,按日期时间戳的关注度数据)
3b2c85f43d6410d6 20141224:6
82c85c2cdf16dcee 20141216:1
8480986fd88c1a39 20141212:3
3671c0efbe01ae88 201412142
baf933cac7dd2814 12141109:2
65ae48cfaae57972 20140907:1
732106051f4a2ef8 20140907:1
f3b59010d3f8fb2d 20140907:2
402480df0adfbcf9 20150216:1
9171607fa5190507 20141204:2
296be556a86dd505 20150716:3
上一篇:box-shadow IE8兼容处理


下一篇:【JQGRID DOCUMENTATION】.学习笔记.1.安装jqGrid