HbaseRowkey设计以及列族和列的关系

HbaseRowkey设计以及列族和列的关系_第1张图片

 Hbase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读数据;这里主要介绍写数据的部分,即Memstore。当RegionServer(RS)收到写请求的时候(writerequest),RS会将请求转至相应的Region。每一个Region都存储着一些列(a set of rows)。根据其列族的不同,将这些列数据存储在相应的列族中(Column Family,简写CF)。不同的CF中的数据存储在各自的HStore中,HStore由一个Memstore及一系列HFile组成。Memstore位于RS的主内存中,而HFiles被写入到HDFS中。当RS处理写请求的时候,数据首先写入到Memstore,然后当到达一定的阀值的时候,Memstore中的数据会被刷到HFile中。

  用到Memstore最主要的原因是:存储在HDFS上的数据需要按照row key 排序。而HDFS本身被设计为顺序读写(sequential reads/writes),不允许修改。这样的话,HBase就不能够高效的写数据,因为要写入到HBase的数据不会被排序,这也就意味着没有为将来的检索优化。为了解决这个问题,HBase将最近接收到的数据缓存在内存中(in Memstore),在持久化到HDFS之前完成排序,然后再快速的顺序写入HDFS。(这里参考:http://www.cnblogs.com/shitouer/archive/2013/02/05/configuring-hbase-memstore-what-you-should-know.html)

这里就留下个问题,MemStore何时刷写成HFile?

个人总结如下:

1.Region级别的触发刷写。

(1)hbase.hregion.memstore.flush.size

  单个region内所有的memstore大小总和超过指定值时,flush该region的所有memstore。这里为什么是所有memsotre?因为一张表可能有多个CF,其对应的一个Region自然包含多个CF(即HStore),每个Store都有自己的memstore,这个配置值是所有的store的memstore的总和。当这个总和达到配置值时,即针对每个HSotre,都触发其Memstore,刷写成storefile(HFile的封装)文件。

(2)hbase.hstore.blockingStoreFiles 默认值:7

  说明:在flush时,当一个region中的Store(Coulmn Family)内有超过7个storefile时,则block所有的写请求进行compaction,以减少storefile数量。

  调优:block写请求会严重影响当前regionServer的响应时间,但过多的storefile也会影响读性能。从实际应用来看,为了获取较平滑的响应时间,可将值设为无限大。如果能容忍响应时间出现较大的波峰波谷,那么默认或根据自身场景调整即可。这个值设置比较大,会增加客户端的负载处理能力(即影响读取性能),但是如果你的服务器一直处于一个高的水平,那说明你的机器已经达到性能瓶颈,需要其他方式解决。

(3)hbase.hregion.memstore.block.multiplier默认值:2

说明:当一个region里总的memstore占用内存大小超过hbase.hregion.memstore.flush.size两倍的大小时,block该region的所有请求,进行flush,释放内存。虽然我们设置了region所占用的memstores总内存大小,比如64M,但想象一下,在最后63.9M的时候,我Put了一个200M的数据,此时memstore的大小会瞬间暴涨到超过预期的hbase.hregion.memstore.flush.size的几倍。这个参数的作用是当memstore的大小增至超过hbase.hregion.memstore.flush.size2倍时,block所有请求,遏制风险进一步扩大。

调优:这个参数的默认值还是比较靠谱的。如果你预估你的正常应用场景(不包括异常)不会出现突发写或写的量可控,那么保持默认值即可。如果正常情况下,你的写请求量就会经常暴长到正常的几倍,那么你应该调大这个倍数并调整其他参数值,比如hfile.block.cache.size和hbase.regionserver.global.memstore.upperLimit/lowerLimit,以预留更多内存,防止HBase server OOM。

 

2.RegionServer全局性的触发刷写。

(1)hbase.regionserver.global.memstore.upperLimit

当ReigonServer内所有region的memstores所占用内存总和达到heap的40%时,HBase会强制block所有的更新并flush这些region以释放所有memstore占用的内存。

(2)hbase.regionserver.global.memstore.lowerLimit

同upperLimit,只不过lowerLimit在所有region的memstores所占用内存达到Heap的35%时,不flush所有的memstore。它会找一个memstore内存占用最大的region,做个别flush,此时写更新还是会被block。lowerLimit算是一个在所有region强制flush导致性能降低前的补救措施。在日志中,表现为“** Flush thread woke up with memory above low water.”。

调优:这是一个Heap内存保护参数,默认值已经能适用大多数场景。

 

3. HLog (WAL)引起的regionserver全局性的触发刷写。

当数据被写入时会默认先写入Write-ahead Log(WAL)。WAL中包含了所有已经写入Memstore但还未Flush到HFile的更改(edits)。在Memstore中数据还没有持久化,当RegionSever宕掉的时候,可以使用WAL恢复数据。

若是关闭WAL,则在hbase-site.xml新增hbase.regionserver.hlog.enabled配置,设为false即可,不建议关闭。

当WAL(在HBase中成为HLog)变得很大的时候,在恢复的时候就需要很长的时间。因此,对WAL的大小也有一些限制,当达到这些限制的时候,就会触发Memstore的flush。Memstore flush会使WAL减少,因为数据持久化之后(写入到HFile),就没有必要在WAL中再保存这些修改。有两个属性可以配置:

(1)hbase.regionserver.hlog.blocksize

(2)hbase.regionserver.maxlogs

WAL的最大值由hbase.regionserver.maxlogs*hbase.regionserver.hlog.blocksize (2GB by default)决定。一旦达到这个值,Memstore flush就会被触发。所以,当你增加Memstore的大小以及调整其他的Memstore的设置项时,你也需要去调整HLog的配置项。否则,WAL的大小限制可能会首先被触发,因而,你将利用不到其他专门为Memstore而设计的优化。抛开这些不说,通过WAL限制来触发Memstore的flush并非最佳方式,这样做可能会会一次flush很多Region,尽管“写数据”是很好的分布于整个集群,进而很有可能会引发flush“大风暴”。

提示:最好将hbase.regionserver.hlog.blocksize* hbase.regionserver.maxlogs设置为稍微大于hbase.regionserver.global.memstore.lowerLimit* HBASE_HEAPSIZE.

Table中Family和Qualifier的关系与区别

就像用MySQL一样,我们要做的是表设计,MySQL中的表,行,列的在HBase已经有所区别了,在HBase中主要是TableFamilyQualifier,这三个概念。Table可以直接理解为表,而Family和Qualifier其实都可以理解为列,一个Family下面可以有多个Qualifier,所以可以简单的理解为,HBase中的列是二级列,也就是说Family是第一级列,Qualifier是第二级列。两个是父子关系。

测试发现:

在实际应用场景中,对于单column qualifier和多column qualifier两种情况,

如果value长度越长,row key长度越短,字段数(column qualifier数)越少,前者和后者在实际传输数据量上会相差小些;反之则相差较大。

如果采用多column qualifier的方式存储,且客户端采取批量写入的方式,则可以根据实际情况,适当增大客户端的write buffer大小,以便能够提高客户端的写入吞吐量。

 

从性能的角度谈table中family和qualifier的设置
  对于传统关系型数据库中的一张table,在业务转换到hbase上建模时,从性能的角度应该如何设置family和qualifier呢?
  最极端的,①每一列都设置成一个family,②一个表仅有一个family,所有列都是其中的一个qualifier,那么有什么区别呢?


  从读的方面考虑:
  family越多,那么获取每一个cell数据的优势越明显,因为io和网络都减少了。
  如果只有一个family,那么每一次读都会读取当前rowkey的所有数据,网络和io上会有一些损失。
  当然如果要获取的是固定的几列数据,那么把这几列写到一个family中比分别设置family要更好,因为只需一次请求就能拿回所有数据。

  从写的角度考虑:
  首先,内存方面来说,对于一个Region,会为每一个表的每一个Family分配一个Store,而每一个Store,都会分配一个MemStore,所以更多的family会消耗更多的内存。
  其次,从flush和compaction方面说,目前版本的hbase,在flush和compaction都是以region为单位的,也就是说当一个family达到flush条件时,该region的所有family所属的memstore都会flush一次,即使memstore中只有很少的数据也会触发flush而生成小文件。这样就增加了compaction发生的机率,而compaction也是以region为单位的,这样就很容易发生compaction风暴从而降低系统的整体吞吐量。
  第三,从split方面考虑,由于hfile是以family为单位的,因此对于多个family来说,数据被分散到了更多的hfile中,减小了split发生的机率。这是把双刃剑。更少的split会导致该region的体积比较大,由于balance是以region的数目而不是大小为单位来进行的,因此可能会导致balance失效。而从好的方面来说,更少的split会让系统提供更加稳定的在线服务。而坏处我们可以通过在请求的低谷时间进行人工的split和balance来避免掉。
     因此对于写比较多的系统,如果是离线应该,我们尽量只用一个family好了,但如果是在线应用,那还是应该根据应用的情况合理地分配family。

Rowkey 设计

  1.  Rowkey散列原则
  2.  Rowkey唯一原则
  3. Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。

原因如下:

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

(2)MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。

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

热点

hbase 中的行是以 rowkey 的字典序排序的,这种设计优化了scan 操作,可以将相关的 行 以及会被一起读取的行 存取在临近位置,便于 scan 。 然而,糟糕的 rowkey 设计是 热点 的源头。 热点发生在大量的客户端直接访问集群的一个或极少数节点。访问可以是读,写,或者其他操作。大量访问会使 热点region 所在的单个机器超出自身承受能力,引起性能下降甚至是 region 不可用。这也会影响同一个 regionserver 的其他 regions,由于主机无法服务其他region 的请求。设计良好的数据访问模式以使集群被充分,均衡的利用。

为了避免写热点,设计 rowkey 使得 不同行在同一个 region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。下面是一些常见的避免 热点的方法以及它们的优缺点:

加盐

这里的加盐不是密码学中的加盐,而是在rowkey 的前面增加随机数。具体就是给 rowkey 分配一个随机前缀 以使得它和之前排序不同。分配的前缀种类数量应该和你想使数据分散到不同的 region 的数量一致。 如果你有一些 热点 rowkey 反复出现在其他分布均匀的 rwokey 中,加盐是很有用的。考虑下面的例子:它将写请求分散到多个 RegionServers,但是对读造成了一些负面影响。

假如你有下列 rowkey,你表中每一个 region 对应字母表中每一个字母。 以 'a' 开头是同一个region, 'b'开头的是同一个region。在表中,所有以 'f'开头的都在同一个 region, 它们的 rowkey 像下面这样:

foo0001
foo0002
foo0003
foo0004

现在,假如你需要将上面这个 region 分散到 4个 region。你可以用4个不同的盐:'a', 'b', 'c', 'd'.在这个方案下,每一个字母前缀都会在不同的 region 中。加盐之后,你有了下面的 rowkey:

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

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

现在,如果再增加一行,它将随机分配a,b,c,d中的一个作为前缀,并以一个现有行作为尾部结束:

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

因为分配是随机的,所以如果你想要以字典序取回数据,你需要做更多工作。加盐这种方式增加了写时的吞吐量,但是当读时有了额外代价。

哈希

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

哈希例子

像在加盐方法中给出的那个例子,你可以使用某种哈希方法使得 foo0003 这样的 rowkey 的前缀永远是 ‘a',然后,为了取得某一行,你可以通过哈希获得 相应的 rowkey. 你也可以优化哈希方法,使得某些rowkey 永远在同一个 region.

翻转key

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

单调递增 rwokey(时间连续序列)

在《Hadoop 权威指南》中,有一个优化的注意点:当所有客户端一段时间内一致写入某一个region,然后再接着写入下一个 region。例如:像单调递增的 rowkey(时间戳) ,就会发生这种现象。 可以查看 Kai Lan的漫画 monotonically increasing values are bad. 说明了为什么单调递增的rowkey 在分布式表格系统(Hbase)中是有问题的。这种单调递增的rowkey 堆积在同一个region 的问题可以通过 随机化 输入记录来缓和。但通常来讲我们应该避免使用时间戳或者序列(1,2,3)来作为主键。

如果你的确需要在Hbase存储时间序列数据,可以学习 OpenTSDB,它是个成功的例子。链接schema有一页描述了OpenTSDB在hbase中使用的模式。OpenTSDB中的key模式是:[元数据类型][时间戳],初看起来这似乎违反了不使用时间戳作为rowkey的原则,然而,区别是时间戳并没有在rowkey的关键位置,而且这个设计假设拥有许多元数据类型。因此,即使有连续的混合着元数据的输入数据,它们也会Put进入表中不同的regions.

尽量减少行和列的大小

在Hbase中,value永远是和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,甚至可以和具体的值相比较,那么你将会遇到一些有趣的情况。HBase storefiles中的索引(有助于随机访问)最终占据了HBase 分配的大量内存,因为具体的值和他的key很大。可以增加 block 大小使得 storefiles 索引在更大的时间间隔增加,或者修改表的模式以减小rowkey 和 列名的大小。压缩也有助于更大的索引。

大多时候较小的低效率是无关紧要的,但是在这种情况下,任何访问模式都需要列族名,列名,rowkey,所以它们会被访问数十亿次在你的数据中。

列族

尽可能使列族名越短越好,最好是一个字符。(例如:'d' 代表data/default)。

属性

冗长的属性名("myVeryImportantAttribute")是易读的,但是更短的属性名("via")存储在HBase中更好。

Rowkey 长度

让 Rowkey 越短越好是合理的,这对必需的数据访问(get,scan)是有益的。但是当短 key 对数据访问是无用时它不及长 key 拥有更好的get/scan属性。当设计 rowkey 时,我们需要权衡,折中。

翻转时间戳

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

比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计rowkey的时候,可以这样设计 
[userId反转][Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候,直接指定反转后的userId,startRow是[userId反转][000000000000],stopRow是[userId反转][Long.Max_Value - timestamp] 

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

Hbase中Rowkey设计对入库效率的影响

建表预分区原则
Hbase Region在大小达到一定阈值后(目前是10G),就会Splite(分裂)两个Region,而当一个Region的Hfile个数达到一定的阈值(目前为4个),就会对这些Hfile进行Compact(合并)。Splite和Compact都会比较消耗IO,所以要尽量减少Region分裂和合并的次数。这就需要对数据的大小进行评估,建表时预先分区。如果不预分区,默认就只有一个分区,在导入数据时就只会起一个Reduce任务处理,而且Region达到阈值大小后会不断分裂,非常影响入库效率。所以需要合理估算分区数,过小会导致Region不断分裂,过大又会增加MasterServer管理的压力。计算分区数原则如下:
N = TotalSize/hbase.hregion.max.filesize(目前是10G)
例如目前http接口信令数据一天的大小约为6.5T,那么分区数为:6.5*1024/10=665.6,因为建表时一般会指定压缩方式,入库后的数据会变小,故分区数设置为600就足够了。DNS接口每天大小约2T,分区数为:2*1024/10=204.8,分区数设置为了200就可以了。
建议Hbase按以下方式来建表: 

create 'ns_boco:test ', {METADATA => {'SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy'}},{NAME => 'info', COMPRESSION => 'SNAPPY' },{NUMREGIONS => 600, SPLITALGO => 'HexStringSplit'}

该建表语句指定了表ns_boco:test Region split策略为按固定大小分裂(ConstantSizeRegionSplitPolicy),压缩方式采用SNAPPY,表预分600(需按业务实际情况估算)个Region,列族为info

下面测试Rowkey在有散列字段和无散列字段对HDFS文件入库Hbase效率的影响。

1、    使用bulkload方式把HDFS的文件导入Hbase,大小均为340.7G。建表均采用预分40个Region。
 

create 'ns_boco:import_test1', {METADATA => {'SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy'}},{NAME => 'info', COMPRESSION => 'SNAPPY' },{NUMREGIONS => 40, SPLITALGO => 'HexStringSplit'}
create 'ns_boco:import_test2', {METADATA => {'SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy'}},{NAME => 'info', COMPRESSION => 'SNAPPY' },{NUMREGIONS => 40, SPLITALGO => 'HexStringSplit'}

2、 import_test2 Rowkey设计为MD5(MSISDN)取前4位+MSISDN+时间,高位使用MD5得到散列字段。如41c11877721065620170801142654645。

入库时间:09:23:35–08:51:53≈31分钟。

可以发现入库后各Region的数据量分布比较均衡,每个Region约分配到了7.5G的数据。

3、 import_test1 RowkeyRowkey设计为MSISDN(8-11位)+MSISDN+时间,高位直接取手机号码8-11位,没做散列处理,如06561877721065620170801142654645。

入库时间:10:20:12-09:25:54≈54分钟。
入库后各Region的数据量分布不均衡,40个Region中有19个(大小为63字节的目录)没有分配到数据,有些Region分配到的数据又比较大,大于20G。

由此可见,Rowkey设计时对高位进行散列处理,不仅能提高读写效率,还能让数据均衡的分布到各个Regions上,实现负载均衡

 

 

你可能感兴趣的:(HbaseRowkey设计以及列族和列的关系)