文章目录
- 一、ROWKEY的设计原则
- 1.1、热点问题
- 1.2、rowkey的重要性
- 1.3、rowkey的设计原则
- 1.4、案例演示:多条件的rowkey设计:
- 1.5、针对事务数据Rowkey设计
- 1.6、针对统计数据的Rowkey设计
- 1.7、针对通用数据的Rowkey设计
- 二、Hbase的二级索引
- 三、Hbase的协处理器
- 3.1、协处理器的引入
- 3.2、协处理器的分类
- 3.3、协处理器的应用:二级索引表的创建
- 3.4、协处理器+二级索引案例
- 四、Hbase的优化参数
- 4.1、服务端(hbase-site.xml中设置)
- 4.2、jvm和垃圾收集参数
- 4.3、客户端
- 4.4、zookeeper调优
- 4.5、其他
- 五、总结
一、ROWKEY的设计原则
1.1、热点问题
当大量的客户端访问请求集中到一个机器或者是某几个regionserver上,造成regionserver超负载运行,严重影响了查询性能,会导致卡顿甚至宕机。这种情况就是数据热点问题。
如何避免数据热点问题???
通过设计rowkey的方案将数据散列到不同的region里存储
1.2、rowkey的重要性
– Hbase的特点之一,就是高性能的读写,因此企业中的数据仓库中应用hbase作为存储软件比较多。
– hbase的高性能除了体现在维护内存外,还有就是通过rowkey可以避免全表扫描,加快查询效率。
如上需求,rowkey的设计在hbase中非常重要。
因此,在插入某些数据之前,应该先思考未来要经常进行什么条件的查询,比如可能要经常做按照部门,日期,地域性等查询。那么就可以将这些信息维护到rowkey上。
1.3、rowkey的设计原则
1、唯一性原则
如果rowkey不一致,并且version设置为1个的话,后面添加的相同rowkey的数据会覆盖之前的数据
2、长度统一原则
因为rowkey是按照ascii码进行排序的,所以长度不统一查询时会查到多余的数据
3、散列原则
为了避免热点问题(热点问题:就是大量客户端访问一个或者仅仅几个regionserver,造成regionserver负载太大导致卡顿甚至宕机)
1)reverse反转(将原来的rowkey反过来)
2)加盐(随机生成字符串加载rowkey前面)
3)hashcode或求模
通过一个固定的算法计算hash值获得一个字符串加载rowkey前面
如果前缀是纯数字的可以使用求模方法
- 唯一性原则
rowkey的设计要满足唯一性,如果不唯一,可能会出现单元格的覆盖,比如张三有自己的rowkey,另外
一个人的rowkey与张三相同,那么另外一个人的数据就会把张三的数据覆盖掉 - 长度统一原则
hbase中的表的rowkey的长度应该统一。如果不统一,可能会出现在查询时,数据包含了我们不想要的数据。
下面举例说明:
– 假如现在有5个Rowkey,分别是:“012”, “0”, “123”, “234”, “3”,“0555”。ascii码 排序后是如下顺序:
“0”
“012”
“0555”
“123”
“234”
“3”
需求如果是查询rowkey从0到123,想要的3没有被查询出来,"0555"却被查询出来
如果统一,这五个rowkey设置统一4个长度,应该如下:
“0000”
“0003”
“0012”
“0123”
“0234”
“0555”
建议:rowkey的长度是8字节的整数倍,比如16字节,最好在10~100字节之间,不过,在满足记录数的前提下,越短越好 - 散列原则:(为了避免热点问题)
假如普通的rowkey 如下
region:
1231450 1231450-xxxxx
1231456
1231457
1231458
1231459
当大量访问1231450以后的数据,可能会出现热点问题,都访问这一个region.
方法1:reverse反转
将普通rowkey反转后当成新的rowkey
6541321
7541321
8541321
9541321
上述设计,就可以将本来应该存储到一个reigon里的数据散列到不同的region里。避免了热点问题
方法2:salting(加盐): 为普通rowkey 加前缀,比如增加1个字符,或者是相同长度的n个字符。
aaaa-1231458
afge-1231456
ffab-1231459
gefa-1231457
缺点明显:由于前缀是随机的,因此某一个普通rowkey到底存入到哪一个region中不可控。
(,a],[a,b),[b,c),[c,)
方法3:hash算法或者是纯数字的mod
为了弥补加盐的缺陷。可以使用固定算法,比如hashcode,md5等,算出普通字段的对应的值,然后取出前几位
作为前缀添加到普通rowkey前。这个好处是可以提前推断我们要查询的数据是不是在一个region中。
纯数字的普通rowkey,可以使用取模的的值作为前缀添加到rowkey上。
1.4、案例演示:多条件的rowkey设计:
1)提示说明
HBase按指定的条件获取一批记录时,使用的就是scan方法。 scan方法有以下特点:
-1. scan可以通过setCaching与setBatch方法提高速度。(以空间换时间)
-2. scan可以通过setStartRow与setEndRow来限定范围。(范围越小,性能越高)
通过巧妙的RowKey设计使我们批量获取记录集合中的元素挨在一起(应该在同一个Region下),可以在遍历结果时获得很好的性能。
-3. scan可以通过setFilter方法添加过滤器,这也是分页、多条件查询的基础。
因此,在满足长度、散列、唯一原则后,我们只需要考虑如何通过巧妙设计RowKey来利用scan方法的范围功能,使得获取一批记录的查询速度能提高。
2)案例需求
下例就描述如何将多个列组合成一个RowKey,使用scan的range来达到较快查询速度。
以图为例,我们在表中存储的是文件信息,每个文件有5个属性:文件id(long,全局唯一)、创建时间(long)、文件名(String)、分类名(String)、所有者(User)。
这里UserID对应另一张User表,暂不列出。我们只需知道UserID的含义。
1代表 浙江卫视;
2代表 好声音剧组;
3代表 XX微博;
4代表 赞助商。
我们的需求是按照以下条件进行查询:
-1. 文件创建时间区间 (比如从20120901到20120914期间创建的文件)
-2. 文件名(“中国好声音”),
-3. 分类(“综艺”),
-4. 所有者(“浙江卫视”)。
调用查询接口的时候将上述5个条件同时输入到find(20120901,20120914,”中国好声音”,”综艺”,”浙江卫视”)。此时我们应该得到的记录应该有第1、2、3、4、5、7条。第6条由于不属于“浙江卫视”应该不被选中。
3)rowkey设计
我们在设计RowKey时可以这样做:采用 UserID + CreateTime + FileID组成RowKey,这样既能满足多条件查询,又能有很快的查询速度。
需要注意以下几点:
- 每条记录的RowKey,每个字段都需要填充到相同长度。
假如预期我们最多有10万量级的用户,则userID应该统一填充至6位,如000001,000002… - 结尾添加全局唯一的FileID的用意也是使每个文件对应的记录全局唯一。避免当UserID与CreateTime相同时的两个不同文件记录相互覆盖。
按照这种RowKey存储上述文件记录,在HBase表中是下面的结构:
rowKey(userID 6 + time 8 + fileID 6) name category ….
00000120120902000001
00000120120904000002
00000120120906000003
00000120120908000004
00000120120910000005
00000120120914000007
00000220120912000006
00000220120916000008
00000320120918000009
00000420120920000010
4)查询这张表
-1. 在建立一个scan对象后,我们setStartRow(00000120120901),setEndRow(00000120120914)。
这样,scan时只扫描userID=1的数据,且时间范围限定在这个指定的时间段内,满足了按用户以及按时间范围对结果的筛选。并且由于记录集中存储,性能很好。
-2. 然后使用 SingleColumnValueFilter(org.apache.hadoop.hbase.filter.SingleColumnValueFilter),共4个,分别约束name的上下限,与category的上下限。满足按同时按文件名以及分类名的前缀匹配。
(注意:使用SingleColumnValueFilter会影响查询性能,在真正处理海量数据时会消耗很大的资源,且需要较长的时间)
- 如果需要分页还可以再加一个PageFilter限制返回记录的个数。
以上,我们完成了高性能的支持多条件查询的HBase表结构设计。
1.5、针对事务数据Rowkey设计
事务数据是带时间属性的,建议将时间信息存入到Rowkey中,这有助于提示查询检索速度。对于事务数据建议就按天为数据建表,
这样设计的好处是多方面的。按天分表后,时间信息就可以去掉日期部分只保留小时分钟毫秒,这样4个字节即可搞定。加上散列字段
2个字节一共6个字节即可组成唯一 Rowkey。如下图所示:
这样的设计从操作系统内存管理层面无法节省开销,因为64位操作系统是必须8字节对齐。但是对于持久化存储中Rowkey部分可
以节省25%的开销。也许有人要问为什么不将时间字段以主机字节序保存,这样它也可以作为散列字段了。这是因为时间范围内
的数据还是尽量保证连续,相同时间范围内的数据查找的概率很大,对查询检索有好的效果,因此使用独立的散列字段效果更好
,对于某些应用,我们可以考虑利用散列字段全部或者部分来存储某些数据的字段信息,只要保证相同散列值在同一时间(毫秒)
唯一。
1.6、针对统计数据的Rowkey设计
统计数据也是带时间属性的,统计数据最小单位只会到分钟(到秒预统计就没意义了)。同时对于统计数据我们也缺省采用按天
数据分表,这样设计的好处无需多说。按天分表后,时间信息只需要保留小时分钟,那么0~1400只需占用两个字节即可保存时
间信息。由于统计数据某些维度数量非常庞大,因此需要4个字节作为序列字段,因此将散列字段同时作为序列字段使用也是6
个字节组成唯一Rowkey。如下图所示:
同样这样的设计从操作系统内存管理层面无法节省开销,因为64位操作系统是必须8字节对齐。但是对于持久化存储中Rowkey
部分可以节省25%的开销。预统计数据可能涉及到多次反复的重计算要求,需确保作废的数据能有效删除,同时不能影响散列
的均衡效果,因此要特殊处理。
1.7、针对通用数据的Rowkey设计
通用数据采用自增序列作为唯一主键,用户可以选择按天建分表也可以选择单表模式。这种模式需要确保同时多个入库加载模块
运行时散列字段(序列字段)的唯一性。可以考虑给不同的加载模块赋予唯一因子区别。设计结构如下图所示。
- 需要知道数据是什么 ,
- 需要知道要经常查询的操作,定义方法
find(long starttime,long endtime,string filename,string type,int userid) - 设计rowkey
rowkey的形式如下:userid+createtime+fileid
注意:userid要统一长度,fileid要统一长度
userid在前面可以立即限定查询的用户,rowkey的范围作了第一次限定
createtime,又一次缩小了行的范围
fileid,为了区分不同的文件 - 编写方法的具体逻辑
find(long starttime,long endtime,string filename,string type,int userid){
//可以使用scan来限定行范围,rowkey中包含了time,userid
//使用过滤器和比较器来限定filename,type
} - 某一次查询的需求
-1. 文件创建时间区间 (比如从20120901到20120914期间创建的文件)
-2. 文件名(“中国好声音”),
-3. 分类(“综艺”),
-4. 所有者(“浙江卫视”)。
4. 编写方法的具体逻辑
find(long starttime,long endtime,string filename,string type,int userid){
scan.startrow(“00000120120901”);
scan.endrow(“00000120120914”);
SingleColumnValueFilter(,,,"中国好声音")
SingleColumnValueFilter(,,,"综艺")
}
二、Hbase的二级索引
第一张表:
rk00001 filename:中国好声音第一期, time:
rk00002 filename:中国花絮 time: userid:
rk00003 filename:中国好声音第二期 time: userid:
…
rk01001 filename:中国声音第三期
另外一张表:
“中国好声音” filename1: rk00001
filename2: rk00003
filename3: rk01001
“中国花絮” filename1: rk00002
…
…
需求: 查询filename是中国好声音的所有行的time单元格的数据
如果维护了第二章表:
可以现在第二张表中查询rowkey是中国好声音的所有单元格(单元格中维护的是第一张表的有中国好声音的单元格的所有的rowkey)
再使用获取的rowkey去第一张表中,按照rowkey查询指定行中是否有time单元,如果有就返回
什么是二级索引表?
就是在表B中维护另一张表A中的列值与rowkey的映射关系。 在按照指定列查询表A时,先去表B中查询对应的rowkey,然后再返回表A中,通过rowkey查询其他指定字段。表B就是二级索引表,用空间换取时间。
三、Hbase的协处理器
3.1、协处理器的引入
hbase在低版本时,是很难轻易建立二级索引表、实现求和、记数、排序等操作的。所以在高版本中引入了协处理器的技术,来实现上述需求。
3.2、协处理器的分类
分两大类:
1、observer
- 细分子类型
(1)RegionObserver
(2)RegionServerObserver
(3)WALObserver
(4)MasterOberver
- 类似于 RDBMS 中的触发器,主要在服务端工作
- 可以实现权限管理、优先级设置、监控、ddl 控制、二级索引等功能
2、endpoint
- 类似于 RDBMS 中的存储过程,主要在服务端工作
- 可以实现 min、max、avg、sum、distinct、group by 等功能
3.3、协处理器的应用:二级索引表的创建
- 关注表
create ‘guanzhu’,‘f1’
rowkey: 用户名-明星名
column1: from:用户名
to:明星名 - 粉丝表
create ‘fensi’,‘f1’
rowkey: to-明星名
s001:粉丝名
需求:当关注表添加一条记录时,那么粉丝表对应的数据应该自动添加一条信息
- 自定义一个类型,继承BaseRegionObserver类型,重写prePut()方法
- 写好程序,打包,上传到hdfs上
- 将协处理器挂载到关注表中
alter ‘guanzhu’,METHOD => ‘table_att’,‘coprocessor’=>‘hdfs://qianfeng01/jar/mycoprocessor.jar|com.qf.hbase.coprocessor.FensiRegionObserver|1001|’ - 可以使用API向关注表中添加数据
- 查看粉丝表的数据
协处理器的加载方式:
- 静态加载: 在hbase-site.xml文件配置协处理器,全局效果,也就是所有的region都会执行该处理器
- 动态加载: 在hbase shell中为某一个表添加。
注意:卸载时,应该先禁用表,然后执行卸载语句。
动态加载时,最好也先禁用表,在执行加载语句
3.4、协处理器+二级索引案例
- create ‘guanzhu’,‘f1’
rowkey: 用户名-明星名
column1: from:用户名
to:明星名
create ‘fensi’,‘f1’
rowkey: to-明星名
s001:粉丝名 - 需求:在向关注表插入数据时,通过协处理器向粉丝表(二级索引表)插入数据。
- 继承BaseRegionObserver,重写prePut(…)
- 导出jar包,上传到集群上
- 将协处理器挂载到关注表中
alter ‘guanzhu’,METHOD => ‘table_att’,‘coprocessor’=>‘hdfs://qianfeng01:8020/jar/nz201_hbase-1.0-SNAPSHOT.jar|com.qf.hbase.coprocessor.FensiObServer|1001|’
6. 通过api向关注表模拟插入数据操作
7. 查询fensi表的数据。
package com.xx.hbase.coprocessor;
import com.qf.hbase.util.HbaseUtil;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Durability;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver;
import org.apache.hadoop.hbase.coprocessor.ObserverContext;
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.List;
public class FensiObServer extends BaseRegionObserver {
/**
* 监测region的put操作。在执行前会进行拦截
* @param e
* @param put put形参就是拦截到的put对象 put 'guanzhu','wcm-gyy','f1:from','wcm'
* @param edit put 'guanzhu','wcm-gyy','f1:to','gyy'
* @param durability
* @throws IOException put 'fensi','to-gyy','f1:s1','wcm'
*/
@Override
public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability) throws IOException {
//获取指定单元格数据 也就是key-value
List<Cell> c1 = put.get(Bytes.toBytes("f1"), Bytes.toBytes("from"));
//获取value值,获取粉丝名称
String fans = new String(CellUtil.cloneValue(c1.get(0)));
//获取明星名
List<Cell> c2 = put.get(Bytes.toBytes("f1"), Bytes.toBytes("to"));
String star = new String(CellUtil.cloneValue(c2.get(0)));
//将明星名和粉丝名插入到粉丝表中
Table table = HbaseUtil.getTable("fensi");
Put newput = new Put(Bytes.toBytes("to-"+star));
newput.addColumn(Bytes.toBytes("f1"),"s1".getBytes(),fans.getBytes());
table.put(newput);
HbaseUtil.closeTable(table);
}
}
四、Hbase的优化参数
4.1、服务端(hbase-site.xml中设置)
1.hbase.regionserver.handler.count:rpc请求的线程数量,默认值是10,生产环境建议使用100,也不是越大越好,特别是当请求内容很大的时候,比如scan/put几M的数据,会占用过多的内存,有可能导致频繁的GC,甚至出现内存溢出(OOM)。
2.hbase.master.distributed.log.splitting:默认值为true,建议设为false。关闭hbase的分布式日志切割,在log需要replay时,由master来负责重放
3.hbase.regionserver.hlog.splitlog.writer.threads:默认值是3,建议设为10,日志切割所用的线程数
4.hbase.snapshot.enabled:快照功能,默认是false(不开启),建议设为true,特别是对某些关键的表,定时用快照做备份是一个不错的选择。
5.hbase.hregion.max.filesize:默认是10G, 如果任何一个column familiy里的StoreFile超过这个值, 那么这个Region会一分为二,因为region分裂会有短暂的region下线时间(通常在5s以内),为减少对业务端的影响,建议手动定时分裂,可以设置为60G。
6.hbase.hregion.majorcompaction:hbase的region主合并的间隔时间,默认为1天,建议设置为0,禁止自动的major主合并,major合并会把一个store下所有的storefile重写为一个storefile文件,在合并过程中还会把有删除标识的数据删除,在生产集群中,主合并能持续数小时之久,为减少对业务的影响,建议在业务低峰期进行手动或者通过脚本或者api定期进行major合并。
7.hbase.hregion.memstore.flush.size:默认值128M,单位字节,一旦有memstore超过该值将被flush,如果regionserver的jvm内存比较充足(16G以上),可以调整为256M。
8.hbase.hregion.memstore.block.multiplier:默认值2,如果一个memstore的内存大小已经超过hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier,则会阻塞该memstore的写操作,为避免阻塞,建议设置为5,如果太大,则会有OOM的风险。如果在regionserver日志中出现"Blocking updates for ‘’ on region : memstore size <多少M> is >= than blocking <多少M> size"的信息时,说明这个值该调整了。
9.hbase.hstore.compaction.min:默认值为3,如果任何一个store里的storefile总数超过该值,会触发默认的合并操作,可以设置5~8,在手动的定期major compact中进行storefile文件的合并,减少合并的次数,不过这会延长合并的时间,以前的对应参数为hbase.hstore.compactionThreshold。
10.hbase.hstore.compaction.max:默认值为10,一次最多合并多少个storefile,避免OOM。
11.hbase.hstore.blockingStoreFiles:默认为7,如果任何一个store(非.META.表里的store)的storefile的文件数大于该值,则在flush memstore前先进行split或者compact,同时把该region添加到flushQueue,延时刷新,这期间会阻塞写操作直到compact完成或者超过hbase.hstore.blockingWaitTime(默认90s)配置的时间,可以设置为30,避免memstore不及时flush。当regionserver运行日志中出现大量的“Region has too many store files; delaying flush up to 90000ms"时,说明这个值需要调整了
12.hbase.regionserver.global.memstore.upperLimit:默认值0.4,regionserver所有memstore占用内存在总内存中的upper比例,当达到该值,则会从整个regionserver中找出最需要flush的region进行flush,直到总内存比例降到该数以下,采用默认值即可。
13.hbase.regionserver.global.memstore.lowerLimit:默认值0.35,采用默认值即可。
14.hbase.regionserver.thread.compaction.small:默认值为1,regionserver做Minor Compaction时线程池里线程数目,可以设置为5。
15.hbase.regionserver.thread.compaction.large:默认值为1,regionserver做Major Compaction时线程池里线程数目,可以设置为8。
16.hbase.regionserver.lease.period:默认值60000(60s),客户端连接regionserver的租约超时时间,客户端必须在这个时间内汇报,否则则认为客户端已死掉。这个最好根据实际业务情况进行调整
17.hfile.block.cache.size:默认值0.25,regionserver的block cache的内存大小限制,在偏向读的业务中,可以适当调大该值,需要注意的是hbase.regionserver.global.memstore.upperLimit的值和hfile.block.cache.size的值之和必须小于0.8。
18.dfs.socket.timeout:默认值60000(60s),建议根据实际regionserver的日志监控发现了异常进行合理的设置,比如我们设为900000,这个参数的修改需要同时更改hdfs-site.xml
19.dfs.datanode.socket.write.timeout:默认480000(480s),有时regionserver做合并时,可能会出现datanode写超时的情况,480000 millis timeout while waiting for channel to be ready for write,这个参数的修改需要同时更改hdfs-site.xml
4.2、jvm和垃圾收集参数
export HBASE_REGIONSERVER_OPTS=“-Xms36g -Xmx36g -Xmn1g -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=15 -XX:CMSInitiatingOccupancyFraction=70 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/data/logs/gc-$(hostname)-hbase.log”
由于我们服务器内存较大(96G),我们给一部分regionserver的jvm内存开到64G,到现在为止,还没有发生过一次full gc,hbase在内存使用控制方面确实下了不少功夫,比如各种blockcache的实现,可以看源码。
4.3、客户端
1.hbase.client.write.buffer:默认为2M,写缓存大小,推荐设置为5M,单位是字节,当然越大占用的内存越多,此外测试过设为10M下的入库性能,反而没有5M好
2.hbase.client.pause:默认是1000(1s),如果你希望低延时的读或者写,建议设为200,这个值通常用于失败重试,region寻找等
3.hbase.client.retries.number:默认值是10,客户端最多重试次数,可以设为11,结合上面的参数,共重试时间71s
4.hbase.ipc.client.tcpnodelay:默认是false,建议设为true,关闭消息缓冲
5.hbase.client.scanner.caching:scan缓存,默认为1,避免占用过多的client和rs的内存,一般1000以内合理,如果一条数据太大,则应该设置一个较小的值,通常是设置业务需求的一次查询的数据条数
如果是扫描数据对下次查询没有帮助,则可以设置scan的setCacheBlocks为false,避免使用缓存;
6.table用完需关闭,关闭scanner
7.限定扫描范围:指定列簇或者指定要查询的列,指定startRow和endRow
8.使用Filter可大量减少网络消耗
9.通过Java多线程入库和查询,并控制超时时间。后面会共享下我的hbase单机多线程入库的代码
10.建表注意事项:
开启压缩
合理的设计rowkey
进行预分区
开启bloomfilter
4.4、zookeeper调优
1.dfs.name.dir:namenode的数据存放地址,可以配置多个,位于不同的磁盘并配置一个nfs远程文件系统,这样namenode的数据可以有多个备份
2.dfs.namenode.handler.count:namenode节点RPC的处理线程数,默认为10,可以设置为60
3.dfs.datanode.handler.count:datanode节点RPC的处理线程数,默认为3,可以设置为30
4.dfs.datanode.max.xcievers:datanode同时处理文件的上限,默认为256,可以设置为8192
4.5、其他
列族名、column名、rowkey均会存储到hfile中,因此这几项在设计表结构时都尽量短些
regionserver的region数量不要过1000,过多的region会导致产生很多memstore,可能会导致内存溢出,也会增加major compact的耗时
五、总结
1 hbase的概念
2.hbase的表模型(重点)
3.hbase的体系结构(重点)
4.hbase的安装
5.hbase的shell操作(熟悉)
6.hbase的api(重点)
7.hbase的工作机制(重点)
- 寻址机制
- 存储机制
- 读写流程
8.布隆过滤器的原理(重点)
9.rowkey设计原则(重点)
10.二级索引表的概念
11.协处理器(重点)
12.优化参数