Lars Hofhansl写了一篇关于RegionServer内存大小的博客文章。结果是您可能需要比您认为需要更多的内存。他深入研究了区域大小,memstore大小,HDFS复制因子以及其他要检查的内容。
就个人而言,我会将每台机器的最大磁盘空间放置在大约6T的HBase上,除非你的工作量非常大。在这种情况下,Java堆应该是32GB(20G区域,128M存储器,其余默认值)。
- Lars Hofhansl <cite>http://hadoop-hbase.blogspot.com/2013/01/hbase-region-server-memory-sizing.html</cite>
HBase目前不适用于两个或三个列系列以上的任何内容,因此请保持模式中的列族数量较少。目前,冲洗和压缩是基于每个区域进行的,因此如果一个列族携带大量数据带来冲洗,则即使它们携带的数据量很小,相邻的族也将被刷新。当存在许多列族时,冲洗和压缩交互可以产生一堆不必要的i / o(通过改变刷新和压缩以按列系列工作来解决)。有关压缩的更多信息,请参见压缩。
如果可以在模式中使用,请尝试使用一个列族。在数据访问通常是列作用域的情况下,仅引入第二和第三列族;即您查询一个列族或另一个列族,但通常不是同时查询两个列族。
如果单个表中存在多个ColumnFamilies,请注意基数(即行数)。如果ColumnFamilyA有100万行而ColumnFamilyB有10亿行,则ColumnFamilyA的数据可能会分布在许多区域(和RegionServers)中。这使得ColumnFamilyA的大规模扫描效率降低。
HBase中的行按行按字典顺序排序。此设计优化了扫描,允许您将相关的行或将要一起读取的行存储在彼此附近。但是,设计不良的行键是_热点_的常见来源。当大量客户端流量指向群集的一个节点或仅几个节点时,就会发生热点。此流量可能表示读取,写入或其他操作。流量超过负责托管该区域的单个机器,导致性能下降并可能导致区域不可用。这也可能对同一区域服务器托管的其他区域产生负面影响,因为该主机无法为请求的负载提供服务。设计数据访问模式非常重要,这样才能完全均匀地利用集群。
为了防止写入热点,设计行键使得真正需要位于同一区域的行是,但从更大的角度来看,数据被写入群集中的多个区域,而不是一次一个。下面描述了一些避免热点的常用技术,以及它们的一些优点和缺点。
盐
在这种意义上,Salting与加密无关,但是指的是将随机数据添加到行键的开头。在这种情况下,salting指的是向行键添加随机分配的前缀,以使其排序与其他方式不同。可能的前缀数对应于您希望跨数据传播的区域数。如果您有一些“热”行键模式在其他更均匀分布的行中反复出现,则Salting可能会有所帮助。请考虑以下示例,该示例显示salting可以在多个RegionServers之间传播写入负载,并说明了对读取的一些负面影响。
实施例11.腌制实施例
假设您有以下行键列表,并且您的表被拆分,以便字母表中的每个字母都有一个区域。前缀“a”是一个区域,前缀“b”是另一个区域。在此表中,以“f”开头的所有行都在同一区域中。此示例关注具有以下键的行:
foo0001
foo0002
foo0003
foo0004
现在,想象一下,您希望将它们分布在四个不同的地区。您决定使用四种不同的盐:a,b,c和d。在这种情况下,这些字母前缀中的每一个都将位于不同的区域。应用盐后,您将改为使用以下rowkeys。既然你现在可以写入四个不同的区域,理论上你在写入时的吞吐量是所有写入到同一区域时的四倍。
a-foo0003
b-foo0001
c-foo0004
d-foo0002
然后,如果添加另一行,将随机分配四个可能的盐值中的一个,并最终靠近其中一个现有行。
a-foo0003
b-foo0001
c-foo0003
c-foo0004
d-foo0002
由于此分配是随机的,因此如果要按字典顺序检索行,则需要执行更多工作。通过这种方式,salting尝试增加写入的吞吐量,但在读取期间会产生成本。
哈希
您可以使用单向_散列_而不是随机分配,这将导致给定行始终使用相同的前缀“加盐”,从而将负载分散到RegionServers,但是允许读取期间的可预测性。使用确定性哈希允许客户端重建完整的rowkey并使用Get操作正常检索该行。
示例12.散列示例在上面的salting示例中给出相同的情况,您可以改为应用单向散列,该散列将导致具有键foo0003的行始终并且可预测地接收a前缀。然后,要检索该行,您就已经知道了该密钥。您还可以优化事物,以便某些键对始终位于同一区域,例如,反转键
防止热点的第三个常见技巧是反转固定宽度或数字行键,以便最常更改(最低有效数字)的部分是第一个。这有效地使行键随机化,但牺牲了行排序属性。
参见 https://communities.intel.com/community/itpeernetwork/datastack/blog/2013/11/10/discussion-on-designing-hbase-tables 和关于盐渍表的文章[来自Phoenix项目的HTG3]以及 HBASE-11682 的评论中有关避免热点的更多信息的讨论。
在Tom White的书 Hadoop:The Definitive Guide (O'Reilly)的HBase章节中,有一个优化注释,用于观察导入过程与所有客户在音乐会中锁定步骤的现象敲击表中的一个区域(因此,单个节点),然后移动到下一个区域,等等。单调增加行键(即使用时间戳),这将发生。看看IKai Lan的这个漫画,为什么在类似BigTable的数据存储中单调增加行键是有问题的:单调递增值很差。通过将输入记录随机化为不按排序顺序,可以减轻由单调增加的密钥引起的单个区域的堆积,但通常最好避免使用时间戳或序列(例如,1,2,3)作为行键。
如果你确实需要将时间序列数据上传到HBase,你应该学习 OpenTSDB 作为一个成功的例子。它有一个描述它在HBase中使用的模式的页面。 OpenTSDB中的关键格式实际上是[metric_type] [event_timestamp],乍一看似乎与先前关于不使用时间戳作为关键字的建议相矛盾。然而,不同之处在于时间戳不在密钥的_前导_位置,并且设计假设是存在数十或数百(或更多)不同的度量类型。因此,即使具有混合度量类型的连续输入数据流,Puts也分布在表中的各个区域点上。
有关一些rowkey设计示例,请参见 schema.casestudies 。
在HBase中,值总是用它们的坐标运算;当一个单元格值通过系统时,它将伴随着它的行,列名和时间戳 - 总是如此。如果您的行和列名称很大,特别是与单元格值的大小相比,那么您可能会遇到一些有趣的场景。其中一个是Marc Limotte在 HBASE-3551 尾部描述的情况(推荐!)。其中,为了便于随机访问而保留在HBase存储文件( StoreFile(HFile))上的索引可能最终占用HBase分配的RAM的大块,因为单元值坐标很大。上面引用的注释中的标记建议增加块大小,以便存储文件索引中的条目以更大的间隔发生,或者修改表模式,从而使得行和列名称更小。压缩也将使更大的指数。在用户邮件列表中查看主题问题storefileIndexSize 。
大多数情况下,小的低效率并不重要。不幸的是,这是他们这样做的情况。无论为ColumnFamilies,属性和行键选择何种模式,它们都可以在数据中重复数十亿次。
有关HBase在内部存储数据的详细信息,请参阅 keyvalue ,了解其重要性。
尽量保持ColumnFamily名称尽可能小,最好是一个字符(例如“d”表示数据/默认值)。
有关HBase在内部存储数据的详细信息,请参阅 KeyValue 以了解其重要性。
尽管详细的属性名称(例如,“myVeryImportantAttribute”)更易于阅读,但是更喜欢较短的属性名称(例如,“via”)来存储在HBase中。
See keyvalue for more information on HBase stores data internally to see why this is important.
保持它们尽可能短,以便它们仍然可用于所需的数据访问(例如Get vs. Scan)。对数据访问无用的短键并不比具有更好的get / scan属性的更长键更好。在设计rowkeys时期望权衡。
长是8个字节。您可以在这八个字节中存储最多18,446,744,073,709,551,615的无符号数。如果将此数字存储为字符串 - 假设每个字符有一个字节 - 则需要将近3倍的字节。
不相信?下面是一些您可以自己运行的示例代码。
// long
//
long l = 1234567890L;
byte[] lb = Bytes.toBytes(l);
System.out.println("long bytes length: " + lb.length); // returns 8
String s = String.valueOf(l);
byte[] sb = Bytes.toBytes(s);
System.out.println("long as string length: " + sb.length); // returns 10
// hash
//
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(Bytes.toBytes(s));
System.out.println("md5 digest bytes length: " + digest.length); // returns 16
String sDigest = new String(digest);
byte[] sbDigest = Bytes.toBytes(sDigest);
System.out.println("md5 digest as string length: " + sbDigest.length); // returns 26
不幸的是,使用类型的二进制表示将使您的数据更难在代码之外读取。例如,这是在增加值时在shell中看到的内容:
hbase(main):001:0> incr 't', 'r', 'f:q', 1
COUNTER VALUE = 1
hbase(main):002:0> get 't', 'r'
COLUMN CELL
f:q timestamp=1369163040570, value=\x00\x00\x00\x00\x00\x00\x00\x01
1 row(s) in 0.0310 seconds
shell尽最大努力打印一个字符串,在这种情况下,它决定只打印十六进制。区域名称中的行键也会发生相同的情况。如果你知道存储了什么就可以了,但如果可以将任意数据放在同一个单元格中,它也可能是不可读的。这是主要的权衡。
反向扫描AP
HBASE-4811 实现了一个API来反向扫描表格或表格中的范围,从而减少了优化模式以进行正向或反向扫描的需要。此功能在HBase 0.98及更高版本中可用。有关详细信息,请参阅 Scan.setReversed()。
数据库处理中的一个常见问题是快速找到最新版本的值。使用反向时间戳作为密钥的一部分的技术可以极大地帮助解决此问题的特殊情况。同样可以在Tom White的书Hadoop:The Definitive Guide(O'Reilly)的HBase章节中找到,该技术涉及将(Long.MAX_VALUE - timestamp)附加到任何键的末尾,例如: [键] [reverse_timestamp]。
通过执行Scan [key]并获取第一条记录,可以找到表中[key]的最新值。由于HBase键是按排序顺序排列的,因此该键在[key]的任何旧行键之前排序,因此是第一个。
将使用此技术而不是使用版本号,其意图是“永久”(或很长时间)保留所有版本,同时通过使用快速获取对任何其他版本的访问权限相同的扫描技术。
Rowkeys的作用域为ColumnFamilies。因此,在没有冲突的表中存在的每个ColumnFamily中可以存在相同的rowkey。
Rowkeys无法更改。它们可以在表中“更改”的唯一方法是删除行然后重新插入。这是关于HBase dist-list的一个相当常见的问题,因此第一次(和/或在插入大量数据之前)使rowkeys正确是值得的。
如果你预分割你的表,那么_关键_就可以理解你的rowkey如何在区域边界上分布。作为为什么这很重要的一个例子,考虑使用可显示的十六进制字符作为密钥的前导位置的例子(例如,“0000000000000000”到“fffffffffffffffff”)。通过Bytes.split运行这些键范围(这是在Admin.createTable(byte[] startKey, byte[] endKey, numRegions)中为10个区域创建区域时使用的拆分策略将生成以下拆分...
48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 // 0
54 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 // 6
61 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -68 // =
68 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -126 // D
75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72 // K
82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14 // R
88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44 // X
95 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -102 // _
102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 // f
(注意:前导字节在右侧列为注释。)鉴于第一次拆分是'0'而最后一次拆分是'f',一切都很好,对吧?没那么快。
问题是所有数据都将堆积在前2个区域和最后一个区域,从而产生“块状”(可能是“热”)区域问题。要了解原因,请参阅 ASCII表。 '0'是字节48,'f'是字节102,但字节值(字节58到96)之间存在巨大差距,_永远不会出现在此键空间_中,因为唯一的值是[0 -9]和[af]。因此,中间区域永远不会被使用。为了使用该示例键空间进行预分割工作,需要自定义分割(即,不依赖于内置分割方法)。
第1课:预分割表通常是最佳实践,但您需要预先拆分它们,以便在密钥空间中可以访问所有区域。虽然此示例演示了十六进制键空间的问题,但_任何_键空间都会出现同样的问题。了解您的数据。
第2课:尽管通常不可取,但只要在密钥空间中可以访问所有创建的区域,使用十六进制密钥(更常见的是可显示的数据)仍可以使用预分割表。
总结此示例,以下是如何为十六进制密钥预先创建适当的拆分的示例:
public static boolean createTable(Admin admin, HTableDescriptor table, byte[][] splits)
throws IOException {
try {
admin.createTable( table, splits );
return true;
} catch (TableExistsException e) {
logger.info("table " + table.getNameAsString() + " already exists");
// the table already exists...
return false;
}
}
public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) {
byte[][] splits = new byte[numRegions-1][];
BigInteger lowestKey = new BigInteger(startKey, 16);
BigInteger highestKey = new BigInteger(endKey, 16);
BigInteger range = highestKey.subtract(lowestKey);
BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
lowestKey = lowestKey.add(regionIncrement);
for(int i=0; i < numRegions-1;i++) {
BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
byte[] b = String.format("%016x", key).getBytes();
splits[i] = b;
}
return splits;
}
通过 HColumnDescriptor 按列系列配置要存储的最大行版本数。最大版本的默认值为1.这是一个重要的参数,因为如数据模型部分所述HBase执行_而不是_覆盖行值,而是每行按时间存储不同的值(和预选赛)。在主要压缩过程中删除了多余的版本。根据应用需求,可能需要增加或减少最大版本的数量。
不建议将最大版本的数量设置为非常高的级别(例如,数百或更多),除非这些旧值非常珍贵,因为这将大大增加StoreFile的大小。
与最大行版本数一样,通过 HColumnDescriptor 按列系列配置要保留的最小行版本数。最小版本的默认值为0,表示该功能已禁用。行版本参数的最小数量与生存时间参数一起使用,并且可以与行版本参数的数量组合以允许配置,例如“保留最后T分钟的数据,最多N个版本,_但至少保持_的M个版本“(其中M是最小行数版本的值,M
HBase通过 Put 和 Result 支持“字节输入/字节输出”接口,因此任何可以转换为字节数组的都可以存储为值。输入可以是字符串,数字,复杂对象,甚至是图像,只要它们可以呈现为字节。
值的大小存在实际限制(例如,在HBase中存储10-50MB对象可能要求太多);在邮件列表中搜索有关此主题的对话。 HBase中的所有行都符合数据模型,包括版本控制。在进行设计时要考虑到这一点,以及ColumnFamily的块大小。
值得特别提及的一种受支持的数据类型是“计数器”(即,能够进行数字的原子增量)。参见Table中的增量。
计数器上的同步在RegionServer上完成,而不是在客户端中完成。
如果您有多个表,请不要忘记将 Joins 的可能性考虑到架构设计中。
ColumnFamilies可以设置TTL长度(以秒为单位),HBase将在到达到期时间后自动删除行。这适用于_行的所有_版本 - 即使是当前版本。在HBase中为行编码的TTL时间以UTC指定。
在次要压缩时删除仅包含过期行的存储文件。将hbase.store.delete.expired.storefile设置为false将禁用此功能。将最小版本数设置为0以外也会禁用此功能。
有关详细信息,请参阅 HColumnDescriptor 。
最新版本的HBase还支持按每个单元格设置生存时间。有关详细信息,请参阅 HBASE-10560 。使用Mutation#setTTL将细胞TTL作为突变请求(Appends,Increments,Puts等)的属性提交。如果设置了TTL属性,则它将应用于操作在服务器上更新的所有单元。细胞TTL处理和ColumnFamily TTL之间存在两个显着差异:
-
Cell TTL以毫秒而不是秒为单位表示。
-
细胞TTL不能延长细胞的有效寿命超过ColumnFamily水平TTL设置。
默认情况下,删除标记会延伸回到时间的开头。因此,即使获取或扫描操作指示放置删除标记之前的时间范围, Get 或 Scan 操作也不会看到已删除的单元格(行或列)。
ColumnFamilies可以选择保留已删除的单元格。在这种情况下,仍然可以检索已删除的单元格,只要这些操作指定的时间范围在任何将影响单元格的删除的时间戳之前结束。即使存在删除,这也允许进行时间点查询。
删除的单元格仍然受TTL限制,并且永远不会有超过“最大版本数量”的已删除单元格。新的“原始”扫描选项将返回所有已删除的行和删除标记。
使用HBase Shell更改KEEP_DELETED_CELLS的值
hbase> hbase> alter ‘t1′, NAME => ‘f1′, KEEP_DELETED_CELLS => true
示例13.使用API更改KEEP_DELETED_CELLS的值
...
HColumnDescriptor.setKeepDeletedCells(true);
...
让我们说明在表上设置KEEP_DELETED_CELLS属性的基本效果。
首先,没有:
create 'test', {NAME=>'e', VERSIONS=>2147483647}
put 'test', 'r1', 'e:c1', 'value', 10
put 'test', 'r1', 'e:c1', 'value', 12
put 'test', 'r1', 'e:c1', 'value', 14
delete 'test', 'r1', 'e:c1', 11
hbase(main):017:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0120 seconds
hbase(main):018:0> flush 'test'
0 row(s) in 0.0350 seconds
hbase(main):019:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
1 row(s) in 0.0120 seconds
hbase(main):020:0> major_compact 'test'
0 row(s) in 0.0260 seconds
hbase(main):021:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
1 row(s) in 0.0120 seconds
注意如何释放删除单元格。
现在让我们只在表上设置KEEP_DELETED_CELLS的情况下运行相同的测试(你可以做table或per-column-family):
hbase(main):005:0> create 'test', {NAME=>'e', VERSIONS=>2147483647, KEEP_DELETED_CELLS => true}
0 row(s) in 0.2160 seconds
=> Hbase::Table - test
hbase(main):006:0> put 'test', 'r1', 'e:c1', 'value', 10
0 row(s) in 0.1070 seconds
hbase(main):007:0> put 'test', 'r1', 'e:c1', 'value', 12
0 row(s) in 0.0140 seconds
hbase(main):008:0> put 'test', 'r1', 'e:c1', 'value', 14
0 row(s) in 0.0160 seconds
hbase(main):009:0> delete 'test', 'r1', 'e:c1', 11
0 row(s) in 0.0290 seconds
hbase(main):010:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0550 seconds
hbase(main):011:0> flush 'test'
0 row(s) in 0.2780 seconds
hbase(main):012:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0620 seconds
hbase(main):013:0> major_compact 'test'
0 row(s) in 0.0530 seconds
hbase(main):014:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0650 seconds
KEEP_DELETED_CELLS是为了避免在_仅_移除它们的原因是删除标记时从HBase中删除Cell。因此,如果启用了KEEP_DELETED_CELLS,则删除的单元格将被删除,如果您编写的版本多于配置的最大版本,或者您有TTL且单元格超出配置的超时等。
这部分也可以标题为“如果我的表rowkey看起来像_这个_,但我也想像_那样查询我的表_。” dist-list上的一个常见示例是行密钥的格式为“user-timestamp”,但对于某些时间范围的用户活动有报告要求。因此,用户选择很容易,因为它处于键的引导位置,但时间不是。
关于处理这个问题的最佳方法没有一个答案,因为它取决于......
-
用户数量
-
数据大小和数据到达率
-
报告要求的灵活性(例如,完全临时日期选择与预配置范围)
-
期望的查询执行速度(例如,对于某些特定报告,90秒可能是合理的,而对于其他人来说可能太长)
并且解决方案也受到群集大小以及解决方案需要多少处理能力的影响。常用技术在下面的小节中。这是一个全面但并非详尽无遗的方法清单。
二级索引需要额外的集群空间和处理应该不足为奇。这正是RDBMS中发生的情况,因为创建备用索引的行为需要更新空间和处理周期。在这方面,RDBMS产品更先进,可以开箱即用地处理备用索引管理。但是,HBase在更大的数据量下可以更好地扩展,因此这是一项功能权衡。
在实施任何这些方法时,请注意 Apache HBase性能调整。
另外,请看这个dist-list线程中的David Butler响应 HBase,mail#user - Stargate + hbase
根据具体情况,使用客户端请求过滤器可能是合适的。在这种情况下,不会创建二级索引。但是,不要尝试从应用程序(即单线程客户端)对这样的大型表进行全扫描。
可以在另一个表中创建二级索引,该表通过MapReduce作业定期更新。该作业可以在日内执行,但根据加载策略,它仍可能与主数据表不同步。
有关详细信息,请参阅 mapreduce.example.readwrite 。
另一种策略是在向集群发布数据时构建二级索引(例如,写入数据表,写入索引表)。如果在数据表已经存在之后采用这种方法,那么使用MapReduce作业的二级索引将需要自举(参见 secondary.indexes.periodic )。
如果时间范围非常宽(例如,一年的报告)并且数据量很大,则汇总表是一种常用方法。这些将通过MapReduce作业生成到另一个表中。
有关详细信息,请参阅 mapreduce.example.summary 。
协处理器就像RDBMS触发器一样。这些是在0.92中添加的。有关更多信息,请参阅协处理器
HBase目前支持传统(SQL)数据库用语中的“约束”。约束的建议用法是强制执行表中属性的业务规则(例如,确保值在1-10范围内)。约束也可用于强制引用完整性,但强烈建议不要这样做,因为它会显着降低启用完整性检查的表的写入吞吐量。自版本0.94起,可以在 Constraint 中找到有关使用约束的详细文档。
下面将介绍HBase的一些典型数据摄取用例,以及如何处理rowkey设计和构造。注意:这只是潜在方法的说明,而不是详尽的清单。了解您的数据,了解您的处理要求。
在阅读这些案例研究之前,强烈建议您先阅读 HBase和Schema Design 的其余部分。
描述了以下案例研究:
-
记录数据/时间序列数据
-
在类固醇上记录数据/时间序列
-
顾客订单
-
高/宽/中图式设计
-
列表数据
假设正在收集以下数据元素。
-
主机名
-
时间戳
-
记录事件
-
值/消息
我们可以将它们存储在名为LOG_DATA的HBase表中,但是rowkey是什么?从这些属性中,rowkey将是hostname,timestamp和log-event的某种组合 - 但具体是什么?
rowkey [timestamp][hostname][log-event]遭受单调递增行键/时间序列数据中描述的单调递增的rowkey问题。
通过对时间戳执行mod操作,在dist-lists中经常提到的关于“bucketing”时间戳的另一种模式。如果面向时间的扫描很重要,这可能是一种有用的方法。必须注意桶的数量,因为这将需要相同数量的扫描才能返回结果。
long bucket = timestamp % numBuckets;
构建:
[bucket][timestamp][hostname][log-event]
如上所述,要选择特定时间范围的数据,需要为每个存储桶执行扫描。例如,100个存储桶将在密钥空间中提供广泛的分布,但是需要100个扫描才能获得单个时间戳的数据,因此需要权衡利弊。
如果存在大量主机以在密钥空间上传播写入和读取,则rowkey [hostname][log-event][timestamp]是候选者。如果按主机名扫描是优先事项,则此方法很有用。
如果最重要的访问路径是拉取最近的事件,那么将时间戳存储为反向时间戳(例如,timestamp = Long.MAX_VALUE – timestamp)将创建能够在[hostname][log-event]上执行扫描以获取最近捕获的事件的属性。
这两种方法都不对,它只取决于最适合这种情况的方法。
Reverse Scan AP
HBASE-4811 implements an API to scan a table or a range within a table in reverse, reducing the need to optimize your schema for forward or reverse scanning. This feature is available in HBase 0.98 and later. See Scan.setReversed() for more information.
重要的是要记住,在HBase的每一列上都标记了rowkeys。如果主机名是a且事件类型是e1,则生成的rowkey将非常小。但是,如果摄取的主机名是myserver1.mycompany.com而事件类型是com.package1.subpackage2.subsubpackage3.ImportantService怎么办?
在rowkey中使用一些替换可能是有意义的。至少有两种方法:散列和数字。在Rowkey Lead Position示例中的Hostname中,它可能如下所示:
带哈希的复合Rowkey:
-
[主机名的MD5哈希值] = 16个字节
-
[事件类型的MD5哈希] = 16个字节
-
[timestamp] = 8个字节
具有数字替换的复合Rowkey:
对于这种方法,除了LOG_DATA之外,还需要另一个查找表,称为LOG_TYPES。 LOG_TYPES的rowkey是:
-
[type](例如,表示主机名与事件类型的字节) -
原始主机名或事件类型的
[bytes]可变长度字节。
此rowkey的列可以是带有指定编号的long,可以通过使用 HBase计数器获得
因此生成的复合rowkey将是:
-
[取代主机名长] = 8个字节
-
[代替事件类型的长度] = 8个字节
-
[timestamp] = 8 bytes
在散列或数字替换方法中,hostname和event-type的原始值可以存储为列。
这实际上是OpenTSDB方法。 OpenTSDB所做的是重写数据并将行打包到特定时间段的列中。有关详细说明,请参阅: http://opentsdb.net/schema.html 和来自HBaseCon2012的OpenTSDB 的经验教训。
但这就是一般概念的工作原理:例如以这种方式摄取数据......
[hostname][log-event][timestamp1]
[hostname][log-event][timestamp2]
[hostname][log-event][timestamp3]
每个详细事件都有单独的rowkeys,但是会像这样重写...
[hostname][log-event][timerange]
并且每个上述事件被转换成以相对于开始时间范围的时间偏移(例如,每5分钟)存储的列。这显然是一种非常先进的处理技术,但HBase使这成为可能。
假设HBase用于存储客户和订单信息。摄取了两种核心记录类型:客户记录类型和订单记录类型。
客户记录类型将包括您通常期望的所有内容:
-
顾客号码
-
顾客姓名
-
地址(例如,城市,州,邮编)
-
电话号码等
订单记录类型包括以下内容:
-
Customer number
-
订单号
-
销售日期
-
用于装运位置和项目的一系列嵌套对象(有关详细信息,请参阅订单对象设计)
假设客户编号和销售订单的组合唯一地标识订单,这两个属性将组成rowkey,特别是组合键,例如:
[customer number][order number]
对于ORDER表。但是,还有更多的设计决策要做: raw 值是rowkeys的最佳选择吗?
日志数据用例中的相同设计问题在这里面对我们。什么是客户编号的密钥空间,以及格式是什么(例如,数字?字母数字?)因为在HBase中使用固定长度密钥是有利的,以及可以支持密钥空间中合理传播的密钥,类似选项出现:
Composite Rowkey With Hashes:
-
[客户编号的MD5] = 16个字节
-
[订货号的MD5] = 16个字节
复合数字/哈希组合Rowkey:
-
[代替客户编号] = 8个字节
-
[MD5 of order number] = 16 bytes
传统的设计方法将为CUSTOMER和SALES提供单独的表。另一种选择是将多种记录类型打包到一个表中(例如,CUSTOMER ++)。
客户记录类型Rowkey:
-
[顾客ID]
-
[type] =表示客户记录类型为“1”的类型
订单记录类型Rowkey:
-
[customer-id]
-
[type] =表示订单记录类型的“2”的类型
-
[订购]
这种特殊的CUSTOMER ++方法的优点是可以按客户ID组织许多不同的记录类型(例如,单次扫描可以获得有关该客户的所有信息)。缺点是扫描特定记录类型并不容易。
现在我们需要解决如何为Order对象建模。假设类结构如下:
订购
(订单可以有多个ShippingLocations
的LineItem
(ShippingLocation可以有多个LineItems
存储此数据有多种选择。
使用这种方法,ORDER,SHIPPING_LOCATION和LINE_ITEM会有单独的表。
ORDER表的rowkey如上所述: schema.casestudies.custorder
SHIPPING_LOCATION的复合rowkey是这样的:
-
[order-rowkey] -
[shipping location number](例如,第1位,第2位等)
LINE_ITEM表的复合rowkey将是这样的:
-
[order-rowkey] -
[shipping location number](e.g., 1st location, 2nd, etc.) -
[line item number](例如,第1个lineitem,第2个等)
这样的规范化模型很可能是使用RDBMS的方法,但这不是您使用HBase的唯一选择。这种方法的缺点是,要检索有关任何订单的信息,您将需要:
-
获取订单的ORDER表
-
在SHIPPING_LOCATION表上扫描该订单以获取ShippingLocation实例
-
在LINE_ITEM上扫描每个ShippingLocation
无论如何,这就是RDBMS所做的事情,但由于HBase中没有连接,你只是更加意识到这一事实。
使用这种方法,将存在一个包含的单个表ORDER
Order rowkey如上所述: schema.casestudies.custorder
-
[order-rowkey] -
[ORDER record type]
ShippingLocation复合rowkey将是这样的:
-
[order-rowkey] -
[SHIPPING record type] -
[shipping location number](e.g., 1st location, 2nd, etc.)
LineItem复合rowkey将是这样的:
-
[order-rowkey] -
[LINE record type] -
[shipping location number](e.g., 1st location, 2nd, etc.) -
[line item number](e.g., 1st lineitem, 2nd, etc.)
具有记录类型的单表方法的变体是对一些对象层次结构进行非规范化和展平,例如将ShippingLocation属性折叠到每个LineItem实例上。
The LineItem composite rowkey would be something like this:
-
[order-rowkey] -
[LINE record type] -
[line item number](例如,第1个lineitem,第2个等,必须注意整个订单中有唯一性)
而LineItem列将是这样的:
-
项目编号
-
数量
-
价钱
-
shipToLine1(从ShippingLocation非规范化)
-
shipToLine2(从ShippingLocation非规范化)
-
shipToCity(从ShippingLocation非规范化)
-
shipToState(从ShippingLocation非规范化)
-
shipToZip(从ShippingLocation非规范化)
这种方法的优点包括不太复杂的对象层次结构,但其中一个缺点是,如果任何此类信息发生更改,更新会变得更加复杂。
使用这种方法,整个Order对象图以某种方式被视为BLOB。例如,ORDER表的rowkey如上所述: schema.casestudies.custorder ,一个名为“order”的列将包含一个可以反序列化的对象,该对象包含容器Order,ShippingLocations和LineItems。
这里有很多选项:JSON,XML,Java Serialization,Avro,Hadoop Writables等。所有这些都是相同方法的变体:将对象图编码为字节数组。在对象模型发生变化的情况下,应该注意这种方法以确保向后兼容性,以便仍然可以从HBase中读回较旧的持久性结构。
专业人员能够以最少的I / O管理复杂的对象图(例如,在这个例子中,单个HBase Get per Order),但是缺点包括前面提到的关于序列化的向后兼容性,序列化的语言依赖性的警告(例如,Java序列化)只适用于Java客户端),事实上你必须反序列化整个对象以获取BLOB中的任何信息,并且难以获得像Hive这样的框架来处理像这样的自定义对象。
本节将介绍出现在dist列表中的其他架构设计问题,特别是有关高表和宽表的问题。这些是一般准则,而不是法律 - 每个应用程序必须考虑自己的需求。
一个常见的问题是,是否应该选择行或HBase的内置版本。上下文通常是要保留行的“很多”版本的地方(例如,它明显高于HBase默认的1个最大版本)。行方法需要在rowkey的某些部分中存储时间戳,以便它们不会在每次连续更新时覆盖。
偏好:行(一般来说)。
另一个常见问题是,是否应该更喜欢行或列。上下文通常在宽表的极端情况下,例如具有1行具有100万个属性,或者1百万行,每行1列。
偏好:行(一般来说)。需要明确的是,本指南在非常广泛的情况下,而不是在需要存储几十或一百列的标准用例中。但是这两个选项之间也有一条中间路径,那就是“Rows as Columns”。
行与列之间的中间路径是打包数据,对于某些行,这些数据将成为列中的单独行。 OpenTSDB是这种情况的最佳示例,其中单行表示定义的时间范围,然后将离散事件视为列。这种方法通常更复杂,可能需要重新编写数据的额外复杂性,但具有I / O效率的优势。有关此方法的概述,请参见 schema.casestudies.log-steroids 。
以下是来自用户dist-list的关于一个相当常见的问题的交换:如何处理Apache HBase中的每用户列表数据。
- 问题 *****
我们正在研究如何在HBase中存储大量(每用户)列表数据,并且我们试图找出哪种访问模式最有意义。一种选择是将大部分数据存储在密钥中,因此我们可以使用以下内容:
<FixedWidthUserName><FixedWidthValueId1>:"" (no value)
<FixedWidthUserName><FixedWidthValueId2>:"" (no value)
<FixedWidthUserName><FixedWidthValueId3>:"" (no value)
我们的另一个选择是完全使用:
<FixedWidthUserName><FixedWidthPageNum0>:<FixedWidthLength><FixedIdNextPageNum><ValueId1><ValueId2><ValueId3>...
<FixedWidthUserName><FixedWidthPageNum1>:<FixedWidthLength><FixedIdNextPageNum><ValueId1><ValueId2><ValueId3>...
其中每行包含多个值。所以在一个案例中,读取前30个值将是:
scan { STARTROW => 'FixedWidthUsername' LIMIT => 30}
在第二种情况下,它将是
get 'FixedWidthUserName\x00\x00\x00\x00'
一般使用模式是只读取这些列表的前30个值,不经常访问读入更深入列表。有些用户在这些列表中总共有30个值,有些用户会有数百万(即幂律分布)
单值格式似乎会占用HBase上更多的空间,但会提供一些改进的检索/分页灵活性。是否可以通过获取与分页扫描分页来获得显着的性能优势?
我最初的理解是,如果我们的分页大小未知(并且适当地设置了缓存),那么扫描应该更快,但如果我们总是需要相同的页面大小,则应该更快。我最后听到不同的人告诉我有关表现的事情。我假设页面大小相对一致,因此对于大多数用例,我们可以保证在固定页面长度的情况下我们只需要一页数据。我还假设我们不经常更新,但可能会插入这些列表的中间(意味着我们需要更新所有后续行)。
感谢您的帮助/建议/后续问题。
- 答案 *****
如果我理解正确,你最终会尝试以“user,valueid,value”的形式存储三元组,对吧?例如,类似于:
"user123, firstname, Paul",
"user234, lastname, Smith"
(但用户名是固定宽度,valueids是固定宽度)。
并且,您的访问模式如下:“对于用户X,列出接下来的30个值,从valueid Y开始”。是对的吗?这些值应该按valueid返回?
tl; dr版本是你可能应该为每个用户+值一行,而不是自己构建一个复杂的行内分页方案,除非你真的确定它是需要的。
您的两个选项反映了人们在设计HBase模式时遇到的常见问题:我应该“高”还是“宽”?您的第一个架构是“高”:每行代表一个用户的一个值,因此每个用户的表中有很多行;行键是user + valueid,并且(可能)是单列限定符,表示“值”。如果你想按行排序按行排序扫描行(这是我上面的问题,关于这些id是否正确排序),这是很好的。您可以在任何用户+ valueid处开始扫描,阅读下一个30,然后完成。您放弃的是能够为一个用户的所有行提供事务保证,但听起来并不像您需要的那样。通常建议这样做(参见 https://hbase.apache.org/book.html#schema.smackdown )。
您的第二个选项是“宽”:您使用不同的限定符(其中限定符是valueid)将一堆值存储在一行中。这样做的简单方法是将一个用户的所有值存储在一行中。我猜你跳到了“分页”版本,因为你假设在一行中存储数百万列对性能有害,这可能是也可能不是真的;只要你不是在单个请求中尝试做太多,或者做一些事情,比如扫描并返回行中的所有单元格,它就不应该从根本上变得更糟。客户端具有允许您获取特定切片列的方法。
请注意,这两种情况都不会从根本上占用更多的磁盘空间;您只是将标识信息的一部分“移动”到左侧(进入行键,选项1)或右侧(进入选项2中的列限定符)。在封面下,每个键/值仍然存储整个行键和列族名称。 (如果这有点令人困惑,请花一个小时观看Lars George关于理解HBase架构设计的精彩视频: http://www.youtube.com/watch?v=_HLoH_PgrLk )。
正如您所注意到的那样,手动分页版本具有更多复杂性,例如必须跟踪每个页面中有多少内容,如果插入新值则重新进行混洗等。这看起来要复杂得多。它可能在极高的吞吐量下具有一些轻微的速度优势(或缺点!),并且真正了解它的唯一方法是尝试它。如果你没有时间来构建它并进行比较,我的建议是从最简单的选项开始(每个用户一行+值)。开始简单并迭代! :)
-
将
hbase.regionserver.handler.count(在hbase-site.xml中)设置为核心x主轴以实现并发。 -
(可选)将呼叫队列拆分为单独的读写队列,以实现差异化服务。参数
hbase.ipc.server.callqueue.handler.factor指定呼叫队列的数量:-
0表示单个共享队列 -
1表示每个处理程序的一个队列。 -
0和1之间的值分配队列数与处理程序数成比例。例如,.5的值在每两个处理程序之间共享一个队列。
-
-
使用
hbase.ipc.server.callqueue.read.ratio(0.98中的hbase.ipc.server.callqueue.read.share)将呼叫队列分成读写队列:-
0.5表示将有相同数量的读写队列 -
< 0.5更多读取而不是写入 -
> 0.5写入比读取更多
-
-
设置
hbase.ipc.server.callqueue.scan.ratio(HBase 1.0+)将读取呼叫队列拆分为小读取和长读取队列:-
0.5表示将有相同数量的短读取和长读取队列
-
< 0.5更多简短阅读 -
> 0.5更长时间阅读
-
禁用Nagle的算法。延迟的ACK可以累加到RPC往返时间〜200ms。设置以下参数:
-
在Hadoop的
core-site.xml中:-
ipc.server.tcpnodelay = true -
ipc.client.tcpnodelay = true
-
-
在HBase的
hbase-site.xml中:-
hbase.ipc.client.tcpnodelay = true -
hbase.ipc.server.tcpnodelay = true
-
尽可能快地检测regionserver故障。设置以下参数:
-
在
hbase-site.xml中,将zookeeper.session.timeout设置为30秒或更短时间以限制故障检测(20-30秒是一个良好的开始)。 -
检测并避免不健康或失败的HDFS DataNodes:在
hdfs-site.xml和hbase-site.xml中,设置以下参数:-
dfs.namenode.avoid.read.stale.datanode = true -
dfs.namenode.avoid.write.stale.datanode = true
-
当RegionServer通过利用HDFS的短路本地读取工具从HDFS读取时,跳过网络寻找本地块。注意如何在连接的datanode和dfsclient端完成设置 - 即在RegionServer以及两端如何加载hadoop native .so库。将hadoop设置 dfs.client.read.shortcircuit 配置为 true 并配置datanode的 dfs.domain.socket.path 路径并分享dfsclient后然后重新启动,接下来配置regionserver / dfsclient端。
-
在
hbase-site.xml中,设置以下参数:-
dfs.client.read.shortcircuit = true -
dfs.client.read.shortcircuit.skip.checksum = true所以我们不进行双重校验和(HBase自己进行校验和以节省i / OS。有关详细信息,请参阅hbase.regionserver.checksum.verify。 -
dfs.domain.socket.path匹配为datanodes设置的内容。 -
dfs.client.read.shortcircuit.buffer.size = 131072重要的是要避免OOME - 如果未设置,hbase有一个默认值,参见hbase.dfs.client.read.shortcircuit.buffer.size;它的默认值是131072。
-
-
确保数据位置。在
hbase-site.xml中,设置hbase.hstore.min.locality.to.skip.major.compact = 0.7(表示0.7 <= n <= 1) -
确保DataNodes具有足够的块传输处理程序。在
hdfs-site.xml中,设置以下参数:-
dfs.datanode.max.xcievers >= 8192 -
dfs.datanode.handler.count =锭数
-
重启后检查RegionServer日志。如果配置错误,您应该只会看到投诉。否则,短路读取在后台安静地运行。它没有提供指标,因此没有关于其有效性的光学器件,但读取延迟应显示出显着的改进,特别是如果良好的数据位置,大量随机读取和数据集大于可用缓存。
您可能会使用的其他高级配置,尤其是在日志中抱怨短路功能时,包括dfs.client.read.shortcircuit.streams.cache.size和dfs.client.socketcache.capacity。这些选项的文档很少。你必须阅读源代码。
有关短路读取的更多信息,请参阅Colin关于推出的旧博客,如何改进短路本地读取为Hadoop带来更好的性能和安全性。 HDFS-347 问题也引发了一个有趣的读物,显示HDFS社区处于最佳状态(请注意几点评论)。
-
使用CMS收集器:
-XX:+UseConcMarkSweepGC -
保持伊甸园空间尽可能小,以尽量减少平均收集时间。例:
-XX:CMSInitiatingOccupancyFraction=70 -
优化低收集延迟而不是吞吐量:
-Xmn512m -
并行收集伊甸园:
-XX:+UseParNewGC -
避免在压力下收集:
-XX:+UseCMSInitiatingOccupancyOnly -
限制每个请求扫描仪结果大小,所以一切都适合幸存者空间,但没有任期。在
hbase-site.xml中,将hbase.client.scanner.max.result.size设置为伊甸园空间的1/8(使用 -Xmn512m,这是~51MB) -
设置
max.result.sizexhandler.count小于幸存者空间
-
关闭透明大页面(THP):
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag -
设置
vm.swappiness = 0 -
将
vm.min_free_kbytes设置为至少1GB(较大内存系统上为8GB) -
使用
vm.zone_reclaim_mode = 0禁用NUMA区域回收
-
在客户端的
hbase-site.xml中,设置以下参数:-
设置
hbase.client.pause = 1000 -
设置
hbase.client.retries.number = 3 -
如果你想骑过分裂和区域移动,大幅增加
hbase.client.retries.number(&gt; = 20) -
设置RecoverableZookeeper重试次数:
zookeeper.recovery.retry = 1(不重试)
-
-
在服务器端的
hbase-site.xml中,设置Zookeeper会话超时以检测服务器故障:zookeeper.session.timeout⇐30秒(20-30是好的)。
**HBase时间轴一致性(HBASE-10070)**启用只读副本后,区域(副本)的只读副本将分布在群集上。一个RegionServer为默认或主副本提供服务,这是唯一可以为写入提供服务的副本。其他RegionServers服务于辅助副本,遵循主RegionServer,并且只能查看已提交的更新。辅助副本是只读的,但可以在主服务器故障转移时立即提供读取,从而将读取可用性从几秒钟缩短到几毫秒。 Phoenix支持时间轴一致性,自4.4.0提示:
-
部署HBase 1.0.0或更高版本。
-
在服务器端启用时间轴一致的副本。
-
使用以下方法之一设置时间轴一致性:
-
使用
ALTER SESSION SET CONSISTENCY = 'TIMELINE’ -
在JDBC连接字符串中将连接属性
Consistency设置为timeline
-
有关操作和性能架构设计选项的更多信息,请参见性能部分 perf.schema ,例如布隆过滤器,表配置的区域大小,压缩和块大小。