Hbase热点问题

@Author  : Spinach | GHB
@Link    : http://blog.csdn.net/bocai8058

Hbase结构及rowkey

Hbase结构

Hbase的表组成:一个表可以理解成是行的集合,行(记录)是列族的集合,列族是列的集合。

  1. 列族column family:它是column的集合,在创建表的时候就指定,不能频繁修改。值得注意的是,列族的数量越少越好,因为过多的列族相互之间会影响,生产环境中的列族一般是一个到两个。

数据的持久化文件HFile中是按照Key-Value存储的,同一个列族的所有列存储在同一个底层存储文件里。Hbase的数据在HDFS中的路径结构如下:

hdfs://h201:8020/hbase/data/${名字空间}/${表名}/${区域名称}/${列族名称}/${文件名}
# 举例:/hbase/data/ns1/t1/a4d63a61a8da24a863bff3c8d7cd20de/f1/c2a7fa8c41304b9e9b8b24b4a89171ce

其中{区域名称}是t1的region, 由每张表切割形成,一张表由若干个region组成,不同的region分到不同的region server以便均衡负载
  1. 列column:和列族的限制数量不同,列族可以包含很多个列,前面说的“几十亿行*百万列”就是这个意思。
  2. 列的值value:存在单元格(cell)中。每一列的值允许有多个版本,由timestamp来区分不同版本。多个版本产生原因:向同一行下面的同一个列多次插入数据,每插入一次就有一个对应版本的value。
# 从以下示例中可以看出habse存储的数据格式
# hbase(main):010:0>scan 'ns1:t1',{STARTROW => 'row1', LIMIT => 5}
ROW                           COLUMN+CELL                           
row10                         column=f1:age,timestamp=1490608685532,value=\x00\x00\x00\x0A
row10                         column=f1:id,timestamp=1490608685532,value=\x00\x00\x00\x0A
row10                         column=f1:name,timestamp=1490608685532,value=tonykidkid10
row100                        column=f1:age,timestamp=1490608685532,value=\x00\x00\x00\x00
row100                        column=f1:id,timestamp=1490608685532,value=\x00\x00\x00d
row100                        column=f1:name,timestamp=1490608685532,value=tonykidkid100
row1000                       column=f1:age,timestamp=1490608685532,value=\x00\x00\x00\x00
row1000                       column=f1:id,timestamp=1490608685532,value=\x00\x00\x03\xE8
row1000                       column=f1:name,timestamp=1490608685532,value=tonykidkid1000 
row1001                       column=f1:age,timestamp=1490608685532,value=\x00\x00\x00\x01
row1001                       column=f1:id,timestamp=1490608685532,value=\x00\x00\x03\xE9
row1001                       column=f1:name,timestamp=1490608685532,value=tonykidkid1001 
row1002                       column=f1:age,timestamp=1490608685532,value=\x00\x00\x00\x02
row1002                       column=f1:id,timestamp=1490608685532,value=\x00\x00\x03\xEA
row1002                       column=f1:name,timestamp=1490608685532,value=tonykidkid1002
5 row(s) in0.0550 seconds

以row1002这一条记录来说明——

row1002是row key   .row key在hbase里是唯一的,而且只出现一次,否则就是在更新同一行。
也就是说有几个不同的row key就有几条不同的记录。
我们可以通过不同的行健来增加多行记录。行健的唯一性这个特性类似于关系型数据库的主键。

column=f1:name, timestamp=1490608685532,value=tonykidkid1002
// 表示列族f1包含name列,“列族+列名”决定了不同的列。
// Timestamp是时间戳,表示此列对应值的版本,默认VERSIONS=1,value就是列族f1下name的值了。

需要明确的一点,hbase是通过3个维度来对记录进行快速定位:行健 + (列族+列名) + 时间戳,即:
            row key àcolumn family + qualifier à timestamp

结合上面的例子,t1表的每一行有3个column, 分别是age,id, name.
比如我想查rowkey为row1002的name的值,命令行下的查询语法:

# hbase(main):021:0>get 'ns1:t1', 'row1002' ,'f1:name'
COLUMN                 CELL                                 
f1:name               timestamp=1490608685532,value=tonykidkid1002 
1 row(s) in0.0420 seconds

