Songtao Hu

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

1        架构(schema)定义

HBase中创建一个表隐含地涉及到表的架构定义,同样还有表中所包含的列族(column families)的架构(schema)。它们定义了关于表中的数据和列最终是怎样以及何时被存储的相关特性。

1.1    (Table)

存储在HBase中的每件事物最终被分组到一个或多个表。拥有表的首要理由是能够控制这个表共享的所有列的某些特性。你将要为一个表定义的典型事物是列族(column families)Java中的表描述符(table descriptor)的构造函数看起来像这样:

HTableDescriptor();

HTableDescriptor(string name);

HTableDescriptor(byte[] name);

HTableDescriptor(HTableDescriptor desc);

 

Writable接口和无参数的构造函数

你将发现API提供的,以及贯穿本章所讨论的大多数类的确拥有一个指定的构造函数,这是一个没有任何参数的构造函数。这是由于这些类实现HadoopWritable接口。

每一个相互分解的远程子系统之间的通信 例如,客户端要和服务端通信,但是服务端之间也相互通信 都是用Hadoop RPC 框架。该框架采用Writable类来表示可以在网络上被发送的对象。这些对象实现了两个必需的Writable方法:

void write(DataOutput out) throws IOException;

void readFields(DataInput in)throws IOException;

这两个方法被框架所调用,将对象的数据写入到输出流(ouput stream),随后在接收系统中读出来。为此,框架在发送方调用write(),序列化对象的字段(fields) – 这期间框架会留心注意对类名以及代表它们的其他细节进行注解。

在接收服务一端,框架读取元数据,然后创建类的一个空实例,再调用刚创建的实例的readFields()方法。这将读回字段(field)数据,留给你一个完全可工作的发送对象的初始化拷贝。

由于接收方需要使用反射创建类,这暗示着它必须可以利用匹配的,编译的类。通常就是这样,服务端和客户端都使用相同的HBase Java archive文件,或JAR

但是如果你开发自己的HBase扩展 例如,过滤器(filters)和协同处理程序(coprocessors),正如我们在第4章讨论的 你必须确保你的定制类遵守这些准则:

l  RPC通信通道的两方,也就是发送方和接收方,都应该是可用的。

l  应实现Writable接口,包括write()readFields()方法。

l  拥有无参数构造函数,也就是一个没有任何参数的构造函数。

如果没有提供这个指定的无参数构造函数,将导致运行时错误。从你的代码中显式调用这个构造函数也是徒劳的,因为这会给你生成一个未初始化的实例,它的行为绝对不会如你预期的。

作为一个客户端API开发者,你应该知晓底层对RPC的依赖,以及它怎样对自身列出清单(manifest)。作为一名对HBase进行扩展的高级开发者,你需要恰当地实现和部署你的定制代码。160页上的定制过滤器(Custom Fillters)有一个实例和进一步的注解。

你可以用一个名称或已经存在的描述符(descriptor)创建一个表。无任何参数的构造函数仅用于序列化目的,不应该直接使用。你可以用一个Java Stringbyte[] 指定表的名称。HBase Java API中的很多函数有两种选择。String版本明显是为了方便和内部转换成byte数组表现形式,正如HBase将所有数据都看作byte数组。你可以使用提供的Byte类来实现这一点:

Byte[] name = Buytes.toBytes(“test”);

HTableDescriptor desc = new HTableDescriptor(name);

对于你用来创建表名的字符有一定的限制。表的名称被用于实际存储文件的路径的一部分,因而遵守文件命名规则。你可以随后浏览低级存储系统 例如,HDFS – 甚至当你需要的时候将表看做单独的目录。

HBase的面向列的存储格式允许你存储很多细节到相同的表中,而在关系型数据库模型中,将会被分成很多单独的表。

HBase中,表的数量很少。

尽管从概念上来说在HBase中一个表是带有列的行的集合,物理上它们被存储在称为regions的单独的分区中。插图5-1展示了存储的数据在逻辑和物理布局上的不同。每一个regsion都正好被一个region server所管理, region server轮流直接为客户端提供已存储的值。

 

插图5-1 在区块(regions)中的行(rows)的逻辑和物理布局

1.2    表的属性

表的描述符提供了getterssetters[1]来设置表的其它选项。在实践中,很多选项不经常使用,但是重要的是要知道所有这些选项,它们可能被用于微调表的性能。

