理解HBase和BigTable与比较

学习 HBase 最难的地方在于要让你的脑子真正理解它是什么。

HBase:Google BigTable 的开源实现

我们经常会把关系型数据库(RDBMS,比如 MySQL)和 HBase 搞混,因为在这两个系统中都包含 table 和 base(HBase,Database)。

这篇文章的目标是从概念上来说清楚 HBase 这个分布式的数据存储系统。读完后,你应该可以很清楚的知道什么情况下 HBase 更好,什么情况下传统的关系型数据库更好。

关于一些术语

幸运的是,Google 的 BigTable论文清楚的解释了 BigTable 到底是什么。下面是论文中数据模型章节的第一句话:

BigTable 是一个稀疏的、分布式的、可持久化的多维有序 map。

在这个节骨眼上,我想给读者一个机会,让他们在读到最后一行字时,能够收集到他们脑壳里的活动信息(这可能是个笑话,但我没懂^v^)。

论文中,继续解释如下:

map 通过 rowKey,columnKey 和时间戳进行索引,map 中的每个值都是一个连续的字节数组。

注:rowKey 是记录的主键,唯一标识一行记录

在 Hadoop 的官方文档中,也对 HBase 的架构做了说明:

HBase 使用了与 BigTable 非常类似的数据模型。用户存储数据行到特定的表中。一个数据行有一个可排序的 rowKey 和数量不定的列。这个表是稀疏的,只要用户愿意,这个表不同行可以有完全不同的列。

这些话看起来相当费解,让人摸不着头脑,但如果你把这些话拆成一个个词,意思就慢慢变的清晰了。我将按照以下的顺序来讨论这些词:map,持久化,分布式,有序的,多维的,稀疏。

我发现循序渐进地建立一个思维框架要比一次性勾画一个完整的系统更加容易。

map

从根本来上来,HBase/BigTable 是一个 map。map 在不同的编程语言中有不同的叫法,比如 PHP 中的 array,Python 的 dictionary,Ruby 中的 Hash,或者 JavaScript 中的 Object。

维基百科上对于 map 的定义是:map 是一个抽象的数据类型,包含了一组 key 和一组 value,每个 key 关联一个 value。

如果用 JavaScript 的对象来表示 map,这里有一个简单的例子,其中所有的 value 都是字符串:

{
  "zzzzz" : "woot",
  "xyz" : "hello",
  "aaaab" : "world",
  "1" : "x",
  "aaaaa" : "y"
}

持久化的

持久化的意思仅仅是指你放进这个特殊 map 的数据会在你的程序执行完成之后被保存下来。它和其他的持久化存储系统中持久化的概念没有任何区别,比如存一个文件到一个文件系统。我们继续...

分布式的

HBase 和 BigTable 都建立在分布式文件系统上,所以底层文件可以被分散存储到不同的机器上。

HBase 可以存储到 HDFS(Hadoop's Distributed File System)上,也可以存储到 亚马逊的 S3(Simple Storage Service)上,而 BigTable 使用的是 GFS(Google File System)。

同一份数据会被复制存储到多个节点上,类似于 RAID(独立冗余磁盘阵列,利用冗余存储的数据使损坏数据得以恢复,从而保护数据不丢失)系统中数据在磁盘上的复制存储到多块磁盘的方式。

在这篇文章中,我们不关心具体使用哪种分布式文件系统。重要的是,要理解这个文件系统是分布式的,即使集群中某个节点出现故障,也可以保证数据的完整性和安全性。

有序的

和其他大多数 map 的实现不同,HBase 和 BigTable 的键值对的顺序严格按照字母顺序来排列。所以 rowKey 为 "aaaaa" 的下一条记录的 rowKey 就是 "aaaab",并且会离 “zzzz” 非常远。

继续看上面的那个 JSON 例子,排行序之后是下面这样的:

{
  "1" : "x",
  "aaaaa" : "y",
  "aaaab" : "world",
  "xyz" : "hello",
  "zzzzz" : "woot"
}

因为这个系统是分布式的,而且会越来越大,因此排序这个特性非常重要。这样就会把 rowKey 相近的记录放在一起,在某些情况下,如果你必须要扫描表(通常不推荐),那就能保证你需要获取的记录都在一块。

那么如何选择 rowKey 就非常重要。比如说,一个表的 rowKey 就是域名。一个比较好的方式就是将域名进行反转来作为 rowKey(使用 “com.jimbojw.www”,而不要使用 “www.jimbojw.com”),这样,同一个域名下的记录就可以存储在相邻的位置。

继续上面的域名例子,rowKey 为 “mail.jimbojw.com” 行应该与 “www.jimbojw.com” 行更近,而不是 “mail.xyz.com”,如果不把域名反转存储,就会发生这种情况。

需要注意的是,在 HBase / BigTable 中,有序并不意味着值是有序的。除了 rowKey 以外,没有任内容会被排序,在这点上和普通 map 的实现一致。

多维的

到目前为止,我们还没有提过任何关于列的概念,而是将表在概念上当做常规的 map。我是故意这么做的。列和表、base 等词一样,都带有传统关系型数据库多年的情感包袱。

然而,我发现把 HBase 理解为一个多维的 map 会容易很多,map 的 map。给上面的 JSON 再加上一列:

{
  "1" : {
    "A" : "x",
    "B" : "z"
  },
  "aaaaa" : {
    "A" : "y",
    "B" : "w"
  },
  "aaaab" : {
    "A" : "world",
    "B" : "ocean"
  },
  "xyz" : {
    "A" : "hello",
    "B" : "there"
  },
  "zzzzz" : {
    "A" : "woot",
    "B" : "1337"
  }
}

在上面的例子中你可以看到每个 key 都指向了另一个 map,其中包含着 A 和 B 两个 key。在这里,我们将最上面那层键值对称为行。并且在 HBase / BigTable 的术语表中,A 和 B 的映射称之为列族。

一个表的列族在表创建的时候就会被创建好,而且后续修改很困难,添加一个新列族的开销同样也很大,所以在创建表的时候应当将后续会用到的所有列族创建好。

好在一个列族可以有任意数量的列。称之为为列限定符(qualifier)或者标签(label)。

下面是我们上面 JSON 例子的子集,这次加入了 qualifier 的维度:

{
  // ...
  "aaaaa" : {
    "A" : {
      "foo" : "y",
      "bar" : "d"
    },
    "B" : {
      "" : "w"
    }
  },
  "aaaab" : {
    "A" : {
      "foo" : "world",
      "bar" : "domination"
    },
    "B" : {
      "" : "ocean"
    }
  },
  // ...
}

注意在上面的两行数据中,A 列族有两列:foo 和 bar,B 列族只有一列,而且 qualifier 是一个空字符串。

当访问 HBase / BigTable 中的数据时,你需要提供完整的列名::。举个例子,上面总共有三列,分别是:A:foo,A:bar 和 B:。

列族虽然基本固定不变,但是列不是,来看下面的例子:

{
  // ...
  "zzzzz" : {
    "A" : {
      "catch_phrase" : "woot",
    }
  }
}

在这个例子中,zzzzz 行有一个列 A:catch_phrase。因为每一行可以有任意数量的列,所以没有内置方法可以从所有行中的所有列中查询出一个列表。为了获取到那些信息,你需要做全表扫描。但是你可以查询所有的列族,因为它们是不变的(基本不变)。

HBase / BigTable 中最后的一个维度是时间。所有数据默认通过时间戳(1970年以来的秒数)来表示版本,或者你也可以指定一个其他的整数。客户端在插入数据的时候可以指定这个时间戳。

在最新的例子中,我们使用任意的整数来作为版本标识:

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java