首先说说业务情况,前端采集系统采集到用户日志信息,包括用户类型type,用户账号uid等等,送到后端经过分析后存储到HBase中。存储是按照同一个uid一天只存储一条记录原则进行的,查询是按照type,uid查询其在某一时间段内的数据。附上我的建表语句:
create 'TEST', {NAME => 'INFO', TTL => 31536000, VERSIONS => '400', COMPRESSION => 'snappy'}, {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'}
下面细细来说:
1、表名:USER_INFO
2、rowkey设计 HBase表设计最重要的一点
rowkey是HBase查询直接关系者,所以rowkey中最好包含常用查询条件,这样才能最快找到目标数据。我这里的查询业务很简单,就是根据用户类型type和用户id查询其在某端时间t1~t2内的信息,所以理想情况rowkey应该是 type_uid_timestamp,但是考虑到我是一个uid一天存一条记录,以10亿用户账号为例,一年下来,理论存在3650亿条记录,明显数据扩张速度太快,再加上按时间段查询的时候需要scan,查询耗时严重,所以timestamp不能加入rowkey中。不能加入时间的话,只能通过多版本来存储同一个uid的数据了,还是以10亿用户账号为例,一年下来,依然是10亿条数据,只不过每条记录下需要挂365个版本数据。这样在针对uid查询时,只需要使用get即可,而且数据记录可控,单个reigon块记录数更少,查询更快。但是带来的问题是,单次get查询需要返回的数据量太大,目前的解决方案是按照timestamp分页返回数据。这种多版本的设计方式其实是没经过测试的,随着数据的积累,也不知道以后会怎么样~~
rowkey设计第二点就是要有很好的散列化,由于不能保证type和uid的绝对散列化,所以按照type_uid或者uid_type这样的方式设计rowkey的话,在写入数据时,很可能发生region请求过热的情况,即在同一时间所有的写入请求都指向了同一个region块,这明显是不合理的,我们要实现的是对于每个region块的写入负载均衡。目前比较好的散列化算法比如说MD5,hash算法。hash算法比MD5快很多,这是其一大优势,但是hash值存在正负数,用起来很不爽。而MD5计算的结果是8位或者16位的16进制字符串,配合下面要说到的预建region很好用。一般的话,直接使用MD5(type_uid)的值即可作为rowkey,但是我这里需要考虑到以后数据的逆向回收,就选择了密文+明文的方式MD5(uid_type)_uid_type。另外,考虑到rowkey设计时尽可能短一些,所以我从8位MD5中截取了前四位作为密文,为此还特意写了程序测试4位MD5也是绝对散列的,现在想来挺笨的,因为8位MD5本身就是截取的,既然8位绝对散列,4位肯定也是绝对散列。
3、唯一列族:INFO
表设计时抛弃了”寛表“,选择了”高表“,所以只设计了一个列族。这样的好处是在写的时候节省了memstore,在读的时候只需要读取一个文件,不需要跨多文件查询。同时还值得一提的是,业务系统定义的字段大概10~15个,其中大概5个(A,B,C,D,E)是常用的,剩下的字段(F,G,H,I,J.K.....)使用率低得多,所以在向INFO列族下挂列时,特意将不常用字段组装成了REMARK字段,格式按照JSON格式字符串存储,这样INFO下面的实际列只有6个,即A,B,C,D,E,REMARK,这样既节省了存储空间,又加快了查询速度
4、列存活时间:TTL
默认是Integer,MAX_VALUE,我这里设置为1年(31536000s)。数据暂时存储一年,以后有需要的时候再进行修改
5、列版本数:VERSIONS
同样的,存一年365个版本,我这里设置成了400;配合TTL,能够保证至少1年内数据有效
6、压缩:COMPRESSION
默认是none,我使用了比较大众的snappy。snappy库需要手动安装,同时在hbase集群中加入压缩算法配置。贴一个网上常见的hbase常用压缩算法的性能比较。
Algorithm | % remaining | Encoding | Decoding |
GZIP | 13.4% | 21 MB/s | 118 MB/s |
LZO | 20.5% | 135 MB/s | 410 MB/s |
Zippy/Snappy | 22.2% | 172 MB/s | 409 MB/s |
这份数据是来自于几年前的测试,可见snappy压缩剩余比最高达22.2%,但是压缩解压速度最快。但是目前我的HBase集群单个region块压缩前25G大小,压缩后仅有3.68G大小,压缩剩余比达到14.7%,早期比例大约30%,随着数据的写入,比例逐渐在下降,所以snappy压缩能力可见一斑
7、预分region:NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'
hbase建表默认创建一个region,但是随着数据写入,region块会自动均匀split成2个小region。集群初期,region如果太少,肯定会经常的split,这明显不是个好事情!所以,我在建表的时候一起预创建了多个region块。预创建的方式有很多,我这里选择的是HexStringSplit,同时创建了16个region,创建的rowkey起始结束key分别如下:
region0 100000
region1 100000 200000
region2 200000 300000
region3 300000 400000
region4 400000 500000
region5 500000 600000
region6 600000 700000
region7 700000 800000
region8 800000 900000
region9 900000 a00000
region10 a00000 b00000
region11 b00000 c00000
region12 c00000 d00000
region13 d00000 e00000
region14 e00000 f00000
region15 f00000
可以看出来,同时创建16个region的话,会已1-f开头,各生成一个region,这样配合MD5散列化之后的rowkey能够完美将写请求均匀分散到每个region上,实际测试时,效果也非常棒。
表创建好之后,使用describe查看的话,可以发现在列族级别有很多属性,比如说DATA_BLOCK_ENCODING,BLOOMFILTER,BLOCKSIZE,IN_MEMORY,BLOCKCACHE等等,这些我都采用了默认设置。在以后运行期需要优化时再酌情修改。
好了,我的hbase表设计大概就是这样。