Data Model
本篇内容是http://hbase.apache.org/1.2/book.html#datamodel 的自我翻译。
在HBase中,数据存储在表里,表由行和列组成。这些术语和关系型数据库(RDBMS)相同,但这种类比是没用的。它只能用来帮助我们把HBase表理解成一个多维Map。
HBase Data Model 术语
表
一个HBase表由多个行组成
行
一个HBase行由一个行键和一个或多个列以及列值组成。行都按照row key的字典序排列。因此对行键的设计非常重要。以这样的方式存储数据的目的是,使相关联的行相互靠近。
一个普通的行键式样是一个网站域名。如果你的行键是域名,你应该反向存储他们(org.apache.www, org.apache.mail, org.apache.jira)。
这样,所有的Apache域名在表中相互靠近,而不是基于子域名的首字母而分散开来。
列
一个HBase列由一个列族和一个列标识符组成,列族和列标识符由 : 冒号分开
列族
列族物理上是集合在一起的列和它们的值,这样设置是基于性能的考虑。每个列族都有一系列的存储属性,例如它的值是否应该被缓存到内存,它的数据如何被压缩或者它的行键如何被编码等等。
表中的每一行有相同的列族,但是每行的列族中的列不一定相同。
列标识
一个列标识符被添加到列族里用来提供一个部分数据的索引。给定一个列族content,一个列标识符可以是content:html,另一个可以是content:pdf。
虽然列族在表创建时就固定下来了,但是列标识符是可变的且在不同行中可以不同。
单元
一个单元是一组由行、列族、列标识符及其包含的值和时间戳组合而成的,时间戳用来表示值的版本。
时间戳
一个时间戳被赋予每个值,用来标识这个值的版本。默认情况下,时间戳是在数据被写入时RegionServer的当前时间,但是你可以在put数据到单元时指定不同的时间戳值。
20. 概念视图
你可以在Jim R. Wilson发表的博客 Understanding HBase and BigTable 里阅读到一个关于HBase数据模型非常容易理解的解释。
另一个好的解释是Amandeep Khurana写的PDF Introduction to Basic Schema Design。
这可以通过阅读不同的观点而把HBase shema设计搞懂很有帮助。这两篇文章都涵盖了本节的内容。
下面的例子稍微修改了BigTable论文的第二页的示例。有一个表webtable,有两行 (com.cnn.www和
com.example.www
)、三个列族contents
, anchor
, people。
在这个例子中,
第一行(com.cnn.www
)的anchor有两个列(anchor:cssnsi.com
, anchor:my.look.ca
),contents有一个列(contents:html
)。第一行有5个版本的值,第二行com.example.www有一个版本的值。
contents:html列标识符含有给定站点的整个HTML。每个列族anchor的标识符包含的是外部网站链接到当前行所表示的网站的链接中的文本。people列表示和当前站点有联系的人。
列名
按照惯例,一个列名由列族做前缀再加上一个列标识符组成。例如,列contents:html 是由列族 |
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>…" |
表中的单元显示为空的,在HBase实际上是不占位、不存在的。这就造成了HBase是稀疏的。一个表格视图不是查看HBase中数据的唯一方式,也不是最准确的。
以下方式表示了作为多维map相同的内容,这只是为了说明而采用的模拟手段,也不是严格准确的。
{ "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" } } }
21. 物理视图
虽然在概念层面,表看起来像由稀疏行组成的,但是在物理上表中数据是以列族的方式进行存储的。一个新的列标识符(column_family:column_qualifier)可以在任何时候被加到已存在的列族中。
Table 5. ColumnFamily anchor
Row Key | Time Stamp | Column Family anchor |
---|---|---|
"com.cnn.www" |
t9 |
|
"com.cnn.www" |
t8 |
|
Table 5. 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>…" |
在概念视图中空的单元根本没有被存储。因此请求contents:html在时间戳t8的值将无值返回,同样地,请求anchor:my.look.ca在时间戳t9的值也将无值返回。
然而如果没有指定时间戳,指定列的最新的值将被返回。给定多个版本的,最新的值就是按照存储的时间戳倒序排列第一个被找到的值。
因此一个没有指定时间戳的对com.cnn.www行所有列的请求将返回:时间戳t6的contents:html,t9的anchor:cnnsi.com,t8的anchor:my.look.ca
要了解Apache HBase内部如何存储数据,参见 regions.arch。
22. 命名空间
一个命名空间,类似于关系型数据库的一个数据库,是一个表的逻辑分组。这个抽象为即将到来的多租户相关特性奠定了基础:
(多租户是指一个软件系统可以同时被多个实体所使用,每个实体之间是逻辑隔离、互不影响的。一个租户可以是一个应用,也可以是一个组织。)
-
配额管理 Quota Management (HBASE-8410) - Restrict the amount of resources (i.e. regions, tables) a namespace can consume.
-
命名空间安全管理 Namespace Security Administration (HBASE-9206) - Provide another level of security administration for tenants.
-
Region服务器分组 Region server groups (HBASE-6721) - A namespace/table can be pinned onto a subset of RegionServers thus guaranteeing a course level of isolation.
22.1. 命名空间管理
一个命名空间可以被创建、删除或更改。命名空间的成员在通过指定完全限定表名的方式创建表时就被确定了:
<table namespace>:<table qualifier>
Example 12. Examples
#Create a namespace create_namespace 'my_ns' #create my_table in my_ns namespace create 'my_ns:my_table', 'fam' #drop namespace drop_namespace 'my_ns' #alter namespace alter_namespace 'my_ns', {METHOD => 'set', 'PROPERTY_NAME' => 'PROPERTY_VALUE'}
22.2. 预定义命名空间
有两种预定义好的命名空间:
-
hbase - 系统命名空间,用来组织HBase内部表
-
default - 没有显示指定命名空间的表将会自动落在这个命名空间
#namespace=foo and table qualifier=bar create 'foo:bar', 'fam' #namespace=default and table qualifier=bar create 'bar', 'fam'
23. 表
表在schema定义时就被预先声明了。
24. 行
行键是不可分割的字节数组。行是按字典排序由低到高存储在表中的。一个空的数组是用来标识表的命名空间的起始或者结尾。
25. 列族
列在HBase中是归入到列族里面的。一个列的所有列成员都涌向相同的前缀。例如,列courses:history和cources:math是cources列族的成员,冒号用于将列族和列限定符分开。列族前缀必须由可打印的字符组成。列限定符可以由任意字节组成。列族必须在结构(schema)定义阶段预先声明好而列则不需要再结构设计阶段预先定义而是可以在表的创建和运行阶段快速的加入。
物理上来说,所有的列族成员都是存储在文件系统。因为调试和存储技术参数都是作用在列族这个层次上,建议所有的列族都要拥有相同的通用访问格式和大小特征。
26. 单元
一个 {row, column, version} 元组就是一个HBase中的一个 cell
。Cell的内容是不可分割的字节数组。
27. 数据模型操作
数据模型的四个主要操作是Get,Put,Scan和Delete。可以通过 Table实例进行操作。
27.2. Put 插入
Put 操作是在行键不存在时添加新行或者行键已经存在时进行更新。Puts 是通过 Table.put(writeBuffer) 或 Table.batch (non-writeBuffer)执行的。
27.3. Scan 扫描
Scan 允许为指定属性迭代多行。
下面是在表中Scan的例子。假设一个表格里面有行键"row1", "row2", "row3",然后有另外一组行键为"abc1", "abc2",和"abc3"。下面的例子展示如何设置一个Scan实例来返回以“row”开头的行键的行。
public static final byte[] CF = "cf".getBytes(); public static final byte[] ATTR = "attr".getBytes(); ... Table table = ... // instantiate a Table instance Scan scan = new Scan(); scan.addColumn(CF, ATTR); scan.setRowPrefixFilter(Bytes.toBytes("row")); ResultScanner rs = table.getScanner(scan); try { for (Result r = rs.next(); r != null; r = rs.next()) { // process result... } } finally { rs.close(); // always close the ResultScanner! }
需要说明的是通常最简单的指定Scan的一个特定停止点的方法是使用 InclusiveStopFilter 类。
27.4. 删除 Delete
Delete 将一行从表中移除。Deletes通过 Table.delete 执行。
HBase不会就地修改数据,所以删除的处理是给数据创建一个新的tombstones(墓碑)标签。这些墓碑,会随着已死亡的数值(被删除的行)在major compaction(合并)时被清除。
参见 version.delete 获取更多关于删除列的版本的信息, 参见 compaction 获取更多关于合并的信息.
28. 版本 Versions
一个{row, column, version} tuple在HBase中完全指定一个单元cell。有可能会有很多的单元的行和列是相同的,但是可以使用版本来区分不同的单元。
行和列使用字节来表达,而版本是通过长整型来指定的。典型来说,这个长时间实例就像java.util.Date.getTime() 或者 System.currentTimeMillis()返回的一样,以毫秒为单位,返回当前时间和January 1, 1970 UTC的时间差。
rows和column key是用字节数组表示的,version则是用一个长整型表示。这个long的值使用 java.util.Date.getTime()
或者 System.currentTimeMillis()
产生的time实例。这就意味着他的含义是“当前时间和1970-01-01 UTC的时间差,单位毫秒。”
在HBase中,版本是按倒序排列的,因此当读取这个文件的时候,最先找到的是最近的版本。
有些人不是很理解HBase单元(cell
)的意思。一个常见的问题是:
-
如果有多个包含版本写操作同时发起,HBase会保存全部还是会保持最新的一个?目前,只有最新的那个是可以获取到的。
-
可以发起包含版本的写操作,但是他们的版本顺序和操作顺序相反吗? 可以
下面我们介绍下在HBase中版本是如何工作的。参考关于HBase版本的讨论HBASE-2406。
Bending time in HBase是关于HBase中版本或时间维度的好读物,它提供了比这里提供的更多的关于版本的细节。
正如这里写到的,文章中提到的 覆盖存在的时间戳的值 的限制将不再存在。这部分只是Bruno Dumon所写的关于版本的基本大纲。
28.1. 指定版本存储的数量
一个列能存储的最大版本数量是列schema的一部分,在创建表时、或利用alter命令修改表时、或使用HColumnDescriptor.DEFAULT_VERSIONS指定。
在HBase 0.96之前的版本,默认的版本数是3,在0.96及以后变成了1。
Example 14. Modify the Maximum Number of Versions for a Column Family
这个例子使用HBase Shell设置为列族f1的所有列保留的最大版本数是5。你也可以使用 HColumnDescriptor.
hbase> alter ‘t1′, NAME => ‘f1′, VERSIONS => 5
Example 15. Modify the Minimum Number of Versions for a Column Family
你也可以通过指定最小版本数来存储列族。默认情况下,该值为零,意味着这个属性是禁用的。
下面的例子是通过HBase Shell设置列族f1中的所有列的最小版本数为2。你也可以通过 HColumnDescriptor来实现。
hbase> alter ‘t1′, NAME => ‘f1′, MIN_VERSIONS => 2
从HBase0.98.2开始,你可以通过设定在hbase-site.xml中设置hbase.column.max.version属性为所有新建的列指定一个全局的默认的最大版本数。
28.2. 版本和HBase操作
在这部分我们来看一下版本维度在HBase的每个核心操作中的表现。
28.2.1. Get/Scan
默认情况下,如果你没有指定明确的版本,当你执行一个Get操作时,那个版本为最大值的单元将被返回(可能是也可能不是最新写人的那个)。默认的行为可以通过下面方式来修改:
- 返回不止一个版本 查看 Get.setMaxVersions()
- 返回最新版本以外的版本, 查看 Get.setTimeRange()
想要获得小于或等于固定值的最新版本,仅仅通过使用一个从0到期望版本的范围并且设置最大版本数为1,就可以实现获得一个特定时间点的最新版本的记录。
28.2.2. 默认 Get 的例子
下面例子仅仅返回行的当前版本。
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
28.2.3. 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
28.2.4. Put
Put操作常常是以固定的时间戳来创建一个新单元。默认情况下,系统使用服务器的 currentTimeMillis,但是你也可以为每一个列指定版本(长整型)。
这就意味着你可以指定一个过去或者未来的时间点,或者不是时间格式的长整型。
为了覆盖已经存在的值,对和那个你想要覆盖的单元完全一样的row、column和version进行put操作。
隐式版本的例子Implicit Version Example
下面的 Put 将会被HBase隐式地把当前时间设置为版本
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);
显示版本的例子Explicit Version Example
下面的 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内部用来计算数据的存活时间(time-to-live, TTL)的。最好避免由你自己设置这个时间戳。
最好是将时间戳作为行的单独属性或者作为key的一部分,或者两者都有。
28.2.5. Delete
内部的删除标记有三种不同的类型。参见ars Hofhansl所写的博客Scanning in HBase: Prefix Delete Marker 。
-
Delete: 删除列的指定版本
-
Delete column: 删除列的所有版本
-
Delete family: 删除一个列族的所有列
当要删除整个行时,HBase将会在内部为每一个列族创建一个墓碑,而不是为每一个列创建。
删除通过创建一个墓碑标签来工作。例如,让我们来设想我们要删除一个行。为此你可指定一个版本,或者使用默认的currentTimeMillis 。这就是删除小于等于该版本的所有单元。
HBase不会修改数据,例如删除操作将不会立刻删除满足删除条件的文件。相反的,称为墓碑的会被写入,用来掩饰被删除的数据。
当HBase执行一个major compaction操作(可以理解为清理),墓碑将会执行一个真正地删除死亡值和墓碑自己的删除操作。如果你的删除操作指定的版本大于目前所有的版本,那么可以认为是删除整个行的数据。
你可以在Put w/timestamp → Deleteall → Put w/ timestamp fails 用户邮件列表中查看关于删除和版本之间的相互影响的有益信息。
还可以参照 keyvalue 获取更多关于内部KeyValue格式的信息
删除标签会在下一次存储空间的major compaction中被清理掉,除非为列族设置了 KEEP_DELETED_CELLS (查看 Keeping Deleted Cells)。
为了保证删除时间的可配置性,你可以通过在 hbase-site.xml.中hbase.hstore.time.to.purge.deletes属性来设置 删除TTL(生存时间)。
如果 hbase.hstore.time.to.purge.deletes没有设置或者设置为0,所有的删除标签包括那些带有未来时间戳的标签都会在下一次major compaction操作中被干掉。
此外,带有未来时间戳的删除标签的数据将会保持,直到 代表标签的时间戳的时间与hbase.hstore.time.to.purge.deletes相加的和(单位是毫秒) 所表示的时间以后的那次major compact操作。
这样的做法是针对HBase0.94中一个未按照预期而产生的变化的修正,在 HBASE-10118 中修改,这个修改已经在HBase0.94以及新的版本生效。 |
28.3. 现有的限制Current Limitations
28.3.1. 删除标记误标记新put的数据 Deletes mask Puts
当put操作在delete操作后执行,删除标记操作可能会标记其后put的数据。可以查看 HBASE-2256。记住,当写下一个墓碑标记后,只有下一个主紧缩操作(major compaction)发起之后,墓碑才会清除。假设你删除所有<= 时间T的数据。但之后,你又执行了一个Put操作,时间戳<= T。就算这个Put发生在删除操作之后,他的数据也打上了墓碑标记。这个Put并不会失败,但你做Get操作时,会注意到Put没有产生影响。只有一个主紧缩(major compaction)执行后,一切才会恢复正常。如果你的Put操作一直使用升序的版本,这个问题不会有影响。但是即使你不关心时间,也可能出现该情况。当删除和插入的操作迅速交替发生,就有机会在同一毫秒中遇到。
创建三个版本为t1,t2,t3的单元,并且设置最大版本数为2.所以当我们查询所有版本时,只会返回t2和t3。但是当你删除版本t2和t3的时候,版本t1会重新出现。但是如果在删除之前,发生了major compaction操作,那么什么值都不返回了。(查看 Bending time in HBase.)
29. 排序Sort Order
所有数据模型操作 HBase 都返回排序的数据。先是行,再是列族,然后是列标识符(column qualifier), 最后是时间戳(反向排序,所以最新的在前).
30. 列元数据Column Metadata
对于列族,没有在内部的KeyValue以外的地方保存的列的元数据,即所有列的元数据都存储在一个列族的内部KeyValue实例中。
这样,HBase不仅在一行中支持很多列,而且支持行之间不同的列。由你自己负责跟踪列名。
唯一获取列族的所有列名的方法是处理所有行。HBase内部保存数据更多信息,请参考keyvalue .
31. Joins
HBase是否支持join是一个常见的问题,答案是没有,至少没办法像RDBMS那样支持(例如等价式join或者外部join)。正如本章节所阐述的,HBase中读取数据的模型的操作是Get和Scan。
然而,这不意味着等价式join功能没办法在你的应用中实现,但是你必须自己实现。
两种主要策略是要么将数据非结构化地写到HBase中,要么查找表然后在应用程序或MapReduce代码中实现HBase表间的join操作(正如RDBMS所演示的,将根据表格的大小会有几种不同的策略,例如嵌套使循环和hash-join)。哪个是最好的方法?这将依赖于你想做什么,没有一种方案能够应对各种情况。
32. ACID
详见 ACID Semantics。 Lars Hofhansl 也写了关于 ACID in HBase 的笔记。