栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 前沿技术 > 大数据 > 大数据系统

Hbase的热点问题,为什么不合理的rowkey会导致数据不均 ?

Hbase的热点问题,为什么不合理的rowkey会导致数据不均 ?

1,rowkey的长度原则

                rowkey是一个二进制码流,可以是任意字符串,最大长度64kb,实际应用中一般为10-100bytes,以byte[]形式保存,一般设计成定长。

建议越短越好,不要超过16个字节,原因如下:

  1. 数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;
  2. MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

2,rowkey的散列原则

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

3,rowkey的唯一原则

        必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。

什么是热点?

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

        热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,因为主机无法服务其他region的请求。

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

        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的rowkey还是有序的。

下面是一些常见的避免热点的方法以及它们的优缺点:



1 加盐

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



2 哈希

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



3 反转

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

反转rowkey的例子以手机号为rowkey,可以将手机号反转后的字符串作为rowkey,这样的就避免了以手机号那样比较固定开头导致热点问题



3 时间戳反转

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

其他一些建议:

尽量减少行键和列族的大小在Hbase中,value永远和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,这个时候它们将会占用大量的存储空间。

列族尽可能越短越好,最好是一个字符。

冗长的属性名虽然可读性好,但是更短的属性名存储在Hbase中会更好。

Hbase的基本架构

主从的架构,Hbase强依赖zookeeper和hadoop

主节点:HMaster , 负责维护管理整个Hbase集。由HMaster把分裂出来的region分配给不同的服务器,实现负载均衡。

从节点:HRegionServer 存储数据,存储集群当中的数据。当region的大小达到一定阈值时(根据storeFile的大小来判定),会split操作,把region分割成两个,并由HMaster进行负载均衡。

数据是以HFlie的形式存在HDFS中

最终是以key-value的形式存储。

 开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了(没有类型,全部都是字节码形式存储)。

Hbase的插入和删除等操作都是新插入一条数据,Key Type标识操作类型,TimeStamp更新为最新的,这样下次读取时就能读到正确的值。在compact时会合并这些修改。

JavaAPI:

Cell中包含rowKey, 列族,列名,列值等所有信息

KeyValue是Cell的实现类,所有数据都装在一个字节数组里面

protected byte [] bytes = null;  // an immutable byte array that contains the KV
protected int offset = 0;  // offset into bytes buffer KV starts at
protected int length = 0;  // length of the KV starting from offset.

通过bytes与offset的组合可以取到rowKey, 列族,列名,列值等数据。

例如,取keyLength,长度固定为4B。则从byte[]数组bytes中位置offset开始,获取一个int,也就是4B。源码片段如下:

Bytes.toInt(this.bytes, this.offset);

取ValueLength也是一样,只不过offset要加4,即 Bytes.toInt(this.bytes, this.offset + 4);

RowLength的长度只有2B,应从bytes下标为8的位置取2个byte,返回一个长度为2的字节数组({bytes[8],bytes[9]})。

源码片段:

Bytes.toShort(this.bytes, getKeyOffset());

  
  public int getKeyOffset() {
    return this.offset + ROW_OFFSET;
  }

  // How far into the key the row starts at. First thing to read is the short
  // that says how long the row is.
  public static final int ROW_OFFSET =
    Bytes.SIZEOF_INT  +
    Bytes.SIZEOF_INT ;

// Bytes.SIZEOF_INT 即 java中int类型的字节数(4)

当RowLength的值获得后,就可以得到rowKey的长度,而起始下标也可得出为4+4+2=10,从bytes中下标为10的位置开始,连续取RowLength - 1个元素所组成的字节数组即为RowKey的字节数组。

依此类推,列族,列名,列值等数据都可取得。注意,因为keyLength的值已得出,列的长度可以倒推出,所以Hbase的Cell就没有额外保存 ‘列长度’ 这个信息。

常用API:


@Test
public void searchData() throws IOException {
    Get get = new Get(Bytes.toBytes("0003"));
    Result result = myuser.get(get);
    //默认获取的就是最近一个版本的cell
    Cell[] cells = result.rawCells();
    printValue(cells);

}
private void printValue(Cell[] cells) throws IOException {
    //获取所有列的值
        for (Cell cell:cells){
        //获取单元格当中的值
            byte[] value = CellUtil.clonevalue(cell);
        //获取数据值是在哪一个列里面
            byte[] bytes = CellUtil.cloneQualifier(cell);
        //列名
            String cloumnName = new String(bytes);
        //列族名
            byte[] family = CellUtil.cloneFamily(cell);
         //rowKey
            byte[] rowKey = CellUtil.cloneRow(cell);
            System.out.println(new String(rowKey) + ":"+new String(family) + ":"+cloumnName);

        if ("age".equals(cloumnName) || "id".equals(cloumnName)){
            System.out.println(Bytes.toInt(value));
        }else{
            //注意如果使用new String的方式打印数据,只能打印字符串的数据
            System.out.println(new String(value));
        }

    }
    myuser.close();
}

@Test
public void scanRowKey() throws IOException {
    Scan scan = new Scan();
    scan.setStartRow("0004".getBytes());
    scan.setStopRow("0006".getBytes());
    ResultScanner resultScanner = myuser.getScanner(scan);
    for (Result result:resultScanner){
        //获取rowKey
        System.out.println(Bytes.toString(result.getRow()));
        //遍历获取所有得到的列族以及所有的列名
        Cell[] cells = result.rawCells();
        printValue(cells);
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/423234.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号