• HBase rowkey与预分区知识点


    一、引言

    HBase其存储和读写的高性能,作为Nosql数据库的一员,HBase查询只能通过其Rowkey来查询(Rowkey用来表示唯一一行记录),Rowkey设计的优劣直接影响读写性能。HBase中的数据是按照Rowkey的ASCII字典顺序进行全局排序的,有伙伴可能对ASCII字典序印象不够深刻,下面举例说明:

    假如有5个Rowkey:"012", "0", "123", "234", "3",按ASCII字典排序后的结果为:"0", "012", "123", "234", "3"。(注:文末附常用ASCII码表)

    Rowkey排序时会先比对两个Rowkey的第一个字节,如果相同,然后会比对第二个字节,依次类推...  对比到第X个字节时,已经超出了其中一个Rowkey的长度,短的Rowkey排在前面。

    由于HBase是通过Rowkey查询的,一般Rowkey上都会存一些比较关键的检索信息,我们需要提前想好数据具体需要如何查询,根据查询方式进行数据存储格式的设计,要避免做全表扫描,因为效率特别低。

    二、Rowkey设计原则

    Rowkey设计应遵循以下原则:

    1.Rowkey的唯一原则

        必须在设计上保证其唯一性。由于在HBase中数据存储是Key-Value形式,若HBase中同一表插入相同Rowkey,则原先的数据会被覆盖掉(如果表的version设置为1的话),所以务必保证Rowkey的唯一性

    2. Rowkey的排序原则

        HBase的Rowkey是按照ASCII有序设计的,我们在设计Rowkey时要充分利用这点。比如视频网站上对影片《泰坦尼克号》的弹幕信息,这个弹幕是按照时间倒排序展示视频里,这个时候我们设计的Rowkey要和时间顺序相关。可以使用"Long.MAX_VALUE - 弹幕发表时间"的 long 值作为 Rowkey 的前缀

    3. Rowkey的散列原则

        我们设计的Rowkey应均匀的分布在各个HBase节点上。拿常见的时间戳举例,假如Rowkey是按系统时间戳的方式递增,Rowkey的第一部分如果是时间戳信息的话将造成所有新数据都在一个RegionServer上堆积的热点现象,也就是通常说的Region热点问题, 热点发生在大量的client直接访问集中在个别RegionServer上(访问可能是读,写或者其他操作),导致单个RegionServer机器自身负载过高,引起性能下降甚至Region不可用,常见的是发生jvm full gc或者显示region too busy异常情况,当然这也会影响同一个RegionServer上的其他Region。

    通常有3种办法来解决这个Region热点问题:

       ΩΩ1、Reverse反转

    针对固定长度的Rowkey反转后存储,这样可以使Rowkey中经常改变的部分放在最前面,可以有效的随机Rowkey。

    反转Rowkey的例子通常以手机举例,可以将手机号反转后的字符串作为Rowkey,这样的就避免了以手机号那样比较固定开头(137x、15x等)导致热点问题,

    这样做的缺点是牺牲了Rowkey的有序性。

        ΩΩ2、Salt加盐

    Salting是将每一个Rowkey加一个前缀,前缀使用一些随机字符,使得数据分散在多个不同的Region,达到Region负载均衡的目标。

    比如在一个有4个Region(注:以 [ ,a)、[a,b)、[b,c)、[c, )为Region起至)的HBase表中,

    加Salt前的Rowkey:abc001、abc002、abc003

    我们分别加上a、b、c前缀,加Salt后Rowkey为:a-abc001、b-abc002、c-abc003 

    可以看到,加盐前的Rowkey默认会在第2个region中,加盐后的Rowkey数据会分布在3个region中,理论上处理后的吞吐量应是之前的3倍。由于前缀是随机的,读这些数据时需要耗费更多的时间,所以Salt增加了写操作的吞吐量,不过缺点是同时增加了读操作的开销。

    ΩΩ3、Hash散列或者Mod

    用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推断。确定性Hash(比如md5后取前4位做前缀)能让客户端重建完整的RowKey,可以使用get操作直接get想要的行。

    例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下

    9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0)

    7006-abc002

    95e6-abc003

    若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,这种设计会使得分区之间更加均衡。

    如果Rowkey是数字类型的,也可以考虑Mod方法。

    4. Rowkey的长度原则

      Rowkey长度设计原则:Rowkey是一个二进制,Rowkey的长度被很多开发者建议说设计在10~100个字节,建议是越短越好。

    原因有两点:

    其一是HBase的持久化文件HFile是按照KeyValue存储的,如果Rowkey过长比如500个字节,1000万列数据光Rowkey就要占用500*1000万=50亿个字节,将近1G数据,这会极大影响HFile的存储效率

    其二是MemStore缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统无法缓存更多的数据,这会降低检索效率

    需要指出的是不仅Rowkey的长度是越短越好,而且列族名、列名等尽量使用短名字,因为HBase属于列式数据库,这些名字都是会写入到HBase的持久化文件HFile中去,过长的Rowkey、列族、列名都会导致整体的存储量成倍增加。

    三、HBase Rowkey设计实战

    在实际的设计中我们可能更多的是结合多种设计方法来实现Rowkey的最优化设计,比如设计订单状态表时使用:Rowkey: reverse(order_id) + (Long.MAX_VALUE – timestamp),这样设计的好处一是通过reverse订单号避免Region热点,二是可以按时间倒排显示。

    使用HBase作为事件(事件指的的终端在APP中发生的行为,比如登录、下单等等统称事件(event))的临时存储(HBase只存储了最近10分钟的热数据)来举例:

    设计event事件的Rowkey为:两位随机数Salt + eventId + Date + kafka的Offset

    这样设计的好处是:

    设计加盐的目的是为了增加查询的并发性,假如Salt的范围是0~n,那我们在查询的时候,可以将数据分为n个split同时做scan操作。经过我们的多次测试验证,增加并发度能够将整体的查询速度提升5~20倍以上。随后的eventId和Date是用来做范围Scan使用的。在我们的查询场景中,大部分都是指定了eventId的,因此我们把eventId放在了第二个位置上,同时呢,eventId的取值有几十个,通过Salt + eventId的方式可以保证不会形成热点。在单机部署版本中,HBase会存储所有的event数据,所以我们把date放在rowkey的第三个位置上以实现按date做scan,批量Scan性能甚至可以做到毫秒级返回。

    这样的rowkey设计能够很好的支持如下几个查询场景:

    1、全表scan

    在这种情况下,我们仍然可以将全表数据切分成n份并发查询,从而实现查询的实时响应。

    2、只按照event_id查询

    3、按照event_id和date查询

    使用HBase做用户画像的标签存储方案,存储每个app的用户的人口学属性和商业属性等标签信息,由于其设计的更为复杂,后续会另起篇幅详细展开。

    最后我们顺带提下HBase的表设计,HBase表设计通常可以是宽表(wide table)模式,即一行包括很多列。同样的信息也可以用高表(tall table)形式存储,通常高表的性能比宽表要高出 50%以上,所以推荐大家使用高表来完成表设计。表设计时,我们也应该要考虑HBase数据库的一些特性:

           1、在HBase表中是通过Rowkey的字典序来进行数据排序的

           2、所有存储在HBase表中的数据都是二进制的字节

           3、原子性只在行内保证,HBase不支持跨行事务

           4、列族(Column Family)在表创建之前就要定义好

           5.  列族中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加

    四、总结

    在做Rowkey设计时,请先考虑业务是读比写多、还是读比写少,HBase本身是为写优化的,即便是这样,也可能会出现热点问题,而如果我们读比较多的话,除了考虑以上Rowkey设计原则外,还可以考虑HBase的Coprocessor甚至elastic search结合的方法,无论哪种方式,都建议做实际业务场景下数据的压力测试以得到最优结果。

    一.预分区
    默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候, 所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。 但是region切分是非消耗IO资源的一种操作,对我们写入的速度肯定会产生影响,一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入 HBase时,会按照region分区情况,在集群内做数据的负载均衡。

    那么怎么区实现预分区呢?

    hbase中
    假设不进行预分区,最终region的数量为10,那么我们进行预分区的数量就应该是分10块

    比如我这里的一组数据,他的rowkey是连续的1500100001-1500101000

    一共1000条数据,分10个区

    首先创建一个分区信息文件 split.txt,内容如下:

    1500100100|
    1500100200|
    1500100300|
    1500100400|
    1500100500|
    1500100600|
    1500100700|
    1500100800|
    1500100900|
    为什么分区后面写'|','|'的ascll码是最大 124,所以小于等于'|'前面的内容都会被分配在切分的左边

    1500100100 和 1500100100|

    这里两个比较,你看下面的比上面的多出后面一位,按字典顺序肯定是1500100100在上面

    1500100100| 和 1500100101,我们看对应位置的大小,这2个数的前9位都是相同的,1500100101最后一位比前面的数大了,所以字典顺序就是比他大,不会再去看下一位

    //创建一个表,指定列簇名,分区文件信息要写全路径
    create 'student_split','info',{SPLITS_FILE=>'/root/split.txt'}

    不用文件导入的话就把得上面的内容一个一个写入
    create 'student_split','info',{SPLITS_FILE=>['1500100100|','1500100200|','1500100300|'.......]}
    到hbase的网页端口查看一下这张表的信息:

    插入数据后观察,写入请求分配的很均衡

    phoneix中

    在创建表的时候指定salting。会再rowkey前面加上一个随机的前缀,这适用于我们不知道rowkey内容到底是怎样的场合

    优点:不需要知道rowkey的分布情况
    缺点:不能再hbase中对数据进行查询和修改,改变的rowkey我们也不知道到底是什么样子的

    CREATE TABLE IF NOT EXISTS STUDENT3 (
    id VARCHAR NOT NULL PRIMARY KEY,
    name VARCHAR,
    age BIGINT,
    gender VARCHAR ,
    clazz VARCHAR
    )salt_buckets=6;
    upsert into STUDENT3 values('1500100004','葛德曜',24,'男','理科三班');
    upsert into STUDENT3 values('1500100005','宣谷芹',24,'男','理科六班');
    upsert into STUDENT3 values('1500100006','羿彦昌',24,'女','理科三班');


    可以看到,这个'\x01','\x02','\x03'...就是给我们的rowkey打上的标记,因为标记随机分配, 我刚才传入的数据连续的rowkey被打上标记后可能就不在一个分区了

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

    hase中,无论是rowkey,列簇,列名,这些都会占用内存的, 拿写入数据的过程来说,memstore会把部分数据写入内存,hfile存储数据也会包含这些rowkey等,太长了就压缩了其他信息的存储空间

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

    rowkey唯一原则
    必须在设计上保证其唯一性, rowkey可以锁定唯一的一行数据,rowkey重复的话后put的数据会覆盖前面插入的数据

    三.热点问题
    rowkey设计是热点的源头,热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。

    为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。

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

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

    10001,10002,10003这样的三个rowkey给他们加上随机的前缀
    ##$10001,$%#10002,&%$10003
    分配region就不会总是在一个region内了
    哈希
    哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。 根据哈希值可以反推出完整的rowkey,可以使用get操作准确获取某一个行数据

    哈希有多种算法,这里就拿md5来计算1002的哈希值,多次计算都是这个结果
    fba9d88164f3e2d9109ee770223212a0
    那么别人要是看到这个哈希值也可以通过算法,工具计算出你原来的rowkey是什么样的
    反转
    第三种防止热点的方法是反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。

    还是拿1001,1002,1003来说,这3个rowkey连续将改变得数据放到前面
    1100,2100,3100,这样就不连续了
    时间戳反转
    一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到key的末尾,

    简单说:时间戳反转做的是用一个大的数值减去时间戳

    原来的roweky
    1638711499_user01
    1638711500_user02
    1638711501_user03
    用一个较大数10000000减去时间戳
    8361288499_user03
    8361288500_user02
    8361288501_user01
    这样新的时间戳的信息会在被排在前面

  • 相关阅读:
    尽量采用as操作符而不是旧式C风格做强制类型转换
    尽量使用条件属性(Conditional Attribute)而不是#if/#endif预处理
    C#跟踪和调试程序-Debug类使用
    C#使用ConditionalAttribute特性来实现代码调试
    微软认知服务:QnA Maker使用示例
    PHP使用微软认知服务Face API
    微软认知服务识别名人和地标
    基于apache httpclient 调用Face++ API
    认知服务调用如何使用图片的DataURL
    Microsoft Azure Storage Exployer使用指南
  • 原文地址:https://www.cnblogs.com/felixzh/p/16306929.html
Copyright © 2020-2023  润新知