名称(Name)

构造函数已经有一个参数来指定表名。Java API有一个额外的方法来访问表名或修改它。

Byte[]  getName();

String  getNameAsString();

void  setName(byte[] name);

 

 

表名必须不以.(句点)或“-(连字符)开头。此外,它只能包含拉丁字母或数字,还有“_(下划线),“-(连字符),或“.(句点)。在正则表达式语法中,这可以被表示为[a-zA-Z_0-9-.]

例如,.testtable是错误的,但是test.table是允许的。

更多细节详见212页的列族“Column Families”,插图5-2展示了一个关于表名是怎样被用于形成文件系统路径的一个例子。

列族(Column families)

这是关于定义一个表的最重要的部分。你需要指定用于正在创建的表的column families

void  addFamily(HColumnDescriptor family);

Boolean  hasFamily(byte[] c);

HColumnDescriptor  getColumnFamilies();

HColumnDescriptor  getFamily(byte[]  column);

HColumnDescriptor  removeFamily(byte[]  column);

你可以选择添加一个列族,基于它的名称来检查它是否存在,获取一个所有已知的列族的清单,获取或移除一个指定的列族。更多关于定义一个需要的HColumnDescriptor的内容在212页的“Column Families”中解释。

最大文件尺寸(Maximum file size)

这个参数是指定在一个表内部的region能够增长到的最大尺寸。用字节数指定,使用以下方法读取和设置:

long getMaxFileSize();

void setMaxFileSize(long maxFileSize);

 

最大文件尺寸实际上是用词不当,因为它实际上是关于每一个存储(store)的最大尺寸,所有的文件归属于每一个列族(column family)。如果一个单独的列族超出了这个最大尺寸,region就会分裂。因而在实践中,这涉及到多个文件,更好的    命名是maxStoreSize

最大文件尺寸(maximum  size)有助于当区块(region)达到配置的尺寸时系统对区块进行分裂(split)操作。正如在第16页讨论过的“构造块(Building  Blocks)”,在HBase中的可扩展和负载平衡的单元是区块(region)。虽然你需要决定这个尺寸的一个合适的值,但默认设置是256MB,对于大多数使用场景都是合适的,但是当你拥有大量数据时可能需要一个更大的值。

请注意,这或多或少是一个理想中的最大尺寸,在给定某些条件的情况下,这个尺寸可能被超越,实际上是没有影响地完全呈现。作为一个例子,你可以设置最大文件尺寸(maximum  size)10MB,插入到某行中一个20MB的单元格(cell)中。由于一行不能被跨区块(region)地分裂(split),你最终得到一个至少20MB的区块(region),系统对此不会采取任何动作。

只读(Read-only)

默认情况下,所有的表都是可写的(writable),但是对于特定的表指定一个read-only选项可能是有意义的。如果标志被设置为true,你就只能从表中读取数据,而根本不能修改它。标志是通过这些方法来设置和读取:

Boolean  isReadOnly();

Void  setReadONly(Boolean  readOnly);

内存存储刷新大小(Memstore flush size)

我们先前讨论了存储模型,识别了HBase是怎样在使用一个称为flush的操作中将值作为一个新的存储文件写到磁盘之前,使用一个内存存储来缓存这些值。这个参数控制了何时这个动作要发生,以及用数组大小来指定。它被以下调用来控制:

Long  getMemStoreFlushSize();

Void  setMemStoreFlushSize(long  memstoreFlushSize);

当你要利用上述的最大文件尺寸(maximum  size)时,当你将这个值设置为超过默认的64MB时,你需要检查需求。一个更大的值意味着你正在生成更大的存储文件,这很好。另一方面,如果区块服务器(region server)不能持续刷新(flushing)新增的数据,你可能陷入到更长的阻塞(blocking)周期这样一个问题当中。同时,这也增加了当服务器崩溃掉,所有内存更新(in-memory updates)丢失以后根据预写日志(write-ahead log)(the WAL)恢复数据时所需要的时间。

延迟日志刷新(Deferred log flush)

我们将在第333页的预写日志“Write-Ahead Log”中更详细地讨论日志刷新(log flushing),在那里解释这个选项。现在,请注意HBase 使用这两种方法中的其中一个来保存预写日志(write-ahead-log)条目到磁盘上。你可以使用延迟日志刷新(deferred log flushing),也可以不使用。这是一个布尔选项,默认设置为false。这里是怎样通过Java API访问这个参数的代码:

