HBase的Row Key设计

HBase中的行是以row key进行字典排序的,这种设计优化了scan的操作,将以将相关的行或者一起读取的行存储在临近的位置,以便于scan. 但是row key设计不好就会造成读写热点问题,造成大量客户端直接访问集群某一个或者极少数的节点,造成节点性能下降或者Region不可用。同时还会对其他Region或者业务造成影响。集群不能被充分使用。

 

首先我们得确保row key必须唯一,而且row key不要太大,虽然标准是10-100字节,但是我们最好不要超过16字节,你如果100字节,很有可能导致写数据延迟等问题

 

一 加盐

在row key前面增加随机数。具体就是给row key分配一个随机前缀以使得它和之前的排序不同,分配的前缀种类数应该和你想使得数据分散到不同的region的数量一致

加盐例子:

加入你有下列row key,你表中每一个region对应一个字母。以a开头的是同一个Region,以b开头的是同一个region:

原始row key如下:

foo0001 foo0002 foo0003 foo0004

假设你现在需要将上面的region分散到到4个region,你可以用四个不同的盐:'a','b','c','d'

然后你就有了下面的盐:

a-foo0003 b-foo0001 c-foo0004 d-foo0002

所以,你可以向4个不同的 region 写,理论上说,如果所有人都向同一个region 写的话,你将拥有之前4倍的吞吐量。

 

但是我们要按照row key进行排序,这种我们需要额外想办法,加盐这种方式增加了写的时候吞吐量,如果要进行读,特别是排序,这种就有性能代价了

 

二 哈希(散列)

哈希会使得同一行永远使用同一个加盐前缀,也可以使得负载分散到整个集群,但是是可以预测的。使用确定的哈希可以让客户端重构完成row key

 

三 反转

我们反转固定格式的长度或者数字格式的row key,比如手机号,这样使得row key经常改变的部分位于前面,变动较少的部分位于后面。这样可以有效散列row key,但是牺牲了row key的排序,给scan操作带来一定的问题

 

四 避免单调递增或递减row key(时间或者ID连续系列)

如果row key是那种序列化的id(1000,1001,1002诸如此类)或者时间戳(20170101,20170102)这种格式。它会导致在某一个时间段内,所有的数据全部写入到了同一个region

 

五 反转时间戳

一个常见的问题是快速获取数据的最近版本,我们可以将时间戳反转作为row key一部分,可以解决这个问题,可以将Long.MAX_VALUE-

Timestamp追加到到row key末尾,比如[key][reverse_timestamp]。

表中[key]的最新值可以通过scan[key]获取[key]的第一条记录,因为HBase中row key是按照字典排序的,最新的[key]在任何旧的key的前面,所以第一条记录就是最新的

这个技巧可以替代使用多版本数据,对版本数据会很长时间保存数据的版本,同时这个技巧用一个scan操作就可以获得所有数据。

如果需要查询某段时间的操作记录,startRow是[key][Long.Max_Value - 起始时间],stopRow是[key][Long.Max_Value - 结束时间]

 

六 Row Key设计案例

6.1 日志数据/时间序列数据

假设我么可以获取以下的数据元素:

hostname timestamp log_event value

 

然后我们就要分析我们当前的业务主要是哪一种类型:

6.1.1 如果时间维度上的scan操作很多很重要

[row key] :[timestamp][hostname][log_event]

如果这样我们会遇到时间戳单调递增的情况,怎么解决呢?

我们通过对时间戳取模来实现,即

[row key] :[timestamp%桶数] [timestamp][hostname][log_event]

要选择一个时间范围内的数据,scan操作需要对每一个桶执行,假如有100个桶,就需要100scan才能获得完整的数据。所以我们需要一个权衡。

 

6.1.2 如果我们在主机名的维度上查询比较多

[rowkey] :[hostname][log-event][timestamp]是一个备选,如果有大量的主机名需要读写,如果通过主机名的scan 操作是主要的这种方式就很有用。

 

6.1.3获取最新的数据是最主要的访问数据方式

那么存储 时间戳 或者翻转时间戳(timestamp = Long.MAX_VALUE – timestamp),可以通过 scan [hostname][log-event]快速获取最新的数据。

在put的时候我们自己设置时间戳: public Put addColumn(byte [] family, byte [] qualifier,long ts, byte [] value)

 

6.1.4变长还是固定长度的 rowkey

如果存储的主机名是hadoop-all-01,事件类型是 com.hbase.client.tools.put呢?这就显得row key有点长而且还不固定,
所以使用一些方法来代替是有必要的,比如hash和数字化

6.1.4.1 [MD5 hash of hostname] = 16 bytes

[MD5 hash of event-type] = 16 bytes

[timestamp] = 8 bytes

6.1.4.2 用数字 合成rowkey

这种方法,除了 LOG_DATA 之外,我们还需要一张查找表 LOG_TYPES。LOG_TYPES的主键可以是:

[type] (表明是 hostname 还是 event-type)

[bytes] ( hostname or event-type的 原始字节长度)

 

6.2 顾客/订单表

