1. 表结构设计至关重要
在使用HBase的过程中,HBase表结构设计的优劣对读写性能有着较大影响。由于HBase只能对RowKey进行索引,因此若RowKey没有针对具体需求做特殊设计,Client端在Get/Scan数据表时,往往需要遍历Row中的所有数据列,并做一些较为复杂的逻辑处理,才能得到所需数据,读写性能较差。RowKey的设计还需要考虑到HBase的RowKey采用从小到大字典排序的特点,且RowKey相同的数据会在同一个HRegionServer中,当RowKey相似或存在数据热点时,大量的数据会存储在同一个HRegionServer中,对数据读写时会存在单点性能瓶颈,无法发挥HBase分布式存储的优势。ColumnFamily数量、数据的压缩方式、数据版本数、数据时间周期TTL以及二级索引表等,对HBase的性能也有着重要影响,这些参数在进行HBase表结构设计时,都需要着重考虑,只有全面考虑才能最大发挥HBase的性能优势。
2. 建表示例
create 'TestTable', {NAME=>'CF1', BLOOMFILTER=>'ROW', BLOCKSIZE=>'65536', VERSIONS=>'3', COMPRESSION=>'SNAPPY', BLOCKCACHE=>'true', IN_MEMORY=>'false', TTL=>'3600'}
以上建表语句,表示建立一个名为TestTable的表,只有一个名为CF1的列簇,该列簇中BLOOMFILTER为ROW方式,HFile数据块大小为65536B(64KB),版本数为3,数据采用SNAPPY的压缩方式,BLOCKCACHE为true(默认),IN_MEMORY为false(默认)
, 数据存储周期为3600s。建表语句中具体参数含义会下面会具体解释。
3. RowKey设计
3.1 排序问题
HBase只支持基于RowKey的索引,因此RowKey的设计决定了HBase的读写性能。HBase将不同的RowKey,按照范围StartKey~EndKey存储在不同的Region中,且HBase对RowKey按从小到大字典排序。具体应用中RowKey一般为单个关键数据或多个关键数据拼接而成,若RowKey中包含时间戳,建议时间戳采用将Long类型转换为Bytes的方式,若索引数据时要按照时间从新到旧的数据,则建议对RowKey中的时间戳进行(Integer.MAX_VALUE-timestamp)处理,实现RowKey按照时间从新到旧排序。
3.2 数据聚集的负载均衡问题
HBase中若出现数据聚集的情况,即数据大多集中在个别RowKey中,则会出现HBase只对个别HRegionServer进行读写访问,负载较重,而集群中其他HRegionServer处于闲置状态,造成HBase集群读写性能差的问题。
为了防止这种问题,在设计RowKey时需要考虑负责均衡,将RowKey尽量均匀的分布在HBase集群中。这主要通过将字符串的Hash值或字符串的MD5值等作为RowKey来实现。比如对UID取Hash值,或使用UID的MD5值前6位等。从而将RowKey均为分布到HBase集群中。
3.3 二级索引
实际应用中只根据RowKey进行数据索引有时候无法满足要求,往往需要对数据进行二次索引,二级索引的实现有多种方式,这里我们简单讲一下使用Client-Managed方式,即由Client维护二级索引。
假设存在一张表table1,其中RowKey为uid,ColumnFamily为attr,数据为用户的基本属性province、age、gender等。具体需求除了根据uid查找用户属性外,还需要根据省份、年龄等统计用户的省份分布、年龄分布等。针对第二个需求,如果没有二级索引,我们需要遍历整张table1表,提取每行的省份、年龄等数据,并进行统计,一般使用MapReduce完成,整个统计过程较慢。
另一种方式,我们可以建立二级索引来实现,根据属性表,采用反向索引,建立索引表table2,其中RowKey为province_age,ColumnFamily为user,Column为具体uid,Value为1。当我们想要统计辽宁省20岁的用户时,我们可以根据RowKey:辽宁省_20,在table2中索引,得到该行中user列簇下的所有列,即为辽宁省20岁的所有用户uid列表。同时,我们还可以根据结果中的特定uid,在用户属性表table1中查询该uid的具体用户属性,完成二级索引的过程。这种二级索引的方式,可以大大加快数据统计的过程。
4. ColumnFamily设计
4.1 列簇长度
在HFile中DataBlock的KeyValue结构中除了保存Value值之外,还保存有RowKey、ColumnFamily和ColumnQuantifier,因此在设计RowKey和ColumnFamily的长度在保证可读性的前提下要尽量做到最短,特别是ColumnFamily的长度要尽可能的短,如’cf’等。
4.2 列簇数量
ColumnFamily的数量建议是越少越好,建议1至2个,3个及3个以上HBase的性能会出现下降。因为每个Region中的每个Store都对应一个ColumnFamily。而MemStore的flush溢出磁盘操作和对HFile的Compaction合并操作是基于Region来操作的。即若设置为5个ColumFamily,Region中会出现5个Store,其中任何一个Store的MemStore满,都会导致所有5个Store一起进行flush操作。任何一个Sotre中的HFile数量满足Compaction条件,所有5个Store的HFile都会进行Compaction操作。
另一方面,假如表Table1有两个ColumnFamily,分别为CF1和CF2,其中CF1的有100万行,CF2有1亿行,则受CF2数量大的影响,CF1中的数据也会分布在多个Region中,因此当对CF1进行Scan操作时,性能会很差。
因此在进行表结构设计时,ColumnFamily的数量越少越好,1个最佳。
4.3 BLOCKSIZE
BlockSize决定了HFile中DataBlock数据块的大小,默认64KB,实际使用中需要根据具体应用场景进行设置。DataBlock越小,IndexBlock就越大,加载HFile时会占用更多的内存空间,但其随机查找性能会更好。若应用场景中多为顺序Scan操作,则一次读取更多的数据到内存中更为合理,这时DataBlock要设大一些。
4.4 VERSIONS
HBase中每个单元格都有版本号,多以时间戳作为版本号,默认情况下,每个单元格只有3个时间版本。HBase中不存在真正的更新操作,更新操作实际上是对单元格增加一个新的版本。
如果我们只需要一个版本,可以将列簇的VERSIONS设为1。
4.5 COMPRESSION
HFile可以压缩后存放在HDFS中,节约存储空间,但是以牺牲CPU和读写时间为代价的,因此我们需要选取压缩比高且压缩、解压缩速度快的压缩方式。
HBase支持LZO、SNAPPY和GZIP等压缩方式,建议使用SNAPPY方式。
4.6 BLOCKCACHE
Blockcache为读缓存,默认开启,当应用场景中很少进行读数据或多为顺序读时,可以考虑关闭BlockCache。当应用场景为随机读数据时,BlockCache可以将热点数据放入BlockCache中,当再读取该数据时可以避免从HFile中查询,而是从BlockCache中直接读取,大大加快了读取速度。
4.7 BLOOMFILTER
BloomFilter,分为None/Row/Row-Col三种,当BloomFilter开启时,查询数据的过程中可以利用BloomFilter快速的过滤掉不包含所需数据的HFile文件,可以大大提升读取速度。当数据查询多以Row为索引时,BloomFilter可以设置为Row,当数据查询多以Row-Column为索引时,BloomFilter可以设置为Row-Col,这时HFile占用的存储空间会大一些。因为BloomFilter的信息是保存在HFile文件中的。
建议开启BLOOMFILTER。
4.8 TTL
TTL为生成时间,即单元格中数据的保存时间,单位为s,当TTL设为18000s,列簇中超过18000s的数据将会在下次Major-Compaction时被删除。
4.9 IN_MEMORY
IN_MEMORY默认为false,当设为true时,即指定该列簇中的数据保存在内存中,而非HFile中。该配置一般适用于表特别小且频繁查询的场景,比如可以考虑将元数据表的列簇IN_MEMORY设为ture。