RegionServer 大小调整规则
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>
36.关于列族的数量
HBase 目前不适用于两个或三个列系列以上的任何内容,因此请保持模式中的列族数量较少。目前,冲洗和压缩是基于每个区域进行的,因此如果一个列族携带大量数据带来冲洗,则即使它们携带的数据量很小,相邻的族也将被刷新。当存在许多列族时,冲洗和压缩交互可以产生一堆不必要的 i / o(通过改变刷新和压缩以按列系列工作来解决)。有关压缩的更多信息,请参见压缩。
如果可以在模式中使用,请尝试使用一个列族。在数据访问通常是列作用域的情况下,仅引入第二和第三列族;即您查询一个列族或另一个列族,但通常不是同时查询两个列族。
36.1。 ColumnFamilies 的基数
如果单个表中存在多个 ColumnFamilies,请注意基数(即行数)。如果 ColumnFamilyA 有 100 万行而 ColumnFamilyB 有 10 亿行,则 ColumnFamilyA 的数据可能会分布在许多区域(和 RegionServers)中。这使得 ColumnFamilyA 的大规模扫描效率降低。
37. Rowkey Design
37.1。 Hotspotting
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 的评论中有关避免热点的更多信息的讨论。
37.2。单调递增行键/时间序列数据
在 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 。
37.3。尽量减少行和列的大小
在 HBase 中,值总是用它们的坐标运算;当一个单元格值通过系统时,它将伴随着它的行,列名和时间戳 - 总是如此。如果您的行和列名称很大,特别是与单元格值的大小相比,那么您可能会遇到一些有趣的场景。其中一个是 Marc Limotte 在 HBASE-3551 尾部描述的情况(推荐!)。其中,为了便于随机访问而保留在 HBase 存储文件( StoreFile(HFile))上的索引可能最终占用 HBase 分配的 RAM 的大块,因为单元值坐标很大。上面引用的注释中的标记建议增加块大小,以便存储文件索引中的条目以更大的间隔发生,或者修改表模式,从而使得行和列名称更小。压缩也将使更大的指数。在用户邮件列表中查看主题问题 storefileIndexSize 。
大多数情况下,小的低效率并不重要。不幸的是,这是他们这样做的情况。无论为 ColumnFamilies,属性和行键选择何种模式,它们都可以在数据中重复数十亿次。
有关 HBase 在内部存储数据的详细信息,请参阅 keyvalue ,了解其重要性。
37.3.1。列族
尽量保持 ColumnFamily 名称尽可能小,最好是一个字符(例如“d”表示数据/默认值)。
有关 HBase 在内部存储数据的详细信息,请参阅 KeyValue 以了解其重要性。
37.3.2。属性
尽管详细的属性名称(例如,“myVeryImportantAttribute”)更易于阅读,但是更喜欢较短的属性名称(例如,“via”)来存储在 HBase 中。
See keyvalue for more information on HBase stores data internally to see why this is important.
37.3.3。 Rowkey 长度
保持它们尽可能短,以便它们仍然可用于所需的数据访问(例如 Get vs. Scan)。对数据访问无用的短键并不比具有更好的 get / scan 属性的更长键更好。在设计 rowkeys 时期望权衡。
37.3.4。字节模式
长是 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 尽最大努力打印一个字符串,在这种情况下,它决定只打印十六进制。区域名称中的行键也会发生相同的情况。如果你知道存储了什么就可以了,但如果可以将任意数据放在同一个单元格中,它也可能是不可读的。这是主要的权衡。
37.4。反向时间戳
反向扫描 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]的任何旧行键之前排序,因此是第一个。
将使用此技术而不是使用版本号,其意图是“永久”(或很长时间)保留所有版本,同时通过使用快速获取对任何其他版本的访问权限相同的扫描技术。
37.5。 Rowkeys 和 ColumnFamilies
Rowkeys 的作用域为 ColumnFamilies。因此,在没有冲突的表中存在的每个 ColumnFamily 中可以存在相同的 rowkey。
37.6。 Rowkeys 的不变性
Rowkeys 无法更改。它们可以在表中“更改”的唯一方法是删除行然后重新插入。这是关于 HBase dist-list 的一个相当常见的问题,因此第一次(和/或在插入大量数据之前)使 rowkeys 正确是值得的。
37.7。 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;
}
38.版本数量
38.1。最大版本数
通过 HColumnDescriptor 按列系列配置要存储的最大行版本数。最大版本的默认值为 1.这是一个重要的参数,因为如数据模型部分所述 HBase 执行 _ 而不是 _ 覆盖行值,而是每行按时间存储不同的值(和预选赛)。在主要压缩过程中删除了多余的版本。根据应用需求,可能需要增加或减少最大版本的数量。
不建议将最大版本的数量设置为非常高的级别(例如,数百或更多),除非这些旧值非常珍贵,因为这将大大增加 StoreFile 的大小。
38.2。最小版本数
与最大行版本数一样,通过 HColumnDescriptor 按列系列配置要保留的最小行版本数。最小版本的默认值为 0,表示该功能已禁用。行版本参数的最小数量与生存时间参数一起使用,并且可以与行版本参数的数量组合以允许配置,例如“保留最后 T 分钟的数据,最多 N 个版本,_ 但至少保持 _ 的 M 个版本“(其中 M 是最小行数版本的值,M
39.支持的数据类型
HBase 通过 Put 和 Result 支持“字节输入/字节输出”接口,因此任何可以转换为字节数组的都可以存储为值。输入可以是字符串,数字,复杂对象,甚至是图像,只要它们可以呈现为字节。
值的大小存在实际限制(例如,在 HBase 中存储 10-50MB 对象可能要求太多);在邮件列表中搜索有关此主题的对话。 HBase 中的所有行都符合数据模型,包括版本控制。在进行设计时要考虑到这一点,以及 ColumnFamily 的块大小。
39.1。计数器
值得特别提及的一种受支持的数据类型是“计数器”(即,能够进行数字的原子增量)。参见Table
中的增量。
计数器上的同步在 RegionServer 上完成,而不是在客户端中完成。
40.加入
如果您有多个表,请不要忘记将 Joins 的可能性考虑到架构设计中。
41.生存时间(TTL)
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 设置。
42.保留被删除的细胞
默认情况下,删除标记会延伸回到时间的开头。因此,即使获取或扫描操作指示放置删除标记之前的时间范围, 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 且单元格超出配置的超时等。
43.二级索引和备用查询路径
这部分也可以标题为“如果我的表 rowkey 看起来像 _ 这个 _,但我也想像 _ 那样查询我的表 _。” dist-list 上的一个常见示例是行密钥的格式为“user-timestamp”,但对于某些时间范围的用户活动有报告要求。因此,用户选择很容易,因为它处于键的引导位置,但时间不是。
关于处理这个问题的最佳方法没有一个答案,因为它取决于......
用户数量
数据大小和数据到达率
报告要求的灵活性(例如,完全临时日期选择与预配置范围)
期望的查询执行速度(例如,对于某些特定报告,90 秒可能是合理的,而对于其他人来说可能太长)
并且解决方案也受到群集大小以及解决方案需要多少处理能力的影响。常用技术在下面的小节中。这是一个全面但并非详尽无遗的方法清单。
二级索引需要额外的集群空间和处理应该不足为奇。这正是 RDBMS 中发生的情况,因为创建备用索引的行为需要更新空间和处理周期。在这方面,RDBMS 产品更先进,可以开箱即用地处理备用索引管理。但是,HBase 在更大的数据量下可以更好地扩展,因此这是一项功能权衡。
在实施任何这些方法时,请注意 Apache HBase 性能调整。
另外,请看这个 dist-list 线程中的 David Butler 响应 HBase,mail#user - Stargate + hbase
43.1。过滤查询
根据具体情况,使用客户端请求过滤器可能是合适的。在这种情况下,不会创建二级索引。但是,不要尝试从应用程序(即单线程客户端)对这样的大型表进行全扫描。
43.2。定期更新二级索引
可以在另一个表中创建二级索引,该表通过 MapReduce 作业定期更新。该作业可以在日内执行,但根据加载策略,它仍可能与主数据表不同步。
有关详细信息,请参阅 mapreduce.example.readwrite 。
43.3。双写二级索引
另一种策略是在向集群发布数据时构建二级索引(例如,写入数据表,写入索引表)。如果在数据表已经存在之后采用这种方法,那么使用 MapReduce 作业的二级索引将需要自举(参见 secondary.indexes.periodic )。
43.4。汇总表
如果时间范围非常宽(例如,一年的报告)并且数据量很大,则汇总表是一种常用方法。这些将通过 MapReduce 作业生成到另一个表中。
有关详细信息,请参阅 mapreduce.example.summary 。
43.5。协处理器二级索引
协处理器就像 RDBMS 触发器一样。这些是在 0.92 中添加的。有关更多信息,请参阅协处理器
44.制约因素
HBase 目前支持传统(SQL)数据库用语中的“约束”。约束的建议用法是强制执行表中属性的业务规则(例如,确保值在 1-10 范围内)。约束也可用于强制引用完整性,但强烈建议不要这样做,因为它会显着降低启用完整性检查的表的写入吞吐量。自版本 0.94 起,可以在 Constraint 中找到有关使用约束的详细文档。
45.图式设计案例研究
下面将介绍 HBase 的一些典型数据摄取用例,以及如何处理 rowkey 设计和构造。注意:这只是潜在方法的说明,而不是详尽的清单。了解您的数据,了解您的处理要求。
在阅读这些案例研究之前,强烈建议您先阅读 HBase 和 Schema Design 的其余部分。
描述了以下案例研究:
记录数据/时间序列数据
在类固醇上记录数据/时间序列
顾客订单
高/宽/中图式设计
列表数据
45.1。案例研究 - 日志数据和时间序列数据
假设正在收集以下数据元素。
主机名
时间戳
记录事件
值/消息
我们可以将它们存储在名为 LOG_DATA 的 HBase 表中,但是 rowkey 是什么?从这些属性中,rowkey 将是 hostname,timestamp 和 log-event 的某种组合 - 但具体是什么?
45.1.1。 Rowkey 主位的时间戳
rowkey [timestamp][hostname][log-event]
遭受单调递增行键/时间序列数据中描述的单调递增的 rowkey 问题。
通过对时间戳执行 mod 操作,在 dist-lists 中经常提到的关于“bucketing”时间戳的另一种模式。如果面向时间的扫描很重要,这可能是一种有用的方法。必须注意桶的数量,因为这将需要相同数量的扫描才能返回结果。
long bucket = timestamp % numBuckets;
构建:
[bucket][timestamp][hostname][log-event]
如上所述,要选择特定时间范围的数据,需要为每个存储桶执行扫描。例如,100 个存储桶将在密钥空间中提供广泛的分布,但是需要 100 个扫描才能获得单个时间戳的数据,因此需要权衡利弊。
45.1.2。主机在 Rowkey 主位
如果存在大量主机以在密钥空间上传播写入和读取,则 rowkey [hostname][log-event][timestamp]
是候选者。如果按主机名扫描是优先事项,则此方法很有用。
45.1.3。时间戳或反向时间戳?
如果最重要的访问路径是拉取最近的事件,那么将时间戳存储为反向时间戳(例如,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.
45.1.4。可变长度或固定长度的 Rowkeys?
重要的是要记住,在 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 的原始值可以存储为列。
45.2。案例研究 - 类固醇的日志数据和时间序列数据
这实际上是 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 使这成为可能。
45.3。案例研究 - 客户/订单
假设 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
45.3.1。单表?多个表?
传统的设计方法将为 CUSTOMER 和 SALES 提供单独的表。另一种选择是将多种记录类型打包到一个表中(例如,CUSTOMER ++)。
客户记录类型 Rowkey:
[顾客 ID]
[type] =表示客户记录类型为“1”的类型
订单记录类型 Rowkey:
[customer-id]
[type] =表示订单记录类型的“2”的类型
[订购]
这种特殊的 CUSTOMER ++方法的优点是可以按客户 ID 组织许多不同的记录类型(例如,单次扫描可以获得有关该客户的所有信息)。缺点是扫描特定记录类型并不容易。
45.3.2。订单对象设计
现在我们需要解决如何为 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 非规范化)
这种方法的优点包括不太复杂的对象层次结构,但其中一个缺点是,如果任何此类信息发生更改,更新会变得更加复杂。
对象 BLOB
使用这种方法,整个 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 这样的框架来处理像这样的自定义对象。
45.4。案例研究 - “高/宽/中”架构设计 Smackdown
本节将介绍出现在 dist 列表中的其他架构设计问题,特别是有关高表和宽表的问题。这些是一般准则,而不是法律 - 每个应用程序必须考虑自己的需求。
45.4.1。行与版本
一个常见的问题是,是否应该选择行或 HBase 的内置版本。上下文通常是要保留行的“很多”版本的地方(例如,它明显高于 HBase 默认的 1 个最大版本)。行方法需要在 rowkey 的某些部分中存储时间戳,以便它们不会在每次连续更新时覆盖。
偏好:行(一般来说)。
45.4.2。行与列
另一个常见问题是,是否应该更喜欢行或列。上下文通常在宽表的极端情况下,例如具有 1 行具有 100 万个属性,或者 1 百万行,每行 1 列。
偏好:行(一般来说)。需要明确的是,本指南在非常广泛的情况下,而不是在需要存储几十或一百列的标准用例中。但是这两个选项之间也有一条中间路径,那就是“Rows as Columns”。
45.4.3。行作为列
行与列之间的中间路径是打包数据,对于某些行,这些数据将成为列中的单独行。 OpenTSDB 是这种情况的最佳示例,其中单行表示定义的时间范围,然后将离散事件视为列。这种方法通常更复杂,可能需要重新编写数据的额外复杂性,但具有 I / O 效率的优势。有关此方法的概述,请参见 schema.casestudies.log-steroids 。
45.5。案例研究 - 列出数据
以下是来自用户 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 )。
您的第二个选项是“宽”:您使用不同的限定符(其中限定符是 valueid)将一堆值存储在一行中。这样做的简单方法是将一个用户的所有值存储在一行中。我猜你跳到了“分页”版本,因为你假设在一行中存储数百万列对性能有害,这可能是也可能不是真的;只要你不是在单个请求中尝试做太多,或者做一些事情,比如扫描并返回行中的所有单元格,它就不应该从根本上变得更糟。客户端具有允许您获取特定切片列的方法。
请注意,这两种情况都不会从根本上占用更多的磁盘空间;您只是将标识信息的一部分“移动”到左侧(进入行键,选项 1)或右侧(进入选项 2 中的列限定符)。在封面下,每个键/值仍然存储整个行键和列族名称。 (如果这有点令人困惑,请花一个小时观看 Lars George 关于理解 HBase 架构设计的精彩视频: http://www.youtube.com/watch?v=_HLoH_PgrLk )。
正如您所注意到的那样,手动分页版本具有更多复杂性,例如必须跟踪每个页面中有多少内容,如果插入新值则重新进行混洗等。这看起来要复杂得多。它可能在极高的吞吐量下具有一些轻微的速度优势(或缺点!),并且真正了解它的唯一方法是尝试它。如果你没有时间来构建它并进行比较,我的建议是从最简单的选项开始(每个用户一行+值)。开始简单并迭代! :)
46.运营和性能配置选项
46.1。调整 HBase 服务器 RPC 处理
将
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
更长时间阅读
46.2。禁用 Nagle for RPC
禁用 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
46.3。限制服务器故障影响
尽可能快地检测 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
46.4。在服务器端优化以实现低延迟
当 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 社区处于最佳状态(请注意几点评论)。
46.5。 JVM 调优
46.5.1。调整 JVM GC 以获得低收集延迟
使用 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.size
xhandler.count
小于幸存者空间
46.5.2。操作系统级调整
关闭透明大页面(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 区域回收
47.特殊情况
47.1。对于快速失败比等待更好的应用
在客户端的
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 是好的)。
47.2。适用于可以容忍稍微过时信息的应用程序
**HBase 时间轴一致性(HBASE-10070)**启用只读副本后,区域(副本)的只读副本将分布在群集上。一个 RegionServer 为默认或主副本提供服务,这是唯一可以为写入提供服务的副本。其他 RegionServers 服务于辅助副本,遵循主 RegionServer,并且只能查看已提交的更新。辅助副本是只读的,但可以在主服务器故障转移时立即提供读取,从而将读取可用性从几秒钟缩短到几毫秒。 Phoenix 支持时间轴一致性,自 4.4.0 提示:
部署 HBase 1.0.0 或更高版本。
在服务器端启用时间轴一致的副本。
使用以下方法之一设置时间轴一致性:
使用
ALTER SESSION SET CONSISTENCY = 'TIMELINE’
在 JDBC 连接字符串中将连接属性
Consistency
设置为timeline
47.3。更多信息
有关操作和性能架构设计选项的更多信息,请参见性能部分 perf.schema ,例如布隆过滤器,表配置的区域大小,压缩和块大小。