# 或者这样查也是对的:
# hbase(main):022:0>get 'ns1:t1', 'row1002' , {COLUMN => 'f1:name'}
COLUMN                 CELL                                         
f1:name               timestamp=1490608685532,value=tonykidkid1002  
1 row(s) in0.0300 seconds

行健RowKey

HBase是采用Key-Value格式来存储数据,那么Row key就是Key-Value中的Key。

Rowkey是表记录在hbase表中的唯一标识,作为检索表记录的唯一“主键”。hbase加载数据时,也是根据row key的二进制顺序由小到大进行的。

Row key的最大长度为64KB,它是一段二进制码流(byte[ ]),所以任何数据类型都可以用来做row key,内容可以由我们用户自定义、自设计。

HBase根据Row key来进行检索,系统通过找到某个Row key (或者某个 Row key 范围)所在的Region,然后将查询数据的请求路由到该Region获取此条记录。HBase的检索有3种方式:

  • 通过get方式,指定rowkey获取唯一一条记录
  • 通过scan方式,设置起始行和结束行参数进行范围匹配
  • 全表扫描,即直接扫描整张表中所有行记录

row key按照字典顺序排序的规则:在字典顺序中按照二进制逐个字节、从左到右对比每一个row key,例如row1001小于row1002,rowxxa小于rowxxb等等。这种设计优化了scan,可以将相关的行以及会被一起读取的行存在相近位置,便于scan。

热点/数据倾斜问题

热点:检索habse的记录首先要通过row key来定位数据行。当大量的client访问hbase集群的一个或少数几个节点,造成少数region server的读/写请求过多、负载过大,而其他region server负载却很小,就造成了“热点”现象。

数据倾斜:Hbase可以被划分为多个Region,但是默认创建时只有一个Region分布在集群的一个节点上,数据一开始时都集中在这个Region,也就是集中在这一个节点上,就算region存储达到临界值时被划分,数据也是存储在少数节点上。这就是数据倾斜。

HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。

rowkey设计是热点的源头。

HBase中,表会被划分为1…n个Region,被托管在RegionServer中。Region有二个重要的属性:StartKey与EndKey表示这个Region维护的rowKey范围,当我们要读/写数据时,如果rowKey落在某个start-end key范围内,那么就会定位到目标region并且读/写到相关的数据。

默认的情况下,创建一张表是,只有1个region,start-end key没有边界,所有数据都在这个region里装,然而,当数据越来越多,region的size越来越大时,大到一定的阀值,hbase认为再往这个region里塞数据已经不合适了,就会找到一个midKey将region一分为二,成为2个region,这个过程称为分裂(region-split)。而midKey则为这二个region的临界(这个中间值这里不作讨论是如何被选取的)。

此时,我们假设假设rowkey小于midKey则为会被塞到1区,大于等于midKey则会被塞到2区,如果rowkey还是顺序增大的,那数据就总会往2区里面写数据,而1区现在处于一个被冷落的状态,而且是半满的。2区的数据满了会被再次分裂成2个区,如此不断产生被冷落而且不满的Region,当然,这些region有提供数据查询的功能。

这种设计是分布式系统一个很大的弊端,而且这样导致数据倾斜和热点问题,从而导致集群的资源得不到很好的利用。

数据倾斜的解决方法

  • 预分区

预分区,让表的数据可以均衡的分散在集群中,而不是默认只有一个region分布在集群的一个节点上。(预分区个数=节点的倍数,看数据量估算,region不足了会被分列,预分区后每个region的rowkey还是有序的)

一个RegionServer能管理10-1000个Region,0.92.x版本后,默认的Region大小为10G,向下可以支持256MB,向上可以支持到20G,也就是说,每个RegionServer能管理的数据量为2.5GB-20TB。

如果有5个节点,3年内数据量为5T,那么分区数可以预设为:
5000G/10G=500个region

这500个Region就会被均衡的分布在集群各个节点上(具体分布看机器的性能和存储空间而定),机器硬盘不足可以添加硬盘,性能不足可以添加新节点(添加新机器)。

  • 加盐

这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。给多少个前缀?这个数量应该和我们想要分散数据到不同的region的数量一致(类似hive里面的分桶)。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。

  • 哈希

哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据。

  • 反转

