第3章 Hbase数据存储模型与工作组件
Data格式设计的的总体原则是按照需求要求,依据Hbase性能的相关标准规范和文件,并遵循“统一规范、统一数据模型、统一规划集群、分步实施”的原则,注重实际应用,力求设计方案达到资源利用最大化,以及方案切实可行。
3.1 Data Model
3.1.1 数据物理格式基本模型
在HBase中,数据存储在具有行和列的表中。这是与关系数据库(RDBMS)重叠的术语,但这不是一个有用的类比。相反,将HBase表视为多维地图会很有帮助.
一个HBase表由多行组成.
Row
HBase中的一行由一个行键和一个或多个与其相关的值组成。行存储时按行键字母顺序排序。为此,行键的设计非常重要。目标是以相关行彼此靠近的方式存储数据。常见的行键模式是一个网站域。如果你的行键是域,你可能应该将它们存储在相反的位置(org.apache.www,org.apache.mail,org.apache.jira)。这样,所有的Apache域在表中都彼此靠近,而不是根据子域的第一个字母来分布.
Column
HBase中的列由一个列族和一个列限定符组成,这些列由一个:(冒号)字符分隔.
Column Family
经常出于性能原因,列系列物理地共同定位一组列和它们的值。 每个列族都有一组存储属性,比如它的值是否应该被缓存在内存中,数据如何被压缩或者其行编码被编码等等。表中的每一行都有相同的列族,但给定的行可能不会在给定的列族中存储任何内容.
Column Qualifier
列限定符被添加到列族以提供给定数据的索引。给定一个列族内容,列限定符可能是content:html,另一个可能是content:pdf。 虽然列族固定在表创建,列限定符是可变的,并可能有很大的不同行之间.
Cell
单元格是行,列族和列限定符的组合,并包含值和时间戳,表示值的版本。
Timestamp
时间戳与每个值一起写入,并且是给定版本的值的标识符。 默认情况下,时间戳表示写入数据时RegionServer上的时间,但在将数据放入单元格时可以指定不同的时间戳值。
3.1.2 概念模型
您可以在Jim R. Wilson的博客文章Understanding HBase and BigTable(http://jimbojw.com/#understanding%20hbase)中阅读HBase数据模型的一个非常容易理解的解释。 另一个很好的解释是在Amandeep Khurana的PDF基本模式设计简介(http://0b4af6cdc2f0c5998459-c0245c5c937c5dedcca3f1764ecc9b2f.r43.cf2.rackcdn.com/9353-login1210_khurana.pdf).
通过阅读不同的角度来了解HBase架构设计。 链接的文章与本节中的信息涵盖了相同的内容。
The following example is a slightly modified form of the one on page 2 of the BigTable
(http://research.google.com/archive/bigtable.html)文件。 有一个名为webtable的表格,其中包含两行
(com.cnn.www和com.example.www)和三个名为内容,主播和人员的专栏家庭。 在这个例子中,对于第一行(com.cnn.www),anchor包含两列(anchor:cssnsi.com,anchor:my.look.ca),内容包含一列(contents:html)。 此示例包含具有行密钥com.cnn.www的行的5个版本以及具有行密钥com.example.www的行的一个版本。 内容:html列限定符包含给定网站的整个HTML。 锚列家族的限定符每个都包含链接到该行所代表的站点的外部站点,以及在其链接的锚点中使用的文本。 人员专栏代表与该网站相关的人员.
Column Names
|
按照惯例,列名由其列名前缀和限定符组成。 例如,列内容:html由列族内容和html限定符组成。 冒号字符(:)从列族限定符分隔列族.
Table 5. Table webtable
Row Key |
Time Stamp |
ColumnFamily contents |
ColumnFamily anchor |
ColumnFamily people |
"com.cnn.www" |
t9 |
|
anchor:cnnsi.com = "CNN" |
|
"com.cnn.www" |
t8 |
|
anchor:my.look.ca = "CNN.com" |
|
"com.cnn.www" |
t6 |
contents:html = " <html>…" |
|
|
"com.cnn.www" |
t5 |
contents:html = " <html>…" |
|
|
"com.cnn.www" |
t3 |
contents:html = " <html>…" |
|
|
"com.example.www" |
t5 |
contents:html = " <html>…" |
|
people:author = "John Doe" |
HBase中的这个表中的单元格看起来是空的,并不占用空间或实际存在。 这正是HBase“稀疏”的原因。 表格视图并不是查看HBase数据的唯一可能的方法,甚至是最准确的。 以下表示与多维地图相同的信息。 这只是一个模拟的目的,可能不是严格的准确.
{
"com.cnn.www": { contents: {
t6: contents:html: "<html>..." t5: contents:html: "<html>..." t3: contents:html: "<html>..."
}
anchor: {
t9: anchor:cnnsi.com = "CNN"
t8: anchor:my.look.ca = "CNN.com"
}
people: {}
}
"com.example.www": { contents: {
t5: contents:html: "<html>..."
}
anchor: {} people: {
t5: people:author: "John Doe"
}
}
}
Although at a conceptual level tables may be viewed as a sparse set of rows, they are physically stored by column family. A new column qualifier (column_family:column_qualifier) can be added to an existing column family at any time.
Table 6. ColumnFamily anchor
Row Key |
Time Stamp |
Column Family anchor |
"com.cnn.www" |
t9 |
anchor:cnnsi.com = "CNN" |
"com.cnn.www" |
t8 |
anchor:my.look.ca = "CNN.com" |
Table 7. ColumnFamily contents
Row Key |
Time Stamp |
ColumnFamily contents: |
"com.cnn.www" |
t6 |
contents:html = "<html>…" |
"com.cnn.www" |
t5 |
contents:html = "<html>…" |
"com.cnn.www" |
t3 |
contents:html = "<html>…" |
概念视图中显示的空单元根本不存储。 因此,在时间戳t8处请求内容:html列的值将不返回任何值。 同样,在时间戳t9请求anchor:my.look.ca值也不会返回任何值。 但是,如果未提供时间戳,则会返回特定列的最新值。 给定多个版本,最近的也是第一个找到的,因为时间戳按降序存储。 因此,如果没有指定时间戳,则对com.cnn.www行中的所有列的值的请求将是:内容:来自时间戳t6的html,锚定的值:来自时间戳t9的cnnsi.com, 主播:来自时间戳t8的my.look.ca。
有关Apache HBase如何存储数据的内部信息,请参阅regions.arch。
名称空间是与关系数据库系统中的数据库类似的表的逻辑分组。 这种抽象为即将出现的多租户相关功能奠定了基础:
配额管理(H BASE-8410(https://issues.apache.org/jira/browse/HBASE-8410)) - 限制名称空间可消耗的资源量(即区域,表)。
命名空间安全管理(H BASE-9206(https://issues.apache.org/jira/browse/HBASE-9206)) - 为租户提供另一级别的安全管理。
区域服务器组(H BASE-6721(https://issues.apache.org/jira/browse/HBASE-6721)) - 名称空间/表可以固定在RegionServers的子集上,从而保证粗略的隔离级别。
3.1.3 命名空间管理(NameSpace Management)
Namespace management
名称空间可以创建,删除或更改。 名称空间成员资格是在创建表时通过指定表单的完全限定表名来确定的:
<table namespace>:<table qualifier>
XML
Example 11. Examples
Predefined namespaces
有两个预定义的特殊名称空间:
hbase - 系统名称空间,用于包含HBase内部表
默认 - 没有显式指定名称空间的表将会
automatically fall into this namespace Example 12. Examples
3.1.4 Table
表在架构定义时被预先声明
行键是未解释的字节。 行按字典顺序排列,最低顺序首先出现在表中。 空字节数组用于表示表名字空间的开始和结束
Apache HBase中的列被分组为列族。 列族的所有列成员具有相同的前缀。 例如,专栏课程:历史和课程:数学都是课程专栏家族的成员。 冒号字符(:)从列族限定符分隔列族。 列族前缀必须由可打印字符组成。 限定尾部,列族限定符可以由任意字节组成。 列族必须是
在架构定义时预先声明,而不需要在架构时定义列,但可以在表启动并运行时动态地变换列。
在物理上,所有列家族成员一起存储在文件系统上。 由于调音和存储规范是在列族级完成的,因此建议所有列族成员具有相同的一般访问模式和大小特征。
3.1.5 单元格
{row,column,version}元组精确地指定了HBase中的单元格。 单元格内容是未解释的字节
四个主要的数据模型操作是Get,Put,Scan和Delete。操作通过表应用
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Table.html)实例。
Get
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Get.html)返回指定行的属性。获取通过Table.get执行(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Table.html#get-org.apache.hadoop.hbase.client.Get-)
Put
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Put.html)将新行添加到表中(如果键是新的)或者可以更新现有行(如果键已经存在)。 Puts通过Table.put执行(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Table.html#put-org.apache.hadoop.hbase.client.Put-)(非writeBuffer)或Table.batch(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Table.html#batch-java.util.List-java.lang.Object:A - )(非writeBuffer)
Scan
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Scan.html)允许针对指定属性遍历多行。
以下是扫描表格实例的示例。假设一个表中填充了行“键1”,“行2”,“行3”,然后是具有键“abc1”,“abc2”和“abc3”的另一组行。以下示例显示如何设置Scan实例以返回以“row”开头的行。
public static final byte [] CF =“cf”.getBytes();
public static final byte [] ATTR =“attr”.getBytes();
...
Table table = ... //实例化一个Table实例
Scan scan = new Scan();
scan.addColumn(CF,ATTR);
scan.setRowPrefixFilter(Bytes.toBytes( “行”));
ResultScanner rs = table.getScanner(scan);尝试{
for(Result r = rs.next(); r!= null; r = rs.next())
{
//处理结果...
JAVA
}
} finally {
rs.close(); //总是关闭ResultScanner!
}
请注意,通常,指定扫描的特定停止点的最简单方法是使用InclusiveStopFilter
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/filter/InclusiveStopFilter.html)类。
Delete
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Delete.html)从表中删除一行。删除通过Table.delete执行(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Table.html#delete-org.apache.hadoop.hbase.client.Delete-)
HBase不会修改数据,因此通过创建名为墓碑的新标记来处理删除操作。这些墓碑,以及死亡的价值,都在重大的压缩中清理干净。
有关删除列版本的更多信息,请参阅version.delete,并参阅压缩以获取有关压缩的更多信息。
{row,column,version}元组精确地指定了HBase中的单元格。可能有无限数量的单元格,其中行和列是相同的,但单元格地址仅在其版本维度上有所不同。
虽然行和列键以字节表示,但版本是使用长整数指定的。通常,这个long包含时间实例,例如java.util.Date.getTime()或System.currentTimeMillis()返回的时间实例,即:1970年1月1日UTC和当前时间和午夜之间的差值(以毫秒为单位)。
HBase版本维度按降序存储,因此从存储文件读取时,会先查找最新的值。
在HBase中,单元版本的语义存在很多混淆。尤其是:
如果对一个单元格的多次写入具有相同的版本,则只有最后一次写入是可以读取的。
以非递增版本顺序编写单元格是可以的。
下面我们描述HBase当前的版本维度如何工作。有关HBase版本的讨论,请参阅HBASE-2406(https://issues.apache.org/jira/browse/HBASE-2406)。 HBase中的弯曲时间(https://www.ngdata.com/bending-time-in-hbase/)使得我们可以很好地阅读HBase中的版本或时间维度。它的版本比这里提供的更多。在撰写本文时,文章中提到的限制覆盖现有时间戳的值不再适用于HBase。本节基本上是Bruno Dumon撰写的这篇文章的简介。
对管理员的行为顺序进行了一个系统的描述。管理员操作时序图如图3-5所示。
指定存储版本号(Specifying the Number of Versions to Store)
为给定列存储的最大版本数是列架构的一部分,可在创建表时或通过alter命令通过HColumnDescriptor.DEFAULT_VERSIONS指定。 在HBase 0.96之前,保留的默认版本数是3,但是在0.96和更新版本中已经更改为1。
例13.修改一个列族的最大版本数量
示例14.修改列族的最小版本数
从HBase 0.98.2开始,您可以通过在hbase-site.xml中设置hbase.column.max.version来为所有新创建列保留的最大版本数指定全局默认值。 请参阅hbase.column.max.version。
3.1.6 Version and Hbase Operations
在本节中,我们将了解每个核心HBase操作的版本维度的行为。
获取/扫描(Get/Scan)
获取在Scans之上实现。 Get(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Get.html)的以下讨论同样适用于Scans(https://hbase.apache.org/apidocs/org /apache/hadoop/hbase/client/Scan.html)。
默认情况下,即如果不指定显式版本,则在执行get操作时,会返回版本值最大的单元格(可能是也可能不是最新的,请参阅后面的内容)。默认行为可以通过以下方式进行修改:
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Get.html#setMaxVersions--)
要返回除最新版本以外的其他版本,请参阅Get.setTimeRange()
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Get.html#setTimeRange-long-long-)
要检索小于或等于给定值的最新版本,从而在某个时间点给出记录的“最新”状态,只需使用从0到所需版本的范围,并将最大版本设置为1 。
默认获取示例
以下获取将只检索行的当前版本
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(Bytes.toBytes("row1"));
Result r = table.get(get);
byte[] b = r.getValue(CF, ATTR); // returns current version of value
版本化获取示例
以下Get将返回该行的最后3个版本。
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(Bytes.toBytes("row1"));
get.setMaxVersions(3); // will return last 3 versions of row
Result r = table.get(get);
byte[] b = r.getValue(CF, ATTR); // returns current version of value
List<KeyValue> kv = r.getColumn(CF, ATTR); // returns all versions of this column
Put
做一个PUT总是创建一个新的版本的单元格,在一定的时间戳。默认情况下,系统使用服务器的currentTimeMillis,但您可以在每列级别上自己指定版本(=长整数)。这意味着您可以分配过去或未来的时间,或将长时间值用于非时间目的。
要覆盖现有值,请将其放入与要覆盖的单元格完全相同的行,列和版本中。
隐式版本示例
HBase会在当前时间隐式地对以下Put进行版本管理。
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put(Bytes.toBytes(row));
put.add(CF, ATTR, Bytes.toBytes( data));
table.put(put);
显式版本示例
以下Put具有明确设置的版本时间戳。
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put( Bytes.toBytes(row));
long explicitTimeInMs = 555; // just an example
put.add(CF, ATTR, explicitTimeInMs, Bytes.toBytes(data));
table.put(put);
注意:版本时间戳由HBase内部使用,用于诸如生存时间计算之类的事情。通常最好避免自己设置时间戳。优先使用该行的单独时间戳属性,或者将时间戳记作为行键的一部分,或者同时使用两者。
删除(Delete)
有三种不同类型的内部删除标记。请参阅Lars Hofhansl的博客,讨论他尝试添加另一项,HBase扫描:前缀删除标记(http://hadoop-hbase.blogspot.com/2012/01/scanning-in-hbase.html)。
删除列:适用于所有版本的列。
删除系列:针对特定ColumnFamily的所有列
当删除整行时,HBase将在内部为每个ColumnFamily(即不是每个单独的列)创建一个逻辑删除。
通过创建墓碑标记来删除作品。例如,假设我们想要删除一行。为此,您可以指定一个版本,否则默认使用currentTimeMillis。这意味着删除版本小于或等于此版本的所有单元格。 HBase从不修改数据,例如删除不会立即删除(或标记为已删除)存储文件中对应于删除条件的条目。相反,写一个所谓的墓碑,它会掩盖删除的值。当HBase进行重大压缩时,墓碑将被处理以实际移除死亡值以及墓碑本身。如果您在删除行时指定的版本大于行中任何值的版本,则可以考虑删除整行。
有关删除和版本控制如何交互的信息性讨论,请参阅线程Put w / timestamp→Deleteall→让w / timestamp失败(http://comments.gmane.org/gmane.comp.java.hadoop.hbase.user/28421 )在用户邮件列表上。
另请参阅keyvalue获取有关内部的更多信息
3.1.7 目前的局限性
删除蒙板
删除掩码放入,甚至在输入删除后发生。请参阅HBASE-2256(https://issues.apache.org/jira/browse/HBASE-2256)。请记住,删除操作会写入逻辑删除,只有在下一次主要压缩运行后才会消失。假设你删除了所有东西After T.在这之后,你用一个时间戳do做一个新的放置
T.即使它发生在删除之后,它也会被删除逻辑删除。执行投入不会失败,但是当你做出投注时,你会注意到投注没有任何效果。重大压缩后,它将再次开始工作。这些问题不应该是一个问题,如果您使用永远增加的版本来连接新的投入。但即使您不在乎时间,也可能发生这种情况:只需删除并立即放置在对方之后,并且有可能在同一毫秒内发生。
主要的压缩改变了查询结果
...在t1,t2和t3创建三个单元版本,最大版本设置为2.因此,获取所有版本时,只会返回t2和t3的值。但是如果在t2或t3删除版本,则t1中的版本将再次出现。
显然,一旦大规模压缩已经结束,这种行为就不会再出现了......(请参阅HBase中的垃圾收集时间(https://www.ngdata.com/bending-time-in-hbase/)。)
所有数据模型操作HBase以排序顺序返回数据。首先按行,然后按ColumnFamily,然后是列限定符,最后是时间戳(反向排序,因此最新的记录会首先返回)。
ColumnFamily的内部KeyValue实例之外不存储列元数据。因此,尽管HBase不仅可以支持每行大量的列,还可以支持行之间的异构列,但您有责任跟踪列名。
获得ColumnFamily存在的一组完整列的唯一方法是处理所有行。有关HBase如何在内部存储数据的更多信息,请参阅keyvalue。
HBase是否支持连接是dist-list上的一个常见问题,并且有一个简单的答案:它不是,至少在RDBMS'支持它们的方式中(例如,使用SQL中的等连接或外连接)。如本章所述,HBase中读取的数据模型操作是Get和Scan。
但是,这并不意味着您的应用程序不支持等效的连接功能,但您必须自己动手。两个主要策略是在写入HBase时对数据进行非规格化,或者在您的应用程序或MapReduce代码中使用查找表并进行HBase表之间的连接(并且正如RDBMS'演示的那样,有几种策略取决于HBase的大小表格,例如,嵌套循环与散列连接)。那么最好的方法是什么?这取决于你想要做什么,因此没有一个适用于每个用例的答案。
请参阅ACID语义。 Lars Hofhansl也在HBase上写了关于ACID的注释
(http://hadoop-hbase.blogspot.com/2012/03/acid-in-hbase.html)。
3.1.7 Hbase and Schema Design
在Ian Varley的硕士论文“无关系:非关系数据库的混合描述”(http://ianvarley.com/UT/MR/Varley_MastersReport_Full_2009)中可以找到有关各种非rdbms数据存储库的优缺点建模的详细介绍-08-07.pdf)。它现在有点过时了,但是如果您有一段关于HBase架构建模与RDBMS中的完成方式不同的话,可以阅读好背景知识。此外,请阅读HBase内部存储数据的keyvalue以及schema.casestudies部分。
Cloud Bigtable网站上的文档,设计您的模式(https://cloud.google.com/bigtable/docs/schema-design)是相关的,很好地完成,并且在那里学到的经验同样适用于HBase的土地;只需将所有引用值除以〜10即可得到适用于HBase的内容:例如它说单个值的大小可以达到〜10MB,HBase可以做类似的工作 - 如果可能的话,可以尽可能小一些 - 以及Cloud Bigtable中最多有100个列族的位置,在HBase上建模时会想到~10。
另请参阅Robert Yokota的H Base Application Archetypes(https://blogs.apache.org/hbase/entry/hbase-application-archetypes-redux)(由其他HBasers完成的工作更新),以便对用例进行有用的分类在HBase模型之上做得很好。
可以使用Apache HBase Shell或使用Admin来创建或更新HBase架构
(https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Admin.html)。
进行ColumnFamily修改时,必须禁用(disable)表格,例如:
Configuration config = HBaseConfiguration.create();
Admin admin = new Admin(conf);
TableName table = TableName.valueOf("myTable");
admin.disableTable(table);
HColumnDescriptor cf1 = ...;
admin.addColumn(table, cf1); // adding new ColumnFamily HColumnDescriptor cf2 = ...;
admin.modifyColumn(table, cf2); // modifying existing ColumnFamily admin.enableTable(table);
有关配置客户端连接的更多信息,请参阅客户端依赖关系
0.92.x代码库支持在线模式更改,但0.90.x代码库要求禁用表。
Schema Updates
当对Tables或ColumnFamilies进行更改时(例如,区域大小,块大小),这些更改将在下次进行重大压缩并重新写入StoreFiles时生效。
有关StoreFiles的更多信息,请参阅商店。
有许多不同的数据集,具有不同的访问模式和服务级别期望。因此,这些经验法则只是一个概述。阅读本章的其余部分以获得更多详细信息。
目标是规模在10到50 GB之间的区域。
目标是让单元格不超过10 MB,如果使用则为50 MB。否则,请考虑将您的单元格数据存储在HDFS中,并将指针存储在HBase中。
典型的架构每个表有1到3个列族。 HBase表不应该被设计成模仿RDBMS表。
对于具有1或2列系列的表格,大约50-100个区域是很好的数字。请记住,某个地区是列家族的连续部分。
尽量缩短列名。为每个值存储列名(忽略前缀编码)。它们不应该像典型的RDBMS一样具有自我记录和描述性。
如果您正在存储基于时间的机器数据或日志记录信息,并且行密钥基于设备ID或服务ID加上时间,则最终可能会出现一种模式,即旧数据区域在某个时间段之后永远不会有额外的写入操作。在这种情况下,最终会有少量活动区域和大量没有新写入的较旧区域。对于这些情况,您可以容忍更多区域,因为您的资源消耗仅由活动区域驱动。
如果只有一个列族忙于写入,则只有该列族兼容内存。分配资源时请注意写入模式。
3.2 RegionServer调整大拇指规则
Lars Hofhansl写了一篇关于RegionServer内存大小的优秀博文(http://hadoop-hbase.blogspot.com/2013/01/hbase-region-server-memory-sizing.html)。 结果是你可能需要比你想象的更多的记忆。 他进入了区域大小,memstore大小,HDFS复制因子和其他事物的影响。
“就我个人而言,我会把每台机器的最大磁盘空间放在6T左右的HBase上,除非你有一个非常繁重的工作量。 在那种情况下,Java堆应该是
32GB(20G区域,128M内存,其余默认值)。
- LarsHofhansl http://hadoop-hbase.blogspot.com/2013/01/hbase-region-server-memory-sizing.html
HBase目前对于两列或三列以上的任何项目都不太好,所以要保持架构中的列族数目不多。 目前,Flush和压缩是在每个地区的基础上完成的,所以如果一个专栏家庭正在承载大量的Flush数据,即使所携带的数据量很小,也会冲刷相邻的家庭。 当存在许多色谱柱系列时,Flush和压缩相互作用可能会造成一堆不必要的I / O(要通过改变Flush和压缩以每列家族为基础来处理)。 有关压缩的更多信息,请参阅压缩.
如果你可以在你的模式中尝试使用一个列家族。 在数据访问通常是列的范围的情况下,只引入第二和第三列家族; 即查询一个列家族或另一个列族,但通常不是同一时间.
3.2.1 Hotspotting
HBase中的行按行按键按字典排序。 这种设计优化了扫描,允许您将相关的行或彼此靠近的行一起读取。 然而,设计不佳的行键是热点的常见来源。 当大量客户端通信量指向群集中的一个节点或仅少数几个节点时,会发生热点。 此流量可能表示读取,写入或其他操作。 流量压倒负责托管该区域的单个机器,导致性能下降并可能导致区域不可用性。 这也会对由同一台区域服务器托管的其他区域产生不利影响,因为该主机无法为请求的负载提供服务。 设计数据访问模式以使群集得到充分和均匀的利用非常重要.
为了防止热点写入,设计行键,使真正需要在同一个区域的行,但在更大的图片,数据被写入整个群集的多个区域,而不是一次一个。 以下描述了一些避免热点的常用技术,以及它们的一些优点和缺点.
Salting
从这个意义上来说,腌制与密码学无关,而是指将随机数据添加到行密钥的开头。 在这种情况下,salting是指为行键添加一个随机分配的前缀,使其排序与其他方式不同。 可能的前缀数量对应于您想要传播数据的区域数量。 如果你有几个“热”行键模式,反复出现在其他更均匀分布的行中,盐分可能会有帮助。 请考虑下面的示例,其中显示了salting可以跨多个RegionServer传播写入负载,并说明了读取的一些负面影响.
Example 15. Salting Example 假设您有以下的行键列表,并且您的表被分割,以便每个字母的字母都有一个区域。 前缀“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
由于这个任务是随机的,如果你想按字典顺序检索行,你需要做更多的工作。 以这种方式,腌制尝试增加写入吞吐量,但在读取期间具有成本. |
Hashing
您可以使用单向散列,而不是随机分配,这样可以使给定的行始终以相同的前缀“被盐化”,从而将负载分散到RegionServers上,但允许在读取期间进行预测。 使用确定性散列允许客户端重建完整的rowkey并使用Get操作正常检索该行.
考虑到上面salting示例中的相同情况,您可以改为应用单向散列,这将导致密钥为foo0003的行始终可预见地接收前缀。 然后,为了检索该行,您已经知道了密钥。 例如,您也可以优化事物,以便某些键对总是在相同的区域中。
反转关键
防止热点的第三种常用技巧是反转固定宽度或数字行键,以便最经常变化的部分(最低有效数字)首先出现。 这有效地使行键随机化,但牺牲了行排序属性。
请参阅https://communities.intel.com/community/itpeernetwork/datastack/blog/2013/11/10/discussion-on-designing-hbase-tables以及关于盐分表的文章(https://phoenix.apache.org /salted.html)以及HBASE-11682(https://issues.apache.org/jira/browse/HBASE-11682)评论中的讨论以获取有关避免热点的更多信息。
3.2.2 单调递增行键/时间序列数据
Tom White的书“Hadoop:权威指南”(http://oreilly.com/catalog/9780596521981)(O'Reilly)的HBase一章中,有一个关于注意导入过程走向的现象的优化说明与所有客户端一起敲击表格的区域(因此,单个节点),然后移动到下一个区域等等。对于单调递增的行键(即使用时间戳),这将发生。关于为什么单调递增的行键在BigTable类数据存储中存在问题的原因,请参阅IKai Lan的这部漫画:单递增的值不好(http://ikaisays.com/2011/01/25/app-engine-datastore-tip-monotonically-增加值,是坏/)。通过将输入记录随机化为不按排序顺序排列,可以减轻单调增加密钥带来的单个区域的堆积,但通常最好避免使用时间戳或序列(例如1,2,3)作为行键。
如果您确实需要将时间序列数据上传到HBase中,您应该研究OpenTSDB(http://opentsdb.net/)作为一个成功的例子。它有一个页面描述它在HBase中使用的模式(http://opentsdb.net/schema.html)。 OpenTSDB中的关键格式实际上是[metric_type] [event_timestamp],它会在第一眼看起来与之前关于不使用时间戳作为关键的建议相矛盾。但是,区别在于时间戳不在密钥的主导位置,并且设计假设是有几十个或几百个(或更多)不同的度量标准类型。因此,即使连续输入数据和多种度量类型,Puts也会分布在表中不同的地区。
有关rowkey设计示例,请参阅schema.casestudies。
3.2.3 尽量减少行和列的大小
在HBase中,值总是随着坐标而运行;当单元格值通过系统时,它将伴随其行,列名称和时间戳 - 始终。如果你的行和列的名字很大,特别是与单元格的大小相比,那么你可能会遇到一些有趣的场景。其中之一就是Marc Limotte在HBASE-3551尾部描述的情况
(https://issues.apache.org/jira/browse/HBASE-3551?page=com.atlassian.jira.plugin.system.issuetabpanels:comment- tabpanel&focusedCommentId = 13005272#comment-13005272)
(推荐的!)。其中,保存在HBase商店文件(StoreFile(HFile))上以促进随机访问的索引可能最终占用大量HBase分配的RAM,因为单元格值坐标很大。上面引用的注释中的标记建议增加块大小,以便存储文件索引中的条目以更大的间隔发生,或者修改表模式,以便使用较小的行和列名称。压缩也会使更大的指数。在用户邮件列表中查看线索问题storefileIndexSize(http://search-hadoop.com/m/hemBv1LiN4Q1/a+question+storefileIndexSize&subj=a+question+storefileIndexSize)。
大多数时候,小的低效率并不重要。不幸的是,这是他们的情况。无论为ColumnFamilies,属性和rowkeys选择哪种模式,都可以在数据中重复数十亿次。
有关HBase在内部存储数据的更多信息,请参阅keyvalue以了解为什么这很重要。
Column Families
尽量保持ColumnFamily名称尽可能小,最好是一个字符(例如数据/默认的“d”)。
有关HBase在内部存储数据的更多信息,请参阅KeyValue以了解为什么这很重要。
Attributes
(例如“via”)存储在HBase中。
有关HBase在内部存储数据的更多信息,请参阅keyvalue以了解为什么这很重要。
Rowkey Length
尽可能缩短它们,以便它们仍可用于所需的数据访问(例如获取与扫描)。 对数据访问无用的短密钥并不比具有更好获取/扫描属性的更长密钥更好。 设计行键时需要权衡
Byte Patterns
长是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
第三章未完待续。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。