synchronized boolean isDeferredLogFlush();

void setDeferredLogFlush(boolean isDeferredLogFlush);

各种杂项

除了已经提到的之外,还有几个方法让你可以设置任意的键/(key/value)对:

byte[] getValue(byte[] key) {

String getValue(String key)

Map<ImmutableBytesWritable, ImmutableBytesWritable> getValues()

void setValue(byte[] key, byte[] value)

void setValue(String key, String value)

void remove(byte[] key)

这些值和表的定义一起被存储,当需要的时候可以获取它们。在HBase中的一个实际的用例是协同处理程序(coprocessors)的加载,这将在第179页的“协同处理程序的加载(Coprocessor Loading)”中详细说明。在怎样指定键值对这方面,你有一些选择,或者使用String,或者使用byte数组。在内部它们都被作为ImmutableBytesWritable存储,这是为了序列化的目的而产生的需要(见第207页的“Writable接口和无参数的构造函数”)。

1.3    列族(column families)

我们刚才看到了HTableDescriptor是怎样暴露方法来添加column families到表中。与此类似的是一个称为HColumnDescriptor的类,它将每一个column family的设置封装到一个专用的Java类中。在其它编程语言中,你可以找到某些概念或一些指定列族(column family)属性的其它方式。

Java中的类有一些误拼。一个更合适的名称是HColumnFamilyDescriptor,它指出它的目的是定义列族(column family)参数而不是实际的列(column)

列族(column families)定义了应用到其中的所有列的共享特性。客户端可以在系统运行过程中通过简单地使用new column qualifiers来创建任意数量的列。列是用列族(column family)名和列限定符(column qualifier)的组合来称呼的,两部分用一个冒号分开:

family:qualifier

列族(column family)名称必须由可打印字符组成:限定符(qualifier)可以由任意二进制字符组成。回忆早先提到的Bytes类,你可以用它将你选定的名称转换为byte数组。列族名称必须为可打印字符的理由是因为在列族名称被低级别的存储层用于作为存储目录名的一部分。列族名称被添加到路径中,并且必须遵守文件命名标准。好处是你当你有一个可阅读格式的名称时,可以在文件系统层级很容易地访问列族。

列限定符(qualifier),又被称为列的键(key),它唯一标识这个列族中的某一列。

你也应该知道空列(empty column)限定符。你可以简单地忽略限定符,而只指定列族名。HBase然后创建一个带有特殊的空限定符的列。你可以像读写任何其它列一样读写那一列,但是明显地,这样的列只能有一个,你必须命名其他所有列以便区别它们。

 

当你创建一个列族(column family)时,你可以指定各种参数来控制它的所有特性。Java类有很多构造函数允许你在创建一个实例时指定大多数参数。这里有可选的形式:

HColumnDescriptor();

HColumnDescriptor(String familyName),

HColumnDescriptor(byte[] familyName);

 

HColumnDescriptor(HColumnDescriptor desc);

HColumnDescriptor(byte[] familyName, int maxVersions, String compression,

boolean inMemory, boolean blockCacheEnabled, int timeToLive,

String bloomFilter);

HColumnDescriptor(byte [] familyName, int maxVersions, String compression,

boolean inMemory, boolean blockCacheEnabled, int blocksize,

int timeToLive, String bloomFilter, int scope);

第一个是无参数构造函数,仅用于内部反序列化。接下来两个简单地接收一个Stringbyte[]作为名称,通常的字节数组我们已经见过多次了。另一个接收一个已经存在的HColumnDescriptor,最后两个列出了所有可用的参数。

取代使用构造函数,你也可以使用gettersetter来指定各种细节。我们现在讨论它们。

名称(Name)

每一个列族有一个名称,你可以使用以下方法从一个已经存在的HColumnDescriptor实例来获取它:

Byte[]  getName();

Sting  getNameAsString();

 

 

 

列族不能被重命名。重命名一个列族的通常途径是使用API创建一个有着期望名称的新的列族,然后将数据复制过去。

 

你不能设置名称,但是你可以使用这些构造函数接收名称。记住对于名称的要求是可打印(printable)字符。

 

列族的名称不能从一个“.”(英文句号)开始,也不能包含“:”(英文冒号),“/”(斜杠),或者ISO控制字符,换句话说,如果它的编码在\u0000\u001F或者在\u007F\u009F之间,这属于ISO控制字符,是不允许的。

 

最大版本(Maximum versions)

对于每一个列族,你可以指定你希望在每一个值上面保持多少版本。回顾一下早先提到的HBase的内部管理中移除超过设置的最大版本的那些值,即称之为predicate deletion(预告删除)的操作。使用以下API调用可以获取和设置最大版本。

int  getMaxVersions();

void  setMaxVersions(int  maxVersions);

缺省值是3,但是你可以减少到1,例如,当你确信你将不再需要查看哪些旧的值时。

压缩(Compression)

HBase有可插入的压缩算法支持(你可以在424页的“压缩”中找到关于这一主题的更多信息),允许你对于存储在特定列族中的选择最好的压缩 或者不压缩。可能的算法列出在表5.1中。

列表5.1。支持的压缩算法

Value

Description

NONE

禁用压缩 (默认地)

GZ

使用Java提供的或原生的GZip压缩。

LZO

启用LZO压缩;必须被单独安装。

SNAPPY

启用Snappy压缩;二进制文件必须被单独安装。

默认值NONE – 也就是说,当你创建一个列族(column family)时不启用任何压缩。一旦你处理Java API和列描述符(column  descriptor),你可以使用这些方法来改变值:

Compression.Algorithm getCompression();

Compression.Algorithm getCompressionType();

void setCompressionType(Compression.Algorithm type);

Compression.Algorithm getCompactionCompression();

Compression.Algorithm getCompactionCompressionType();

void setCompactionCompressionType(Compression.Algorithm type);

请注意这些值不是一个String,而是一个与列表5-1中暴露相同值的Compression.Algorithm枚举。HColumnDescriptor的构造函数采用相同值的字符串作为参数。

我们观察到有两套方法,其中一个是通常的压缩设置,另一个是压实(compaction)压缩设置。同时,每一组有一个getCompress()getCompressionType()(或者是getCompactionCompressiongetCompactionCompressionType()),返回值是相同类型的。它们的确是冗余的,你可以使用任何一组来获取当前压缩算法类型。[2]

我们将在第424页的“压缩(Compression)”中更详细地研究这个主题。

块尺寸(Block Size)

HBase中所有的存储文件被分成更小的块(block),这些块在getscan操作期间被加载,这类似于关系数据库关系系统(RDBMSes)中的页(pages)。块尺寸默认设置为64KB,可以使用以下方法调整:

synchronized int getBlocksize();

void setBlocksize(int s);

该值是用byte数组指定的,可以被用于在检索以及由于随后的访问而缓存在内存中时,控制HBase需要从存储文件读取多少数据。在第436页的“配置(Configuration)”可以找到这个参数是怎样被用于对你的设置程序进行精细调整。

 

在列族块尺寸(column family block size),或HFile block size以及在HDFS层级指定的块尺寸(block size)之间有一个重要区别。Hadoop,特别是HDFS,使用块尺寸 默认地 – 64MB,来为了分布式的,使用MapReduce框架的并行处理。对于HBase来说,HFile block size默认是64KB,或者是HDFS块尺寸的1/1024HBase使用的存储文件使用这个更细粒度的尺寸来在块操作(block operations)时更加有效地加载和缓存数据。它不依赖于HDFS块尺寸,而仅仅在HBase内部使用。更多细节详见第319页的“存储(Storage)”,特别是插图8-3,展示了两个不同的块类型。

块缓存(Block Cache)

为了高效的I/O使用,HBase读取全部的数据块(block),它将这些块(block)保留在内存中,这样后续的读取就不需要任何磁盘操作了。默认是true,为每一次读操作启用块缓存。但是如果你的用例永远是仅在特定的列族上顺序读取数据,那么建议你通过将块缓存(block cache)标志设置为false来禁止它。这里有用于改变这个标志的API

boolean isBlockCacheEnabled();

void setBlockCacheEnabled(boolean blockCacheEnabled);

还有一些用于影响块缓存的使用的其他选项,例如,在scan操作期间。这在全表扫描(full table scan)期间是有用的,这样你就不会在缓存上引起频繁填充(major churn)。关于这一特性的更多特性请见“配置(Configuration)”。

生存时间(Time-to-live)

HBase在为每个值保持的版本数上支持断言删除(predicate deletions),但是也可以是指定的次数。生存时间(time-to-liveTTL)设置一个基于值的时间戳的阀值,并且如果一个值超过了它的TTL,内部的处理机制就会来自动检查。如果是那种情况,在主压实(major compactions)期间会被丢弃。API提供了以下的gettersetter来读写TTL

int getTimeToLive();

void setTimeToLive(int timeToLive);

值是用秒为单位指定的,默认设置为Interger.MAX_VALUE2,147,483,647秒。默认值也被看作是永久保持值的这种特殊情况,也就是,任何小于默认值的正数值都会启用这个特性。

驻留内存(In-memory)

我们提到了块缓存(block cache)以及HBase为了高效地顺序访问数据是怎样使用块缓存将数据的所有块都保持在内存中。驻留(in-memory)标志默认为false,但是可以被以下方法修改:

boolean isInMemory();

void setInMemory(boolean inMemory);

设置为true不保证一个列族的所有块都加载到内存中,也不保证它们会保持在那里。将这个设置值看作一个承诺,或提升的优先权,只要它们在常规的获取操作中被加载,就将它们保持在内存中,一直到堆(基于Java的服务器进程的有效内存)上的压力过高为止,那是就需要强行忽略这个设置。

通常,这一设置对于只有少数几个值的小型列族(column family)是好的,诸如用户表的密码,这样登录(login)就能被很快处理。

布隆过滤器(Bloom filter)

HBase中一个可用的高级特性是布隆过滤器(Bloom filters),为你提供一种特定的访问模式,允许你改进查找次数(详情请见第377页的“布隆过滤器(Bloom filters)”)。由于它们在存储和内存方面增加了开销,默认是关闭的。表5-2展示了可能的选项。

5-2 支持的布隆过滤器类型

Type

Description

NONE

Disables the filter (default)

ROW

Use the row key for the filter

ROWCOL

Use the row key and column key (family+qualifier) for the filter

最后一个选项,ROWCOL,由于有比行更多的列(除非在每一行中仅有一列),需要最大数量的空间。虽然,这更加细粒度,由于它知道每一个行/列的组合,而不是仅知道行。

布隆过滤器可以用这些调用改变和获取:

StoreFile.BloomType getBloomFilterType();

void setBloomFilterType(StoreFile.BloomType bt);

对于压缩的值,这些方法采用一个StoreFile.BloomType类型,列描述符的构造函数让你将上述类型作为一个string来指定。后面的转换不重要,因此你可以使用“row”。“Bloom Filters”这一节中有关于怎样才能最好地使用它们的内容。

复制范围(Replication scope)

HBase提供的另外一个更加高级的特性是复制(replication)。它让你可以有多个群集,跨网络地传送本地更新,因此这些更新被应用到远程拷贝。

默认的,复制是禁用的,复制范围(replication scope)设置为0,意味着它是禁用的。你可以用这些功能改变范围:

int getScope();

void setScope(int scope);

唯一的其他支持的值(在编写本书时)是1,这启用复制到远程群集。在未来可能有更多的范围值。支持的值的列表详见表5-3

5-3 支持的复制范围

Scope

Description

0

Local scope, i.e., no replication for this family (default)

1

Global scope, i.e., replicate family to a remote cluster

 

全部细节可以在第462页的“复制(replication)”中找到。

最后,Java类有一个helper方法来检验列族名是否是合法的:

static byte[] isLegalFamilyName(byte[] b);

在你的程序中使用它来校验符合规范的用户提供的输入,这些输入需要提供列名。它不返回一个布尔标志,但是当名称有缺陷时,会抛出一个IllegalArgumentException。否则,它返回的就是你提供的那个参数,没有任何改变。完全专用的构造函数展示了在内部更早地使用这个方法,来校验给出的名称;在这种情况下,你不需要事先调用这个方法。



[1] Java中的GettersSetters是以一种受控的方式暴露出来的类的方法。它们通常像字段一样被命名,用getset作为前缀 例如,getName()setName()

[2]毕竟,这是开源产品,并且像这样的冗余经常是由于先前的遗留代码引起的。请尽管清理掉这些并且把这个清理工作贡献给HBase项目。

posted on 2012-04-20 23:09  Songtao Hu  阅读(2223)  评论(0编辑  收藏  举报