第三种防止热点的方法是反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。

反转rowkey的例子:以手机号为rowkey,可以将手机号反转后的字符串作为rowkey,从而避免诸如139、158之类的固定号码开头导致的热点问题。

  • 时间戳反转

一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用Long.Max_Value - timestamp追加到key的末尾,例如[key][reverse_timestamp] ,[key] 的最新值可以通过scan [key]获得[key]的第一条记录,因为HBase中rowkey是有序的,第一条记录是最后录入的数据。

整个rowkey(timestamp并不是必要的,视业务而定)
rowkey=哈希(主键<递增的id\手机号码等>)+Long.Max_Value - timestamp

rowkey设计原则

  • rowkey唯一原则

必须在设计上保证其唯一性,rowkey是按照二进制字节数组排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。所以设计rwo key时尽量把体现业务特征的信息、业务上有唯一性的信息编进row key。

  • rowkey长度原则

rowkey是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为10-100byte,以byte[] 形式保存,一般设计成定长。建议越短越好,不要超过16个字节,2个原因:

  1. 原因1:数据的持久化文件HFile中是按照(Key,Value)存储的,如果rowkey过长,例如超过100byte,那么1000万行的记录计算,仅row key就需占用100*1000万=10亿byte,近1Gb。这样会极大影响HFile的存储效率!

  2. 原因2:MemStore将缓存部分数据到内存,若 rowkey字段过长,内存的有效利用率就会降低,就不能缓存更多的数据,从而降低检索效率。

目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。

  • rowkey散列原则

如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer上,这样在数据检索的时候负载会集中在个别的RegionServer上,造成热点问题,会降低查询效率。

预分区splitkeys选取

  1. 取样,先随机生成一定数量的rowkey(10万、100万),将取样数据按升序排序放到一个集合里。
  2. 根据预分区的region个数,对整个集合平均分割,即是相关的splitkeys。
  3. HBaseAdmin.createTable(HTableDescriptor tableDescriptor,byte[][] splitkeys)可以指定预分区的splitkey,即指定region间的rowkey临界值。

Column Family列族的设计数量不宜过多(建议不设置多个)

  1. 这里必须先知道Hbase的架构设计
    HBase的表是由一到多个Region组成的;
    Region是由一到多个HStore组成的,HStore对应列族,也就是表中有多个CF,就会有多个个HStore;而分列的时候是根据Region的大小切分的。
  2. 现在已经知道必须要先做预分区和key的散列了,那么,假设表中有多个列族,也就是多个CF,对应也就有多个HStore,而此时,假设多个列族的数据分配不均衡就会出现下面情况:
    如果某个Region里面的A HStore有1000万条数据,而B HStore里面只有100条数据。那么,这100条数据会被分到多个region中,读取B HStore的数据时,跨了多个region,导致查询效率降低。
  3. Hbase的列族设计是为了加快读取速度的,同一个表的数据,按列族把数据划分后,数据查询时能缩小数据的范围(查询数据时指定列族),查询效率会加快,然而,如果数据分配不均衡就会导致效率降低,所以并不建议多个列族,可以建多个表,数据量小的表Region数量也可以设置小一点。

一对多设计和宽表

假设,现在有用户表和银行卡表,一个用户对应多张银行卡
传统的关系型数据(RMDB),我们会设计成两张表,通过关联查询获取数据;
如果Hbase也设计成两张表,那么如果想获取用户和银行卡的数据,就得查询两次才能获取到数据。
如果设计成一张宽表,把用户数据放到银行卡的表上,也就是用户的数据被存放了多次,但是获取数据的时候只需要查询一次就能把用户和用户银行卡的数据查询出来。

宽表的缺点:浪费存储空间,如果修改用户数据,那么是覆盖多条数据,操作繁琐,但是并不影响性能。

宽表的优点:查询效率提高。

两种设计都有优点和缺点,浪费性能还是浪费存储空间,这需要视具体情况而定,需要作出取舍。

引用:https://blog.csdn.net/qq_31598113/article/details/71278857 | https://blog.csdn.net/weixin_41279060/article/details/78855679 | https://gitee.com/boat824109722/hbase-api-demo


发布了53 篇原创文章 · 获赞 23 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/bocai8058/article/details/82956638