假如hbase被用来存储顾客和订单信息,将由俩类主要记录类型:顾客记录类型和订单记录类型。顾客记录包含如下信息:

  • 顾客 ID
  • 顾客 name
  • 地址(城市,国家,邮编)
  • 手机号等 订单记录包含如下信息:
  • 顾客ID
  • 订单ID
  • 交易日期
  • 一系列运送的地点和信息

假设顾客ID和订单信息的组合可以唯一确定一笔订单,这俩个属性将会组合为 rwokey.一个特别的 ORDER表达的rowkey如下:

[customer number][order number]

然而,还有更多的设计需要做决定:原始的值是rowkey的最佳选择吗?

这里我们遇到了之前日志数据案例的相同问题, customer number的字符空间是什么?格式是什么?(数字型的,数字字母混合的)在hbase中使用定长 rowkey 是有益的,rowkey也需要支持字符空间的合理分布,相似的选项出现了:

用哈希组合 rowkey:

[MD5 of customer number] =16 bytes

 

[MD5 of order number] =16 bytes

用哈希,数字组合 rowkey:

[customer number的长整形] = 8 bytes

 

[MD5 of order number] =16 bytes

单表?多表?

传统的方法是为顾客和订单建立各自的表,另一个选项是将所有的记录存入一张表。(例如:CUSTOMER++

顾客记录的 rowkey

[customer-id]

 

[type] =类型 `1'代表 customer recordtype

订单记录的 rowkey:

[customer-id]

 

[type] =类型 `2'代表for order recordtype

 

[order]

特殊的CUSTOMER++方法的优点是可以通过 customer-id 组织所有不同类型的数据。(例如:1scan就可以获得一个顾客的所有信息)缺点是不容易 scan特殊的记录类型。

订单对象设计

现在我们需要考虑订单对象的建模,假设类结构如下: Order一个 Order 有许多 ShippingLocations(运送位置) LineItem一个 ShippingLocations有多个 LineItem存储这种数据有多种选择:

  • 完全范式化 用这种方法,将分成3个独立的表:ORDER,SHIPPING_LOCATION和 LINE_ITEM. ORDER 表的rowkey 在上面已经提到, SHIPPING_LOCATION表的 复合 rowkey 如下:

[order-rowkey]

 

[shipping location number] (例如:第一个位置,第二个等等)

LINE_ITEM表的复合rowkey如下:

[order-rowkey]

 

[shipping location number] (例如:第一个位置,第二个等等)

 

[line item number] (例如:,第一个 lineitem,第二个等等)

这个范式化设计和RDBMS很想,但这不是你使用 hbase的唯一选择。这种方法下,你要获取任何一个订单信息,你需要:

1.    order表中获取订单

2.    scan SHIPPING_LOCATION表获取订单的ShippingLocation信息

3.    scan LINE_ITEM获取每一个ShippingLocation的具体信息

就算 RDBMS会在幕后都做这些,但是你必须认识到一个现实:那就是在hbase中没有join.

  • 用单表存储所有记录 在这种方法中:所有信息都会存储在ORDER表中:
    • ORDER 的rowkey 设计如下:

o   [order-rowkey]

o    

o   [ORDER record type]

    • ShippingLocation 的复合 rowkey 如下:

o   [order-rowkey]

o    

o   [SHIPPING record type]

o    

o   [shipping location number] (e.g.,1st location,2nd, etc.)

    • LineItem 的复合 rowkey 如下:

o   [order-rowkey]

o    

o   [LINE record type]

o    

o   [shipping location number] (e.g.,1st location,2nd, etc.)

o    

o   [line item number] (e.g.,1st lineitem,2nd, etc.)

  • 反范式化: 一种上面单表的变体方法是反范式化,摊平一些对象层次结构,例如折叠ShippingLocation 属性到 每一个 LineItem 实例。

LineItem的复合 rowkey 如下:

[order-rowkey]

 

[LINE record type]

 

[line item number] (e.g.,1st lineitem,2nd, etc., 必须意识到 line item number对于整个表式唯一的)

LineItem列族情况如下:

itemNumber

 

quantity

 

price

 

shipToLine1 (denormalizedfromShippingLocation)

 

shipToLine2 (denormalizedfromShippingLocation)

 

shipToCity (denormalizedfromShippingLocation)

 

shipToState (denormalizedfromShippingLocation)

 

shipToZip (denormalizedfromShippingLocation)

这种设计的优点是没有复杂的对象层次,缺点是任何信息的更新将会十分复杂。

二进制大对象

这种方法是把整个表作为一个二进制大对象,例如: ORDER表的rwokey设计和之前一样,有一个"order"的单列是一个可以反序列化为 Order,ShippingLocations, and LineItems.的对象。

可以有许多选择: JSON, XML, Java Serialization, Avro, Hadoop Writables它们的原理都是一样的:将对象用二进制编码。这种方法必须注意向后兼容性,即使对象模型改变了,我们也可以从hbase读取对象的旧版本。

优点是可以用最小的IO管理复杂的对象,(例如:hbase get 每个 order)确定值前面提到的向后兼容性,序列化的语言依赖(例如:Java Serialization只能在java客户端工作),事实上,你想要获得二级制大对象的任意小的信息,你都必须反序列化整个对象,而且你很难用像hive这样的框架去处理你自定义的对象。

 

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值