Bootstrap

HBASE从入门到精通


一、 HBase技术介绍

 

HBase简介

HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,利用HBase技术可在廉价PC Server上搭建起大规模结构化存储集群。

HBase是Google Bigtable的开源实现,类似Google Bigtable利用GFS作为其文件存储系统,HBase利用Hadoop HDFS作为其文件存储系统;Google运行MapReduce来处理Bigtable中的海量数据,HBase同样利用Hadoop MapReduce来处理HBase中的海量数据;Google Bigtable利用 Chubby作为协同服务,HBase利用Zookeeper作为对应。

 

上图描述了Hadoop EcoSystem中的各层系统,其中HBase位于结构化存储层,Hadoop HDFS为HBase提供了高可靠性的底层存储支持,Hadoop MapReduce为HBase提供了高性能的计算能力,Zookeeper为HBase提供了稳定服务和failover机制。

此外,Pig和Hive还为HBase提供了高层语言支持,使得在HBase上进行数据统计处理变的非常简单。 Sqoop则为HBase提供了方便的RDBMS数据导入功能,使得传统数据库数据向HBase中迁移变的非常方便。

HBase访问接口

1.       Native Java API,最常规和高效的访问方式,适合Hadoop MapReduce Job并行批处理HBase表数据

2.       HBase Shell,HBase的命令行工具,最简单的接口,适合HBase管理使用

3.       Thrift Gateway,利用Thrift序列化技术,支持C++,PHP,Python等多种语言,适合其他异构系统在线访问HBase表数据

4.       REST Gateway,支持REST 风格的Http API访问HBase, 解除了语言限制

5.       Pig,可以使用Pig Latin流式编程语言来操作HBase中的数据,和Hive类似,本质最终也是编译成MapReduce Job来处理HBase表数据,适合做数据统计

6.       Hive,当前Hive的Release版本尚没有加入对HBase的支持,但在下一个版本Hive 0.7.0中将会支持HBase,可以使用类似SQL语言来访问HBase

HBase数据模型

Table & Column Family

Row Key

Timestamp

Column Family

URI

Parser

r1

t3

url=http://www.taobao.com

title=天天特价

t2

host=taobao.com

 

t1

 

 

r2

t5

url=http://www.alibaba.com

content=每天…

t4

host=alibaba.com

 

Ø  Row Key: 行键,Table的主键,Table中的记录按照Row Key排序

Ø  Timestamp: 时间戳,每次数据操作对应的时间戳,可以看作是数据的version number

Ø  Column Family:列簇,Table在水平方向有一个或者多个Column Family组成,一个Column Family中可以由任意多个Column组成,即Column Family支持动态扩展,无需预先定义Column的数量以及类型,所有Column均以二进制格式存储,用户需要自行进行类型转换。

Table & Region

当Table随着记录数不断增加而变大后,会逐渐分裂成多份splits,成为regions,一个region由[startkey,endkey)表示,不同的region会被Master分配给相应的RegionServer进行管理:

 

-ROOT- && .META. Table

HBase中有两张特殊的Table,-ROOT-和.META.

Ø  .META.:记录了用户表的Region信息,.META.可以有多个regoin

Ø  -ROOT-:记录了.META.表的Region信息,-ROOT-只有一个region

Ø  Zookeeper中记录了-ROOT-表的location

 

Client访问用户数据之前需要首先访问zookeeper,然后访问-ROOT-表,接着访问.META.表,最后才能找到用户数据的位置去访问,中间需要多次网络操作,不过client端会做cache缓存。

MapReduce on HBase

在HBase系统上运行批处理运算,最方便和实用的模型依然是MapReduce,如下图:

 

HBase Table和Region的关系,比较类似HDFS File和Block的关系,HBase提供了配套的TableInputFormat和TableOutputFormat API,可以方便的将HBase Table作为Hadoop MapReduce的Source和Sink,对于MapReduce Job应用开发人员来说,基本不需要关注HBase系统自身的细节。

HBase系统架构

 

Client

HBase Client使用HBase的RPC机制与HMaster和HRegionServer进行通信,对于管理类操作,Client与HMaster进行RPC;对于数据读写类操作,Client与HRegionServer进行RPC

Zookeeper

Zookeeper Quorum中除了存储了-ROOT-表的地址和HMaster的地址,HRegionServer也会把自己以Ephemeral方式注册到Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的健康状态。此外,Zookeeper也避免了HMaster的单点问题,见下文描述

HMaster

HMaster没有单点问题,HBase中可以启动多个HMaster,通过Zookeeper的Master Election机制保证总有一个Master运行,HMaster在功能上主要负责Table和Region的管理工作:

1.       管理用户对Table的增、删、改、查操作

2.       管理HRegionServer的负载均衡,调整Region分布

3.       在Region Split后,负责新Region的分配

4.       在HRegionServer停机后,负责失效HRegionServer 上的Regions迁移

HRegionServer

HRegionServer主要负责响应用户I/O请求,向HDFS文件系统中读写数据,是HBase中最核心的模块。

 

HRegionServer内部管理了一系列HRegion对象,每个HRegion对应了Table中的一个Region,HRegion中由多个HStore组成。每个HStore对应了Table中的一个Column Family的存储,可以看出每个Column Family其实就是一个集中的存储单元,因此最好将具备共同IO特性的column放在一个Column Family中,这样最高效。

HStore存储是HBase存储的核心了,其中由两部分组成,一部分是MemStore,一部分是StoreFiles。MemStore是Sorted Memory Buffer,用户写入的数据首先会放入MemStore,当MemStore满了以后会Flush成一个StoreFile(底层实现是HFile),当StoreFile文件数量增长到一定阈值,会触发Compact合并操作,将多个StoreFiles合并成一个StoreFile,合并过程中会进行版本合并和数据删除,因此可以看出HBase其实只有增加数据,所有的更新和删除操作都是在后续的compact过程中进行的,这使得用户的写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。当StoreFiles Compact后,会逐步形成越来越大的StoreFile,当单个StoreFile大小超过一定阈值后,会触发Split操作,同时把当前Region Split成2个Region,父Region会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer上,使得原先1个Region的压力得以分流到2个Region上。下图描述了Compaction和Split的过程:

 

在理解了上述HStore的基本原理后,还必须了解一下HLog的功能,因为上述的HStore在系统正常工作的前提下是没有问题的,但是在分布式系统环境中,无法避免系统出错或者宕机,因此一旦HRegionServer意外退出,MemStore中的内存数据将会丢失,这就需要引入HLog了。每个HRegionServer中都有一个HLog对象,HLog是一个实现Write Ahead Log的类,在每次用户操作写入MemStore的同时,也会写一份数据到HLog文件中(HLog文件格式见后续),HLog文件定期会滚动出新的,并删除旧的文件(已持久化到StoreFile中的数据)。当HRegionServer意外终止后,HMaster会通过Zookeeper感知到,HMaster首先会处理遗留的 HLog文件,将其中不同Region的Log数据进行拆分,分别放到相应region的目录下,然后再将失效的region重新分配,领取 到这些region的HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到MemStore中,然后flush到StoreFiles,完成数据恢复。

HBase存储格式

HBase中的所有数据文件都存储在Hadoop HDFS文件系统上,主要包括上述提出的两种文件类型:

1.       HFile, HBase中KeyValue数据的存储格式,HFile是Hadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile

2.       HLog File,HBase中WAL(Write Ahead Log) 的存储格式,物理上是Hadoop的Sequence File

HFile

下图是HFile的存储格式:

 

首先HFile文件是不定长的,长度固定的只有其中的两块:Trailer和FileInfo。正如图中所示的,Trailer中有指针指向其他数据块的起始点。File Info中记录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index块记录了每个Data块和Meta块的起始点。

Data Block是HBase I/O的基本单元,为了提高效率,HRegionServer中有基于LRU的Block Cache机制。每个Data块的大小可以在创建一个Table的时候通过参数指定,大号的Block有利于顺序Scan,小号Block利于随机查询。每个Data块除了开头的Magic以外就是一个个KeyValue对拼接而成, Magic内容就是一些随机数字,目的是防止数据损坏。后面会详细介绍每个KeyValue对的内部构造。

HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

 

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。

HLogFile

 

上图中示意了HLog文件的结构,其实HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是“写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。

HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue,可参见上文描述。

结束

本文对HBase技术在功能和设计上进行了大致的介绍,由于篇幅有限,本文没有过多深入地描述HBase的一些细节技术。目前一淘的存储系统就是基于HBase技术搭建的,后续将介绍“一淘分布式存储系统”,通过实际案例来更多的介绍HBase应用。

 

 

 

 

 

 

 

 

 

 

 

 

二、 HBASE官方文档

HBase 官方文档

Copyright © 2010 Apache Software Foundation, 盛大游戏-数据仓库团队-颜开(译)

Revision History

Revision 0.90.4

 

配置,数据模型使用入门

Abstract

这是 Apache HBase的官方文档, Hbase是一个分布式,版本化(versioned),构建在 Apache Hadoop和 Apache ZooKeeper上的列数据库.

我(译者)熟悉Hbase的源代码,从事Hbase的开发运维工作,如果有什么地方不清楚,欢迎一起讨论。邮箱[email protected] 

Table of Contents

1. 入门

1.1. 介绍

1.2. 快速开始

1.2.1. 下载解压最新版本

1.2.2. 启动 HBase

1.2.3. Shell 练习

1.2.4. 停止 HBase

1.2.5. 下一步该做什么

1.3. 慢速开始(相对快速开始)

1.3.1. 需要的软件

1.3.2. HBase运行模式:单机和分布式

1.3.3. 配置例子

2. 升级

2.1. 从HBase 0.20.x or 0.89.x 升级到 HBase 0.90.x 

3. 配置

3.1. hbase-site.xml 和 hbase-default.xml

3.1.1. HBase 默认配置

3.2. hbase-env.sh

3.3. log4j.properties

3.4. 重要的配置

3.5. 必须的配置 

3.6. 推荐的配置

3.6.1. zookeeper.session.timeout

3.6.2. hbase.regionserver.handler.count

3.6.3. 大内存机器的配置 

3.6.4. LZO 压缩

3.6.5. 更大的 Regions

3.6.6. 管理 Splitting

3.7. 连接Hbase集群的客户端配置和依赖

3.7.1. Java客户端配置

4. The HBase Shell

4.1. 使用脚本

4.2. Shell 技巧

4.2.1. irbrc

4.2.2. LOG 时间转换

4.2.3. Debug

5. 构建 HBase

5.1. 将一个 HBase release 加入到 Apache's Maven Repository

6. Developers

6.1. IDEs

6.1.1. Eclipse

6.2. 单元测试

6.2.1. Mocito

7. HBase 和 MapReduce

7.1. 默认 HBase MapReduce 分割器(Splitter)

7.2. HBase Input MapReduce 例子

7.3. 在一个MapReduce Job中访问其他的HBase Tables

7.4. 预测执行

8. HBase 的 Schema 设计

8.1. Schema 创建 

8.2. column families的数量 

8.3. 单调递增Row Keys/时序数据(log) 

8.4. 尽量最小化row和column的大小

8.5. 版本的时间 

9. Metrics

9.1. Metric 安装

9.2. RegionServer Metrics

9.2.1. hbase.regionserver.blockCacheCount

9.2.2. hbase.regionserver.blockCacheFree

9.2.3. hbase.regionserver.blockCacheHitRatio

9.2.4. hbase.regionserver.blockCacheSize

9.2.5. hbase.regionserver.compactionQueueSize

9.2.6. hbase.regionserver.fsReadLatency_avg_time

9.2.7. hbase.regionserver.fsReadLatency_num_ops

9.2.8. hbase.regionserver.fsSyncLatency_avg_time

9.2.9. hbase.regionserver.fsSyncLatency_num_ops

9.2.10. hbase.regionserver.fsWriteLatency_avg_time

9.2.11. hbase.regionserver.fsWriteLatency_num_ops

9.2.12. hbase.regionserver.memstoreSizeMB

9.2.13. hbase.regionserver.regions

9.2.14. hbase.regionserver.requests

9.2.15. hbase.regionserver.storeFileIndexSizeMB

9.2.16. hbase.regionserver.stores

9.2.17. hbase.regionserver.storeFiles

10. 跨集群复制

11. 数据模型

11.1. 概念视图

11.2. 物理视图

11.3. 表

11.4. 行

11.5. Column Family

11.6. Cells

11.7. 版本

11.7.1. Hbase的操作(包含版本操作)

11.7.2. 现有的限制

12. 架构

12.1. 客户端

12.1.1. 连接

12.1.2. 写缓冲和批量操作 

12.1.3. Filters

12.2. Daemons

12.2.1. Master

12.2.2. RegionServer

12.3. Regions

12.3.1. Region大小

12.3.2. Region Splits

12.3.3. Region负载均衡

12.3.4. Store

12.4. Write Ahead Log (WAL)

12.4.1. 目的

12.4.2. WAL Flushing

12.4.3. WAL Splitting

13. 性能调优

13.1. Java

13.1.1. 垃圾收集和HBase

13.2. 配置

13.2.1. Regions的数目

13.2.2. 管理压缩

13.2.3. 压缩

13.2.4. hbase.regionserver.handler.count

13.2.5. hfile.block.cache.size

13.2.6. hbase.regionserver.global.memstore.upperLimit

13.2.7. hbase.regionserver.global.memstore.lowerLimit

13.2.8. hbase.hstore.blockingStoreFiles

13.2.9. hbase.hregion.memstore.block.multiplier

13.3. Column Families的数目

13.4. 数据聚集

13.5. 批量Loading

13.5.1. Table创建: 预创建Regions 

13.6. HBase客户端

13.6.1. AutoFlush

13.6.2. Scan Caching

13.6.3. Scan 属性选择

13.6.4. 关闭 ResultScanners

13.6.5. 块缓存

13.6.6. Row Keys的负载优化

14. Bloom Filters

14.1. 配置

14.1.1. HColumnDescriptor 配置

14.1.2. io.hfile.bloom.enabled 全局关闭开关

14.1.3. io.hfile.bloom.error.rate

14.1.4. io.hfile.bloom.max.fold

14.2. Bloom StoreFile footprint

14.2.1. StoreFile中的BloomFilter, FileInfo数据结构

14.2.2. 在 StoreFile 元数据中的BloomFilter entries

15. Hbase的故障排除和Debug

15.1. 一般准则

15.2. Logs

15.2.1. Log 位置

15.3. 工具

15.3.1. search-hadoop.com

15.3.2. tail

15.3.3. top

15.3.4. jps

15.3.5. jstack

15.3.6. OpenTSDB

15.3.7. clusterssh+top

15.4. 客户端

15.4.1. ScannerTimeoutException

15.5. RegionServer

15.5.1. 启动错误

15.5.2. 运行时错误

15.5.3. 终止错误

15.6. Master

15.6.1. 启动错误

15.6.2. 终止错误

A. 工具

A.1. HBase hbck

A.2. HFile 工具

A.3. WAL Tools

A.3.1. HLog 工具

A.4. 压缩工具

A.5. Node下线

A.5.1. 依次重启

B. HBase中的压缩

B.1. 测试压缩工具

B.2. hbase.regionserver.codecs 

B.3. LZO 

B.4. GZIP 

C. FAQ

D. YCSB: 雅虎云服务 测试 和Hbase

Index

List of Tables

11.1. 表 webtable

11.2. ColumnFamily anchor

11.3. ColumnFamily contents

这本书是 HBase 的官方指南。版本为 0.90.4.可以在Hbase官网上找到它。也可以在 javadocJIRA 和 wiki 找到更多的资料。

此书正在编辑中。 可以向 HBase 官方提供补丁JIRA.

这个版本系译者水平限制,没有理解清楚或不需要翻译的地方保留英文原文。

最前面的话

若这是你第一次踏入分布式计算的精彩世界,你会感到这是一个有趣的年代。分布式计算是很难的,做一个分布式系统需要很多软硬件和网络的技能。你的集群可以会因为各式各样的错误发生故障。比如Hbase本身的Bug,错误的配置(包括操作系统),硬件的故障(网卡和磁盘甚至内存) 如果你一直在写单机程序的话,你需要重新开始学习。这里就是一个好的起点: 分布式计算的谬论.

Chapter 1. 入门

Table of Contents

1.1. 介绍

1.2. 快速开始

1.2.1. 下载解压最新版本

1.2.2. 启动 HBase

1.2.3. Shell 练习

1.2.4. 停止 HBase

1.2.5. 下一步该做什么

1.3. 慢速开始(相对快速开始)

1.3.1. 需要的软件

1.3.2. HBase运行模式:单机和分布式

1.3.3. 配置例子

1.1. 介绍

Section 1.2, “快速开始”会介绍如何运行一个单机版的Hbase.他运行在本地磁盘上。 Section 1.3, “慢速开始(相对快速开始)” 会介绍如何运行一个分布式的Hbase。他运行在HDFS上

1.2. 快速开始

本指南介绍了在单机安装Hbase的方法。会引导你通过shell创建一个表,插入一行,然后删除它,最后停止Hbase。只要10分钟就可以完成以下的操作。

1.2.1. 下载解压最新版本

选择一个 Apache 下载镜像,下载 HBase Releases. 点击 stable目录,然后下载后缀为 .tar.gz 的文件; 例如 hbase-0.90.4.tar.gz.

解压缩,然后进入到那个要解压的目录.

$ tar xfz hbase-0.90.4.tar.gz

$ cd hbase-0.90.4

现在你已经可以启动Hbase了。但是你可能需要先编辑 conf/hbase-site.xml 去配置hbase.rootdir,来选择Hbase将数据写到哪个目录 .

 

<?xml version="1.0"?>

<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<configuration>

<property>

<name>hbase.rootdir</name>

<value>file:///DIRECTORY/hbase</value>

</property>

</configuration>

 

将 DIRECTORY 替换成你期望写文件的目录. 默认 hbase.rootdir 是指向 /tmp/hbase-${user.name} ,也就说你会在重启后丢失数据(重启的时候操作系统会清理/tmp目录)

1.2.2. 启动 HBase

现在启动Hbase:

$ ./bin/start-hbase.sh

starting Master, logging to logs/hbase-user-master-example.org.out

现在你运行的是单机模式的Hbaes。所以的服务都运行在一个JVM上,包括Hbase和Zookeeper。Hbase的日志放在logs目录,当你启动出问题的时候,可以检查这个日志。

是否安装了 java ?

你需要确认安装了Oracle的1.6 版本的java.如果你在命令行键入java有反应说明你安装了Java。如果没有装,你需要先安装,然后编辑conf/hbase-env.sh,将其中的JAVA_HOME指向到你Java的安装目录。

1.2.3. Shell 练习

用shell连接你的Hbase

$ ./bin/hbase shell

HBase Shell; enter 'help<RETURN>' for list of supported commands.

Type "exit<RETURN>" to leave the HBase Shell

Version: 0.90.0, r1001068, Fri Sep 24 13:55:42 PDT 2010

 

hbase(main):001:0>

输入 help 然后 <RETURN> 可以看到一列shell命令。这里的帮助很详细,要注意的是表名,行和列需要加引号。

创建一个名为 test 的表,这个表只有一个 column family 为 cf。可以列出所有的表来检查创建情况,然后插入些值。

hbase(main):003:0> create 'test', 'cf'

0 row(s) in 1.2200 seconds

hbase(main):003:0> list 'table'

test

1 row(s) in 0.0550 seconds

hbase(main):004:0> put 'test', 'row1', 'cf:a', 'value1'

0 row(s) in 0.0560 seconds

hbase(main):005:0> put 'test', 'row2', 'cf:b', 'value2'

0 row(s) in 0.0370 seconds

hbase(main):006:0> put 'test', 'row3', 'cf:c', 'value3'

0 row(s) in 0.0450 seconds

以上我们分别插入了3行。第一个行key为row1, 列为 cf:a, 值是 value1。Hbase中的列是由 column family前缀和列的名字组成的,以冒号间隔。例如这一行的列名就是a.

检查插入情况.

Scan这个表,操作如下

hbase(main):007:0> scan 'test'

ROW        COLUMN+CELL

row1       column=cf:a, timestamp=1288380727188, value=value1

row2       column=cf:b, timestamp=1288380738440, value=value2

row3       column=cf:c, timestamp=1288380747365, value=value3

3 row(s) in 0.0590 seconds

Get一行,操作如下

hbase(main):008:0> get 'test', 'row1'

COLUMN      CELL

cf:a        timestamp=1288380727188, value=value1

1 row(s) in 0.0400 seconds

disable 再 drop 这张表,可以清除你刚刚的操作

hbase(main):012:0> disable 'test'

0 row(s) in 1.0930 seconds

hbase(main):013:0> drop 'test'

0 row(s) in 0.0770 seconds

关闭shell

hbase(main):014:0> exit

1.2.4. 停止 HBase

运行停止脚本来停止HBase.

$ ./bin/stop-hbase.sh

stopping hbase...............

1.2.5. 下一步该做什么

以上步骤仅仅适用于实验和测试。接下来你可以看 Section 1.3, “慢速开始(相对快速开始)” ,我们会介绍不同的Hbase运行模式,运行分布式Hbase中需要的软件 和如何配置。

1.3. 慢速开始(相对快速开始)

1.3.1. 需要的软件

Hbase有如下需要,请仔细阅读本章节以确保所有的需要都被满足。如果需求没有能满足,就有可能遇到莫名其妙的错误甚至丢失数据。

1.3.1.1. java

和Hadoop一样,Hbase需要Oracle版本的Java6.除了那个有问题的u18版本其他的都可以用,最好用最新的。

1.3.1.2. hadoop

该版本的Hbase只可以运行在Hadoop 0.20.x,不可以运行于hadoop 0.21.x (0.22.x也不行). HBase运行在没有持久同步功能的HDFS上会丢失数据。 Hadoop 0.20.2 和 Hadoop 0.20.203.0就没有这个功能。现在只有 branch-0.20-append 补丁有这个功能[1]. 现在官方的发行版都没有这个功能,所以你要自己打这个补丁。推荐看 Michael Noll 写的详细的说明, Building an Hadoop 0.20.x version for HBase 0.90.2.

你还可以用 Cloudera's CDH3. CDH 打了这个补丁 (CDH3 betas 就可以满足; b2, b3, or b4).

因为Hbase建立在Hadoop之上,所以他用到了hadoop.jar,这个Jar在 lib 里面。这个jar是hbase自己打了branch-0.20-append 补丁的hadoop.jar. Hadoop使用的hadoop.jar和Hbase使用的 必须 一致。所以你需要将 Hbase lib 目录下的hadoop.jar替换成Hadoop里面的那个,防止版本冲突。比方说CDH的版本没有HDFS-724而branch-0.20-append里面有,这个HDFS-724补丁修改了RPC协议。如果不替换,就会有版本冲突,继而造成严重的出错,Hadoop会看起来挂了。

我可以用Hbase里面的支持sync的hadoop.jar替代Hadoop里面的那个吗?

你可以这么干。详细可以参见这个邮件列表.

Hadoop 安全性

HBase运行在Hadoop 0.20.x上,就可以使用其中的安全特性 -- 只要你用这两个版本0.20S 和CDH3B3,然后把hadoop.jar替换掉就可以了.

1.3.1.3. ssh

必须安装ssh , sshd 也必须运行,这样Hadoop的脚本才可以远程操控其他的Hadoop和Hbase进程。ssh之间必须都打通,不用密码都可以登录,详细方法可以Google一下 ("ssh passwordless login").

1.3.1.4. DNS

HBase使用本地 hostname 才获得IP地址. 正反向的DNS都是可以的.

如果你的机器有多个接口,Hbase会使用hostname指向的主接口.

如果还不够,你可以设置 hbase.regionserver.dns.interface 来指定主接口。当然你的整个集群的配置文件都必须一致,每个主机都使用相同的网络接口

还有一种方法是设置 hbase.regionserver.dns.nameserver来指定nameserver,不使用系统带的.

1.3.1.5. NTP

集群的时钟要保证基本的一致。稍有不一致是可以容忍的,但是很大的不一致会造成奇怪的行为。 运行 NTP 或者其他什么东西来同步你的时间.

如果你查询的时候或者是遇到奇怪的故障,可以检查一下系统时间是否正确!

1.3.1.6.  ulimit 和 nproc

HBase是数据库,会在同一时间使用很多的文件句柄。大多数linux系统使用的默认值1024是不能满足的,会导致FAQ: Why do I see "java.io.IOException...(Too many open files)" in my logs?异常。还可能会发生这样的异常

2010-04-06 03:04:37,542 INFO org.apache.hadoop.hdfs.DFSClient: Exception increateBlockOutputStream java.io.EOFException

2010-04-06 03:04:37,542 INFO org.apache.hadoop.hdfs.DFSClient: Abandoning block blk_-6935524980745310745_1391901

 

所以你需要修改你的最大文件句柄限制。可以设置到10k. 你还需要修改 hbase 用户的 nproc,如果过低会造成 OutOfMemoryError异常。 [2] [3].

需要澄清的,这两个设置是针对操作系统的,不是Hbase本身的。有一个常见的错误是Hbase运行的用户,和设置最大值的用户不是一个用户。在Hbase启动的时候,第一行日志会现在ulimit信息,所以你最好检查一下。 [4]

1.3.1.6.1. 在Ubuntu上设置ulimit

如果你使用的是Ubuntu,你可以这样设置:

在文件 /etc/security/limits.conf 添加一行,如:

hadoop  -       nofile  32768

可以把 hadoop 替换成你运行Hbase和Hadoop的用户。如果你用两个用户,你就需要配两个。还有配nproc hard 和 soft limits. 如:

hadoop soft/hard nproc 32000

.

在 /etc/pam.d/common-session 加上这一行:

session required  pam_limits.so

否则在 /etc/security/limits.conf上的配置不会生效.

还有注销再登录,这些配置才能生效!

1.3.1.7. dfs.datanode.max.xcievers

一个 Hadoop HDFS Datanode 有一个同时处理文件的上限. 这个参数叫 xcievers (Hadoop的作者把这个单词拼错了). 在你加载之前,先确认下你有没有配置这个文件conf/hdfs-site.xml里面的xceivers参数,至少要有4096:

<property>

<name>dfs.datanode.max.xcievers</name>

<value>4096</value>

</property>

 

对于HDFS修改配置要记得重启.

如果没有这一项配置,你可能会遇到奇怪的失败。你会在Datanode的日志中看到xcievers exceeded,但是运行起来会报 missing blocks错误。例如: 10/12/08 20:10:31 INFO hdfs.DFSClient: Could not obtain block blk_XXXXXXXXXXXXXXXXXXXXXX_YYYYYYYY from any node: java.io.IOException: No live nodes contain current block. Will get new block locations from namenode and retry... [5]

1.3.1.8. Windows

HBase没有怎么在Windows下测试过。所以不推荐在Windows下运行.

如果你实在是想运行,需要安装Cygwin 还虚拟一个unix环境.详情请看 Windows 安装指导 . 或者 搜索邮件列表找找最近的关于windows的注意点

1.3.2. HBase运行模式:单机和分布式

HBase有两个运行模式: Section 1.3.2.1, “单机模式” 和 Section 1.3.2.2, “分布式模式”. 默认是单机模式,如果要分布式模式你需要编辑 conf 文件夹中的配置文件.

不管是什么模式,你都需要编辑 conf/hbase-env.sh来告知Hbase java的安装路径.在这个文件里你还可以设置Hbase的运行环境,诸如 heapsize和其他 JVM有关的选项, 还有Log文件地址,等等. 设置 JAVA_HOME指向 java安装的路径.

1.3.2.1. 单机模式

这是默认的模式,在 Section 1.2, “快速开始” 一章中介绍的就是这个模式. 在单机模式中,Hbase使用本地文件系统,而不是HDFS ,所以的服务和zooKeeper都运作在一个JVM中。zookeep监听一个端口,这样客户端就可以连接Hbase了。

1.3.2.2. 分布式模式

分布式模式分两种。伪分布式模式是把进程运行在一台机器上,但不是一个JVM.而完全分布式模式就是把整个服务被分布在各个节点上了 [6].

分布式模式需要使用 Hadoop Distributed File System (HDFS).可以参见 HDFS需求和指导来获得关于安装HDFS的指导。在操作Hbase之前,你要确认HDFS可以正常运作。

在我们安装之后,你需要确认你的伪分布式模式或者 完全分布式模式的配置是否正确。这两个模式可以使用同一个验证脚本Section 1.3.2.3, “运行和确认你的安装”

1.3.2.2.1. 伪分布式模式

伪分布式模式是一个相对简单的分布式模式。这个模式是用来测试的。不能把这个模式用于生产环节,也不能用于测试性能。

你确认HDFS安装成功之后,就可以先编辑 conf/hbase-site.xml。在这个文件你可以加入自己的配置,这个配置会覆盖 Section 3.1.1, “HBase 默认配置” and Section 1.3.2.2.2.3, “HDFS客户端配置”. 运行Hbase需要设置hbase.rootdir 属性.该属性是指Hbase在HDFS中使用的目录的位置。例如,要想 /hbase 目录,让namenode 监听locahost的9000端口,只有一份数据拷贝(HDFS默认是3份拷贝)。可以在 hbase-site.xml 写上如下内容

<configuration>

...

<property>

<name>hbase.rootdir</name>

<value>hdfs://localhost:9000/hbase</value>

<description>The directory shared by RegionServers.

</description>

</property>

<property>

<name>dfs.replication</name>

<value>1</value>

<description>The replication count for HLog & HFile storage. Should not be greater than HDFS datanode count.

</description>

</property>

...

</configuration>

Note

让Hbase自己创建 hbase.rootdir 目录,如果你自己建这个目录,会有一个warning,Hbase会试图在里面进行migration操作,但是缺少必须的文件。

Note

上面我们绑定到 localhost. 也就是说除了本机,其他机器连不上Hbase。所以你需要设置成别的,才能使用它。

现在可以跳到 Section 1.3.2.3, “运行和确认你的安装” 来运行和确认你的伪分布式模式安装了。 [7]

1.3.2.2.2. 完全分布式模式

要想运行完全分布式模式,你要进行如下配置,先在 hbase-site.xml, 加一个属性 hbase.cluster.distributed 设置为 true 然后把 hbase.rootdir 设置为HDFS的NameNode的位置。例如,你的namenode运行在namenode.example.org,端口是9000 你期望的目录是 /hbase,使用如下的配置

<configuration>

...

<property>

<name>hbase.rootdir</name>

<value>hdfs://namenode.example.org:9000/hbase</value>

<description>The directory shared by RegionServers.

</description>

</property>

<property>

<name>hbase.cluster.distributed</name>

<value>true</value>

<description>The mode the cluster will be in. Possible values are

false: standalone and pseudo-distributed setups with managed Zookeeper

true: fully-distributed with unmanaged Zookeeper Quorum (see hbase-env.sh)

</description>

</property>

...

</configuration>

1.3.2.2.2.1. regionservers

完全分布式模式的还需要修改conf/regionservers. 在 Section 1.3.3.1.2, “regionservers 列出了你希望运行的全部 HRegionServer,一行写一个host (就像Hadoop里面的 slaves 一样). 列在这里的server会随着集群的启动而启动,集群的停止而停止.

1.3.2.2.2.2. ZooKeeper

一个分布式运行的Hbase依赖一个zookeeper集群。所有的节点和客户端都必须能够访问zookeeper。默认的情况下Hbase会管理一个zookeep集群。这个集群会随着Hbase的启动而启动。当然,你也可以自己管理一个zookeeper集群,但需要配置Hbase。你需要修改conf/hbase-env.sh里面的HBASE_MANAGES_ZK 来切换。这个值默认是true的,作用是让Hbase启动的时候同时也启动zookeeper.

当Hbase管理zookeeper的时候,你可以通过修改zoo.cfg来配置zookeeper,一个更加简单的方法是在 conf/hbase-site.xml里面修改zookeeper的配置。Zookeep的配置是作为property写在 hbase-site.xml里面的。option的名字是 hbase.zookeeper.property. 打个比方, clientPort 配置在xml里面的名字是 hbase.zookeeper.property.clientPort. 所有的默认值都是Hbase决定的,包括zookeeper, 参见 Section 3.1.1, “HBase 默认配置”. 可以查找 hbase.zookeeper.property 前缀,找到关于zookeeper的配置。 [8]

对于zookeepr的配置,你至少要在 hbase-site.xml中列出zookeepr的ensemble servers,具体的字段是 hbase.zookeeper.quorum. 该这个字段的默认值是 localhost,这个值对于分布式应用显然是不可以的. (远程连接无法使用).

我需要运行几个zookeeper?

你运行一个zookeeper也是可以的,但是在生产环境中,你最好部署3,5,7个节点。部署的越多,可靠性就越高,当然只能部署奇数个,偶数个是不可以的。你需要给每个zookeeper 1G左右的内存,如果可能的话,最好有独立的磁盘。 (独立磁盘可以确保zookeeper是高性能的。).如果你的集群负载很重,不要把Zookeeper和RegionServer运行在同一台机器上面。就像DataNodes 和 TaskTrackers一样

打个比方,Hbase管理着的ZooKeeper集群在节点 rs{1,2,3,4,5}.example.com, 监听2222 端口(默认是2181),并确保conf/hbase-env.sh文件中 HBASE_MANAGE_ZK的值是 true ,再编辑 conf/hbase-site.xml 设置 hbase.zookeeper.property.clientPort 和 hbase.zookeeper.quorum。你还可以设置 hbase.zookeeper.property.dataDir属性来把ZooKeeper保存数据的目录地址改掉。默认值是 /tmp ,这里在重启的时候会被操作系统删掉,可以把它修改到 /user/local/zookeeper.

<configuration>

...

<property>

<name>hbase.zookeeper.property.clientPort</name>

<value>2222</value>

<description>Property from ZooKeeper's config zoo.cfg.

The port at which the clients will connect.

</description>

</property>

<property>

<name>hbase.zookeeper.quorum</name>

<value>rs1.example.com,rs2.example.com,rs3.example.com,rs4.example.com,rs5.example.com</value>

<description>Comma separated list of servers in the ZooKeeper Quorum.

For example, "host1.mydomain.com,host2.mydomain.com,host3.mydomain.com".

By default this is set to localhost for local and pseudo-distributed modes

of operation. For a fully-distributed setup, this should be set to a full

list of ZooKeeper quorum servers. If HBASE_MANAGES_ZK is set in hbase-env.sh

this is the list of servers which we will start/stop ZooKeeper on.

</description>

</property>

<property>

<name>hbase.zookeeper.property.dataDir</name>

<value>/usr/local/zookeeper</value>

<description>Property from ZooKeeper's config zoo.cfg.

The directory where the snapshot is stored.

</description>

</property>

...

</configuration>

1.3.2.2.2.2.1. 使用现有的ZooKeeper例子

让Hbase使用一个现有的不被Hbase托管的Zookeep集群,需要设置 conf/hbase-env.sh文件中的HBASE_MANAGES_ZK 属性为 false

...

# Tell HBase whether it should manage it's own instance of Zookeeper or not.

export HBASE_MANAGES_ZK=false

接下来,指明Zookeeper的host和端口。可以在 hbase-site.xml中设置, 也可以在Hbase的CLASSPATH下面加一个zoo.cfg配置文件。 HBase 会优先加载 zoo.cfg 里面的配置,把hbase-site.xml里面的覆盖掉.

当Hbase托管ZooKeeper的时候,Zookeeper集群的启动是Hbase启动脚本的一部分。但现在,你需要自己去运行。你可以这样做

${HBASE_HOME}/bin/hbase-daemons.sh {start,stop} zookeeper

你可以用这条命令启动ZooKeeper而不启动Hbase. HBASE_MANAGES_ZK 的值是 false,如果你想在Hbase重启的时候不重启ZooKeeper,你可以这样做

对于独立Zoopkeeper的问题,你可以在 Zookeeper启动得到帮助.

1.3.2.2.2.3. HDFS客户端配置

如果你希望Hadoop集群上做HDFS 客户端配置 ,例如你的HDFS客户端的配置和服务端的不一样。按照如下的方法配置,HBase就能看到你的配置信息:

· 在hbase-env.sh里将HBASE_CLASSPATH环境变量加上HADOOP_CONF_DIR 。

· 在${HBASE_HOME}/conf下面加一个 hdfs-site.xml (或者 hadoop-site.xml) ,最好是软连接

· 如果你的HDFS客户端的配置不多的话,你可以把这些加到 hbase-site.xml上面.

例如HDFS的配置 dfs.replication.你希望复制5份,而不是默认的3份。如果你不照上面的做的话,Hbase只会复制3份。

1.3.2.3. 运行和确认你的安装

首先确认你的HDFS是运行着的。你可以运行HADOOP_HOME中的 bin/start-hdfs.sh 来启动HDFS.你可以通过put命令来测试放一个文件,然后有get命令来读这个文件。通常情况下Hbase是不会运行mapreduce的。所以比不需要检查这些。

如果你自己管理ZooKeeper集群,你需要确认它是运行着的。如果是Hbase托管,ZoopKeeper会随Hbase启动。

用如下命令启动Hbase:

bin/start-hbase.sh

这个脚本在HBASE_HOME目录里面。

你现在已经启动Hbase了。Hbase把log记在 logs 子目录里面. 当Hbase启动出问题的时候,可以看看Log.

Hbase也有一个界面,上面会列出重要的属性。默认是在Master的60010端口上H (HBase RegionServers 会默认绑定 60020端口,在端口60030上有一个展示信息的界面 ).如果Master运行在 master.example.org,端口是默认的话,你可以用浏览器在 http://master.example.org:60010看到主界面. .

一旦Hbase启动,参见Section 1.2.3, “Shell 练习”可以看到如何建表,插入数据,scan你的表,还有disable这个表,最后把它删掉。

可以在Hbase Shell停止Hbase

$ ./bin/stop-hbase.sh

stopping hbase...............

停止操作需要一些时间,你的集群越大,停的时间可能会越长。如果你正在运行一个分布式的操作,要确认在Hbase彻底停止之前,Hadoop不能停.

1.3.3. 配置例子

1.3.3.1. 简单的分布式Hbase安装

这里是一个10节点的Hbase的简单示例,这里的配置都是基本的,节点名为 example0, example1... 一直到 example9 . HBase Master 和 HDFS namenode 运作在同一个节点 example0上. RegionServers 运行在节点 example1-example9. 一个 3-节点 ZooKeeper 集群运行在example1, example2, 和 example3,端口保持默认. ZooKeeper 的数据保存在目录 /export/zookeeper. 下面我们展示主要的配置文件-- hbase-site.xmlregionservers, 和 hbase-env.sh -- 这些文件可以在 conf目录找到.

1.3.3.1.1. hbase-site.xml

 

<?xml version="1.0"?>

<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<configuration>

<property>

<name>hbase.zookeeper.quorum</name>

<value>example1,example2,example3</value>

<description>The directory shared by RegionServers.

</description>

</property>

<property>

<name>hbase.zookeeper.property.dataDir</name>

<value>/export/zookeeper</value>

<description>Property from ZooKeeper's config zoo.cfg.

The directory where the snapshot is stored.

</description>

</property>

<property>

<name>hbase.rootdir</name>

<value>hdfs://example0:9000/hbase</value>

<description>The directory shared by RegionServers.

</description>

</property>

<property>

<name>hbase.cluster.distributed</name>

<value>true</value>

<description>The mode the cluster will be in. Possible values are

false: standalone and pseudo-distributed setups with managed Zookeeper

true: fully-distributed with unmanaged Zookeeper Quorum (see hbase-env.sh)

</description>

</property>

</configuration>

 

 

1.3.3.1.2. regionservers

这个文件把RegionServer的节点列了下来。在这个例子里面我们让所有的节点都运行RegionServer,除了第一个节点 example1,它要运行 HBase Master 和 HDFS namenode

example1

example3

example4

example5

example6

example7

example8

example9

 

1.3.3.1.3. hbase-env.sh

下面我们用diff 命令来展示 hbase-env.sh 文件相比默认变化的部分. 我们把Hbase的堆内存设置为4G而不是默认的1G.

 

$ git diff hbase-env.sh

diff --git a/conf/hbase-env.sh b/conf/hbase-env.sh

index e70ebc6..96f8c27 100644

--- a/conf/hbase-env.sh

+++ b/conf/hbase-env.sh

@@ -31,7 +31,7 @@ export JAVA_HOME=/usr/lib//jvm/java-6-sun/

# export HBASE_CLASSPATH=

 

# The maximum amount of heap to use, in MB. Default is 1000.

-# export HBASE_HEAPSIZE=1000

+export HBASE_HEAPSIZE=4096

 

# Extra Java runtime options.

# Below are what we set by default.  May only work with SUN JVM.

 

 

你可以使用 rsync 来同步 conf 文件夹到你的整个集群.

 

[1] See CHANGES.txt in branch-0.20-append to see list of patches involved adding append on the Hadoop 0.20 branch.

[2] See Jack Levin's major hdfs issues note up on the user list.

[3] 这样的需求对于数据库应用来说是很常见的,例如Oracle。 Setting Shell Limits for the Oracle User in Short Guide to install Oracle 10 on Linux.

[4] A useful read setting config on you hadoop cluster is Aaron Kimballs' Configuration Parameters: What can you just ignore?

[5] 参见 Hadoop HDFS: Deceived by Xciever for an informative rant on xceivering.

[6] 这两个命名法来自于Hadoop.

[7] See Pseudo-distributed mode extras for notes on how to start extra Masters and RegionServers when running pseudo-distributed.

[8] For the full list of ZooKeeper configurations, see ZooKeeper's zoo.cfg. HBase does not ship with a zoo.cfg so you will need to browse the conf directory in an appropriate ZooKeeper download.

Chapter 2. 升级

Table of Contents

2.1. 从HBase 0.20.x or 0.89.x 升级到 HBase 0.90.x 

参见 Section 1.3.1, “需要的软件”, 需要特别注意有关Hadoop 版本的信息.

2.1. 从HBase 0.20.x or 0.89.x 升级到 HBase 0.90.x

0.90.x 版本的HBase可以在 HBase 0.20.x 或者 HBase 0.89.x的数据上启动. 不需要转换数据文件, HBase 0.89.x 和 0.90.x 的region目录名是不一样的 -- 老版本用md5 hash 而不是jenkins hash 来命名region-- 这就意味着,一旦启动,再也不能回退到 HBase 0.20.x.

在升级的时候,一定要将hbase-default.xml 从你的 conf目录删掉。 0.20.x 版本的配置对于 0.90.x HBase不是最佳的. hbase-default.xml 现在已经被打包在 HBase jar 里面了. 如果你想看看这个文件内容,你可以在src目录下 src/main/resources/hbase-default.xml 或者在 Section 3.1.1, “HBase 默认配置”看到.

最后,如果从0.20.x升级,需要在shell里检查 .META. schema . 过去,我们推荐用户使用16KB的 MEMSTORE_FLUSHSIZE. 在shell中运行 hbase> scan '-ROOT-'. 会显示当前的.META. schema. 检查 MEMSTORE_FLUSHSIZE 的大小. 看看是不是 16KB (16384)? 如果是的话,你需要修改它(默认的值是 64MB (67108864)) 运行脚本 bin/set_meta_memstore_size.rb. 这个脚本会修改 .META. schema. 如果不运行的话,集群会比较慢[9] .

 

[9] 参见 HBASE-3499 Users upgrading to 0.90.0 need to have their .META. table updated with the right MEMSTORE_SIZE

Chapter 3. 配置

Table of Contents

3.1. hbase-site.xml 和 hbase-default.xml

3.1.1. HBase 默认配置

3.2. hbase-env.sh

3.3. log4j.properties

3.4. 重要的配置

3.5. 必须的配置 

3.6. 推荐的配置

3.6.1. zookeeper.session.timeout

3.6.2. hbase.regionserver.handler.count

3.6.3. 大内存机器的配置 

3.6.4. LZO 压缩

3.6.5. 更大的 Regions

3.6.6. 管理 Splitting

3.7. 连接Hbase集群的客户端配置和依赖

3.7.1. Java客户端配置

Hbase的配置系统和Hadoop一样。在conf/hbase-env.sh配置系统的部署信息和环境变量。 -- 这个配置会被启动shell使用 -- 然后在XML文件里配置信息,覆盖默认的配置。告知Hbase使用什么目录地址,ZooKeeper的位置等等信息。 [10] .

当你使用分布式模式的时间,当你编辑完一个文件之后,记得要把这个文件复制到整个集群的conf 目录下。Hbase不会帮你做这些,你得用 rsync.

3.1. hbase-site.xml 和 hbase-default.xml

正如Hadoop放置HDFS的配置文件hdfs-site.xml,Hbase的配置文件是 conf/hbase-site.xml. 你可以在 Section 3.1.1, “HBase 默认配置”找到配置的属性列表。你也可以看有代码里面的hbase-default.xml文件,他在src/main/resources目录下。

不是所有的配置都在 hbase-default.xml出现.只要改了代码,配置就有可能改变,所以唯一了解这些被改过的配置的办法是读源代码本身。

要注意的是,要重启集群才能是配置生效。

3.1.1. HBase 默认配置

HBase 默认配置

该文档是用hbase默认配置文件生成的,文件源是 hbase-default.xml(因翻译需要,被译者修改成中文注释).

hbase.rootdir

这个目录是region server的共享目录,用来持久化Hbase。URL需要是'完全正确'的,还要包含文件系统的scheme。例如,要表示hdfs中的'/hbase'目录,namenode 运行在namenode.example.org的9090端口。则需要设置为hdfs://namenode.example.org:9000/hbase。默认情况下Hbase是写到/tmp的。不改这个配置,数据会在重启的时候丢失。

默认: file:///tmp/hbase-${user.name}/hbase

hbase.master.port

Hbase的Master的端口.

默认: 60000

hbase.cluster.distributed

Hbase的运行模式。false是单机模式,true是分布式模式。若为false,Hbase和Zookeeper会运行在同一个JVM里面。

默认: false

hbase.tmp.dir

本地文件系统的临时文件夹。可以修改到一个更为持久的目录上。(/tmp会在重启时清楚)

默认: /tmp/hbase-${user.name}

hbase.master.info.port

HBase Master web 界面端口. 设置为-1 意味着你不想让他运行。

默认: 60010

hbase.master.info.bindAddress

HBase Master web 界面绑定的端口

默认: 0.0.0.0

hbase.client.write.buffer

HTable客户端的写缓冲的默认大小。这个值越大,需要消耗的内存越大。因为缓冲在客户端和服务端都有实例,所以需要消耗客户端和服务端两个地方的内存。得到的好处是,可以减少RPC的次数。可以这样估算服务器端被占用的内存: hbase.client.write.buffer * hbase.regionserver.handler.count

默认: 2097152

hbase.regionserver.port

HBase RegionServer绑定的端口

默认: 60020

hbase.regionserver.info.port

HBase RegionServer web 界面绑定的端口设置为 -1 意味这你不想与运行 RegionServer 界面.

默认: 60030

hbase.regionserver.info.port.auto

Master或RegionServer是否要动态搜一个可以用的端口来绑定界面。当hbase.regionserver.info.port已经被占用的时候,可以搜一个空闲的端口绑定。这个功能在测试的时候很有用。默认关闭。

默认: false

hbase.regionserver.info.bindAddress

HBase RegionServer web 界面的IP地址

默认: 0.0.0.0

hbase.regionserver.class

RegionServer 使用的接口。客户端打开代理来连接region server的时候会使用到。

默认: org.apache.hadoop.hbase.ipc.HRegionInterface

hbase.client.pause

通常的客户端暂停时间。最多的用法是客户端在重试前的等待时间。比如失败的get操作和region查询操作等都很可能用到。

默认: 1000

hbase.client.retries.number

最大重试次数。例如 region查询,Get操作,Update操作等等都可能发生错误,需要重试。这是最大重试错误的值。

默认: 10

hbase.client.scanner.caching

当调用Scanner的next方法,而值又不在缓存里的时候,从服务端一次获取的行数。越大的值意味着Scanner会快一些,但是会占用更多的内存。当缓冲被占满的时候,next方法调用会越来越慢。慢到一定程度,可能会导致超时。例如超过了hbase.regionserver.lease.period。

默认: 1

hbase.client.keyvalue.maxsize

一个KeyValue实例的最大size.这个是用来设置存储文件中的单个entry的大小上界。因为一个KeyValue是不能分割的,所以可以避免因为数据过大导致region不可分割。明智的做法是把它设为可以被最大region size整除的数。如果设置为0或者更小,就会禁用这个检查。默认10MB。

默认: 10485760

hbase.regionserver.lease.period

客户端租用HRegion server 期限,即超时阀值。单位是毫秒。默认情况下,客户端必须在这个时间内发一条信息,否则视为死掉。

默认: 60000

hbase.regionserver.handler.count

RegionServers受理的RPC Server实例数量。对于Master来说,这个属性是Master受理的handler数量

默认: 10

hbase.regionserver.msginterval

RegionServer 发消息给 Master 时间间隔,单位是毫秒

默认: 3000

hbase.regionserver.optionallogflushinterval

将Hlog同步到HDFS的间隔。如果Hlog没有积累到一定的数量,到了时间,也会触发同步。默认是1秒,单位毫秒。

默认: 1000

hbase.regionserver.regionSplitLimit

region的数量到了这个值后就不会在分裂了。这不是一个region数量的硬性限制。但是起到了一定指导性的作用,到了这个值就该停止分裂了。默认是MAX_INT.就是说不阻止分裂。

默认: 2147483647

hbase.regionserver.logroll.period

提交commit log的间隔,不管有没有写足够的值。

默认: 3600000

hbase.regionserver.hlog.reader.impl

HLog file reader 的实现.

默认: org.apache.hadoop.hbase.regionserver.wal.SequenceFileLogReader

hbase.regionserver.hlog.writer.impl

HLog file writer 的实现.

默认: org.apache.hadoop.hbase.regionserver.wal.SequenceFileLogWriter

hbase.regionserver.thread.splitcompactcheckfrequency

region server 多久执行一次split/compaction 检查.

默认: 20000

hbase.regionserver.nbreservationblocks

储备的内存block的数量(译者注:就像石油储备一样)。当发生out of memory 异常的时候,我们可以用这些内存在RegionServer停止之前做清理操作。

默认: 4

hbase.zookeeper.dns.interface

当使用DNS的时候,Zookeeper用来上报的IP地址的网络接口名字。

默认: default

hbase.zookeeper.dns.nameserver

当使用DNS的时候,Zookeepr使用的DNS的域名或者IP 地址,Zookeeper用它来确定和master用来进行通讯的域名.

默认: default

hbase.regionserver.dns.interface

当使用DNS的时候,RegionServer用来上报的IP地址的网络接口名字。

默认: default

hbase.regionserver.dns.nameserver

当使用DNS的时候,RegionServer使用的DNS的域名或者IP 地址,RegionServer用它来确定和master用来进行通讯的域名.

默认: default

hbase.master.dns.interface

当使用DNS的时候,Master用来上报的IP地址的网络接口名字。

默认: default

hbase.master.dns.nameserver

当使用DNS的时候,RegionServer使用的DNS的域名或者IP 地址,Master用它来确定用来进行通讯的域名.

默认: default

hbase.balancer.period

Master执行region balancer的间隔。

默认: 300000

hbase.regions.slop

当任一regionserver有average + (average * slop)个region是会执行Rebalance

默认: 0

hbase.master.logcleaner.ttl

Hlog存在于.oldlogdir 文件夹的最长时间, 超过了就会被 Master 的线程清理掉.

默认: 600000

hbase.master.logcleaner.plugins

LogsCleaner服务会执行的一组LogCleanerDelegat。值用逗号间隔的文本表示。这些WAL/HLog cleaners会按顺序调用。可以把先调用的放在前面。你可以实现自己的LogCleanerDelegat,加到Classpath下,然后在这里写下类的全称。一般都是加在默认值的前面。

默认: org.apache.hadoop.hbase.master.TimeToLiveLogCleaner

hbase.regionserver.global.memstore.upperLimit

单个region server的全部memtores的最大值。超过这个值,一个新的update操作会被挂起,强制执行flush操作。

默认: 0.4

hbase.regionserver.global.memstore.lowerLimit

当强制执行flush操作的时候,当低于这个值的时候,flush会停止。默认是堆大小的 35% . 如果这个值和 hbase.regionserver.global.memstore.upperLimit 相同就意味着当update操作因为内存限制被挂起时,会尽量少的执行flush(译者注:一旦执行flush,值就会比下限要低,不再执行)

默认: 0.35

hbase.server.thread.wakefrequency

service工作的sleep间隔,单位毫秒。可以作为service线程的sleep间隔,比如log roller.

默认: 10000

hbase.hregion.memstore.flush.size

当memstore的大小超过这个值的时候,会flush到磁盘。这个值被一个线程每隔hbase.server.thread.wakefrequency检查一下。

默认: 67108864

hbase.hregion.preclose.flush.size

当一个region中的memstore的大小大于这个值的时候,我们又触发了close.会先运行“pre-flush”操作,清理这个需要关闭的memstore,然后将这个region下线。当一个region下线了,我们无法再进行任何写操作。如果一个memstore很大的时候,flush操作会消耗很多时间。"pre-flush"操作意味着在region下线之前,会先把memstore清空。这样在最终执行close操作的时候,flush操作会很快。

默认: 5242880

hbase.hregion.memstore.block.multiplier

如果memstore有hbase.hregion.memstore.block.multiplier倍数的hbase.hregion.flush.size的大小,就会阻塞update操作。这是为了预防在update高峰期会导致的失控。如果不设上界,flush的时候会花很长的时间来合并或者分割,最坏的情况就是引发out of memory异常。(译者注:内存操作的速度和磁盘不匹配,需要等一等。原文似乎有误)

默认: 2

hbase.hregion.memstore.mslab.enabled

体验特性:启用memStore分配本地缓冲区。这个特性是为了防止在大量写负载的时候堆的碎片过多。这可以减少GC操作的频率。(GC有可能会Stop the world)(译者注:实现的原理相当于预分配内存,而不是每一个值都要从堆里分配)

默认: false

hbase.hregion.max.filesize

最大HStoreFile大小。若某个Column families的HStoreFile增长达到这个值,这个Hegion会被切割成两个。 Default: 256M.

默认: 268435456

hbase.hstore.compactionThreshold

当一个HStore含有多于这个值的HStoreFiles(每一个memstore flush产生一个HStoreFile)的时候,会执行一个合并操作,把这HStoreFiles写成一个。这个值越大,需要合并的时间就越长。

默认: 3

hbase.hstore.blockingStoreFiles

当一个HStore含有多于这个值的HStoreFiles(每一个memstore flush产生一个HStoreFile)的时候,会执行一个合并操作,update会阻塞直到合并完成,直到超过了hbase.hstore.blockingWaitTime的值

默认: 7

hbase.hstore.blockingWaitTime

hbase.hstore.blockingStoreFiles所限制的StoreFile数量会导致update阻塞,这个时间是来限制阻塞时间的。当超过了这个时间,HRegion会停止阻塞update操作,不过合并还有没有完成。默认为90s.

默认: 90000

hbase.hstore.compaction.max

每个“小”合并的HStoreFiles最大数量。

默认: 10

hbase.hregion.majorcompaction

一个Region中的所有HStoreFile的major compactions的时间间隔。默认是1天。设置为0就是禁用这个功能。

默认: 86400000

hbase.mapreduce.hfileoutputformat.blocksize

MapReduce中HFileOutputFormat可以写 storefiles/hfiles. 这个值是hfile的blocksize的最小值。通常在Hbase写Hfile的时候,bloocksize是由table schema(HColumnDescriptor)决定的,但是在mapreduce写的时候,我们无法获取schema中blocksize。这个值越小,你的索引就越大,你随机访问需要获取的数据就越小。如果你的cell都很小,而且你需要更快的随机访问,可以把这个值调低。

默认: 65536

hfile.block.cache.size

分配给HFile/StoreFile的block cache占最大堆(-Xmx setting)的比例。默认是20%,设置为0就是不分配。

默认: 0.2

hbase.hash.type

哈希函数使用的哈希算法。可以选择两个值:: murmur (MurmurHash) 和 jenkins (JenkinsHash). 这个哈希是给 bloom filters用的.

默认: murmur

hbase.master.keytab.file

HMaster server验证登录使用的kerberos keytab 文件路径。(译者注:Hbase使用Kerberos实现安全)

默认:

hbase.master.kerberos.principal

例如. "hbase/[email protected] ". HMaster运行需要使用 kerberos principal name. principal name 可以在: user/hostname@DOMAIN 中获取. 如果 "_HOST" 被用做hostname portion,需要使用实际运行的hostname来替代它。

默认:

hbase.regionserver.keytab.file

HRegionServer验证登录使用的kerberos keytab 文件路径。

默认:

hbase.regionserver.kerberos.principal

例如. "hbase/[email protected] ". HRegionServer运行需要使用 kerberos principal name. principal name 可以在: user/hostname@DOMAIN 中获取. 如果 "_HOST" 被用做hostname portion,需要使用实际运行的hostname来替代它。在这个文件中必须要有一个entry来描述 hbase.regionserver.keytab.file

默认:

zookeeper.session.timeout

ZooKeeper 会话超时.Hbase把这个值传递改zk集群,向他推荐一个会话的最大超时时间。详见http://hadoop.apache.org/zookeeper/docs/current/zookeeperProgrammers.html#ch_zkSessions "The client sends a requested timeout, the server responds with the timeout that it can give the client. "。单位是毫秒

默认: 180000

zookeeper.znode.parent

ZooKeeper中的Hbase的根ZNode。所有的Hbase的ZooKeeper会用这个目录配置相对路径。默认情况下,所有的Hbase的ZooKeeper文件路径是用相对路径,所以他们会都去这个目录下面。

默认: /hbase

zookeeper.znode.rootserver

ZNode 保存的 根region的路径. 这个值是由Master来写,client和regionserver 来读的。如果设为一个相对地址,父目录就是 ${zookeeper.znode.parent}.默认情形下,意味着根region的路径存储在/hbase/root-region-server.

默认: root-region-server

hbase.zookeeper.quorum

Zookeeper集群的地址列表,用逗号分割。例如:"host1.mydomain.com,host2.mydomain.com,host3.mydomain.com".默认是localhost,是给伪分布式用的。要修改才能在完全分布式的情况下使用。如果在hbase-env.sh设置了HBASE_MANAGES_ZK,这些ZooKeeper节点就会和Hbase一起启动。

默认: localhost

hbase.zookeeper.peerport

ZooKeeper节点使用的端口。详细参见:http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperStarted.html#sc_RunningReplicatedZooKeeper

默认: 2888

hbase.zookeeper.leaderport

ZooKeeper用来选择Leader的端口,详细参见:http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperStarted.html#sc_RunningReplicatedZooKeeper

默认: 3888

hbase.zookeeper.property.initLimit

ZooKeeper的zoo.conf中的配置。初始化synchronization阶段的ticks数量限制

默认: 10

hbase.zookeeper.property.syncLimit

ZooKeeper的zoo.conf中的配置。发送一个请求到获得承认之间的ticks的数量限制

默认: 5

hbase.zookeeper.property.dataDir

ZooKeeper的zoo.conf中的配置。快照的存储位置

默认: ${hbase.tmp.dir}/zookeeper

hbase.zookeeper.property.clientPort

ZooKeeper的zoo.conf中的配置。客户端连接的端口

默认: 2181

hbase.zookeeper.property.maxClientCnxns

ZooKeeper的zoo.conf中的配置。 ZooKeeper集群中的单个节点接受的单个Client(以IP区分)的请求的并发数。这个值可以调高一点,防止在单机和伪分布式模式中出问题。

默认: 2000

hbase.rest.port

HBase REST server的端口

默认: 8080

hbase.rest.readonly

定义REST server的运行模式。可以设置成如下的值: false: 所有的HTTP请求都是被允许的 - GET/PUT/POST/DELETE. true:只有GET请求是被允许的

默认: false

3.2. hbase-env.sh

在这个文件里面设置HBase环境变量。比如可以配置JVM启动的堆大小或者GC的参数。你还可在这里配置Hbase的参数,如Log位置,niceness(译者注:优先级),ssh参数还有pid文件的位置等等。打开文件conf/hbase-env.sh细读其中的内容。每个选项都是有详尽的注释的。你可以在此添加自己的环境变量。

这个文件的改动系统Hbase重启才能生效。

3.3. log4j.properties

编辑这个文件可以改变Hbase的日志的级别,轮滚策略等等。

这个文件的改动系统Hbase重启才能生效。日志级别的更改会影响到HBase UI

3.4. 重要的配置

下面我们会列举重要 的配置. 这个章节讲述必须的配置和那些值得一看的配置。(译者注:淘宝的博客也有本章节的内容,HBase性能调优,很详尽)。

3.5. 必须的配置

参见 Section 1.3.1, “需要的软件”. 这里列举了运行Hbase至少两个必须的配置: i.e. Section 1.3.1.6, “ ulimit 和 nproc ” 和 Section 1.3.1.7, “dfs.datanode.max.xcievers”.

3.6. 推荐的配置

3.6.1. zookeeper.session.timeout

这个默认值是3分钟。这意味着一旦一个server宕掉了,Master至少需要3分钟才能察觉到宕机,开始恢复。你可能希望将这个超时调短,这样Master就能更快的察觉到了。在你调这个值之前,你需要确认你的JVM的GC参数,否则一个长时间的GC操作就可能导致超时。(当一个RegionServer在运行一个长时间的GC的时候,你可能想要重启并恢复它).

要想改变这个配置,可以编辑 hbase-site.xml, 将配置部署到全部集群,然后重启。

我们之所以把这个值调的很高,是因为我们不想一天到晚在论坛里回答新手的问题。“为什么我在执行一个大规模数据导入的时候Region Server死掉啦”,通常这样的问题是因为长时间的GC操作引起的,他们的JVM没有调优。我们是这样想的,如果一个人对Hbase不很熟悉,不能期望他知道所有,打击他的自信心。等到他逐渐熟悉了,他就可以自己调这个参数了。

3.6.2. hbase.regionserver.handler.count

这个设置决定了处理用户请求的线程数量。默认是10,这个值设的比较小,主要是为了预防用户用一个比较大的写缓冲,然后还有很多客户端并发,这样region servers会垮掉。有经验的做法是,当请求内容很大(上MB,如大puts, 使用缓存的scans)的时候,把这个值放低。请求内容较小的时候(gets, 小puts, ICVs, deletes),把这个值放大。

当客户端的请求内容很小的时候,把这个值设置的和最大客户端数量一样是很安全的。一个典型的例子就是一个给网站服务的集群,put操作一般不会缓冲,绝大多数的操作是get操作。

把这个值放大的危险之处在于,把所有的Put操作缓冲意味着对内存有很大的压力,甚至会导致OutOfMemory.一个运行在内存不足的机器的RegionServer会频繁的触发GC操作,渐渐就能感受到停顿。(因为所有请求内容所占用的内存不管GC执行几遍也是不能回收的)。一段时间后,集群也会受到影响,因为所有的指向这个region的请求都会变慢。这样就会拖累集群,加剧了这个问题。

3.6.3. 大内存机器的配置

Hbase有一个合理的保守的配置,这样可以运作在所有的机器上。如果你有台大内存的集群-Hbase有8G或者更大的heap,接下来的配置可能会帮助你 TODO.(译者注:原文到此为止,汗)

3.6.4. LZO 压缩

你可以考虑使用Lzo压缩,这个可以无缝集成,并且在大多数情况下可以提供性能。

Hbase是Apache的协议,而LZO是GPL的协议。Hbase不能自带LZO,因此LZO需要在安装Hbase之前安装。参见 使用 LZO 压缩介绍了如何在Hbase中使用LZO

一个常见的问题是,用户在一开始使用LZO的时候会很好,但是数月过去,管理员在给集群添加集群的时候,他们忘记了LZO的事情。在0.90.0版本之后,我们会运行失败,但也有可能不。请你要阅读这一段[11].

还要在本书的尾部参见 Appendix B, HBase中的压缩 .

3.6.5. 更大的 Regions

更大的Region可以使你集群上的Region的总数量较少。一般来言,更少的Region可以使你的集群运行更加流畅。(你可以自己随时手工将大Region切割,这样单个热点Region就会被分布在集群的更多节点上)。默认情况下单个Region是256MB.你可以设置为1G。有些人使用更大的,4G甚至更多。可以调整hbase-site.xml中的 hbase.hregion.max.filesize属性.

3.6.6. 管理 Splitting

除了让Hbase自动切割你的Region,你也可以手动切割。 [12] 随着数据量的增大,splite会被持续执行。如果你需要知道你现在有几个region,比如长时间的debug或者做调优,你需要手动切割。通过跟踪日志来了解region级的问题是很难的,因为他在不停的切割和重命名。data offlineing bug和未知量的region会让你没有办法。如果一个 HLog 或者 StoreFile由于一个奇怪的bug,Hbase没有执行它。等到一天之后,你才发现这个问题,你可以确保现在的regions和那个时候的一样,这样你就可以restore或者replay这些数据。你还可以调优你的合并算法。如果数据是均匀的,随着数据增长,很容易导致split / compaction疯狂的运行。因为所有的region都是差不多大的。用手的切割,你就可以交错执行定时的合并和切割操作,降低IO负载。

为什么我关闭自动split呢?因为自动的splite是配置文件中的 hbase.hregion.max.filesize决定的. 你把它设置成ILong.MAX_VALUE是不推荐的做法,要是你忘记了手工切割怎么办.推荐的做法是设置成100GB,一旦到达这样的值,至少需要一个小时执行 major compactions。

那什么是最佳的在pre-splite regions的数量呢。这个决定于你的应用程序了。你可以先从低的开始,比如每个server10个pre-splite regions.然后花时间观察数据增长。有太少的region至少比出错好,你可以之后再rolling split.一个更复杂的答案是这个值是取决于你的region中的最大的storefile。随着数据的增大,这个也会跟着增大。 你可以当这个文件足够大的时候,用一个定时的操作使用Store的合并选择算法(compact selection algorithm)来仅合并这一个HStore。如果你不这样做,这个算法会启动一个 major compactions,很多region会受到影响,你的集群会疯狂的运行。需要注意的是,这样的疯狂合并操作是数据增长造成的,而不是手动分割操作决定的。

如果你 pre-split 导致 regions 很小,你可以通过配置HConstants.MAJOR_COMPACTION_PERIOD把你的major compaction参数调大

如果你的数据变得太大,可以使用org.apache.hadoop.hbase.util.RegionSplitter 脚本来执行针对全部集群的一个网络IO安全的rolling split操作。

3.7. 连接Hbase集群的客户端配置和依赖

因为Hbase的Master有可能转移,所有客户端需要访问ZooKeeper来获得现在的位置。ZooKeeper会保存这些值。因此客户端必须知道Zookeeper集群的地址,否则做不了任何事情。通常这个地址存在 hbase-site.xml 里面,客户端可以从CLASSPATH取出这个文件.

如果你是使用一个IDE来运行Hbase客户端,你需要将conf/放入你的 classpath,这样 hbase-site.xml就可以找到了,(或者把hbase-site.xml放到 src/test/resources,这样测试的时候可以使用).

Hbase客户端最小化的依赖是 hbase, hadoop, log4j, commons-logging, commons-lang, 和 ZooKeeper ,这些jars 需要能在 CLASSPATH 中找到。

下面是一个基本的客户端 hbase-site.xml 例子:

<?xml version="1.0"?>

<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<configuration>

<property>

<name>hbase.zookeeper.quorum</name>

<value>example1,example2,example3</value>

<description>The directory shared by region servers.

</description>

</property>

</configuration>

3.7.1. Java客户端配置

Java是如何读到hbase-site.xml 的内容的

Java客户端使用的配置信息是被映射在一个HBaseConfiguration 实例中. HBaseConfiguration有一个工厂方法, HBaseConfiguration.create();,运行这个方法的时候,他会去CLASSPATH,下找hbase-site.xml,读他发现的第一个配置文件的内容。 (这个方法还会去找hbase-default.xml ; hbase.X.X.X.jar里面也会有一个an hbase-default.xml). 不使用任何hbase-site.xml文件直接通过Java代码注入配置信息也是可以的。例如,你可以用编程的方式设置ZooKeeper信息,只要这样做:

Configuration config = HBaseConfiguration.create();

config.set("hbase.zookeeper.quorum", "localhost");  // Here we are running zookeeper locally

如果有多ZooKeeper实例,你可以使用逗号列表。(就像在hbase-site.xml 文件中做得一样). 这个 Configuration 实例会被传递到 HTable, 之类的实例里面去.

 

[10] Be careful editing XML. Make sure you close all elements. Run your file through xmllint or similar to ensure well-formedness of your document after an edit session.

[11] 参见 Section B.2, “ hbase.regionserver.codecs ” 可以看到关于LZO安装的具体信息,帮助你放在安装失败。

[12] What follows is taken from the javadoc at the head of the org.apache.hadoop.hbase.util.RegionSplitter tool added to HBase post-0.90.0 release.

Chapter 4. The HBase Shell

Table of Contents

4.1. 使用脚本

4.2. Shell 技巧

4.2.1. irbrc

4.2.2. LOG 时间转换

4.2.3. Debug

Hbase Shell is 在(J)Ruby的IRB的基础上加上了HBase的命令。任何你可以在IRB里做的事情都可在在Hbase Shell中做。

你可以这样来运行HBase Shell:

$ ./bin/hbase shell

输入 help 就会返回Shell的命令列表和选项。可以看看在Help文档尾部的关于如何输入变量和选项。尤其要注意的是表名,行,列名必须要加引号。

参见 Section 1.2.3, “Shell 练习”可以看到Shell的基本使用例子。

4.1. 使用脚本

如果要使用脚本,可以看Hbase的bin 目录.在里面找到后缀为 *.rb的脚本.要想运行这个脚本,要这样

$ ./bin/hbase org.jruby.Main PATH_TO_SCRIPT

就可以了

4.2. Shell 技巧

4.2.1. irbrc

可以在你自己的Home目录下创建一个.irbrc文件. 在这个文件里加入自定义的命令。有一个有用的命令就是记录命令历史,这样你就可以把你的命令保存起来。

$ more .irbrc

require 'irb/ext/save-history'

IRB.conf[:SAVE_HISTORY] = 100

IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-save-history"

可以参见 ruby 关于 .irbrc 的文档来学习更多的关于IRB的配置方法。

4.2.2. LOG 时间转换

可以将日期'08/08/16 20:56:29'从hbase log 转换成一个 timestamp, 操作如下:

hbase(main):021:0> import java.text.SimpleDateFormat

hbase(main):022:0> import java.text.ParsePosition

hbase(main):023:0> SimpleDateFormat.new("yy/MM/dd HH:mm:ss").parse("08/08/16 20:56:29", ParsePosition.new(0)).getTime() => 1218920189000

也可以逆过来操作。

hbase(main):021:0> import java.util.Date

hbase(main):022:0> Date.new(1218920189000).toString() => "Sat Aug 16 20:56:29 UTC 2008"

要想把日期格式和Hbase log格式完全相同,可以参见文档 SimpleDateFormat.

4.2.3. Debug

4.2.3.1. Shell 切换成debug 模式

你可以将shell切换成debug模式。这样可以看到更多的信息。 -- 例如可以看到命令异常的stack trace:

hbase> debug <RETURN>

4.2.3.2. DEBUG log level

想要在shell中看到 DEBUG 级别的 logging ,可以在启动的时候加上 -d 参数.

$ ./bin/hbase shell -d

Chapter 5. 构建 HBase

Table of Contents

5.1. 将一个 HBase release 加入到 Apache's Maven Repository

5.1. 将一个 HBase release 加入到 Apache's Maven Repository

可以参考 发布 Maven Artifacts的信息.要想让所有的组件正确运行,关键在于配置好mvn release plugin。确保你在运行mvn release:perform之前使用的是正确的分支版本。这点非常的重要,要手写${HBASE_HOME}下的release.properties文件,然后执行release:perform.。你需要编辑它,这样才能将他指向一个正确的SVN地址。(译者注:可以使用cloudera)

如果你出现了如下的问题,是因为你需要在pom.xml里编辑版本然后加上 -SNAPSHOT 。

[INFO] Scanning for projects...

[INFO] Searching repository for plugin with prefix: 'release'.

[INFO] ------------------------------------------------------------------------

[INFO] Building HBase

[INFO]    task-segment: [release:prepare] (aggregator-style)

[INFO] ------------------------------------------------------------------------

[INFO] [release:prepare {execution: default-cli}]

[INFO] ------------------------------------------------------------------------

[ERROR] BUILD FAILURE

[INFO] ------------------------------------------------------------------------

[INFO] You don't have a SNAPSHOT project in the reactor projects list.

[INFO] ------------------------------------------------------------------------

[INFO] For more information, run Maven with the -e switch

[INFO] ------------------------------------------------------------------------

[INFO] Total time: 3 seconds

[INFO] Finished at: Sat Mar 26 18:11:07 PDT 2011

[INFO] Final Memory: 35M/423M

[INFO] -----------------------------------------------------------------------

Chapter 6. Developers

Table of Contents

6.1. IDEs

6.1.1. Eclipse

6.2. 单元测试

6.2.1. Mocito

6.1. IDEs

6.1.1. Eclipse

参见 HBASE-3678 Add Eclipse-based Apache Formatter to HBase Wiki可以看到一个eclipse的格式化文件,可以帮你把编码转换成符合Hbase的格式。这个issue还包含有使用这个formatter的指导。

6.2. 单元测试

我们在Hbase中使用JUnit 4. 如果你希望跑一个最小化的HDFS, ZooKeeper, HBase, 或者 MapReduce 测试,可以checkoutHBaseTestingUtility. Alex Baranau of Sematext 阐述了怎么使用它 HBase Case-Study: Using HBaseTestingUtility for Local Testing and Development (2010).

6.2.1. Mocito

有些时候你不需要运行一个完全的running server单元测试。比如一些操作org.apache.hadoop.hbase.Server 实例的方法或者使用 org.apache.hadoop.hbase.master.MasterServices 接口而不是 org.apache.hadoop.hbase.master.HMaster类的应用. 这些情况下,你可以不必使用 mocked Server 实例. 比如:

(译者注:原文到此为止)

 

Chapter 7. HBase 和 MapReduce

Table of Contents

7.1. 默认 HBase MapReduce 分割器(Splitter)

7.2. HBase Input MapReduce 例子

7.3. 在一个MapReduce Job中访问其他的HBase Tables

7.4. 预测执行

关于 HBase 和 MapReduce详见 javadocs. 下面是一些附加的帮助文档.

7.1. 默认 HBase MapReduce 分割器(Splitter)

当 MapReduce job的HBase table 使用TableInputFormat为数据源格式的时候,他的splitter会给这个table的每个region一个map。因此,如果一个table有100个region,就有100个map-tasks,不论需要scan多少个column families 。

7.2. HBase Input MapReduce 例子

要想使HBase作为MapReduce的source,Job需要使用TableMapReduceUtil来配置,如下所示...

Job job = ...;

Scan scan = new Scan();

scan.setCaching(500);  // 1 is the default in Scan, which will be bad for MapReduce jobs

scan.setCacheBlocks(false);

// Now set other scan attrs

...

 

TableMapReduceUtil.initTableMapperJob(

tableName,    // input HBase table name

scan,  // Scan instance to control CF and attribute selection

MyMapper.class, // mapper

Text.class, // reducer key

LongWritable.class, // reducer value

job // job instance

);

...mapper需要继承于TableMapper...

public class MyMapper extends TableMapper<Text, LongWritable> {

public void map(ImmutableBytesWritable row, Result value, Context context)

throws InterruptedException, IOException {

// process data for the row from the Result instance.

7.3. 在一个MapReduce Job中访问其他的HBase Tables

尽管现有的框架允许一个HBase table作为一个MapReduce job的输入,其他的Hbase table可以同时作为普通的表被访问。例如在一个MapReduce的job中,可以在Mapper的setup方法中创建HTable实例。

public class MyMapper extends TableMapper<Text, LongWritable> {

private HTable myOtherTable;

 

@Override

public void setup(Context context) {

myOtherTable = new HTable("myOtherTable");

}

7.4. 预测执行

通常建议关掉针对HBase的MapReduce job的预测执行(speculative execution)功能。这个功能也可以用每个Job的配置来完成。对于整个集群,使用预测执行意味着双倍的运算量。这可不是你所希望的。

Chapter 8. HBase 的 Schema 设计

Table of Contents

8.1. Schema 创建 

8.2. column families的数量 

8.3. 单调递增Row Keys/时序数据(log) 

8.4. 尽量最小化row和column的大小

8.5. 版本的时间 

有一个关于NSQL数据库的优点和确定的介绍, No Relation: The Mixed Blessings of Non-Relational Databases. 推荐看一看.

8.1.  Schema 创建

可以使用HBaseAdmin或者Chapter 4, The HBase Shell 来创建和编辑Hbase的schemas

8.2.  column families的数量

现在Hbase并不能很好的处理两个或者三个以上的column families,所以尽量让你的column families数量少一些。目前,flush和compaction操作是针对一个Region。所以当一个column family操作大量数据的时候会引发一个flush。那些不相关的column families也有进行flush操作,尽管他们没有操作多少数据。Compaction操作现在是根据一个column family下的全部文件的数量触发的,而不是根据文件大小触发的。当很多的column families在flush和compaction时,会造成很多没用的I/O负载(要想解决这个问题,需要将flush和compaction操作只针对一个column family)

尽量在你的应用中使用一个Column family。只有你的所有查询操作只访问一个column family的时候,可以引入第二个和第三个column family.例如,你有两个column family,但你查询的时候总是访问其中的一个,从来不会两个一起访问。

8.3.  单调递增Row Keys/时序数据(log)

在Tom White的Hadoop: The Definitive Guide一书中,有一个章节描述了一个值得注意的问题:在一个集群中,一个导入数据的进程一动不动,所以的client都在等待一个region(就是一个节点),过了一会后,变成了下一个region...如果使用了单调递增或者时序的key就会造成这样的问题。详情可以参见IKai画的漫画monotonically increasing values are bad。使用了顺序的key会将本没有顺序的数据变得有顺序,把负载压在一台机器上。所以要尽量避免时间戳或者(e.g. 1, 2, 3)这样的key。

如果你需要导入时间顺序的文件(如log)到Hbase中,可以学习OpenTSDB的做法。他有一个页面来描述他的schema.OpenTSDB的Key的格式是[metric_type][event_timestamp],乍一看,似乎违背了不将timestamp做key的建议,但是他并没有将timestamp作为key的一个关键位置,有成百上千的metric_type就足够将压力分散到各个region了。

8.4. 尽量最小化row和column的大小

在Hbase中,值是作为一个cell保存在系统的中的,要定位一个cell,需要row,column name和timestamp.通常情况下,如果你的row和column的名字要是太大(甚至比value的大小还要大)的话,你可能会遇到一些有趣的情况。例如Marc Limotte 在 HBASE-3551(recommended!)尾部提到的现象。在Hbase的存储文件Section 12.3.4.2, “StoreFile (HFile)”中,有一个索引用来方便value的随机访问,但是访问一个cell的坐标要是太大的话,会占用很大的内存,这个索引会被用尽。所以要想解决,可以设置一个更大的block size,当然也可以使用更小的column name `

8.5.  版本的时间

行的版本的数量是HColumnDescriptor设置的,每个column family可以单独设置,默认是3.这个设置是很重要的,在Chapter 11, 数据模型有描述,因为Hbase是不会去覆盖一个值的,他只会在后面在追加写,用timestamp来区分、过早的版本会在执行major compaction的时候删除。这个版本的值可以根据具体的应用增加减少。

Chapter 9. Metrics

Table of Contents

9.1. Metric 安装

9.2. RegionServer Metrics

9.2.1. hbase.regionserver.blockCacheCount

9.2.2. hbase.regionserver.blockCacheFree

9.2.3. hbase.regionserver.blockCacheHitRatio

9.2.4. hbase.regionserver.blockCacheSize

9.2.5. hbase.regionserver.compactionQueueSize

9.2.6. hbase.regionserver.fsReadLatency_avg_time

9.2.7. hbase.regionserver.fsReadLatency_num_ops

9.2.8. hbase.regionserver.fsSyncLatency_avg_time

9.2.9. hbase.regionserver.fsSyncLatency_num_ops

9.2.10. hbase.regionserver.fsWriteLatency_avg_time

9.2.11. hbase.regionserver.fsWriteLatency_num_ops

9.2.12. hbase.regionserver.memstoreSizeMB

9.2.13. hbase.regionserver.regions

9.2.14. hbase.regionserver.requests

9.2.15. hbase.regionserver.storeFileIndexSizeMB

9.2.16. hbase.regionserver.stores

9.2.17. hbase.regionserver.storeFiles

9.1. Metric 安装

参见 Metrics 可以获得一个enable Metrics emission的指导。

9.2. RegionServer Metrics

9.2.1. hbase.regionserver.blockCacheCount

内存中的Block cache item数量。这个是存储文件(HFiles)的缓存中的数量。

9.2.2. hbase.regionserver.blockCacheFree

内存中的Block cache memory 剩余 (单位 bytes).

9.2.3. hbase.regionserver.blockCacheHitRatio

Block cache 命中率(0 到 100). TODO: 描述当cacheBlocks=false时对这个值得影响

9.2.4. hbase.regionserver.blockCacheSize

内存中的Block cache 大小 (单位 bytes)

9.2.5. hbase.regionserver.compactionQueueSize

compaction队列的大小. 这个值是需要进行compaction的region数目

9.2.6. hbase.regionserver.fsReadLatency_avg_time

文件系统延迟 (ms). 这个值是平均读HDFS的延迟时间

9.2.7. hbase.regionserver.fsReadLatency_num_ops

TODO

9.2.8. hbase.regionserver.fsSyncLatency_avg_time

文件系统同步延迟(ms)

9.2.9. hbase.regionserver.fsSyncLatency_num_ops

TODO

9.2.10. hbase.regionserver.fsWriteLatency_avg_time

文件系统写延迟(ms)

9.2.11. hbase.regionserver.fsWriteLatency_num_ops

TODO

9.2.12. hbase.regionserver.memstoreSizeMB

所有的RegionServer的memstore大小 (MB)

9.2.13. hbase.regionserver.regions

RegionServer服务的regions数量

9.2.14. hbase.regionserver.requests

读写请求的全部数量。请求是指RegionServer的RPC数量,因此一次Get一个情况,一个带缓存的Scan也是一个请求。一个批量load是一个Hfile一个请求。

9.2.15. hbase.regionserver.storeFileIndexSizeMB

当前RegionServer的storefile索引的总大小(MB)

9.2.16. hbase.regionserver.stores

RegionServer打开的stores数量。一个stores对应一个column family。例如,一个表有3个region在这个RegionServer上,对应一个 column family就会有3个store.

9.2.17. hbase.regionserver.storeFiles

RegionServer打开的存储文件(HFile)数量。这个值一定大于等于store的数量。

Chapter 10. 跨集群复制

参见 跨集群复制.

Chapter 11. 数据模型

Table of Contents

11.1. 概念视图

11.2. 物理视图

11.3. 表

11.4. 行

11.5. Column Family

11.6. Cells

11.7. 版本

11.7.1. Hbase的操作(包含版本操作)

11.7.2. 现有的限制

简单来说,应用程序是以表的方式在Hbase存储数据的。表是由行和列构成的,所以的列是从属于某一个column family的。行和列的交叉点称之为cell,cell是版本化的。cell的内容是不可分割的字节数组。

表的row key也是一段字节数组,所以任何东西都可以保存进去,不论是字符串或者数字。Hbase的表是按key排序的,排序方式之针对字节的。所以的表都必须要有主键-key.

11.1. 概念视图

下面是根据BigTable 论文稍加修改的例子。有一个名为webtable的表,包含两个column family:contents和anchor.在这个例子里面,anchor有两个列 (anchor:cssnsi.com, anchor:my.look.ca),contents仅有一列(contents:html)

列名

一个列名是有它的column family前缀和qualifier连接而成。例如列contents:html是column family contents加冒号(:)加 qualifier html组成的。

Table 11.1. 表 webtable

Row Key

Time Stamp

ColumnFamily contents

ColumnFamily anchor

"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>..."

 

 

11.2. 物理视图

尽管在概念视图里,表可以被看成是一个稀疏的行的集合。但在物理上,它的是区分column family 存储的。新的columns可以不经过声明直接加入一个column family.

Table 11.2. 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 11.3. 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>..."


值得注意的是在上面的概念视图中空白cell在物理上是不存储的,因为根本没有必要存储。因此若一个请求为要获取t8时间的contents:html,他的结果就是空。相似的,若请求为获取t9时间的anchor:my.look.ca,结果也是空。但是,如果不指明时间,将会返回最新时间的行,每个最新的都会返回。例如,如果请求为获取row key为"com.cnn.www",没有指明时间戳的话,活动的结果是t6下的contents:html,t9下的anchor:cnnsi.com和t8下anchor:my.look.ca。

11.3. 表

表是在schema声明的时候定义的。

11.4. 行

row key是不可分割的字节数组。行是按字典排序由低到高存储在表中的。一个空的数组是用来标识表空间的起始或者结尾。

11.5. Column Family

在Hbase是column family一些列的集合。一个column family所有列成员是有着相同的前缀。比如,列courses:history 和 courses:math都是 column family courses的成员.冒号(:)是column family的分隔符,用来区分前缀和列名。column 前缀必须是可打印的字符,剩下的部分(称为qualify),可以又任意字节数组组成。column family必须在表建立的时候声明。column就不需要了,随时可以新建。

在物理上,一个的column family成员在文件系统上都是存储在一起。因为存储优化都是针对column family级别的,这就意味着,一个colimn family的所有成员的是用相同的方式访问的。

11.6. Cells

A {row, column, version} 元组就是一个Hbase中的一个 cell。Cell的内容是不可分割的字节数组。

11.7. 版本

一个 {row, column, version} 元组是Hbase中的一个cell .但是有可能会有很多的cell的row和column是相同的,可以使用version来区分不同的cell.

rows和column key是用字节数组表示的,version则是用一个长整型表示。这个long的值使用 java.util.Date.getTime() 或者 System.currentTimeMillis()产生的。这就意味着他的含义是“当前时间和1970-01-01 UTC的时间差,单位毫秒。”

在Hbase中,版本是按倒序排列的,因此当读取这个文件的时候,最先找到的是最近的版本。

有些人不是很理解Hbase的 cell 意思。一个常见的问题是:

· 如果有多个包含版本写操作同时发起,Hbase会保存全部还是会保持最新的一个?[13]

· 可以发起包含版本的写操作,但是他们的版本顺序和操作顺序相反吗?[14]

下面我们介绍下在Hbase中版本是如何工作的。[15].

11.7.1. Hbase的操作(包含版本操作)

在这一章我们来仔细看看在Hbase的各个主要操作中版本起到了什么作用。

11.7.1.1. Get/Scan

Gets实在Scan的基础上实现的。可以详细参见下面的讨论 Get 同样可以用 Scan来描述.

默认情况下,如果你没有指定版本,当你使用Get操作的时候,会返回最近版本的Cell(该Cell可能是最新写入的,但不能保证)。默认的操作可以这样修改:

· 如果想要返回返回两个以上的把版本,参见Get.setMaxVersions()

· 如果想要返回的版本不只是最近的,参见 Get.setTimeRange()

要向查询的最新版本要小于或等于给定的这个值,这就意味着给定的'最近'的值可以是某一个时间点。可以使用0到你想要的时间来设置,还要把max versions设置为1.

11.7.1.2. 默认 Get 例子

下面的Get操作会只获得最新的一个版本。

Get get = new Get(Bytes.toBytes("row1"));

Result r = htable.get(get);

byte[] b = r.getValue(Bytes.toBytes("cf"), Bytes.toBytes("attr"));  // returns current version of value

11.7.1.3. 含有的版本的Get例子

下面的Get操作会获得最近的3个版本。

Get get = new Get(Bytes.toBytes("row1"));

get.setMaxVersions(3);  // will return last 3 versions of row

Result r = htable.get(get);

byte[] b = r.getValue(Bytes.toBytes("cf"), Bytes.toBytes("attr"));  // returns current version of value

List<KeyValue> kv = r.getColumn(Bytes.toBytes("cf"), Bytes.toBytes("attr"));  // returns all versions of this column

 

11.7.1.4. Put

一个Put操作会给一个cell,创建一个版本,默认使用当前时间戳,当然你也可以自己设置时间戳。这就意味着你可以把时间设置在过去或者未来,或者随意使用一个Long值。

要想覆盖一个现有的值,就意味着你的row,column和版本必须完全相等。

11.7.1.4.1. 不指明版本的例子

下面的Put操作不指明版本,所以Hbase会用当前时间作为版本。

Put put = new Put(Bytes.toBytes(row));

put.add(Bytes.toBytes("cf"), Bytes.toBytes("attr1"), Bytes.toBytes( data));

htable.put(put);

 

11.7.1.4.2. 指明版本的例子

下面的Put操作,指明了版本。

Put put = new Put( Bytes.toBytes(row ));

long explicitTimeInMs = 555;  // just an example

put.add(Bytes.toBytes("cf"), Bytes.toBytes("attr1"), explicitTimeInMs, Bytes.toBytes(data));

htable.put(put);

 

11.7.1.5. Delete

当你进行delete操作的是,有两种方式来确定要删除的版本。

· 删除所有比当前早的版本。

· 删除指定的版本。

一个删除操作可以删除一行,也可以是一个column family,或者仅仅删除一个column。你也可以删除指明的一个版本。若你没有指明,默认情况下是删除比当前时间早的版本。

删除操作的实现是创建一个删除标记。例如,我们想要删除一个版本,或者默认是currentTimeMillis。就意味着“删除比这个版本更早的所有版本”.Hbase不会去改那些数据,数据不会立即从文件中删除。他使用删除标记来屏蔽掉这些值。[16]若你知道的版本比数据中的版本晚,就意味着这一行中的所有数据都会被删除。

11.7.2. 现有的限制

关于版本还有一些bug(或者称之为未实现的功能),计划在下个版本实现。

11.7.2.1. 删除标记误删Puts

删除标记操作可能会标记之后put的数据。[17].需要值得注意的是,当写下一个删除标记后,只有下一个major compaction操作发起之后,这个删除标记才会消失。设想一下,当你写下一个删除标记-“删除所有<= 时间T的数据”。但之后,你又执行了一个Put操作,版本<= T。这样就算这个Put发生在删除之后,他的数据也算是打上了删除标记。这个Put并不会失败,但是你需要注意的是这个操作没有任何作用。只有一个major compaction执行只有,一切才会恢复正常。如果你的Put操作一直使用升序的版本,这个错误就不会发生。但是也有可能出现这样的情况,你删除之后,

11.7.2.2. Major compactions 改变查询的结果

“设想一下,你一个cell有三个版本t1,t2和t3。你的maximun-version设置是2.当你请求获取全部版本的时候,只会返回两个,t2和t3。如果你将t2和t3删除,就会返回t1。但是如果在删除之前,发生了major compaction操作,那么什么值都不好返回了。[18]”

 

[13] 目前,只有最新的那个是可以获取到的。.

[14] 可以

[15] See HBASE-2406 for discussion of HBase versions. Bending time in HBase makes for a good read on the version, or time, dimension in HBase. It has more detail on versioning than is provided here. As of this writing, the limiitation Overwriting values at existing timestamps mentioned in the article no longer holds in HBase. This section is basically a synopsis of this article by Bruno Dumon.

[16] 当Hbase执行一次major compaction,标记删除的数据会被实际的删除,删除标记也会被删除。

[17HBASE-2256

[18] See Garbage Collection in Bending time in HBase

Chapter 12. 架构

Table of Contents

12.1. 客户端

12.1.1. 连接

12.1.2. 写缓冲和批量操作 

12.1.3. Filters

12.2. Daemons

12.2.1. Master

12.2.2. RegionServer

12.3. Regions

12.3.1. Region大小

12.3.2. Region Splits

12.3.3. Region负载均衡

12.3.4. Store

12.4. Write Ahead Log (WAL)

12.4.1. 目的

12.4.2. WAL Flushing

12.4.3. WAL Splitting

12.1. 客户端

Hbase客户端的 HTable类负责寻找相应的RegionServers来处理行。他是先查询 .META. 和 -ROOT 目录表。然后再确定region的位置。定位到所需要的区域后,客户端会直接 去访问相应的region(不经过master),发起读写请求。这些信息会缓存在客户端,这样就不用每发起一个请求就去查一下。如果一个region已经废弃(原因可能是master load balance或者RegionServer死了),客户端就会重新进行这个步骤,决定要去访问的新的地址。

管理集群操作是经由HBaseAdmin发起的

12.1.1. 连接

关于连接的配置信息,参见Section 3.7, “连接Hbase集群的客户端配置和依赖”.

HTable不是线程安全的。建议使用同一个HBaseConfiguration实例来创建HTable实例。这样可以共享ZooKeeper和socket实例。例如,最好这样做:

HBaseConfiguration conf = HBaseConfiguration.create();

HTable table1 = new HTable(conf, "myTable");

HTable table2 = new HTable(conf, "myTable");

而不是这样:

HBaseConfiguration conf1 = HBaseConfiguration.create();

HTable table1 = new HTable(conf1, "myTable");

HBaseConfiguration conf2 = HBaseConfiguration.create();

HTable table2 = new HTable(conf2, "myTable");

如果你想知道的更多的关于Hbase客户端connection的知识,可以参照: HConnectionManager.

12.1.2. 写缓冲和批量操作

若关闭了HTable中的 Section 13.6.1, “AutoFlush”,Put操作会在写缓冲填满的时候向RegionServer发起请求。默认情况下,写缓冲是2MB.在Htable被废弃之前,要调用close(), flushCommits()操作,这样写缓冲就不会丢失。

要想更好的细粒度控制 Put或Delete的批量操作,可以参考Htable中的batch 方法.

12.1.3. Filters

Get 和 Scan实例可以使用 filters,这个过滤操作是运行在RegionServer上的。

12.2. Daemons

12.2.1. Master

12.2.2. RegionServer

12.3. Regions

本章节都是再讲Regions.

Note

Regions是由每个Column Family的Store组成。

12.3.1. Region大小

Region的大小是一个棘手的问题,需要考量如下几个因素。

· Regions是可用性和分布式的最基本单位

· HBase通过将region切分在许多机器上实现分布式。也就是说,你如果有16GB的数据,只分了2个region, 你却有20台机器,有18台就浪费了。

· region数目太多就会造成性能下降,现在比以前好多了。但是对于同样大小的数据,700个region比3000个要好。

· region数目太少就会妨碍可扩展性,降低并行能力。有的时候导致压力不够分散。这就是为什么,你向一个10节点的Hbase集群导入200MB的数据,大部分的节点是idle的。

· RegionServer中1个region和10个region索引需要的内存量没有太多的差别。

最好是使用默认的配置,可以把热的表配小一点(或者受到split热点的region把压力分散到集群中)。如果你的cell的大小比较大(100KB或更大),就可以把region的大小调到1GB。

12.3.2. Region Splits

RegionServer的Splits操作是不可见的,因为Master不会参与其中。RegionServer切割region的步骤是,先将该region下线,然后切割,将其子region加入到元信息中,再将他们加入到原本的RegionServer中,最后汇报Master.参见Section 3.6.6, “管理 Splitting”来手动管理切割操作。

12.3.3. Region负载均衡

当没有任何region在进行转换的时候,Hbase会定期执行一个load balance。他会将移动region进行集群的负载均衡。可以配置运行时间间隔。

12.3.4. Store

一个Store包含了一个MemStore和若干个StoreFile(HFile).一个Store可以定位到一个column family中的一个region.

12.3.4.1. MemStore

MemStores是Store中的内存Store,可以进行修改操作。修改的内容是KeyValues。当flush的是,现有的memstore会生成快照,然后清空。在执行快照的时候,Hbase会继续接收修改操作,保存在memstore外面,直到快照完成。

12.3.4.2. StoreFile (HFile)

12.3.4.2.1. HFile Format

hfile文件格式是基于BigTable [2006]论文中的SSTable。构建在Hadoop的tfile上面(直接使用了tfile的单元测试和压缩工具)。 Schubert Zhang's的博客HFile: A Block-Indexed File Format to Store Sorted Key-Value Pairs详细介绍了Hbases的hfile。Matteo Bertozzi也做了详细的介绍HBase I/O: HFile

12.3.4.2.2. HFile工具

要想看到hfile内容的文本化版本,你可以使用org.apache.hadoop.hbase.io.hfile.HFile 工具。可以这样用:

$ ${HBASE_HOME}/bin/hbase org.apache.hadoop.hbase.io.hfile.HFile

例如,你想看文件 hdfs://10.81.47.41:9000/hbase/TEST/1418428042/DSMP/4759508618286845475的内容, 就执行如下的命令:

$ ${HBASE_HOME}/bin/hbase org.apache.hadoop.hbase.io.hfile.HFile -v -f hdfs://10.81.47.41:9000/hbase/TEST/1418428042/DSMP/4759508618286845475

如果你没有输入-v,就仅仅能看到一个hfile的汇总信息。其他功能的用法可以看HFile的文档。

12.3.4.3. 压缩

有两种类型的压缩:minor和major。minor压缩通常会将数个小的相邻的文件合并成一个大的。Minor不会删除打上删除标记的数据,也不会删除过期的数据,Major压缩会删除过期的数据。有些时候minor压缩就会将一个store中的全部文件压缩,实际上这个时候他本身就是一个major压缩。对于一个minor压缩是如何压缩的,可以参见ascii diagram in the Store source code.

在执行一个major压缩之后,一个store只会有一个sotrefile,通常情况下这样可以提供性能。注意:major压缩将会将store中的数据全部重写,在一个负载很大的系统中,这个操作是很伤的。所以在大型系统中,通常会自己Section 3.6.6, “管理 Splitting”

12.4. Write Ahead Log (WAL)

12.4.1. 目的

每个RegionServer会将更新(Puts, Deletes) 先记录到Write Ahead Log中(WAL),然后将其更新在Section 12.3.4, “Store”Section 12.3.4.1, “MemStore”里面。这样就保证了Hbase的写的可靠性。如果没有WAL,当RegionServer宕掉的时候,MemStore还没有flush,StoreFile还没有保存,数据就会丢失。HLog 是Hbase的一个WAL实现,一个RegionServer有一个HLog实例。

WAL 保存在HDFS 的 /hbase/.logs/ 里面,每个region一个文件。

要想知道更多的信息,可以访问维基百科 Write-Ahead Log 的文章.

12.4.2. WAL Flushing

TODO (describe).

12.4.3. WAL Splitting

12.4.3.1. 当RegionServer宕掉的时候,如何恢复

TODO

12.4.3.2. hbase.hlog.split.skip.errors

默认设置为 true,在split执行中发生的任何错误会被记录,有问题的WAL会被移动到Hbase rootdir目录下的.corrupt目录,接着进行处理。如果设置为 false,异常会被抛出,split会记录错误。[19]

12.4.3.3. 如果处理一个发生在当RegionServers' WALs 分割时候的EOFExceptions异常

如果我们在分割日志的时候发生EOF,就是hbase.hlog.split.skip.errors设置为 false,我们也会进行处理。一个EOF会发生在一行一行读取Log,但是Log中最后一行似乎只写了一半就停止了。如果在处理过程中发生了EOF,我们还会继续处理,除非这个文件是要处理的最后一个文件。[20]

 

[19] See HBASE-2958 When hbase.hlog.split.skip.errors is set to false, we fail the split but thats it. We need to do more than just fail split if this flag is set.

[20] 要想知道背景知识, 参见 HBASE-2643 Figure how to deal with eof splitting logs

Chapter 13. 性能调优

Table of Contents

13.1. Java

13.1.1. 垃圾收集和HBase

13.2. 配置

13.2.1. Regions的数目

13.2.2. 管理压缩

13.2.3. 压缩

13.2.4. hbase.regionserver.handler.count

13.2.5. hfile.block.cache.size

13.2.6. hbase.regionserver.global.memstore.upperLimit

13.2.7. hbase.regionserver.global.memstore.lowerLimit

13.2.8. hbase.hstore.blockingStoreFiles

13.2.9. hbase.hregion.memstore.block.multiplier

13.3. Column Families的数目

13.4. 数据聚集

13.5. 批量Loading

13.5.1. Table创建: 预创建Regions 

13.6. HBase客户端

13.6.1. AutoFlush

13.6.2. Scan Caching

13.6.3. Scan 属性选择

13.6.4. 关闭 ResultScanners

13.6.5. 块缓存

13.6.6. Row Keys的负载优化

可以从 wiki Performance Tuning看起。这个文档讲了一些主要的影响性能的方面:RAM, 压缩, JVM 设置, 等等。然后,可以看看下面的补充内容。

打开RPC-level日志

在RegionServer打开RPC-level的日志对于深度的优化是有好处的。一旦打开,日志将喷涌而出。所以不建议长时间打开,只能看一小段时间。要想启用RPC-level的职责,可以使用RegionServer UI点击Log Level。将 org.apache.hadoop.ipc 的日志级别设为DEBUG。然后tail RegionServer的日志,进行分析。

要想关闭,只要把日志级别设为INFO就可以了.

13.1. Java

13.1.1. 垃圾收集和HBase

13.1.1.1. 长时间GC停顿

在这个PPT Avoiding Full GCs with MemStore-Local Allocation Buffers, Todd Lipcon描述列在Hbase中常见的两种stop-the-world的GC操作,尤其是在loading的时候。一种是CMS失败的模式(译者注:CMS是一种GC的算法),另一种是老一代的堆碎片导致的。要想定位第一种,只要将CMS执行的时间提前就可以了,加入-XX:CMSInitiatingOccupancyFraction参数,把值调低。可以先从60%和70%开始(这个值调的越低,触发的GC次数就越多,消耗的CPU时间就越长)。要想定位第二种错误,Todd加入了一个实验性的功能,在Hbase 0.90.x中这个是要明确指定的(在0.92.x中,这个是默认项),将你的Configuration中的hbase.hregion.memstore.mslab.enabled设置为true。详细信息,可以看这个PPT.

13.2. 配置

参见Section 3.6, “推荐的配置”.

13.2.1. Regions的数目

Hbase中region的数目可以根据Section 3.6.5, “更大的 Regions”调整.也可以参见 Section 12.3.1, “Region大小”

13.2.2. 管理压缩

对于大型的系统,你需要考虑管理压缩和分割

13.2.3. 压缩

生产环境中的系统需要在column family的定义中使用Section 3.6.4, “LZO 压缩”之类的压缩。

13.2.4. hbase.regionserver.handler.count

参见hbase.regionserver.handler.count.这个参数的本质是设置一个RegsionServer可以同时处理多少请求。 如果定的太高,吞吐量反而会降低;如果定的太低,请求会被阻塞,得不到响应。你可以打开RPC-level日志读Log,来决定对于你的集群什么值是合适的。(请求队列也是会消耗内存的)

13.2.5. hfile.block.cache.size

参见 hfile.block.cache.size. 对于RegionServer进程的内存设置。

13.2.6. hbase.regionserver.global.memstore.upperLimit

参见 hbase.regionserver.global.memstore.upperLimit. 这个内存设置是根据RegionServer的需要来设定。

13.2.7. hbase.regionserver.global.memstore.lowerLimit

参见 hbase.regionserver.global.memstore.lowerLimit. 这个内存设置是根据RegionServer的需要来设定。

13.2.8. hbase.hstore.blockingStoreFiles

参见hbase.hstore.blockingStoreFiles. 如果在RegionServer的Log中block,提高这个值是有帮助的。

13.2.9. hbase.hregion.memstore.block.multiplier

参见 hbase.hregion.memstore.block.multiplier. 如果有足够的RAM,提高这个值。

13.3. Column Families的数目

参见 Section 8.2, “ column families的数量 ”.

13.4. 数据聚集

如果你的数据总是往一个region写。你可以再看看处理时序数据 这一章.

13.5. 批量Loading

如果可以的话,尽量使用批量导入工具,参见 Bulk Loads.否则就要详细看看下面的内容。

13.5.1.  Table创建: 预创建Regions

默认情况下Hbase创建Table会新建一个region。执行批量导入,意味着所有的client会写入这个region,直到这个region足够大,以至于分裂。一个有效的提高批量导入的性能的方式,是预创建空的region。最好稍保守一点,因为过多的region会实实在在的降低性能。下面是一个预创建region的例子。 (注意:这个例子里需要根据应用的key进行调整。):

public static boolean createTable(HBaseAdmin 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;

}

13.6. HBase客户端

13.6.1. AutoFlush

当你进行大量的Put的时候,要确认你的HTable的setAutoFlush是关闭着的。否则的话,每执行一个Put就要想RegionServer发一个请求。通过 htable.add(Put) 和 htable.add( <List> Put)来将Put添加到写缓冲中。如果 autoFlush = false,要等到写缓冲都填满的时候才会发起请求。要想显式的发起请求,可以调用flushCommits。在HTable实例上进行的close操作也会发起flushCommits

13.6.2. Scan Caching

如果Hbase的输入源是一个MapReduce Job,要确保输入的Scan的setCaching值要比默认值0要大。使用默认值就意味着map-task每一行都会去请求一下region-server。可以把这个值设为500,这样就可以一次传输500行。当然这也是需要权衡的,过大的值会同时消耗客户端和服务端很大的内存,不是越大越好。

13.6.3. Scan 属性选择

当Scan用来处理大量的行的时候(尤其是作为MapReduce的输入),要注意的是选择了什么字段。如果调用了 scan.addFamily,这个column family的所有属性都会返回。如果只是想过滤其中的一小部分,就指定那几个column,否则就会造成很大浪费,影响性能。

13.6.4. 关闭 ResultScanners

这与其说是提高性能,倒不如说是避免发生性能问题。如果你忘记了关闭ResultScanners,会导致RegionServer出现问题。所以一定要把ResultScanner包含在try/catch 块中...

Scan scan = new Scan();

// set attrs...

ResultScanner rs = htable.getScanner(scan);

try {

for (Result r = rs.next(); r != null; r = rs.next()) {

// process result...

} finally {

rs.close();  // always close the ResultScanner!

}

htable.close();

13.6.5. 块缓存

Scan实例可以在RegionServer中使用块缓存,可以由setCacheBlocks方法控制。如果Scan是MapReduce的输入源,要将这个值设置为 false。对于经常读到的行,就建议使用块缓冲。

13.6.6. Row Keys的负载优化

scan一个表的时候,如果仅仅需要row key(不需要no families, qualifiers, values 和 timestamps),在加入FilterList的时候,要使用Scanner的setFilter方法的时候,要填上MUST_PASS_ALL操作参数(译者注:相当于And操作符)。一个FilterList要包含一个 FirstKeyOnlyFilter 和一个 KeyOnlyFilter.通过这样的filter组合,就算在最坏的情况下,RegionServer只会从磁盘读一个值,同时最小化客户端的网络带宽占用。

Chapter 14. Bloom Filters

Table of Contents

14.1. 配置

14.1.1. HColumnDescriptor 配置

14.1.2. io.hfile.bloom.enabled 全局关闭开关

14.1.3. io.hfile.bloom.error.rate

14.1.4. io.hfile.bloom.max.fold

14.2. Bloom StoreFile footprint

14.2.1. StoreFile中的BloomFilter, FileInfo数据结构

14.2.2. 在 StoreFile 元数据中的BloomFilter entries

Bloom filters 是在 HBase-1200 Add bloomfilters上面开发的.[21][22] (译者注:Bloom Filter是一个算法,可以用来快速确认一个Row Key或者值是否在一个Hfile里面。)

14.1. 配置

可以在column family的选项的配置Blooms.可以通过Hbase Shell,也可以用Java代码操作 org.apache.hadoop.hbase.HColumnDescriptor.

14.1.1. HColumnDescriptor 配置

使用 HColumnDescriptor.setBloomFilterType(NONE | ROW | ROWCOL)来控制每个column family的Blooms这种。默认值是 NONE ,如果值是ROW,就会在插入的时候去Hash这个row,加入到Bloom中去。如果值是ROWCOL,就Hash这个row,column family和column family qualifer。(译者注,ROW是哈希row key)

14.1.2. io.hfile.bloom.enabled 全局关闭开关

当有些东西出错的时候,Configuration中的io.hfile.bloom.enabled是一个关闭的开关。 默认是 true.

14.1.3. io.hfile.bloom.error.rate

io.hfile.bloom.error.rate = 平均错误率。默认是 1%.减少一半(如 .5%),就意味着每个bloom entry加一个bit.

14.1.4. io.hfile.bloom.max.fold

io.hfile.bloom.max.fold = 保证最低的fold率。大多数人不该修改这个值,默认是7,就是说可以折叠到原本大小的1/128。参见 Development Process中的文档 BloomFilters in HBase获得更多关于这个配置的信息。

14.2. Bloom StoreFile footprint

Bloom filters在StoreFile加入了一个entry.包括一般的 FileInfo 数据结构和两个额外entries到StoreFile的元数据部分中。

14.2.1. StoreFile中的BloomFilter, FileInfo数据结构

14.2.1.1. BLOOM_FILTER_TYPE

FileInfo有一个 BLOOM_FILTER_TYPE entry,可以被设置为 NONE, ROW 或者 ROWCOL.

14.2.2.  在 StoreFile 元数据中的BloomFilter entries

14.2.2.1. BLOOM_FILTER_META

BLOOM_FILTER_META保存了Bloom的大小,使用的Hash算法等信息。他的大小的很小。 StoreFile.Reader加载的时候会缓存进去。

14.2.2.2. BLOOM_FILTER_DATA

BLOOM_FILTER_DATA是实际的bloomfiter数据。按需获取,保存在LRU缓存中(如果缓存是开启的,默认开启)。

 

[21] For description of the development process -- why static blooms rather than dynamic -- and for an overview of the unique properties that pertain to blooms in HBase, as well as possible future directions, see the Development Process section of the document BloomFilters in HBase attached to HBase-1200.

[22] The bloom filters described here are actually version two of blooms in HBase. In versions up to 0.19.x, HBase had a dynamic bloom option based on work done by the European Commission One-Lab Project 034819. The core of the HBase bloom work was later pulled up into Hadoop to implement org.apache.hadoop.io.BloomMapFile. Version 1 of HBase blooms never worked that well. Version 2 is a rewrite from scratch though again it starts with the one-lab work.

Chapter 15. Hbase的故障排除和Debug

Table of Contents

15.1. 一般准则

15.2. Logs

15.2.1. Log 位置

15.3. 工具

15.3.1. search-hadoop.com

15.3.2. tail

15.3.3. top

15.3.4. jps

15.3.5. jstack

15.3.6. OpenTSDB

15.3.7. clusterssh+top

15.4. 客户端

15.4.1. ScannerTimeoutException

15.5. RegionServer

15.5.1. 启动错误

15.5.2. 运行时错误

15.5.3. 终止错误

15.6. Master

15.6.1. 启动错误

15.6.2. 终止错误

15.1. 一般准则

首先可以看看master的log。通常情况下,他总是一行一行的重复信息。如果不是这样,说明有问题,可以Google或是用search-hadoop.com来搜索遇到的exception。

一个错误通常不是单独出现在Hbase中的,通常是某一个地方发生了异常,然后对其他的地方发生影响。到处都是exception和stack straces。遇到这样的错误,最好的办法是查日志,找到最初的异常。例如Region会在abort的时候打印一下信息。Grep这个Dump就有可能找到最初的异常信息。

RegionServer的自杀是很“正常”的。当一些事情发生错误的,他们就会自杀。如果ulimit和xcievers(最重要的两个设定,详见Section 1.3.1.6, “ ulimit 和 nproc ”)没有修改,HDFS将无法运转正常,在HBase看来,HDFS死掉了。假想一下,你的MySQL突然无法访问它的文件系统,他会怎么做。同样的事情会发生在Hbase和HDFS上。还有一个造成RegionServer切腹(译者注:竟然用日文词)自杀的常见的原因是,他们执行了一个长时间的GC操作,这个时间超过了ZooKeeper的session timeout。关于GC停顿的详细信息,参见Todd Lipcon的 3 part blog post by Todd Lipcon 和上面的 Section 13.1.1.1, “长时间GC停顿”.

15.2. Logs

重要日志的位置( <user>是启动服务的用户,<hostname> 是机器的名字)

NameNode: $HADOOP_HOME/logs/hadoop-<user>-namenode-<hostname>.log

DataNode: $HADOOP_HOME/logs/hadoop-<user>-datanode-<hostname>.log

JobTracker: $HADOOP_HOME/logs/hadoop-<user>-jobtracker-<hostname>.log

TaskTracker: $HADOOP_HOME/logs/hadoop-<user>-jobtracker-<hostname>.log

HMaster: $HBASE_HOME/logs/hbase-<user>-master-<hostname>.log

RegionServer: $HBASE_HOME/logs/hbase-<user>-regionserver-<hostname>.log

ZooKeeper: TODO

15.2.1. Log 位置

对于单节点模式,Log都会在一台机器上,但是对于生产环境,都会运行在一个集群上。

15.2.1.1. NameNode

NameNode的日志在NameNode server上。HBase Master 通常也运行在NameNode server上,ZooKeeper通常也是这样。

对于小一点的机器,JobTracker也通常运行在NameNode server上面。

15.2.1.2. DataNode

每一台DataNode server有一个HDFS的日志,Region有一个Hbase日志。

每个DataNode server还有一份TaskTracker的日志,来记录MapReduce的Task信息。

15.3. 工具

15.3.1. search-hadoop.com

search-hadoop.com将所有的 mailing lists 和 JIRA建立了索引。用它来找Hadoop/HBase的问题很方便。

15.3.2. tail

tail是一个命令行工具,可以用来看日志的尾巴。加入的"-f"参数后,就会在数据更新的时候自己刷新。用它来看日志很方便。例如,一个机器需要花很多时间来启动或关闭,你可以tail他的master log(也可以是region server的log)。

15.3.3. top

top是一个很重要的工具来看你的机器各个进程的资源占用情况。下面是一个生产环境的例子:

top - 14:46:59 up 39 days, 11:55,  1 user,  load average: 3.75, 3.57, 3.84

Tasks: 309 total,   1 running, 308 sleeping,   0 stopped,   0 zombie

Cpu(s):  4.5%us,  1.6%sy,  0.0%ni, 91.7%id,  1.4%wa,  0.1%hi,  0.6%si,  0.0%st

Mem:  24414432k total, 24296956k used,   117476k free,     7196k buffers

Swap: 16008732k total, 14348k used, 15994384k free, 11106908k cached

 

PID USER   PR  NI  VIRT  RES  SHR S %CPU %MEM TIME+  COMMAND

15558 hadoop 18  -2 3292m 2.4g 3556 S   79 10.4   6523:52 java

13268 hadoop 18  -2 8967m 8.2g 4104 S   21 35.1   5170:30 java

8895 hadoop 18  -2 1581m 497m 3420 S   11  2.1   4002:32 java

 

这里你可以看到系统的load average在最近5分钟是3.75,意思就是说这5分钟里面平均有3.75个线程在CPU时间的等待队列里面。通常来说,最完美的情况是这个值和CPU和核数相等,比这个值低意味着资源闲置,比这个值高就是过载了。这是一个重要的概念,要想理解的更多,可以看这篇文章 http://www.linuxjournal.com/article/9001.

处理负载,我们可以看到系统已经几乎使用了他的全部RAM,其中大部分都是用于OS cache(这是一件好事).Swap只使用了一点点KB,这正是我们期望的,如果数值很高的话,就意味着在进行交换,这对Java程序的性能是致命的。另一种检测交换的方法是看Load average是否过高(load average过高还可能是磁盘损坏或者其它什么原因导致的)。

默认情况下进程列表不是很有用,我们可以看到3个Java进程使用了111%的CPU。要想知道哪个进程是什么,可以输入"c",每一行就会扩展信息。输入“1”可以显示CPU的每个核的具体状况。

15.3.4. jps

jps是JDK集成的一个工具,可以用来看当前用户的Java进程id。(如果是root,可以看到所有用户的id),例如:

hadoop@sv4borg12:~$ jps

1322 TaskTracker

17789 HRegionServer

27862 Child

1158 DataNode

25115 HQuorumPeer

2950 Jps

19750 ThriftServer

18776 jmx

 

按顺序看

· Hadoop TaskTracker,管理本地的Task

· HBase RegionServer,提供region的服务

· Child, 一个 MapReduce task,无法看出详细类型

· Hadoop DataNode, 管理blocks

· HQuorumPeer, ZooKeeper集群的成员

· Jps, 就是这个进程

· ThriftServer, 当thrif启动后,就会有这个进程

· jmx, 这个是本地监控平台的进程。你可以不用这个。

你可以看到这个进程启动是全部命令行信息。

hadoop@sv4borg12:~$ ps aux | grep HRegionServer

hadoop   17789  155 35.2 9067824 8604364 ?     S<l  Mar04 9855:48 /usr/java/jdk1.6.0_14/bin/java -Xmx8000m -XX:+DoEscapeAnalysis -XX:+AggressiveOpts -XX:+UseConcMarkSweepGC -XX:NewSize=64m -XX:MaxNewSize=64m -XX:CMSInitiatingOccupancyFraction=88 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/export1/hadoop/logs/gc-hbase.log -Dcom.sun.management.jmxremote.port=10102 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.password.file=/home/hadoop/hbase/conf/jmxremote.password -Dcom.sun.management.jmxremote -Dhbase.log.dir=/export1/hadoop/logs -Dhbase.log.file=hbase-hadoop-regionserver-sv4borg12.log -Dhbase.home.dir=/home/hadoop/hbase -Dhbase.id.str=hadoop -Dhbase.root.logger=INFO,DRFA -Djava.library.path=/home/hadoop/hbase/lib/native/Linux-amd64-64 -classpath /home/hadoop/hbase/bin/../conf:[many jars]:/home/hadoop/hadoop/conf org.apache.hadoop.hbase.regionserver.HRegionServer start

 

15.3.5. jstack

jstack 是一个最重要(除了看Log)的java工具,可以看到具体的Java进程的在做什么。可以先用Jps看到进程的Id,然后就可以用jstack。他会按线程的创建顺序显示线程的列表,还有这个线程在做什么。下面是例子:

这个主线程是一个RegionServer正在等master返回什么信息。

"regionserver60020" prio=10 tid=0x0000000040ab4000 nid=0x45cf waiting on condition [0x00007f16b6a96000..0x00007f16b6a96a70]

java.lang.Thread.State: TIMED_WAITING (parking)

at sun.misc.Unsafe.park(Native Method)

- parking to wait for  <0x00007f16cd5c2f30> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)

at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:198)

at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1963)

at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:395)

at org.apache.hadoop.hbase.regionserver.HRegionServer.run(HRegionServer.java:647)

at java.lang.Thread.run(Thread.java:619)

 

The MemStore flusher thread that is currently flushing to a file:

"regionserver60020.cacheFlusher" daemon prio=10 tid=0x0000000040f4e000 nid=0x45eb in Object.wait() [0x00007f16b5b86000..0x00007f16b5b87af0]

java.lang.Thread.State: WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

at java.lang.Object.wait(Object.java:485)

at org.apache.hadoop.ipc.Client.call(Client.java:803)

- locked <0x00007f16cb14b3a8> (a org.apache.hadoop.ipc.Client$Call)

at org.apache.hadoop.ipc.RPC$Invoker.invoke(RPC.java:221)

at $Proxy1.complete(Unknown Source)

at sun.reflect.GeneratedMethodAccessor38.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

at java.lang.reflect.Method.invoke(Method.java:597)

at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:82)

at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:59)

at $Proxy1.complete(Unknown Source)

at org.apache.hadoop.hdfs.DFSClient$DFSOutputStream.closeInternal(DFSClient.java:3390)

- locked <0x00007f16cb14b470> (a org.apache.hadoop.hdfs.DFSClient$DFSOutputStream)

at org.apache.hadoop.hdfs.DFSClient$DFSOutputStream.close(DFSClient.java:3304)

at org.apache.hadoop.fs.FSDataOutputStream$PositionCache.close(FSDataOutputStream.java:61)

at org.apache.hadoop.fs.FSDataOutputStream.close(FSDataOutputStream.java:86)

at org.apache.hadoop.hbase.io.hfile.HFile$Writer.close(HFile.java:650)

at org.apache.hadoop.hbase.regionserver.StoreFile$Writer.close(StoreFile.java:853)

at org.apache.hadoop.hbase.regionserver.Store.internalFlushCache(Store.java:467)

- locked <0x00007f16d00e6f08> (a java.lang.Object)

at org.apache.hadoop.hbase.regionserver.Store.flushCache(Store.java:427)

at org.apache.hadoop.hbase.regionserver.Store.access$100(Store.java:80)

at org.apache.hadoop.hbase.regionserver.Store$StoreFlusherImpl.flushCache(Store.java:1359)

at org.apache.hadoop.hbase.regionserver.HRegion.internalFlushcache(HRegion.java:907)

at org.apache.hadoop.hbase.regionserver.HRegion.internalFlushcache(HRegion.java:834)

at org.apache.hadoop.hbase.regionserver.HRegion.flushcache(HRegion.java:786)

at org.apache.hadoop.hbase.regionserver.MemStoreFlusher.flushRegion(MemStoreFlusher.java:250)

at org.apache.hadoop.hbase.regionserver.MemStoreFlusher.flushRegion(MemStoreFlusher.java:224)

at org.apache.hadoop.hbase.regionserver.MemStoreFlusher.run(MemStoreFlusher.java:146)

 

一个处理线程是在等一些东西(例如put, delete, scan...):

"IPC Server handler 16 on 60020" daemon prio=10 tid=0x00007f16b011d800 nid=0x4a5e waiting on condition [0x00007f16afefd000..0x00007f16afefd9f0]

java.lang.Thread.State: WAITING (parking)

at sun.misc.Unsafe.park(Native Method)

- parking to wait for  <0x00007f16cd3f8dd8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)

at java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)

at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1925)

at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:358)

at org.apache.hadoop.hbase.ipc.HBaseServer$Handler.run(HBaseServer.java:1013)

 

有一个线程正在忙,在递增一个counter(这个阶段是正在创建一个scanner来读最新的值):

"IPC Server handler 66 on 60020" daemon prio=10 tid=0x00007f16b006e800 nid=0x4a90 runnable [0x00007f16acb77000..0x00007f16acb77cf0]

java.lang.Thread.State: RUNNABLE

at org.apache.hadoop.hbase.regionserver.KeyValueHeap.<init>(KeyValueHeap.java:56)

at org.apache.hadoop.hbase.regionserver.StoreScanner.<init>(StoreScanner.java:79)

at org.apache.hadoop.hbase.regionserver.Store.getScanner(Store.java:1202)

at org.apache.hadoop.hbase.regionserver.HRegion$RegionScanner.<init>(HRegion.java:2209)

at org.apache.hadoop.hbase.regionserver.HRegion.instantiateInternalScanner(HRegion.java:1063)

at org.apache.hadoop.hbase.regionserver.HRegion.getScanner(HRegion.java:1055)

at org.apache.hadoop.hbase.regionserver.HRegion.getScanner(HRegion.java:1039)

at org.apache.hadoop.hbase.regionserver.HRegion.getLastIncrement(HRegion.java:2875)

at org.apache.hadoop.hbase.regionserver.HRegion.incrementColumnValue(HRegion.java:2978)

at org.apache.hadoop.hbase.regionserver.HRegionServer.incrementColumnValue(HRegionServer.java:2433)

at sun.reflect.GeneratedMethodAccessor20.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

at java.lang.reflect.Method.invoke(Method.java:597)

at org.apache.hadoop.hbase.ipc.HBaseRPC$Server.call(HBaseRPC.java:560)

at org.apache.hadoop.hbase.ipc.HBaseServer$Handler.run(HBaseServer.java:1027)

 

还有一个线程在从HDFS获取数据。

 

"IPC Client (47) connection to sv4borg9/10.4.24.40:9000 from hadoop" daemon prio=10 tid=0x00007f16a02d0000 nid=0x4fa3 runnable [0x00007f16b517d000..0x00007f16b517dbf0]

java.lang.Thread.State: RUNNABLE

at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)

at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:215)

at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:65)

at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:69)

- locked <0x00007f17d5b68c00> (a sun.nio.ch.Util$1)

- locked <0x00007f17d5b68be8> (a java.util.Collections$UnmodifiableSet)

- locked <0x00007f1877959b50> (a sun.nio.ch.EPollSelectorImpl)

at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:80)

at org.apache.hadoop.net.SocketIOWithTimeout$SelectorPool.select(SocketIOWithTimeout.java:332)

at org.apache.hadoop.net.SocketIOWithTimeout.doIO(SocketIOWithTimeout.java:157)

at org.apache.hadoop.net.SocketInputStream.read(SocketInputStream.java:155)

at org.apache.hadoop.net.SocketInputStream.read(SocketInputStream.java:128)

at java.io.FilterInputStream.read(FilterInputStream.java:116)

at org.apache.hadoop.ipc.Client$Connection$PingInputStream.read(Client.java:304)

at java.io.BufferedInputStream.fill(BufferedInputStream.java:218)

at java.io.BufferedInputStream.read(BufferedInputStream.java:237)

- locked <0x00007f1808539178> (a java.io.BufferedInputStream)

at java.io.DataInputStream.readInt(DataInputStream.java:370)

at org.apache.hadoop.ipc.Client$Connection.receiveResponse(Client.java:569)

at org.apache.hadoop.ipc.Client$Connection.run(Client.java:477)

 

这里是一个RegionServer死了,master正在试着恢复。

"LeaseChecker" daemon prio=10 tid=0x00000000407ef800 nid=0x76cd waiting on condition [0x00007f6d0eae2000..0x00007f6d0eae2a70]

--

java.lang.Thread.State: WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

at java.lang.Object.wait(Object.java:485)

at org.apache.hadoop.ipc.Client.call(Client.java:726)

- locked <0x00007f6d1cd28f80> (a org.apache.hadoop.ipc.Client$Call)

at org.apache.hadoop.ipc.RPC$Invoker.invoke(RPC.java:220)

at $Proxy1.recoverBlock(Unknown Source)

at org.apache.hadoop.hdfs.DFSClient$DFSOutputStream.processDatanodeError(DFSClient.java:2636)

at org.apache.hadoop.hdfs.DFSClient$DFSOutputStream.<init>(DFSClient.java:2832)

at org.apache.hadoop.hdfs.DFSClient.append(DFSClient.java:529)

at org.apache.hadoop.hdfs.DistributedFileSystem.append(DistributedFileSystem.java:186)

at org.apache.hadoop.fs.FileSystem.append(FileSystem.java:530)

at org.apache.hadoop.hbase.util.FSUtils.recoverFileLease(FSUtils.java:619)

at org.apache.hadoop.hbase.regionserver.wal.HLog.splitLog(HLog.java:1322)

at org.apache.hadoop.hbase.regionserver.wal.HLog.splitLog(HLog.java:1210)

at org.apache.hadoop.hbase.master.HMaster.splitLogAfterStartup(HMaster.java:648)

at org.apache.hadoop.hbase.master.HMaster.joinCluster(HMaster.java:572)

at org.apache.hadoop.hbase.master.HMaster.run(HMaster.java:503)

 

15.3.6. OpenTSDB

OpenTSDB是一个Ganglia的很好的替代品,因为他使用Hbase来存储所有的时序而不需要采样。使用OpenTSDB来监控你的Hbase是一个很好的实践

这里有一个例子,集群正在同时进行上百个compaction,严重影响了IO性能。(TODO: 在这里插入compactionQueueSize的图片)(译者注:囧)

给集群构建一个图表监控是一个很好的实践。包括集群和每台机器。这样就可以快速定位到问题。例如,在StumbleUpon,每个机器有一个图表监控,包括OS和Hbase,涵盖所有的重要的信息。你也可以登录到机器上,获取更多的信息。

15.3.7. clusterssh+top

clusterssh+top,感觉是一个穷人用的监控系统,但是他确实很有效,当你只有几台机器的是,很好设置。启动clusterssh后,你就会每台机器有个终端,还有一个终端,你在这个终端的操作都会反应到其他的每一个终端上。 这就意味着,你在一天机器执行“top”,集群中的所有机器都会给你全部的top信息。你还可以这样tail全部的log,等等。

15.4. 客户端

15.4.1. ScannerTimeoutException

当从客户端到RegionServer的RPC请求超时。例如如果Scan.setCacheing的值设置为500,RPC请求就要去获取500行的数据,每500次.next()操作获取一次。因为数据是以大块的形式传到客户端的,就可能造成超时。将这个 serCacheing的值调小是一个解决办法,但是这个值要是设的太小就会影响性能。

15.5. RegionServer

15.5.1. 启动错误

15.5.1.1. 压缩链接错误

因为LZO压缩算法需要在集群中的每台机器都要安装,这是一个启动失败的常见错误。如果你获得了如下信息

11/02/20 01:32:15 ERROR lzo.GPLNativeCodeLoader: Could not load native gpl library

java.lang.UnsatisfiedLinkError: no gplcompression in java.library.path

at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1734)

at java.lang.Runtime.loadLibrary0(Runtime.java:823)

at java.lang.System.loadLibrary(System.java:1028)

 

就意味着你的压缩库出现了问题。参见配置章节的 LZO compression configuration.

15.5.2. 运行时错误

15.5.2.1. java.io.IOException...(Too many open files)

参见快速入门的章节ulimit and nproc configuration.

15.5.2.2. xceiverCount 258 exceeds the limit of concurrent xcievers 256

这个时常会出现在DataNode的日志中。

参见快速入门章节的 xceivers configuration.

15.5.2.3. 系统不稳定,DataNode或者其他系统进程有 "java.lang.OutOfMemoryError: unable to create new native thread in exceptions"的错误

参见快速入门章节的 ulimit and nproc configuration.

15.5.2.4. DFS不稳定或者RegionServer租期超时

如果你收到了如下的消息

2009-02-24 10:01:33,516 WARN org.apache.hadoop.hbase.util.Sleeper: We slept xxx ms, ten times longer than scheduled: 10000

2009-02-24 10:01:33,516 WARN org.apache.hadoop.hbase.util.Sleeper: We slept xxx ms, ten times longer than scheduled: 15000

2009-02-24 10:01:36,472 WARN org.apache.hadoop.hbase.regionserver.HRegionServer: unable to report to master for xxx milliseconds - retrying

 

或者看到了全GC压缩操作,你可能正在执行一个全GC。

15.5.2.5. "No live nodes contain current block" and/or YouAreDeadException

这个错误有可能是OS的文件句柄溢出,也可能是网络故障导致节点无法访问。

参见快速入门章节 ulimit and nproc configuration,检查你的网络。

15.5.3. 终止错误

15.6. Master

15.6.1. 启动错误

15.6.2. 终止错误

三、 HBase Coprocessor的分析

 

hbase Coprocessor是很多人对hbase-0.92的重大期待之一。它让离线分析和在线应用很好地结合在了一起,另外也极大地拓展了hbase的应用丰富性,不再是简单的k-v类应用。hbase coprocessor的设计来源于hbase-2000和hbase-2001两个issue。那么几年过去了,hbase coprocessor究竟发展到什么程度,可以将它们用于哪些地方呢?下文主要内容来源于Trend Micro Hadoop Group的成员,同时也是hbase coprocessor的作者,不愿意看底下啰嗦废话的同学可以直接跳到最后面的总结看。

https://blogs.apache.org/hbase/entry/coprocessor_introduction

hbase coprocessor随着0.92的release而完整地release出来了,它的设计来源于google bigtable的coprocessor(Jeff Dean的一篇演讲)。coprocessor其实是一个类似mapreduce的分析组件,不过它极大简化了mapreduce模型,只是将请求独立地在各个region中并行地运行,并且提供了一套框架能够让用户非常灵活地编写自定义的coprocessor。hbase的coprocessor与google的coprocessor最大的区别是,hbase的coprocessor是一套在regionserver和master进程内的框架,可以在运行期动态执行用户的代码,而google的coprocessor则是拥有自己独立的进程和地址空间。看起来似乎hbase的coprocessor更加高效,但其实并非如此,拥有独立的地址空间的好处是可以通过与计算资源的绑定更高效地使用硬件,比如通过cgroup的方式绑定到独立的cpu上去工作。
现在hbase的coprocessor有两种完全不同的实现,分别是observer模式与endpoint模式,它们分别对应2000和2001两个issue。我们可以将observer模式看成数据库中的触发器,而endpoint可以看成是存储过程。

关于coprocessor我们可以从类继承关系上看到,如下图所示:

 

共有三个Observer对象,即MasterObserver,RegionObserver和WALObserver。它们的工作原理类似于钩子函数,在真实的函数实现前加入pre(),实现后加入post()方法,来实现对操作进行一些嵌入式的改变。效率上的影响仅仅取决于嵌入的钩子函数本身的影响。如下图所示:

 

比如下面这段代码就实现了在get之前做权限检查:

publicclassAccessControlCoprocessorextends BaseRegionObserver {

@Override

public void preGet(final ObserverContext c,

final Get get, final List result) throws IOException

throws IOException {

 

// check permissions..

if (!permissionGranted()) {

thrownewAccessDeniedException("User is not allowed to access.");

}

}

 

// override prePut(), preDelete(), etc.

}

在hbase的社区中,己经有很多人提出了各种基于Observer的实现方法,来做一些有意义的事。比如可以通过regionObserver实现刚才说的权限,还有故障隔离,优先级等工作。通过MasterObserver则可以实现对ddl操作的监控。通过WALObserver则可以实现二级索引、replication等工作。不过遗憾的是,很少有想法实现了,主要原因还是大家都比较忙,而相应的需求在现阶段还不强烈,所以留待后面来做。由于Observer框架的灵活性和扩展性,用户可以方便地自定义各种符合自己需求的实现。

BaseEndpoint类则是直接实现了Coprocessor接口,它实际上是client-side的实现,即客户端包装了一个HTable的实现,将类似于getMin()的操作包装起来,内部则通过并行scan等操作来实现。这就很类似一个简单的mapreduce,map就是在目标region server上的那些region,而reduce则是客户端。这个方法对于习惯于编写map reduce代码的人来说非常直观,可以很容易地实现sum,min等接口。由于这一层包装,可以屏蔽掉很多开发上的细节,让开发应用的人专注于开发应用,将底层实现交给mapreduce programmer。

一个简单的Endpoint例子如下:

public T getMin(ColumnInterpreter ci, Scan scan)

throws IOException {

T min = null;

T temp;

InternalScanner scanner = ((RegionCoprocessorEnvironment) getEnvironment())

.getRegion().getScanner(scan);

List results = new ArrayList();

byte[] colFamily = scan.getFamilies()[0];

byte[] qualifier = scan.getFamilyMap().get(colFamily).pollFirst();

try {

boolean hasMoreRows = false;

do {

hasMoreRows = scanner.next(results);

for (KeyValue kv : results) {

temp = ci.getValue(colFamily, qualifier, kv);

min = (min == null || ci.compare(temp, min) < 0) ? temp : min;

}

results.clear();

} while (hasMoreRows);

} finally {

scanner.close();

}

log.info("Minimum from this region is "

+ ((RegionCoprocessorEnvironment) getEnvironment()).getRegion()

.getRegionNameAsString() + ": " + min);

return min;

}

Coprocessor可以让用户随时加载自己实现的代码,而不需要重启系统。它可以通过配置来加载,也可以通过shell或java程序来加载。

总结:

· hbase coprocessor是脱胎于google bigtable coprocessor,区别是hbase coprocessor并非独立进程,而是在原来进程中嵌入框架

· hbase coprocessor的实现分为observer与endpoint,其中observer类似于触发器,主要在服务端工作,而endpoint类似于存储过程,主要在client端工作

· observer可以实现权限管理、优先级设置、监控、ddl控制、二级索引等功能,而endpoint可以实现min、mas、avg、sum等功能

· coprocessor可以动态加载

· coprocessor的性能取决于想要加载的方法的性能,不过为了有一个直观的了解,我们仍然近期打算对hbase coprocessor的一些典型场景做性能测试。

四、 HBase在淘宝的应用和优化小结

 

 1 前言

hbase是从hadoop中分离出来的apache顶级开源项目。由于它很好地用java实现了google的bigtable系统大部分特性,因此在数据量猛增的今天非常受到欢迎。对于淘宝而言,随着市场规模的扩大,产品与技术的发展,业务数据量越来越大,对海量数据的高效插入和读取变得越来越重要。由于淘宝拥有也许是国内最大的单一hadoop集群(云梯),因此对hadoop系列的产品有比较深入的了解,也就自然希望使用hbase来做这样一种海量数据读写服务。本篇文章将对淘宝最近一年来在online应用上使用和优化hbase的情况做一次小结。

2 原因

为什么要使用hbase?

淘宝在2011年之前所有的后端持久化存储基本上都是在mysql上进行的(不排除少量oracle/bdb/tair/mongdb等),mysql由于开源,并且生态系统良好,本身拥有分库分表等多种解决方案,因此很长一段时间内都满足淘宝大量业务的需求。

但是由于业务的多样化发展,有越来越多的业务系统的需求开始发生了变化。一般来说有以下几类变化:

· a) 数据量变得越来越多,事实上现在淘宝几乎任何一个与用户相关的在线业务的数据量都在亿级别,每日系统调用次数从亿到百亿都有,且历史数据不能轻易删除。这需要有一个海量分布式文件系统,能对TB级甚至PB级别的数据提供在线服务

· b) 数据量的增长很快且不一定能准确预计,大多数应用系统从上线起在一段时间内数据量都呈很快的上升趋势,因此从成本的角度考虑对系统水平扩展能力有比较强烈的需求,且不希望存在单点制约

· c) 只需要简单的kv读取,没有复杂的join等需求。但对系统的并发能力以及吞吐量、响应延时有非常高的需求,并且希望系统能够保持强一致性

· d) 通常系统的写入非常频繁,尤其是大量系统依赖于实时的日志分析

· e) 希望能够快速读取批量数据

· f ) schema灵活多变,可能经常更新列属性或新增列

· g) 希望能够方便使用,有良好且语义清晰的java接口

以上需求综合在一起,我们认为hbase是一种比较适合的选择。首先它的数据由hdfs天然地做了数据冗余,云梯三年的稳定运行,数据100%可靠己经证明了hdfs集群的安全性,以及服务于海量数据的能力。其次hbase本身的数据读写服务没有单点的限制,服务能力可以随服务器的增长而线性增长,达到几十上百台的规模。LSM-Tree模式的设计让hbase的写入性能非常良好,单次写入通常在1-3ms内即可响应完成,且性能不随数据量的增长而下降。region(相当于数据库的分表)可以ms级动态的切分和移动,保证了负载均衡性。由于hbase上的数据模型是按rowkey排序存储的,而读取时会一次读取连续的整块数据做为cache,因此良好的rowkey设计可以让批量读取变得十分容易,甚至只需要1次io就能获取几十上百条用户想要的数据。最后,淘宝大部分工程师是java背景的同学,因此hbase的api对于他们来说非常容易上手,培训成本相对较低。

当然也必须指出,在大数据量的背景下银弹是不存在的,hbase本身也有不适合的场景。比如,索引只支持主索引(或看成主组合索引),又比如服务是单点的,单台机器宕机后在master恢复它期间它所负责的部分数据将无法服务等。这就要求在选型上需要对自己的应用系统有足够了解。

3 应用情况

我们从2011年3月开始研究hbase如何用于在线服务。尽管之前在一淘搜索中己经有了几十节点的离线服务。这是因为hbase早期版本的目标就是一个海量数据中的离线服务。2009年9月发布的0.20.0版本是一个里程碑,online应用正式成为了hbase的目标,为此hbase引入了zookeeper来做为backupmaster以及regionserver的管理。2011年1月0.90.0版本是另一个里程碑,基本上我们今天看到的各大网站,如facebook/ebay/yahoo内所使用于生产的hbase都是基于这一个版本(fb所采用的0.89版本结构与0.90.x相近)。bloomfilter等诸多属性加入了进来,性能也有极大提升。基于此,淘宝也选用了0.90.x分支作为线上版本的基础。

第一个上线的应用是数据魔方中的prom。prom原先是基于redis构建的,因为数据量持续增大以及需求的变化,因此我们用hbase重构了它的存储层。准确的说prom更适合0.92版本的hbase,因为它不仅需要高速的在线读写,更需要count/group by等复杂应用。但由于当时0.92版本尚未成熟,因此我们自己单独实现了coprocessor。prom的数据导入是来源于云梯,因此我们每天晚上花半个小时将数据从云梯上写入hbase所在的hdfs,然后在web层做了一个client转发。经过一个月的数据比对,确认了速度比之redis并未有明显下降,以及数据的准确性,因此得以顺利上线。

第二个上线的应用是TimeTunnel,TimeTunnel是一个高效的、可靠的、可扩展的实时数据传输平台,广泛应用于实时日志收集、数据实时监控、广告效果实时反馈、数据库实时同步等领域。它与prom相比的特点是增加了在线写。动态的数据增加使hbase上compact/balance/split/recovery等诸多特性受到了极大的挑战。TT的写入量大约一天20TB,读的量约为此的1.5倍,我们为此准备了20台regionserver的集群,当然底层的hdfs是公用的,数量更为庞大(下文会提到)。每天TT会为不同的业务在hbase上建不同的表,然后往该表上写入数据,即使我们将region的大小上限设为1GB,最大的几个业务也会达到数千个region这样的规模,可以说每一分钟都会有数次split。在TT的上线过程中,我们修复了hbase很多关于split方面的bug,有好几个commit到了hbase社区,同时也将社区一些最新的patch打在了我们的版本上。split相关的bug应该说是hbase中会导致数据丢失最大的风险之一,这一点对于每个想使用hbase的开发者来说必须牢记。hbase由于采用了LSM-Tree模型,从架构原理上来说数据几乎没有丢失的可能,但是在实际使用中不小心谨慎就有丢失风险。原因后面会单独强调。TT在预发过程中我们分别因为Meta表损坏以及split方面的bug曾经丢失过数据,因此也单独写了meta表恢复工具,确保今后不发生类似问题(hbase-0.90.5以后的版本都增加了类似工具)。另外,由于我们存放TT的机房并不稳定,发生过很多次宕机事故,甚至发生过假死现象。因此我们也着手修改了一些patch,以提高宕机恢复时间,以及增强了监控的强度。

CTU以及会员中心项目是两个对在线要求比较高的项目,在这两个项目中我们特别对hbase的慢响应问题进行了研究。hbase的慢响应现在一般归纳为四类原因:网络原因、gc问题、命中率以及client的反序列化问题。我们现在对它们做了一些解决方案(后面会有介绍),以更好地对慢响应有控制力。

和Facebook类似,我们也使用了hbase做为实时计算类项目的存储层。目前对内部己经上线了部分实时项目,比如实时页面点击系统,galaxy实时交易推荐以及直播间等内部项目,用户则是散布到公司内各部门的运营小二们。与facebook的puma不同的是淘宝使用了多种方式做实时计算层,比如galaxy是使用类似affa的actor模式处理交易数据,同时关联商品表等维度表计算排行(TopN),而实时页面点击系统则是基于twitter开源的storm进行开发,后台通过TT获取实时的日志数据,计算流将中间结果以及动态维表持久化到hbase上,比如我们将rowkey设计为url+userid,并读出实时的数据,从而实现实时计算各个维度上的uv。

最后要特别提一下历史交易订单项目。这个项目实际上也是一个重构项目,目的是从以前的solr+bdb的方案上迁移到hbase上来。由于它关系到己买到页面,用户使用频率非常高,重要程度接近核心应用,对数据丢失以及服务中断是零容忍。它对compact做了优化,避免大数据量的compact在服务时间内发生。新增了定制的filter来实现分页查询,rowkey上对应用进行了巧妙的设计以避免了冗余数据的传输以及90%以上的读转化成了顺序读。目前该集群存储了超过百亿的订单数据以及数千亿的索引数据,线上故障率为0。

随着业务的发展,目前我们定制的hbase集群己经应用到了线上超过二十个应用,数百台服务器上。包括淘宝首页的商品实时推荐、广泛用于卖家的实时量子统计等应用,并且还有继续增多以及向核心应用靠近的趋势。

4 部署、运维和监控

Facebook之前曾经透露过Facebook的hbase架构,可以说是非常不错的。如他们将message服务的hbase集群按用户分为数个集群,每个集群100台服务器,拥有一台namenode以及分为5个机架,每个机架上一台zookeeper。可以说对于大数据量的服务这是一种优良的架构。对于淘宝来说,由于数据量远没有那么大,应用也没有那么核心,因此我们采用公用hdfs以及zookeeper集群的架构。每个hdfs集群尽量不超过100台规模(这是为了尽量限制namenode单点问题)。在其上架设数个hbase集群,每个集群一个master以及一个backupmaster。公用hdfs的好处是可以尽量减少compact的影响,以及均摊掉硬盘的成本,因为总有集群对磁盘空间要求高,也总有集群对磁盘空间要求低,混合在一起用从成本上是比较合算的。zookeeper集群公用,每个hbase集群在zk上分属不同的根节点。通过zk的权限机制来保证hbase集群的相互独立。zk的公用原因则仅仅是为了运维方便。

由于是在线应用,运维和监控就变得更加重要,由于之前的经验接近0,因此很难招到专门的hbase运维人员。我们的开发团队和运维团队从一开始就很重视该问题,很早就开始自行培养。以下讲一些我们的运维和监控经验。

我们定制的hbase很重要的一部分功能就是增加监控。hbase本身可以发送ganglia监控数据,只是监控项远远不够,并且ganglia的展示方式并不直观和突出。因此一方面我们在代码中侵入式地增加了很多监控点,比如compact/split/balance/flush队列以及各个阶段的耗时、读写各个阶段的响应时间、读写次数、region的open/close,以及具体到表和region级别的读写次数等等。仍然将它们通过socket的方式发送到ganglia中,ganglia会把它们记录到rrd文件中,rrd文件的特点是历史数据的精度会越来越低,因此我们自己编写程序从rrd中读出相应的数据并持久化到其它地方,然后自己用js实现了一套监控界面,将我们关心的数据以趋势图、饼图等各种方式重点汇总和显示出来,并且可以无精度损失地查看任意历史数据。在显示的同时会把部分非常重要的数据,如读写次数、响应时间等写入数据库,实现波动报警等自定义的报警。经过以上措施,保证了我们总是能先于用户发现集群的问题并及时修复。我们利用redis高效的排序算法实时地将每个region的读写次数进行排序,能够在高负载的情况下找到具体请求次数排名较高的那些region,并把它们移到空闲的regionserver上去。在高峰期我们能对上百台机器的数十万个region进行实时排序。

为了隔离应用的影响,我们在代码层面实现了可以检查不同client过来的连接,并且切断某些client的连接,以在发生故障时,将故障隔离在某个应用内部而不扩大化。mapreduce的应用也会控制在低峰期运行,比如在白天我们会关闭jobtracker等。

此外,为了保障服务从结果上的可用,我们也会定期跑读写测试、建表测试、hbck等命令。hbck是一个非常有用的工具,不过要注意它也是一个很重的工操作,因此尽量减少hbck的调用次数,尽量不要并行运行hbck服务。在0.90.4以前的hbck会有一些机率使hbase宕机。另外为了确保hdfs的安全性,需要定期运行fsck等以检查hdfs的状态,如block的replica数量等。
我们会每天根踪所有线上服务器的日志,将错误日志全部找出来并且邮件给开发人员,以查明每一次error以上的问题原因和fix。直至错误降低为0。另外每一次的hbck结果如果有问题也会邮件给开发人员以处理掉。尽管并不是每一次error都会引发问题,甚至大部分error都只是分布式系统中的正常现象,但明白它们问题的原因是非常重要的。

5 测试与发布

因为是未知的系统,我们从一开始就非常注重测试。测试从一开始就分为性能测试和功能测试。性能测试主要是注意基准测试,分很多场景,比如不同混合读写比例,不同k/v大小,不同列族数,不同命中率,是否做presharding等等。每次运行都会持续数小时以得到准确的结果。因此我们写了一套自动化系统,从web上选择不同的场景,后台会自动将测试参数传到各台服务器上去执行。由于是测试分布式系统,因此client也必须是分布式的。

我们判断测试是否准确的依据是同一个场景跑多次,是否数据,以及运行曲线达到99%以上的重合度,这个工作非常烦琐,以至于消耗了很多时间,但后来的事实证明它非常有意义。因为我们对它建立了100%的信任,这非常重要,比如后期我们的改进哪怕只提高2%的性能也能被准确捕捉到,又比如某次代码修改使compact队列曲线有了一些起伏而被我们看到,从而找出了程序的bug,等等。

功能测试上则主要是接口测试和异常测试。接口测试一般作用不是很明显,因为hbase本身的单元测试己经使这部分被覆盖到了。但异常测试非常重要,我们绝大部分bug修改都是在异常测试中发现的,这帮助我们去掉了很多生产环境中可能存在的不稳定因素,我们也提交了十几个相应的patch到社区,并受到了重视和commit。分布式系统设计的难点和复杂度都在异常处理上,我们必须认为系统在通讯的任何时候都是不可靠的。某些难以复现的问题我们会通过查看代码大体定位到问题以后,在代码层面强行抛出异常来复现它。事实证明这非常有用。

为了方便和快速定位问题,我们设计了一套日志收集和处理的程序,以方便地从每台服务器上抓取相应的日志并按一定规律汇总。这非常重要,避免浪费大量的时间到登录不同的服务器以寻找一个bug的线索。

由于hbase社区在不停发展,以及线上或测试环境发现的新的bug,我们需要制定一套有规律的发布模式。它既要避免频繁的发布引起的不稳定,又要避免长期不发布导致生产版本离开发版本越来越远或是隐藏的bug爆发。我们强行规定每两周从内部trunk上release一个版本,该版本必须通过所有的测试包括回归测试,并且在release后在一个小型的集群上24小时不受甘扰不停地运行。每个月会有一次发布,发布时采用最新release的版本,并且将现有的集群按重要性分级发布,以确保重要应用不受新版本的潜在bug影响。事实证明自从我们引入这套发布机制后,由发布带来的不稳定因素大大下降了,并且线上版本也能保持不落后太多。

6 改进和优化

Facebook是一家非常值得尊敬的公司,他们毫无保留地对外公布了对hbase的所有改造,并且将他们内部实际使用的版本开源到了社区。facebook线上应用的一个重要特点是他们关闭了split,以降低split带来的风险。与facebook不同,淘宝的业务数据量相对没有如此庞大,并且由于应用类型非常丰富,我们并们并没有要求用户强行选择关闭split,而是尽量去修改split中可能存在的bug。到目前为止,虽然我们并不能说完全解决了这个问题,但是从0.90.2中暴露出来的诸多跟split以及宕机相关的可能引发的bug我们的测试环境上己经被修复到接近了0,也为社区提交了10数个稳定性相关的patch,比较重要的有以下几个:

· https://issues.apache.org/jira/browse/HBASE-4562

· https://issues.apache.org/jira/browse/HBASE-4563

· https://issues.apache.org/jira/browse/HBASE-5152

· https://issues.apache.org/jira/browse/HBASE-5100

· https://issues.apache.org/jira/browse/HBASE-4880

· https://issues.apache.org/jira/browse/HBASE-4878

· https://issues.apache.org/jira/browse/HBASE-4899

还有其它一些,我们主要将patch提交到0.92版本,社区会有commitor帮助我们backport回0.90版本。所以社区从0.90.2一直到0.90.6一共发布了5个bugfix版本后,0.90.6版本其实己经比较稳定了。建议生产环境可以考虑这个版本。

split这是一个很重的事务,它有一个严重的问题就是会修改meta表(当然宕机恢复时也有这个问题)。如果在此期间发生异常,很有可能meta表、rs内存、master内存以及hdfs上的文件会发生不一致,导致之后region重新分配时发生错误。其中一个错误就是有可能同一个region被两个以上的regionserver所服务,那么就可能出现这一个region所服务的数据会随机分别写到多台rs上,读取的时候也会分别读取,导致数据丢失。想要恢复原状,必须删除掉其中一个rs上的region,这就导致了不得不主动删掉数据,从而引发数据丢失。

前面说到慢响应的问题归纳为网络原因、gc问题、命中率以及client的反序列化问题。网络原因一般是网络不稳定引起的,不过也有可能是tcp参数设置问题,必须保证尽量减少包的延迟,如nodelay需要设置为true等,这些问题我们通过tcpdump等一系列工具专门定位过,证明tcp参数对包的组装确实会造成慢连接。gc要根据应用的类型来,一般在读比较多的应用中新生代不能设置得太小。命中率极大影响了响应的时间,我们会尽量将version数设为1以增加缓存的容量,良好的balance也能帮助充分应用好每台机器的命中率。我们为此设计了表级别的balance。

由于hbase服务是单点的,即宕机一台,则该台机器所服务的数据在恢复前是无法读写的。宕机恢复速度决定了我们服务的可用率。为此主要做了几点优化。首先是将zk的宕机发现时间尽量缩短到1分钟,其次改进了master恢复日志为并行恢复,大大提高了master恢复日志的速度,然后我们修改了openhandler中可能出现的一些超时异常,以及死锁,去掉了日志中可能发生的open…too long等异常。原生的hbase在宕机恢复时有可能发生10几分钟甚至半小时无法重启的问题己经被修复掉了。另外,hdfs层面我们将socket.timeout时间以及重试时间也缩短了,以降低datanode宕机引起的长时间block现象。

hbase本身读写层面的优化我们目前并没有做太多的工作,唯一打的patch是region增加时写性能严重下降的问题。因为由于hbase本身良好的性能,我们通过大量测试找到了各种应用场景中比较优良的参数并应用于生产环境后,都基本满足需求。不过这是我们接下来的重要工作。

7 将来计划

我们目前维护着淘宝内基于社区0.90.x而定制的hbase版本。接下来除继续fix它的bug外,会维护基于0.92.x修改的版本。之所以这样,是因为0.92.x和0.90.x的兼容性并不是非常好,而且0.92.x修改掉的代码非常多,粗略统计会超过30%。0.92中有我们非常看重的一些特性。

· 0.92版本改进了hfile为hfileV2,v2版本的特点是将索引以及bloomfilter进行了大幅改造,以支持单个大hfile文件。现有的HFile在文件大到一定程度时,index会占用大量的内存,并且加载文件的速度会因此下降非常多。而如果HFile不增大的话,region就无法扩大,从而导致region数量非常多。这是我们想尽量避免的事。

· 0.92版本改进了通讯层协议,在通讯层中增加了length,这非常重要,它让我们可以写出nio的客户端,使反序列化不再成为影响client性能的地方。

· 0.92版本增加了coprocessor特性,这支持了少量想要在rs上进行count等的应用。

· 还有其它很多优化,比如改进了balance算法、改进了compact算法、改进了scan算法、compact变为CF级别、动态做ddl等等特性。

除了0.92版本外,0.94版本以及最新的trunk(0.96)也有很多不错的特性,0.94是一个性能优化版本。它做了很多革命性工作,比如去掉root表,比如HLog进行压缩,replication上支持多个slave集群,等等。
我们自己也有一些优化,比如自行实现的二级索引、backup策略等都会在内部版本上实现。

另外值得一提的是hdfs层面的优化也非常重要,hadoop-1.0.0以及cloudera-3u3的改进对hbase非常有帮助,比如本地化读、checksum的改进、datanode的keepalive设置、namenode的HA策略等。我们有一支优秀的hdfs团队来支持我们的hdfs层面工作,比如定位以及fix一些hdfs层面的bug,帮助提供一些hdfs上参数的建议,以及帮助实现namenode的HA等。最新的测试表明,3u3的checksum+本地化读可以将随机读性能提升至少一倍。

我们正在做的一件有意义的事是实时监控和调整regionserver的负载,能够动态地将负载不足的集群上的服务器挪到负载较高的集群中,而整个过程对用户完全透明。

总的来说,我们的策略是尽量和社区合作,以推动hbase在整个apache生态链以及业界的发展,使其能更稳定地部署到更多的应用中去,以降低使用门槛以及使用成本。

 

 

五、 HBase运维实战:disable table失败的处理

 

相信每一个维护hbase集群的运维人员一定碰到过disable失败,陷入无穷的”Region has been PENDING_CLOSE for too long…”状态,此时没有什么好的办法处理。经常需要重启集群。

这个问题产生的原因非常讨厌,经过一段时间的分析和验证,得到了根本原因。要理解它,必须从disable的原理说起:

· disable线程是一个DisableTableHandler类,我们看它的handleDisableTable()方法,在while循环中先获取table的regions列表,然后调用BulkDisabler的bulkAssign()方法,等待bulkAssign()返回为true时则结束

· 在bulkAssign()方法中启动线程池,然后等待线程池超时,超时时间由hbase.bulk.assignment.waiton.empty.rit 控制

· 在每个线程中,先从regions collection中得到regions列表,然后通知rs来处理该region,并且把该region放入RIT列表中,表示该region正在进行处理

· rs处理完region以后,将该region状态在zk上置为closing,此时master得到通知

· master将这个region从RIT列表中删除,并从regions列表中删除。

 

注意以上最后一步,当master把它从RIT中删除以后,还有短暂的时间这个region还在regions列表中,此时另一个线程拿到了这个region,并且此时这个region不处于RIT状态保护,于是另一个线程开始重复以上过程,而前一个线程己经把它从collection中删除了,于是后一个线程再也无法完成closing事件。直到RIT超时(默认30秒)。

 

六、 Hadoop HBase 单机环境简单配置教程

 

Hadoop是Apache的一个项目,它是一个实现了MapReduce计算模型的可以运用于大型集群并行计算的分布式并行计算编程框架,当然分布式计算离不开分布式存储,Hadoop框架包含了分布式存储系统HDFS(Hadoop Distributed File System),其存储和数据结构很类似Google的GFS.
HBase是Hadoop的子项目,它是基于Hadoop HDFS分布存储系统的一个Google BigTable开源实现(最近看了BigTable的Paper,很受鼓舞和启发),它在存储数据结构上并非关系型,而是疏松分布式的,持久并按多维排序并索引的map型,下次我把介绍BigTable和HBase的文章转载过来.
Google BigTable的paper: http://labs.google.com/papers/bigtable-osdi06.pdf

在初接触Hadoop和Hbase的时候,大多数技术人员只希望通过简单的方法初步搭起框架环境,但我找了好多安装手册都是上来就部署集群分布环境(尽管做Hadoop就是为了这个目的),搭建集群环境的时候要设置ssh协议访问权限,要生成访问公钥,并且slaves配置上的小错误也会导致整个部署问题重重,所以我在部署成功后记录下最简单运行的standalone模式的安装配置过程,希望初学者在需要的时候能找到这个小小的guide,以后有时间我会再把集群配置的方法写出来.

开始:
1.在Apache/Hadoop项目的网站(hadoop.apache.org)下载hadoop以及hbase的发行包,此处两个发行包的大版本号一定要一致,譬如都是0.18版本:
hadoop-0.18.2.tar.gz
hbase-0.18.1.tar.gz

2.使用root身份登录目标服务器系统(Suse10 Linux),首先安装java虚拟机,这个比较简单,随便找个绿色的解压就可以了,在这个例子里面我使用IBM WAS6.1附带的jdk,它的home目录是/opt/IBM/WebSphere/AppServer/java,我们只需要配置系统的环境变量就可以了.
编辑全局环境变量文件/etc/profile,在文件后面添加

export JAVA_HOME=/opt/IBM/WebSphere/AppServer/java

export PATH=$JAVA_HOME:$PATH

保存profile文件后使用

source /etc/profile

命令重新加载profile,然后随便在一个目录下面运行

java -version

查看javahome环境变量和path变量是否被正确加载.
另外到 /etc/hosts 文件中查看主机映射是否存在,例如 127.0.0.1 localhost 或者什么其他的名字,在这里默认配置本机为localhost,如果需要做分布式的话,本机要做namenode,所以要把所有的datanode的host添加到这里面.

3.创建hadoop用户,

useradd hadoop

可以用

passwd hadoop

修改hadoop用户的登录密码.

4.创建hadoop用户的home目录,如果打算在别处安装hadoop/hbase的话可以不这样做,这里我们默认将hadoop/hbase安装在/home/${username}目录下.

cd /home

mkdir hadoop

将目录用户指派给hadoop

chown hadoop hadoop

改变目录权限,这里我们配大一些,其实只要644就足够了:

chmod 755 hadoop

5.使用hadoop用户登录系统,将下载的两个发行包文件传到/home/hadoop目录下面,然后给它们加上执行权限:

chmod a+x hadoop-0.18.2.tar.gz

chmod a+x hbase-0.18.1.tar.gz

6.解压hadoop:

tar zxvf hadoop-0.18.2.tar.gz

这样做会在/home/hadoop目录下解压hadoop发行包并创建到/home/hadoop/hadoop-0.18.2目录中,这里可以详细设计目录结构并创建link文件来方便日后升级等工作,这里我们先简单放在这里.

7.修改hadoop环境脚本:
修改文件/home/hadoop/hadoop-0.18.2/conf/hadoop-env.sh,在其中加入JAVA_HOME变量:

export JAVA_HOME=/opt/IBM/WebSphere/AppServer/java

HADOOP_HOME变量我们可以不设置,默认的情况是指定HADOOP_HOME为运行启动脚本当前目录的父目录.

8.修改hadoop启动配置:
参照默认配置文件/home/hadoop/hadoop-0.18.2/conf/hadoop-default.xml 修改用户配置文件/home/hadoop/hadoop-0.18.2/conf/hadoop-site.xml,hadoop启动的时候会加载默认配置文件,然后读取用户配置文件并使用用户配置文件中的属性替换默认配置文件中的值,这里最简单的情况我们只需要修改如下几项即可,如果需要做分布的话也是要在这个文件里面进行配置.将要修改的配置项简单的放到hadoop-site.xml文件的中去:

fs.default.name

hdfs://localhost:9000/

mapred.job.tracker

localhost:9001

9.格式化nodename及启动hdfs守护进程:

/home/hadoop/hadoop-0.18.2/bin/hadoop namenode -format

/home/hadoop/hadoop-0.18.2/bin/start-all.sh

使用shart-all.sh可以方便的启动所有的hdfs守护进程,如果想关闭这些守护进程可以使用stop-all.sh脚本.
启动过程中需要输入登录密码.
启动成功后可以用以下简单方法测试hdfs:

/home/hadoop/hadoop-0.18.2/bin/hadoop dfs -mkdir dir4test

/home/hadoop/hadoop-0.18.2/bin/hadoop dfs -ls

/home/hadoop/hadoop-0.18.2/bin/hadoop dfs -put /home/hadoop/file4test.zip file4test_temp.zip

相当于linux系统下的mkdir ls cp命令.
用浏览器访问 http://localhost:50030/ 和 http://localhost:50070/ 可以查看hdfs拓扑结构和job进程还有hdfs文件系统结构.

10.解压hbase发行包:

tar zxvf hbase-0.18.1.tar.gz

11.修改hbase环境脚本:
修改文件/home/hadoop/hbase-0.18.1/conf/hbase-env.sh,在其中加入JAVA_HOME变量:

export JAVA_HOME=/opt/IBM/WebSphere/AppServer/java

简单启动暂时不需要在用户配置文件/home/hadoop/hbase-0.18.1/conf/hbase-site.xml添加任何替代属性.

12.启动hbase:

/home/hadoop/hbase-0.18.1/bin/start-hbase.sh

成功启动hbase的守护进程.
启动hbase hql shell:

/home/hadoop/hbase-0.18.1/bin/hbase shell

在hql shell中可以进行hbase数据操作,如果需要帮助信息可以键入:

hbase>help

简单测试hbase:
在hbase shell下:

hbase>create 't1','f1','f3'

hbase>list

使用浏览器访问 http://localhost:60010/ 可以查看当前hbase信息.

启动hbase REST服务:

/home/hadoop/hbase-0.18.1/bin/hbase rest start

成功启动hbase REST服务后就可以通过对uri: http://localhost:60050/api/ 的通用REST操作(GET/POST/PUT/DELETE)实现对hbase的REST形式数据操作.

To be continue… 

七、 Cassandra 和 HBase 中使用的 BigTable 模型

 

众所周知,BigTable是NoSQL数据库的王者,其论文更是NoSQL理论的基石,但遗憾的是BigTable不开源,于是有了开源的BigTable版本这一说法,其中的佼佼者包括今天提到的两位:Cassandra和HBase。

本文为翻译文章,翻译均为意译,仅挑选重点部分进行大意翻译。详尽的意思及全文翻译请看原文。

英文原文链接:http://horicky.blogspot.com/2010/10/bigtable-model-with-cassandra-and-hbase.html

中文原文链接:http://lgone.com/html/y2010/812.html

本文主要对Cassandra和HBase特性和实现中对BigTable理论的应用。

1.Fundamentally Distributed(分布式存储)

项目从最初规划上,就是为海量数据服务的,当然分布式存储的思想也是扎根于其血脉中。分布式系统主要需要考虑两个方面:partitioning(分区存储,也可以理解为通常说的Sharding)、replication(数据复制,主要是将数据复制成多份以提高可用性)。

 

2.Column Oriented(列式存储

和普通的RDBMS不一样,普通的RDBMS通常是行式存储的,一行数据是连续存在一段磁盘空间上的。而列式存储是将各个列分别进行连续的存储。也正是因此,它对于处理字段中的NULL字段,能够不占用过多的空间。同时能够支持灵活松散的列定义。也就是我们通常所说的schema-less。

 

3.Sequential write(顺序写磁盘)

BigTable型系统的一个特点是其对写性能进行的优化。它的写都是通过先记一条操作日志,然后直接写在内存中的数据集合,然后其集合按条件或定时将数据flush到磁盘。这里涉及到的记操作日志或者数据flush到磁盘都会顺序的磁盘操作。故而避免了磁盘随机操作造成的无谓的磁盘寻道时间。

4.Merged read(读操作数据合并)

上面说到写操作是通过定时将数据直接flush到磁盘进行的,每次flush都会生成一个数据块,那可能造成一个数据在多个数据块中的情况,而在读的时候就需要将这多个版本中的值进行合并。其中在判断一个数据块是否包含指定值时使用了bloom-filter算法。

5.Periodic Data Compaction(定期数据合并)

同样是上面说到的,一个数据可能存在于多个数据块,如果我们不做处理,随着时间的推移,数据块会越来越多。所以BigTable型系统会进行定时的数据合并。在上面讲到的将内存中的数据直接flush到磁盘的过程中,flush之前进行了一次数据的排序操作,既是说存在磁盘中的块中的数据,都是顺序的,那么对一堆顺序的数据进行排重合并,其实和我们熟知的多路归并排序很相似。故而其定时数据合并的效率也是非常高的。

 

接下来的部分是关于标题中的两个产品Cassandra和HBase在这些理论上的具体实践和修改。暂时就不翻译了。有兴趣的同学可以查看英文原文。 

八、 HBase 文件结构图

 

HFile 是 HBase 的数据文件结构,下图是对HFile 的数据组织结构描述,是理解 HBase 数据存储的绝佳教材。

图片来源及解释:http://th30z.blogspot.com/2011/02/hbase-io-hfile.html

 

相关文章两篇:

1.Hadoop 数据类型与文件结构剖析 Sequence, Map, Set, Array, BloomMap Files

2.图形化理解 HBase 数据写操作、压缩操作过程 

九、 图形化理解 HBase 数据写操作、压缩操作过程

 

HBase 写数据的过程是:先写到内存中(memstore),当内存中的数据达到一定大小,将内存中的数据一次性flush到磁盘上形成数据文件。期间对每一次写操作,都会记一个持久化的日志。那些 flush 到磁盘上的文件,会定时进行压缩。下图形像地说明这一系列过程:

 

原文链接:http://goo.gl/jwKdj

最近还有两篇相关的文章,讲述 Hadoop 及 HBase 的数据文件内部结构的,一并推荐在这里:

1.Hadoop 数据类型与文件结构剖析 Sequence, Map, Set, Array, BloomMap Files

2.HBase 文件结构图 

十、 Facebook为何选择了Hadoop和HBase

前段时间Facebook的新版消息系统发布,Facebook对HBase的成功使用也使得超来超多的HBase案例得以出现,下文是Facebook的Hadoop工程师Dhruba Borthakur同学发表的Hadoop系列文章中的一篇,对Facebook选择Hadoop和HBase的原因做了一个说明。

原文链接:Realtime Hadoop usage at Facebook — Part 1

· 横向扩展性强。对Facebook这种海量数据存储场景来说,扩容几乎是家常便饭,HBase能够使数据扩容非常容易。

· 支持很高的写吞吐。Facebook的消息数据很庞大,每天的写量也很大。

· 在同一个数据同中心,能够保证有较强的一致性。Facebook用HBase来存储消息数据,业务上需要一个能够保证一致性的数据存储(这也是Facebook并没有采用Cassandra的原因之一["We found Cassandra's eventual consistency model to be a difficult pattern to reconcile for our new Messages infrastructure" from quora])

· 有良好的随机读性能。消息系统的业务逻辑导致会有很多穿透缓存层的随机读操作。

· 高可用性,故障可恢复性。由于数据量大,分布的机器也可能很多,出故障或者进行一些日常升级工作会比较频繁,需要能够有很高可用性的系统。

· 错误隔离性。一个结点的错误不会影响到其它结点,磁盘故障只会对相应的小规模的数据产生影响。

· 提供原子性的read-modify-write操作。原子性的increment或者对比后修改的操作,对很多业务上的处理非常方便。

· 提供获取某个范围的数据的功能。比如像获取某人最近100条消息这样的功能,在消息系统里是很常见的需求。

当然,下面几个Hadoop和HBase不太擅长的方面也值得一说:

· 同一个数据中心网络割裂下的容灾性。同一个数据中心的网络出现问题了,导致各结点之间无法正常沟通,这种情况通常可以通过配置一些备用的网络设备来避免。

· 某个数据中心故障不会影响服务。这个情况更是少之又少。

· 在多个数据中心间的实时数据交换。这个不太现实,通常这一点是用Cache层来实现用户对无端数据的实时访问的。

十一、 关于HBase的一些零碎事

 

 

随着Facebook使用HBase来构建实时消息系统,基于Hadoop的面向列存储的HBase持续升温。

目前稳定版本的HBase0.90.2只能基于Hadoop0.20.x系列版本,暂不支持最新的0.21.x。而且官方版本的Hadoop0.20.2(或者0.203.0)缺少一个重要的特性,HDFS不支持sync模式的持久,这样HBase就有较大的丢失数据的风险。要在生产环境使用HBase,有两个选择,一是使用Cloudera的CDH3版本,Cloudera就类似MySQL的Percona,对官方版本的Hadoop做了很多改进工作,而且经典的《Hadoop:The Definitive Guide》一书的作者Tom White就是Cloudera的一员,这也和《High performance MySQL》一书的作者主要来是Percona一样。另外一种选择,就是自行编译Hadoop branch-0.20-append源码分支,这里有详细的说明

对于HBase这种类似BigTable的系统,其优化之一是消除了磁盘的随机写。付出的代价是将最新的数据保存在内存表中,对内存有较大的需求。如果内存表的数量较多,则每个内存表就会在较小的时候刷到磁盘,导致磁盘文件多而且小。范围读取数据的时候就会跨多个数据文件甚至多个节点。为提升读性能,系统都会设计有compaction操作。另外为了防止某些情况下数据文件过大(hbase.hregion.max.filesize,默认256M,太大的数据文件在compaction等操作是对内存的消耗更大),HBase也设计了split操作。Compaction和Split操作,对于在线应用的响应时间都容易造成波动,他们的策略需要根据应用的特性进行调整。建议在业务低峰期手工调整。

HBase的regionserver宕机超过一定时间后,HMaster会将其所管理的region重新分布到其他存活的regionserver,由于数据和日志都持久在HDFS中,因此该操作不会导致数据丢失。但是重新分配的region需要根据日志恢复原regionserver中的内存表,这会导致宕机的region在这段时间内无法对外提供服务。而一旦重分布,宕机的节点起来后就相当于一个新的regionserver加入集群,为了平衡,需要再次将某些region分布到该server。 因此这个超时建议根据情况进行调整,一般情况下,宕机重启后即可恢复,如果重启需要10分钟,region重分布加恢复的时间要超过5分钟,那么还不如等节点重启。Region Server的内存表memstore如何在节点间做到更高的可用,是HBase的一个较大的挑战。Oceanbase也是采用内存表保持最新的更新数据,和HBase不同的是,Oceanbase使用的是集中的UpdateServer,只需要全力做好UpdateServer的容灾切换即可对业务连续性做到最小影响。分布还是集中,哪些功能分布,哪些功能集中,各自取不同平衡,是目前大部分分布式数据库或者存储的一个主要区别。当然,像Cassandra这种全分布的,架构上看起来很完美,实际应用起来反而问题更多。

对于java应用,线上运维最大的挑战之一就是heap内存管理。GC的不同方式,以及使用内存表和cache对内存的消耗,可能导致局部阻塞应用或者stop the world全局阻塞或者OOM。因此HBase的很多参数设置都是针对这两种情况。HBase使用了较新的CMS GC(-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode)。

默认触发GC的时机是当年老代内存达到90%的时候,这个百分比由 -XX:CMSInitiatingOccupancyFraction=N 这个参数来设置。concurrent mode failed发生在这样一个场景:
当年老代内存达到90%的时候,CMS开始进行并发垃圾收集,于此同时,新生代还在迅速不断地晋升对象到年老代。当年老代CMS还未完成并发标记时,年老代满了,悲剧就发生了。CMS因为没内存可用不得不暂停mark,并触发一次全jvm的stop the world(挂起所有线程),然后采用单线程拷贝方式清理所有垃圾对象。这个过程会非常漫长。为了避免出现concurrent mode failed,我们应该让GC在未到90%时,就触发。

通过设置 -XX:CMSInitiatingOccupancyFraction=N

这个百分比, 可以简单的这么计算。如果你的 hfile.block.cache.size 和 hbase.regionserver.global.memstore.upperLimit 加起来有60%(默认),那么你可以设置 70-80,一般高10%左右差不多。

(以上CMS GC的说明引自HBase性能调优

目前关于HBase的书不多,《Hadoop: The Definitive Guide》第二版有一章,另外最权威的要算官方的这本电子书

十二、 HBase性能调优

 

我们经常看到一些文章吹嘘某产品如何如何快,如何如何强,而自己测试时却不如描述的一些数据。其实原因可能在于你还不是真正理解其内部结构,对于其性能调优方法不够了解。本文转自TaoBao的Ken Wu同学的博客,是目前看到比较完整的HBase调优文章。

原文链接:HBase性能调优

官方Book Performance Tuning部分章节没有按配置项进行索引,不能达到快速查阅的效果。所以我以配置项驱动,重新整理了原文,并补充一些自己的理解,如有错误,欢迎指正。

配置优化

zookeeper.session.timeout

默认值:3分钟(180000ms)

说明:RegionServer与Zookeeper间的连接超时时间。当超时时间到后,ReigonServer会被Zookeeper从RS集群清单中移除,HMaster收到移除通知后,会对这台server负责的regions重新balance,让其他存活的RegionServer接管.

调优:这个timeout决定了RegionServer是否能够及时的failover。设置成1分钟或更低,可以减少因等待超时而被延长的failover时间。

不过需要注意的是,对于一些Online应用,RegionServer从宕机到恢复时间本身就很短的(网络闪断,crash等故障,运维可快速介入),如果调低timeout时间,反而会得不偿失。因为当ReigonServer被正式从RS集群中移除时,HMaster就开始做balance了(让其他RS根据故障机器记录的WAL日志进行恢复)。当故障的RS在人工介入恢复后,这个balance动作是毫无意义的,反而会使负载不均匀,给RS带来更多负担。特别是那些固定分配regions的场景。

hbase.regionserver.handler.count

默认值:10

说明:RegionServer的请求处理IO线程数。

调优:这个参数的调优与内存息息相关。

较少的IO线程,适用于处理单次请求内存消耗较高的Big PUT场景(大容量单次PUT或设置了较大cache的scan,均属于Big PUT)或ReigonServer的内存比较紧张的场景。
较多的IO线程,适用于单次请求内存消耗低,TPS要求非常高的场景。设置该值的时候,以监控内存为主要参考。

这里需要注意的是如果server的region数量很少,大量的请求都落在一个region上,因快速充满memstore触发flush导致的读写锁会影响全局TPS,不是IO线程数越高越好。

压测时,开启Enabling RPC-level logging,可以同时监控每次请求的内存消耗和GC的状况,最后通过多次压测结果来合理调节IO线程数。

这里是一个案例 Hadoop and HBase Optimization for Read Intensive Search Applications,作者在SSD的机器上设置IO线程数为100,仅供参考。

hbase.hregion.max.filesize

默认值:256M

说明:在当前ReigonServer上单个Reigon的最大存储空间,单个Region超过该值时,这个Region会被自动split成更小的region。

调优:小region对split和compaction友好,因为拆分region或compact小region里的storefile速度很快,内存占用低。缺点是split和compaction会很频繁。

特别是数量较多的小region不停地split, compaction,会导致集群响应时间波动很大,region数量太多不仅给管理上带来麻烦,甚至会引发一些Hbase的bug。
一般512以下的都算小region。

大region,则不太适合经常split和compaction,因为做一次compact和split会产生较长时间的停顿,对应用的读写性能冲击非常大。此外,大region意味着较大的storefile,compaction时对内存也是一个挑战。
当然,大region也有其用武之地。如果你的应用场景中,某个时间点的访问量较低,那么在此时做compact和split,既能顺利完成split和compaction,又能保证绝大多数时间平稳的读写性能。

既然split和compaction如此影响性能,有没有办法去掉?
compaction是无法避免的,split倒是可以从自动调整为手动。
只要通过将这个参数值调大到某个很难达到的值,比如100G,就可以间接禁用自动split(RegionServer不会对未到达100G的region做split)。
再配合RegionSplitter这个工具,在需要split时,手动split。
手动split在灵活性和稳定性上比起自动split要高很多,相反,管理成本增加不多,比较推荐online实时系统使用。

内存方面,小region在设置memstore的大小值上比较灵活,大region则过大过小都不行,过大会导致flush时app的IO wait增高,过小则因store file过多影响读性能。

hbase.regionserver.global.memstore.upperLimit/lowerLimit

默认值:0.4/0.35

upperlimit说明:hbase.hregion.memstore.flush.size 这个参数的作用是 当单个memstore达到指定值时,flush该memstore。但是,一台ReigonServer可能有成百上千个memstore,每个memstore也许未达到flush.size,jvm的heap就不够用了。该参数就是为了限制memstores占用的总内存。
当ReigonServer内所有的memstore所占用的内存总和达到heap的40%时,HBase会强制block所有的更新并flush这些memstore以释放所有memstore占用的内存。

lowerLimit说明: 同upperLimit,只不过当全局memstore的内存达到35%时,它不会flush所有的memstore,它会找一些内存占用较大的memstore,做个别flush,当然更新还是会被block。lowerLimit算是一个在全局flush导致性能暴跌前的补救措施。为什么说是性能暴跌?可以想象一下,如果memstore需要在一段较长的时间内做全量flush,且这段时间内无法接受任何读写请求,对HBase集群的性能影响是很大的。

调优:这是一个Heap内存保护参数,默认值已经能适用大多数场景。它的调整一般是为了配合某些专属优化,比如读密集型应用,将读缓存开大,降低该值,腾出更多内存给其他模块使用。
这个参数会给使用者带来什么影响?

比如,10G内存,100个region,每个memstore 64M,假设每个region只有一个memstore,那么当100个memstore平均占用到50%左右时,就会达到lowerLimit的限制。假设此时,其他memstore同样有很多的写请求进来。在那些大的region未flush完,就可能又超过了upperlimit,则所有region都会被block,开始触发全局flush。

不过,除了你的内存非常小或你的应用场景里大多数都是读,我觉得不需要去调这个参数。

hfile.block.cache.size

默认值:0.2

说明:storefile的读缓存占用Heap的大小百分比,0.2表示20%。该值直接影响数据读的性能。

调优:当然是越大越好,如果读比写少,开到0.4-0.5也没问题。如果读写较均衡,0.3左右。如果写比读多,果断默认吧。设置这个值的时候,你同时要参考 hbase.regionserver.global.memstore.upperLimit ,该值是memstore占heap的最大百分比,两个参数一个影响读,一个影响写。如果两值加起来超过80-90%,会有OOM的风险,谨慎设置。

hbase.hstore.blockingStoreFiles

默认值:7

说明:在compaction时,如果一个Store(Coulmn Family)内有超过7个storefile需要合并,则block所有的写请求,进行flush,限制storefile数量增长过快。

调优:block写请求会影响当前region的性能,将值设为单个region可以支撑的最大store file数量会是个不错的选择,即允许comapction时,memstore继续生成storefile。最大storefile数量可通过region size/memstore size来计算。如果你将region size设为无限大,那么你需要预估一个region可能产生的最大storefile数。

hbase.hregion.memstore.block.multiplier

默认值:2

说明:当一个region里的memstore超过单个memstore.size两倍的大小时,block该region的所有请求,进行flush,释放内存。虽然我们设置了memstore的总大小,比如64M,但想象一下,在最后63.9M的时候,我Put了一个100M的数据,此时memstore的大小会瞬间暴涨到超过预期的memstore.size。这个参数的作用是当memstore的大小增至超过memstore.size时,block所有请求,遏制风险进一步扩大。

调优: 这个参数的默认值还是比较靠谱的。如果你预估你的正常应用场景(不包括异常)不会出现突发写或写的量可控,那么保持默认值即可。如果正常情况下,你的写请求量就会经常暴长到正常的几倍,那么你应该调大这个倍数并调整其他参数值,比如hfile.block.cache.size和hbase.regionserver.global.memstore.upperLimit/lowerLimit,以预留更多内存,防止HBase server OOM。

其他

启用LZO压缩

LZO对比Hbase默认的GZip,前者性能较高,后者压缩比较高,具体参见 Using LZO Compression。对于想提高HBase读写性能的开发者,采用LZO是比较好的选择。对于非常在乎存储空间的开发者,则建议保持默认。

不要在一张表里定义太多的Column Family

Hbase目前不能良好的处理超过包含2-3个CF的表。因为某个CF在flush发生时,它邻近的CF也会因关联效应被触发flush,最终导致系统产生更多IO。

批量导入

在批量导入数据到Hbase前,你可以通过预先创建regions,来平衡数据的负载。详见 Table Creation: Pre-Creating Regions

避免CMS concurrent mode failure

HBase使用CMS GC。默认触发GC的时机是当年老代内存达到90%的时候,这个百分比由 -XX:CMSInitiatingOccupancyFraction=N 这个参数来设置。concurrent mode failed发生在这样一个场景:

当年老代内存达到90%的时候,CMS开始进行并发垃圾收集,于此同时,新生代还在迅速不断地晋升对象到年老代。当年老代CMS还未完成并发标记时,年老代满了,悲剧就发生了。CMS因为没内存可用不得不暂停mark,并触发一次全jvm的stop the world(挂起所有线程),然后采用单线程拷贝方式清理所有垃圾对象。这个过程会非常漫长。为了避免出现concurrent mode failed,我们应该让GC在未到90%时,就触发。

通过设置 -XX:CMSInitiatingOccupancyFraction=N

这个百分比, 可以简单的这么计算。如果你的 hfile.block.cache.size 和 hbase.regionserver.global.memstore.upperLimit 加起来有60%(默认),那么你可以设置 70-80,一般高10%左右差不多。

Hbase客户端优化

AutoFlush

HTable的setAutoFlush设为false,可以支持客户端批量更新。即当Put填满客户端flush缓存时,才发送到服务端。默认是true。

Scan Caching

scanner一次缓存多少数据来scan(从服务端一次抓多少数据回来scan)。
默认值是 1,一次只取一条。

Scan Attribute Selection

scan时建议指定需要的Column Family,减少通信量,否则scan操作默认会返回整个row的所有数据(所有Coulmn Family)。

Close ResultScanners

通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)。

Optimal Loading of Row Keys

当你scan一张表的时候,返回结果只需要row key(不需要CF, qualifier,values,timestaps)时,你可以在scan实例中添加一个filterList,并设置 MUST_PASS_ALL操作,filterList中add FirstKeyOnlyFilterKeyOnlyFilter。这样可以减少网络通信量。

Turn off WAL on Puts

当Put某些非重要数据时,你可以设置writeToWAL(false),来进一步提高写性能。writeToWAL(false)会在Put时放弃写WAL log。风险是,当RegionServer宕机时,可能你刚才Put的那些数据会丢失,且无法恢复。

启用Bloom Filter

Bloom Filter通过空间换时间,提高读操作性能

十三、 关于HFile的思考

 

 

 

本文是一篇转载文章,原文作者郭鹏(@逖靖寒),国内Cassandra领域的先驱者和实践者。资深软件开发工程师,擅长分布式应用程序的开发和使用,实践经验极其丰富。在本文中,作者推荐了HFile文件格式的经典论文,并对HFile的block size的应用进行了实例探讨。

 

0.90.x版本的HBase中的文件是存储在HFile中的。

关于HFile文件的详细介绍,可以查看这篇文章:hfile.pdf

这篇文章中介绍了以下五点内容:

· HFile的作用。

· HFile的格式。

· HFile的性能。

· HFile的使用注意事项。

· HFile的编程接口。

HFile中有一个很重要的参数,那就是block size。如果我们写入hfile中的某一个value的值大于block size会怎么样?

于是有如下的测试代码:

// create local file system

FileSystem fs = new RawLocalFileSystem();

fs.setConf(new Configuration());

 

// block size = 1kb

HFile.Writer hwriter = new HFile.Writer(fs,

        new Path("hfile"), 1, (Compression.Algorithm) null, null);

 

// create key & value, the value is 8kb, larger than 1kb

byte[] key = "www.data-works.org".getBytes();

byte[] value = new byte[8 * 1024];

for (int i = 0; i < 8 * 1024; i++) {

    value[i] = '0';

}

 

// add values to hfile

for (int i = 0; i < 10; i++) {

    hwriter.append(key, value);

}

 

// close hfile

hwriter.close();

上面的代码可以看出来,每一个value的值都是8kb,已经超过了hfile预设的1kb的block size。

实际的写入情况是如果value大于block size,那么就按照实际的情况来写。

上面的测试用例执行完毕以后,整个hile文件只有1个data block。

这个hfile的读取代码如下:

// create local file system

FileSystem fs = new RawLocalFileSystem();

fs.initialize(URI.create("file:///"), new Configuration());

fs.setConf(new Configuration());

HFile.Reader hreader = new HFile.Reader(fs,

new Path("hfile"), null, false);

 

// loadFileInfo

hreader.loadFileInfo();

 

HFileScanner hscanner = hreader.getScanner(false, false);

 

// seek to the start position of the hfile.

hscanner.seekTo();

 

// print values.

int index = 1;

while (hscanner.next()) {

System.out.println("index: " + index++);

System.out.println("key: " + hscanner.getKeyString());

System.out.println("value: " + hscanner.getValueString());

}

 

// close hfile.

hreader.close();

上面的代码将读取hfile,并将这个文件中的所有kv打印出来。

通过上面的测试可以看出:如果某一个key有非常非常多的value,那么查找这些value就无法通过索引去快速查找,而是需要通过遍历进行。

另外,JIRA上面的HBASE-3857也提出了一种新的HFile格式,HFile v2

他主要是针对现有HFile的两个主要缺陷提出来的:

· 暂用过多内存

· 启动加载时间缓慢

有兴趣的朋友可以详细了解一下。

十四、 HBase性能优化方法总结

 

1. 表的设计

1.1 Pre-Creating Regions

默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡。

有关预分区,详情参见:Table Creation: Pre-Creating Regions,下面是一个例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public static boolean createTable(HBaseAdmin 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; 

}

1.2 Row Key

HBase中row key用来检索表中的记录,支持以下三种方式:

· 通过单个row key访问:即按照某个row key键值进行get操作;

· 通过row key的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描;

· 全表扫描:即直接扫描整张表中所有行记录。

在HBase中,row key可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,存为byte[]字节数组,一般设计成定长的。

row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。

举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为row key的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE – timestamp作为row key,这样能保证新写入的数据在读取时可以被快速命中。

1.3 Column Family

不要在一张表里定义太多的column family。目前Hbase并不能很好的处理超过2~3个column family的表。因为某个column family在flush的时候,它邻近的column family也会因关联效应被触发flush,最终导致系统产生更多的I/O。感兴趣的同学可以对自己的HBase集群进行实际测试,从得到的测试结果数据验证一下。

1.4 In Memory

创建表的时候,可以通过HColumnDescriptor.setInMemory(true)将表放到RegionServer的缓存中,保证在读取的时候被cache命中。

1.5 Max Version

创建表的时候,可以通过HColumnDescriptor.setMaxVersions(int maxVersions)设置表中数据的最大版本,如果只需要保存最新版本的数据,那么可以设置setMaxVersions(1)。

1.6 Time To Live

创建表的时候,可以通过HColumnDescriptor.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置setTimeToLive(2 * 24 * 60 * 60)。

1.7 Compact & Split

在HBase中,数据在更新时首先写入WAL 日志(HLog)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时, 系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了(minor compact)。

StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行分割(split),等分为两个StoreFile。

由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,通常合并过程还是比较快的。

实际应用中,可以考虑必要时手动进行major compact,将同一个row key的修改进行合并形成一个大的StoreFile。同时,可以将StoreFile设置大些,减少split的发生。

2. 写表操作

2.1 多HTable并发写

创建多个HTable客户端用于写操作,提高写数据的吞吐量,一个例子:

1

2

3

4

5

6

7

8

static final Configuration conf = HBaseConfiguration.create(); 

static final String table_log_name = “user_log”; 

wTableLog = new HTable[tableN]; 

for (int i = 0; i < tableN; i++) { 

    wTableLog[i] = new HTable(conf, table_log_name); 

    wTableLog[i].setWriteBufferSize(5 * 1024 * 1024); //5MB 

    wTableLog[i].setAutoFlush(false); 

}

2.2 HTable参数设置

2.2.1 Auto Flush

通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。

2.2.2 Write Buffer

通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。

2.2.3 WAL Flag

在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复。

因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能。

值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。

2.3 批量写

通过调用HTable.put(Put)方法可以将一个指定的row key记录写入HBase,同样HBase提供了另一个方法:通过调用HTable.put(List<Put>)方法可以将指定的row key列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高,网络传输RTT高的情景下可能带来明显的性能提升。

2.4 多线程并发写

在客户端开启多个HTable写线程,每个写线程负责一个HTable对象的flush操作,这样结合定时flush和写buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被flush(如1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush。下面给个具体的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

for (int i = 0; i < threadN; i++) { 

    Thread th = new Thread() { 

        public void run() { 

            while (true) { 

                try { 

                    sleep(1000); //1 second 

                } catch (InterruptedException e) { 

                    e.printStackTrace(); 

                } 

                                synchronized (wTableLog[i]) { 

                    try { 

                        wTableLog[i].flushCommits(); 

                    } catch (IOException e) { 

                        e.printStackTrace(); 

                    } 

                } 

            } 

                } 

    }; 

    th.setDaemon(true); 

    th.start(); 

}

3. 读表操作

3.1 多HTable并发读

创建多个HTable客户端用于读操作,提高读数据的吞吐量,一个例子:

1

2

3

4

5

6

7

static final Configuration conf = HBaseConfiguration.create(); 

static final String table_log_name = “user_log”; 

rTableLog = new HTable[tableN]; 

for (int i = 0; i < tableN; i++) { 

    rTableLog[i] = new HTable(conf, table_log_name); 

    rTableLog[i].setScannerCaching(50); 

}

3.2 HTable参数设置

3.2.1 Scanner Caching

通过调用HTable.setScannerCaching(int scannerCaching)可以设置HBase scanner一次从服务端抓取的数据条数,默认情况下一次一条。通过将此值设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户端的内存来维持这些被cache的行记录。

3.2.2 Scan Attribute Selection

scan时指定需要的Column Family,可以减少网络传输数据量,否则默认scan操作会返回整行所有Column Family的数据。

3.2.3 Close ResultScanner

通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)。

3.3 批量读

通过调用HTable.get(Get)方法可以根据一个指定的row key获取一行记录,同样HBase提供了另一个方法:通过调用HTable.get(List)方法可以根据一个指定的row key列表,批量获取多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高而且网络传输RTT高的情景下可能带来明显的性能提升。

3.4 多线程并发读

在客户端开启多个HTable读线程,每个读线程负责通过HTable对象进行get操作。下面是一个多线程并发读取HBase,获取店铺一天内各分钟PV值的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

public class DataReaderServer { 

     //获取店铺一天内各分钟PV值的入口函数 

     public static ConcurrentHashMap getUnitMinutePV(long uid, long startStamp, long endStamp){ 

         long min = startStamp; 

         int count = (int)((endStamp - startStamp) / (60*1000)); 

         List lst = new ArrayList(); 

         for (int i = 0; i <= count; i++) { 

            min = startStamp + i * 60 * 1000; 

            lst.add(uid + "_" + min); 

         } 

         return parallelBatchMinutePV(lst); 

     } 

      //多线程并发查询,获取分钟PV值 

private static ConcurrentHashMap parallelBatchMinutePV(List lstKeys){ 

        ConcurrentHashMap hashRet = new ConcurrentHashMap(); 

        int parallel = 3; 

        List<List<String>> lstBatchKeys  = null; 

        if (lstKeys.size() < parallel ){ 

            lstBatchKeys  = new ArrayList<List<String>>(1); 

            lstBatchKeys.add(lstKeys); 

        } 

        else{ 

            lstBatchKeys  = new ArrayList<List<String>>(parallel); 

            for(int i = 0; i < parallel; i++  ){ 

                List lst = new ArrayList(); 

                lstBatchKeys.add(lst); 

            } 

  

            for(int i = 0 ; i < lstKeys.size() ; i ++ ){ 

                lstBatchKeys.get(i%parallel).add(lstKeys.get(i)); 

            } 

        } 

  

        List >> futures = new ArrayList >>(5); 

  

        ThreadFactoryBuilder builder = new ThreadFactoryBuilder(); 

        builder.setNameFormat("ParallelBatchQuery"); 

        ThreadFactory factory = builder.build(); 

        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(lstBatchKeys.size(), factory); 

  

        for(List keys : lstBatchKeys){ 

            Callable< ConcurrentHashMap > callable = new BatchMinutePVCallable(keys); 

            FutureTask< ConcurrentHashMap > future = (FutureTask< ConcurrentHashMap >) executor.submit(callable); 

            futures.add(future); 

        } 

        executor.shutdown(); 

  

        // Wait for all the tasks to finish 

        try { 

          boolean stillRunning = !executor.awaitTermination( 

              5000000, TimeUnit.MILLISECONDS); 

          if (stillRunning) { 

            try { 

                executor.shutdownNow(); 

            } catch (Exception e) { 

                // TODO Auto-generated catch block 

                e.printStackTrace(); 

            } 

          } 

        } catch (InterruptedException e) { 

          try { 

              Thread.currentThread().interrupt(); 

          } catch (Exception e1) { 

            // TODO Auto-generated catch block 

            e1.printStackTrace(); 

          } 

        } 

  

        // Look for any exception 

        for (Future f : futures) { 

          try { 

              if(f.get() != null) 

              { 

                  hashRet.putAll((ConcurrentHashMap)f.get()); 

              } 

          } catch (InterruptedException e) { 

            try { 

                 Thread.currentThread().interrupt(); 

            } catch (Exception e1) { 

                // TODO Auto-generated catch block 

                e1.printStackTrace(); 

            } 

          } catch (ExecutionException e) { 

            e.printStackTrace(); 

          } 

        } 

  

        return hashRet; 

    } 

     //一个线程批量查询,获取分钟PV值 

    protected static ConcurrentHashMap getBatchMinutePV(List lstKeys){ 

        ConcurrentHashMap hashRet = null; 

        List lstGet = new ArrayList(); 

        String[] splitValue = null; 

        for (String s : lstKeys) { 

            splitValue = s.split("_"); 

            long uid = Long.parseLong(splitValue[0]); 

            long min = Long.parseLong(splitValue[1]); 

            byte[] key = new byte[16]; 

            Bytes.putLong(key, 0, uid); 

            Bytes.putLong(key, 8, min); 

            Get g = new Get(key); 

            g.addFamily(fp); 

            lstGet.add(g); 

        } 

        Result[] res = null; 

        try { 

            res = tableMinutePV[rand.nextInt(tableN)].get(lstGet); 

        } catch (IOException e1) { 

            logger.error("tableMinutePV exception, e=" + e1.getStackTrace()); 

        } 

  

        if (res != null && res.length > 0) { 

            hashRet = new ConcurrentHashMap(res.length); 

            for (Result re : res) { 

                if (re != null && !re.isEmpty()) { 

                    try { 

                        byte[] key = re.getRow(); 

                        byte[] value = re.getValue(fp, cp); 

                        if (key != null && value != null) { 

                            hashRet.put(String.valueOf(Bytes.toLong(key, 

                                    Bytes.SIZEOF_LONG)), String.valueOf(Bytes 

                                    .toLong(value))); 

                        } 

                    } catch (Exception e2) { 

                        logger.error(e2.getStackTrace()); 

                    } 

                } 

            } 

        } 

  

        return hashRet; 

    } 

//调用接口类,实现Callable接口 

class BatchMinutePVCallable implements Callable>{ 

     private List keys; 

  

     public BatchMinutePVCallable(List lstKeys ) { 

         this.keys = lstKeys; 

     } 

  

     public ConcurrentHashMap call() throws Exception { 

         return DataReadServer.getBatchMinutePV(keys); 

     } 

}

3.5 缓存查询结果

对于频繁查询HBase的应用场景,可以考虑在应用程序中做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询HBase;否则对HBase发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑LRU等常用的策略。

3.6 Blockcache

HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。

写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。当Memstore的总大小超过限制时(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),会强行启动flush进程,从最大的Memstore开始flush直到低于限制。

读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略,因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。

一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize * 0.8,否则HBase不能启动。默认BlockCache为0.2,而Memstore为0.4。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。

有关BlockCache机制,请参考这里:HBase的Block cache,HBase的blockcache机制,hbase中的缓存的计算与使用。

4.数据计算

4.1 服务端计算

Coprocessor运行于HBase RegionServer服务端,各个Regions保持对与其相关的coprocessor实现类的引用,coprocessor类可以通过RegionServer上classpath中的本地jar或HDFS的classloader进行加载。

目前,已提供有几种coprocessor:

Coprocessor:提供对于region管理的钩子,例如region的open/close/split/flush/compact等;
RegionObserver:提供用于从客户端监控表相关操作的钩子,例如表的get/put/scan/delete等;
Endpoint:提供可以在region上执行任意函数的命令触发器。一个使用例子是RegionServer端的列聚合,这里有代码示例。
以上只是有关coprocessor的一些基本介绍,本人没有对其实际使用的经验,对它的可用性和性能数据不得而知。感兴趣的同学可以尝试一下,欢迎讨论。

4.2 写端计算

4.2.1 计数

HBase本身可以看作是一个可以水平扩展的Key-Value存储系统,但是其本身的计算能力有限(Coprocessor可以提供一定的服务端计算),因此,使用HBase时,往往需要从写端或者读端进行计算,然后将最终的计算结果返回给调用者。举两个简单的例子:

PV计算:通过在HBase写端内存中,累加计数,维护PV值的更新,同时为了做到持久化,定期(如1秒)将PV计算结果同步到HBase中,这样查询端最多会有1秒钟的延迟,能看到秒级延迟的PV结果。
分钟PV计算:与上面提到的PV计算方法相结合,每分钟将当前的累计PV值,按照rowkey + minute作为新的rowkey写入HBase中,然后在查询端通过scan得到当天各个分钟以前的累计PV值,然后顺次将前后两分钟的累计PV值相减,就得到了当前一分钟内的PV值,从而最终也就得到当天各个分钟内的PV值。

4.2.2 去重

对于UV的计算,就是个去重计算的例子。分两种情况:

如果内存可以容纳,那么可以在Hash表中维护所有已经存在的UV标识,每当新来一个标识时,通过快速查找Hash确定是否是一个新的UV,若是则UV值加1,否则UV值不变。另外,为了做到持久化或提供给查询接口使用,可以定期(如1秒)将UV计算结果同步到HBase中。
如果内存不能容纳,可以考虑采用Bloom Filter来实现,从而尽可能的减少内存的占用情况。除了UV的计算外,判断URL是否存在也是个典型的应用场景。

4.3 读端计算

如果对于响应时间要求比较苛刻的情况(如单次http请求要在毫秒级时间内返回),个人觉得读端不宜做过多复杂的计算逻辑,尽量做到读端功能单一化:即从HBase RegionServer读到数据(scan或get方式)后,按照数据格式进行简单的拼接,直接返回给前端使用。当然,如果对于响应时间要求一般,或者业务特点需要,也可以在读端进行一些计算逻辑。

5.总结

作为一个Key-Value存储系统,HBase并不是万能的,它有自己独特的地方。因此,基于它来做应用时,我们往往需要从多方面进行优化改进(表设计、读表操作、写表操作、数据计算等),有时甚至还需要从系统级对HBase进行配置调优,更甚至可以对HBase本身进行优化。这属于不同的层次范畴。

总之,概括来讲,对系统进行优化时,首先定位到影响你的程序运行性能的瓶颈之处,然后有的放矢进行针对行的优化。如果优化后满足你的期望,那么就可以停止优化;否则继续寻找新的瓶颈之处,开始新的优化,直到满足性能要求。

以上就是从项目开发中总结的一点经验,如有不对之处,欢迎大家不吝赐教。

十五、 HBase 系统架构

 

HBaseApache Hadoop的数据库,能够对大型数据提供随机、实时的读写访问。HBase的目标是存储并处理大型的数据。HBase是一个开源的,分布式的,多版本的,面向列的存储模型。它存储的是松散型数据。

HBase特性:

高可靠性

高效性

面向列

可伸缩

可在廉价PC Server搭建大规模结构化存储集群

HBaseGoogle BigTable的开源实现,其相互对应如下:

Google            HBase
文件存储系统      GFS              HDFS
海量数据处理      MapReduce Hadoop MapReduce
协同服务管理    Chubby Zookeeper

 

HBase关系图:

 

HBase位于结构化存储层,围绕HBase,各部件对HBase的支持情况:
Hadoop部件            作用
HDFS              高可靠的底层存储支持
MapReduce   高性能的计算能力
Zookeeper            稳定服务和failover机制
Pig&Hive 高层语言支持,便于数据统计
Sqoop 提供RDBMS数据导入,便于传统数据库向HBase迁移

访问HBase的接口

方式            特点              场合
Native Java API      最常规和高效            Hadoop MapReduce Job并行处理HBase表数据
HBase Shell         最简单接口             HBase管理使用
Thrift Gateway      利用Thrift序列化支持多种语言     异构系统在线访问HBase表数据
Rest Gateway 解除语言限制            Rest风格Http API访问
PigPig Latin六十编程语言处理数据   数据统计
Hive 简单,SqlLike

HBase 数据模型

 

组成部件说明:

Row Key:     Table主键 行键 Table中记录按照Row Key排序
Timestamp     每次对数据操作对应的时间戳,也即数据的version number
Column Family:  列簇,一个table在水平方向有一个或者多个列簇,列簇可由任意多个Column组成,列簇支持动态扩展,无须预定义数量及类型,二进制存储,用户需自行进行类型转换

Table&Region

 

1. Table随着记录增多不断变大,会自动分裂成多份Splits,成为Regions
2. 一个region[startkeyendkey)表示
3. 不同region会被Master分配给相应的RegionServer进行管理

两张特殊表:-ROOT- & .META.

 

.META.   记录用户表的Region信息,同时,.META.也可以有多个region
-ROOT-   记录.META.表的Region信息,但是,-ROOT-只有一个region
Zookeeper中记录了-ROOT-表的location
客户端访问数据的流程:
Client -> Zookeeper -> -ROOT- -> .META. -> 用户数据表
多次网络操作,不过client端有cache缓存

HBase 系统架构图

 

组成部件说明
Client
使用HBase RPC机制与HMasterHRegionServer进行通信
ClientHMaster进行通信进行管理类操作
ClientHRegionServer进行数据读写类操作

Zookeeper
Zookeeper Quorum存储-ROOT-表地址、HMaster地址
HRegionServer把自己以Ephedral方式注册到Zookeeper中,HMaster随时感知各个HRegionServer的健康状况
Zookeeper避免HMaster单点问题

HMaster
HMaster没有单点问题,HBase中可以启动多个HMaster,通过ZookeeperMaster Election机制保证总有一个Master在运行
主要负责TableRegion的管理工作:
管理用户对表的增删改查操作
管理HRegionServer的负载均衡,调整Region分布
3 Region Split后,负责新Region的分布
HRegionServer停机后,负责失效HRegionServerRegion迁移

HRegionServer
HBase中最核心的模块,主要负责响应用户I/O请求,向HDFS文件系统中读写数据

 


HRegionServer管理一些列HRegion对象;
每个HRegion对应Table中一个RegionHRegion由多个HStore组成;
每个HStore对应Table中一个Column Family的存储;
Column Family就是一个集中的存储单元,故将具有相同IO特性的Column放在一个Column Family会更高效

HStore
HBase存储的核心。由MemStoreStoreFile组成。
MemStoreSorted Memory Buffer。用户写入数据的流程:

 


Client写入 -> 存入MemStore,一直到MemStore满 -> Flush成一个StoreFile,直至增长到一定阈值 -> 出发Compact合并操作 -> 多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除 -> StoreFiles Compact后,逐步形成越来越大的StoreFile -> 单个StoreFile大小超过一定阈值后,触发Split操作,把当前Region Split2RegionRegion会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer上,使得原先1Region的压力得以分流到2Region
由此过程可知,HBase只是增加数据,有所得更新和删除操作,都是在Compact阶段做的,所以,用户写操作只需要进入到内存即可立即返回,从而保证I/O高性能。

HLog
引入HLog原因:
在分布式系统环境中,无法避免系统出错或者宕机,一旦HRegionServer以外退出,MemStore中的内存数据就会丢失,引入HLog就是防止这种情况
工作机制:
每个HRegionServer中都会有一个HLog对象,HLog是一个实现Write Ahead Log的类,每次用户操作写入Memstore的同时,也会写一份数据到HLog文件,HLog文件定期会滚动出新,并删除旧的文件(已持久化到StoreFile中的数据)。当HRegionServer意外终止后,HMaster会通过Zookeeper感知,HMaster首先处理遗留的HLog文件,将不同regionlog数据拆分,分别放到相应region目录下,然后再将失效的region重新分配,领取到这些regionHRegionServerLoad Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到MemStore中,然后flushStoreFiles,完成数据恢复。

HBase存储格式
HBase中的所有数据文件都存储在Hadoop HDFS文件系统上,格式主要有两种:
1 HFile HBaseKeyValue数据的存储格式,HFileHadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile
2 HLog FileHBaseWALWrite Ahead Log) 的存储格式,物理上是HadoopSequence File

HFile

 


图片解释:
HFile文件不定长,长度固定的块只有两个:TrailerFileInfo
Trailer中指针指向其他数据块的起始点
File Info中记录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY
Data IndexMeta Index块记录了每个Data块和Meta块的起始点
Data BlockHBase I/O的基本单元,为了提高效率,HRegionServer中有基于LRUBlock Cache机制
每个Data块的大小可以在创建一个Table的时候通过参数指定,大号的Block有利于顺序Scan,小号Block利于随机查询
每个Data块除了开头的Magic以外就是一个个KeyValue对拼接而成, Magic内容就是一些随机数字,目的是防止数据损坏

HFile里面的每个KeyValue对就是一个简单的byte数组。这个byte数组里面包含了很多项,并且有固定的结构。

 


KeyLengthValueLength:两个固定的长度,分别代表KeyValue的长度
Key部分:Row Length是固定长度的数值,表示RowKey的长度,Row 就是RowKey
Column Family Length是固定长度的数值,表示Family的长度
接着就是Column Family,再接着是Qualifier,然后是两个固定长度的数值,表示Time StampKey TypePut/Delete
Value部分没有这么复杂的结构,就是纯粹的二进制数据

HLog File

 


HLog文件就是一个普通的Hadoop Sequence FileSequence File KeyHLogKey对象,HLogKey中记录了写入数据的归属信息,除了tableregion名字外,同时还包括 sequence numbertimestamptimestamp写入时间sequence number的起始值为0,或者是最近一次存入文件系统中sequence number
HLog Sequece FileValueHBaseKeyValue对象,即对应HFile中的KeyValue

 

 

十六、 HBase Java客户端编程

 

本文以HBase 0.90.2为例,介绍如何在Windows系统,Eclipse IDE集成环境下,使用Java语言,进行HBase客户端编程,包含建立表、删除表、插入记录、删除记录、各种方式下的查询操作等。 

1. 准备工作

1、下载后安装jdk包(这里使用的是jdk-6u10-rc2-bin-b32-windows-i586-p-12_sep_2008);

2、下载eclipse,解压到本地(这里使用的是eclipse-java-helios-SR2-win32);

3、下载HBase包,解压安装包到本地(这里使用的是hbase-0.90.2)。

2. 搭建开发环境

1、运行Eclipse,创建一个新的Java工程“HBaseClient”,右键项目根目录,选择“Properties”->“Java Build Path”->“Library”->“Add External JARs”,将HBase解压后根目录下的hbase-0.90.2.jar、hbase-0.90.2-tests.jar和lib子目录下所有jar包添加到本工程的Classpath下,如图1所示。

图1 Eclilse IDE下添加HBase的Jar包

2、按照步骤1中的操作,将自己所连接的HBase的配置文件hbase-site.xml添加到本工程的Classpath中,如下所示为配置文件的一个示例。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

<configuration> 

<property> 

<name>hbase.rootdir</name> 

<value>hdfs://hostname:9000/hbase</value> 

</property> 

<property> 

<name>hbase.cluster.distributed</name> 

<value>true</value> 

</property> 

<property> 

<name>hbase.zookeeper.quorum</name> 

<value>*.*.*.*, *.*.*.*, *.*.*.*</value> 

</property> 

<property skipInDoc="true"> 

<name>hbase.defaults.for.version</name> 

<value>0.90.2</value> 

</property> 

</configuration>

3、下面可以在Eclipse环境下进行HBase编程了。

3. HBase基本操作代码示例

3.1 初始化配置

1

2

3

4

5

6

7

private static Configuration conf = null; 

/** 

 * 初始化配置 

 */

static { 

    conf = HBaseConfiguration.create(); 

}

3.2 创建表

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/** 

 * 创建表操作 

 * @throws IOException 

 */

public void createTable(String tablename, String[] cfs) throws IOException { 

    HBaseAdmin admin = new HBaseAdmin(conf); 

    if (admin.tableExists(tablename)) { 

        System.out.println("表已经存在!"); 

    } 

    else { 

        HTableDescriptor tableDesc = new HTableDescriptor(tablename); 

        for (int i = 0; i < cfs.length; i++) { 

            tableDesc.addFamily(new HColumnDescriptor(cfs[i])); 

        } 

        admin.createTable(tableDesc); 

        System.out.println("表创建成功!"); 

    } 

}

3.3 删除表

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

/** 

 * 删除表操作 

 * @param tablename 

 * @throws IOException 

 */

public void deleteTable(String tablename) throws IOException { 

    try { 

        HBaseAdmin admin = new HBaseAdmin(conf); 

        admin.disableTable(tablename); 

        admin.deleteTable(tablename); 

        System.out.println("表删除成功!"); 

    } catch (MasterNotRunningException e) { 

        e.printStackTrace(); 

    } catch (ZooKeeperConnectionException e) { 

        e.printStackTrace(); 

    } 

}

3.4 插入一行记录

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

/** 

 * 插入一行记录 

 * @param tablename 

 * @param cfs 

 */

public void writeRow(String tablename, String[] cfs) { 

    try { 

        HTable table = new HTable(conf, tablename); 

        Put put = new Put(Bytes.toBytes("rows1")); 

        for (int j = 0; j < cfs.length; j++) { 

            put.add(Bytes.toBytes(cfs[j]), 

                    Bytes.toBytes(String.valueOf(1)), 

                    Bytes.toBytes("value_1")); 

            table.put(put); 

        } 

    } catch (IOException e) { 

        e.printStackTrace(); 

    } 

}

3.5 删除一行记录

1

2

3

4

5

6

7

8

9

10

11

12

13

14

/** 

 * 删除一行记录 

 * @param tablename 

 * @param rowkey 

 * @throws IOException 

 */

public void deleteRow(String tablename, String rowkey) throws IOException { 

    HTable table = new HTable(conf, tablename); 

    List list = new ArrayList(); 

    Delete d1 = new Delete(rowkey.getBytes()); 

    list.add(d1); 

    table.delete(list); 

    System.out.println("删除行成功!"); 

}

3.6 查找一行记录

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/** 

 * 查找一行记录 

 * @param tablename 

 * @param rowkey 

 */

public static void selectRow(String tablename, String rowKey) 

        throws IOException { 

    HTable table = new HTable(conf, tablename); 

    Get g = new Get(rowKey.getBytes()); 

    Result rs = table.get(g); 

    for (KeyValue kv : rs.raw()) { 

        System.out.print(new String(kv.getRow()) + "  "); 

        System.out.print(new String(kv.getFamily()) + ":"); 

        System.out.print(new String(kv.getQualifier()) + "  "); 

        System.out.print(kv.getTimestamp() + "  "); 

        System.out.println(new String(kv.getValue())); 

    } 

}

3.7 查询表中所有行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

/** 

 * 查询表中所有行 

 * @param tablename 

 */

public void scaner(String tablename) { 

    try { 

        HTable table = new HTable(conf, tablename); 

        Scan s = new Scan(); 

        ResultScanner rs = table.getScanner(s); 

        for (Result r : rs) { 

            KeyValue[] kv = r.raw(); 

            for (int i = 0; i < kv.length; i++) { 

                System.out.print(new String(kv[i].getRow()) + "  "); 

                System.out.print(new String(kv[i].getFamily()) + ":"); 

                System.out.print(new String(kv[i].getQualifier()) + "  "); 

                System.out.print(kv[i].getTimestamp() + "  "); 

                System.out.println(new String(kv[i].getValue())); 

            } 

        } 

    } catch (IOException e) { 

        e.printStackTrace(); 

    } 

}

 

 

十七、 HBase如何合理设置客户端Write Buffer

HBase客户端API提供了Write Buffer的方式,即批量提交一批Put对象到HBase服务端。本文将结合HBase相关源码,对其进行深入介绍,分析如何在实际项目中合理设置和使用它。

什么时候需要Write Buffer?

默认情况下,一次Put操作即要与Region Server执行一次RPC操作,其执行过程可以被拆分为以下三个部分:

· T1:RTT(Round-Trip Time),即网络往返时延,它指从客户端发送数据开始,到客户端收到来自服务端的确认,总共经历的时延,不包括数据传输的时间;

· T2:数据传输时间,即Put所操作的数据在客户端与服务端之间传输所消耗的时间开销,当数据量大的时候,T2的开销不容忽略;

· T3:服务端处理时间,对于Put操作,即写入WAL日志(如果设置了WAL标识为true)、更新MemStore等。

其中,T2和T3都是不可避免的时间开销,那么能不能减少T1呢?假设我们将多次Put操作打包起来一次性提交到服务端,则可以将T1部分的总时间从T1 * N降低为T1,其中T1为一次RTT时间,N为Put的记录条数。

正是出于上述考虑,HBase为用户提供了客户端缓存批量提交的方式(即Write Buffer)。假设RTT的时间较长,如1ms,则该种方式能够显著提高整个集群的写入性能。

那么,什么场景下适用于该种模式呢?下面简单分析一下:

· 如果Put提交的是小数据(如KB级别甚至更小)记录,那么T2很小,因此,通过该种模式减少T1的开销,能够明显提高写入性能。

· 如果Put提交的是大数据(如MB级别)记录,那么T2可能已经远大于T1,此时T1与T2相比可以被忽略,因此,使用该种模式并不能得到很好的性能提升,不建议通过增大Write Buffer大小来使用该种模式。

如何配置使用Write Buffer?

如果要启动Write Buffer模式,则调用HTable的以下API将auto flush设置为false:

void setAutoFlush(boolean autoFlush)

默认配置下,Write Buffer大小为2MB,可以根据应用实际情况,通过以下任意方式进行自定义:

1)  调用HTable接口设置,仅对该HTable对象起作用:

void setWriteBufferSize(long writeBufferSize) throws IOException

2)  在hbase-site.xml中配置,所有HTable都生效(下面设置为5MB):

<property>

<name>hbase.client.write.buffer</name>

<value>5242880</value>

</property>

该种模式下向服务端提交的时机分为显式和隐式两种情况:

1)  显式提交:用户调用flushCommits()进行提交;

2)  隐式提交:当Write Buffer满了,客户端会自动执行提交;或者调用了HTable的close()方法时无条件执行提交操作。

如何确定每次flushCommits()时实际的RPC次数?

客户端提交后,所有的Put操作可能涉及不同的行,然后客户端负责将这些Put对象根据row key按照 region server分组,再按region server打包后提交到region server,每个region server做一次RPC请求。如下图所示:

 

如何确定每次flushCommits()时提交的记录条数?

下面我们先从HBase存储原理层面“粗略”分析下HBase中的一条Put记录格式:

HBase中Put对象的大小主要由若干个KeyValue对的大小决定(Put继承自org/apache/hadoop/hbase/client/Mutation.java,具体见Mutation的代码所示),而KeyValue类中自带的字段占用约50~60 bytes(参考源码:org/apache/hadoop/hbase/KeyValue.java),那么客户端Put一行数据时,假设column qualifier个数为N,row key长度为L1 bytes,value总长度为L2 bytes,则该Put对象占用大小可按以下公式预估:

Put Size = ((50~60) + L1) * N + L2) bytes

下面我们通过对HBase的源码分析来进一步验证以上理论估算值:

HBase客户端执行put操作后,会调用put.heapSize()累加当前客户端buffer中的数据,满足以下条件则调用flushCommits()将客户端数据提交到服务端:

1)每次put方法调用时可能传入的是一个List<Put>,此时每隔DOPUT_WB_CHECK条(默认为10条),检查当前缓存数据是否超过writeBufferSize,超过则强制执行刷新;

2)autoFlush被设置为true,此次put方法调用后执行一次刷新;

3)autoFlush被设置为false,但当前缓存数据已超过设定的writeBufferSize,则执行刷新。

         private void doPut(final List<Put> puts) throws IOException {

                   int n = 0;

                   for (Put put : puts) {

                            validatePut(put);

                            writeBuffer.add(put);

                            currentWriteBufferSize += put.heapSize();

                            // we need to periodically see if the writebuffer is full instead

                            // of waiting until the end of the List

                            n++;

                            if (n % DOPUT_WB_CHECK == 0

                                               && currentWriteBufferSize > writeBufferSize) {

                                     flushCommits();

                            }

                   }

                   if (autoFlush || currentWriteBufferSize > writeBufferSize) {

                            flushCommits();

                   }

         }

由上述代码可见,通过put.heapSize()累加客户端的缓存数据,作为判断的依据;那么,我们可以编写一个简单的程序生成Put对象,调用其heapSize()方法,就能得到一行数据实际占用的客户端缓存大小(该程序需要传递上述三个变量:N,L1,L2作为参数):

import org.apache.hadoop.hbase.client.Put;

import org.apache.hadoop.hbase.util.Bytes;

public class PutHeapSize {

         /**

          * @param args

          */

         public static void main(String[] args) {

                   if (args.length != 3) {

                            System.out.println(“Invalid number of parameters: 3 parameters!”);

                            System.exit(1);

                   }

                   int N = Integer.parseInt(args[0]);

                   int L1 = Integer.parseInt(args[1]);

                   int L2 = Integer.parseInt(args[2]);

                   byte[] rowKey = new byte[L1];

                   byte[] value = null;

                   Put put = new Put(rowKey);

                   for (int i = 0; i < N; i++) {

                            put.add(Bytes.toBytes(“cf”), Bytes.toBytes(“c” + i), value);

                   }

                   System.out.println(“Put Size: ” + (put.heapSize() + L2) + ” bytes”);

         }

}

该程序可以用来预估当前设置的Write Buffer可以一次性批量提交的记录数:

Puts Per Commit = Write Buffer Size / Put Size

更进一步地,如果知道业务中的每秒产生的数据量,就可知道客户端大概多长时间会隐式调用flushCommits()向服务端提交一次,同时也可反过来根据数据实时刷新频率调整Write Buffer大小。

Write Buffer有什么潜在的问题?

首先,Write Buffer存在于客户端的本地内存中,那么当客户端运行出现问题时,会导致在Write Buffer中未提交的数据丢失;由于HBase服务端还未收到这些数据,因此也无法通过WAL日志等方式进行数据恢复。

其次,Write Buffer方式本身会占用客户端和HBase服务端的内存开销,具体见下节的详细分析。

如何预估Write Buffer占用的内存?

客户端通过Write Buffer方式提交的话,会导致客户端和服务端均有一定的额外内存开销,Write Buffer Size越大,则占用的内存越大。客户端占用的内存开销可以粗略地使用以下公式预估:

hbase.client.write.buffer * number of HTable object for writing

而对于服务端来说,可以使用以下公式预估占用的Region Server总内存开销:

hbase.client.write.buffer * hbase.regionserver.handler.count * number of region server

其中,hbase.regionserver.handler.count为每个Region Server上配置的RPC Handler线程数。

十八、 HBase Block Cache实现机制分析

 

 1. 概述

HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。

· 写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。当Memstore的总大小超过限制时(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),会强行启动flush进程,从最大的Memstore开始flush直到低于限制。

· 读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略,因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。

一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize * 0.8,否则HBase不能正常启动。

默认配置下,BlockCache为0.2,而Memstore为0.4。在注重读响应时间的应用场景下,可以将 BlockCache设置大些,Memstore设置小些,以加大缓存的命中率。

HBase RegionServer包含三个级别的Block优先级队列:

· Single:如果一个Block第一次被访问,则放在这一优先级队列中;

· Multi:如果一个Block被多次访问,则从Single队列移到Multi队列中;

· InMemory:如果一个Block是inMemory的,则放到这个队列中。

以上将Cache分级思想的好处在于:

· 首先,通过inMemory类型Cache,可以有选择地将in-memory的column families放到RegionServer内存中,例如Meta元数据信息;

· 通过区分Single和Multi类型Cache,可以防止由于Scan操作带来的Cache频繁颠簸,将最少使用的Block加入到淘汰算法中。

默认配置下,对于整个BlockCache的内存,又按照以下百分比分配给Single、Multi、InMemory使用:0.25、0.50和0.25。

注意,其中InMemory队列用于保存HBase Meta表元数据信息,因此如果将数据量很大的用户表设置为InMemory的话,可能会导致Meta表缓存失效,进而对整个集群的性能产生影响。

2. 源码分析

下面是对HBase 0.94.1中相关源码(org.apache.hadoop.hbase.io.hfile.LruBlockCache)的分析过程。

2.1加入Block Cache

  /** Concurrent map (the cache) */

  private final ConcurrentHashMap<BlockCacheKey,CachedBlock> map;

  /**

   * Cache the block with the specified name and buffer.

   * <p>

   * It is assumed this will NEVER be called on an already cached block.  If

   * that is done, an exception will be thrown.

   * @param cacheKey block’s cache key

   * @param buf block buffer

   * @param inMemory if block is in-memory

   */

  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {

    CachedBlock cb = map.get(cacheKey);

    if(cb != null) {

      throw new RuntimeException(“Cached an already cached block”);

    }

    cb = new CachedBlock(cacheKey, buf, count.incrementAndGet(), inMemory);

    long newSize = updateSizeMetrics(cb, false);

    map.put(cacheKey, cb);

    elements.incrementAndGet();

    if(newSize > acceptableSize() && !evictionInProgress) {

      runEviction();

    }

  }

  /**

   * Cache the block with the specified name and buffer.

   * <p>

   * It is assumed this will NEVER be called on an already cached block.  If

   * that is done, it is assumed that you are reinserting the same exact

   * block due to a race condition and will update the buffer but not modify

   * the size of the cache.

   * @param cacheKey block’s cache key

   * @param buf block buffer

   */

  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf) {

    cacheBlock(cacheKey, buf, false);

  }

1)  这里假设不会对同一个已经被缓存的BlockCacheKey重复放入cache操作;

2)  根据inMemory标志创建不同类别的CachedBlock对象:若inMemory为true则创建BlockPriority.MEMORY类型,否则创建BlockPriority.SINGLE;注意,这里只有这两种类型的Cache,因为BlockPriority.MULTI在Cache Block被重复访问时才进行创建,见CachedBlock的access方法代码:

  /**

   * Block has been accessed.  Update its local access time.

   */

  public void access(long accessTime) {

    this.accessTime = accessTime;

    if(this.priority == BlockPriority.SINGLE) {

      this.priority = BlockPriority.MULTI;

    }

  }

3)  将BlockCacheKey和创建的CachedBlock对象加入到全局的ConcurrentHashMap map中,同时做一些更新计数操作;

4)  最后判断如果加入后的Block Size大于设定的临界值且当前没有淘汰线程运行,则调用runEviction()方法启动LRU淘汰过程:

  /** Eviction thread */

  private final EvictionThread evictionThread;

  /**

   * Multi-threaded call to run the eviction process.

   */

  private void runEviction() {

    if(evictionThread == null) {

      evict();

    } else {

      evictionThread.evict();

    }

  }

其中,EvictionThread线程即是LRU淘汰的具体实现线程。下面将给出详细分析。

2.2淘汰Block Cache

EvictionThread线程主要用于与主线程的同步,从而完成Block Cache的LRU淘汰过程。

  /*

   * Eviction thread.  Sits in waiting state until an eviction is triggered

   * when the cache size grows above the acceptable level.<p>

   *

   * Thread is triggered into action by {@link LruBlockCache#runEviction()}

   */

  private static class EvictionThread extends HasThread {

    private WeakReference<LruBlockCache> cache;

    private boolean go = true;

    public EvictionThread(LruBlockCache cache) {

      super(Thread.currentThread().getName() + “.LruBlockCache.EvictionThread”);

      setDaemon(true);

      this.cache = new WeakReference<LruBlockCache>(cache);

    }

    @Override

    public void run() {

      while (this.go) {

        synchronized(this) {

          try {

            this.wait();

          } catch(InterruptedException e) {}

        }

        LruBlockCache cache = this.cache.get();

        if(cache == null) break;

        cache.evict();

      }

    }

    public void evict() {

      synchronized(this) {

        this.notify(); // FindBugs NN_NAKED_NOTIFY

      }

    }

    void shutdown() {

      this.go = false;

      interrupt();

    }

  }

EvictionThread线程启动后,调用wait被阻塞住,直到EvictionThread线程的evict方法被主线程调用时执行notify(见上面的代码分析过程,通过主线程的runEviction方法触发调用),开始执行LruBlockCache的evict方法进行真正的淘汰过程,代码如下:

  /**

   * Eviction method.

   */

  void evict() {

    // Ensure only one eviction at a time

    if(!evictionLock.tryLock()) return;

    try {

      evictionInProgress = true;

      long currentSize = this.size.get();

      long bytesToFree = currentSize – minSize();

      if (LOG.isDebugEnabled()) {

        LOG.debug(“Block cache LRU eviction started; Attempting to free ” +

          StringUtils.byteDesc(bytesToFree) + ” of total=” +

          StringUtils.byteDesc(currentSize));

      }

      if(bytesToFree <= 0) return;

      // Instantiate priority buckets

      BlockBucket bucketSingle = new BlockBucket(bytesToFree, blockSize,

          singleSize());

      BlockBucket bucketMulti = new BlockBucket(bytesToFree, blockSize,

          multiSize());

      BlockBucket bucketMemory = new BlockBucket(bytesToFree, blockSize,

          memorySize());

      // Scan entire map putting into appropriate buckets

      for(CachedBlock cachedBlock : map.values()) {

        switch(cachedBlock.getPriority()) {

          case SINGLE: {

            bucketSingle.add(cachedBlock);

            break;

          }

          case MULTI: {

            bucketMulti.add(cachedBlock);

            break;

          }

          case MEMORY: {

            bucketMemory.add(cachedBlock);

            break;

          }

        }

      }

      PriorityQueue<BlockBucket> bucketQueue =

        new PriorityQueue<BlockBucket>(3);

      bucketQueue.add(bucketSingle);

      bucketQueue.add(bucketMulti);

      bucketQueue.add(bucketMemory);

      int remainingBuckets = 3;

      long bytesFreed = 0;

      BlockBucket bucket;

      while((bucket = bucketQueue.poll()) != null) {

        long overflow = bucket.overflow();

        if(overflow > 0) {

          long bucketBytesToFree = Math.min(overflow,

            (bytesToFree – bytesFreed) / remainingBuckets);

          bytesFreed += bucket.free(bucketBytesToFree);

        }

        remainingBuckets–;

      }

      if (LOG.isDebugEnabled()) {

        long single = bucketSingle.totalSize();

        long multi = bucketMulti.totalSize();

        long memory = bucketMemory.totalSize();

        LOG.debug(“Block cache LRU eviction completed; ” +

          “freed=” + StringUtils.byteDesc(bytesFreed) + “, ” +

          “total=” + StringUtils.byteDesc(this.size.get()) + “, ” +

          “single=” + StringUtils.byteDesc(single) + “, ” +

          “multi=” + StringUtils.byteDesc(multi) + “, ” +

          “memory=” + StringUtils.byteDesc(memory));

      }

    } finally {

      stats.evict();

      evictionInProgress = false;

      evictionLock.unlock();

    }

  }

1)首先获取锁,保证同一时刻只有一个淘汰线程运行;

2)计算得到当前Block Cache总大小currentSize及需要被淘汰释放掉的大小bytesToFree,如果bytesToFree小于等于0则不进行后续操作;

3) 初始化创建三个BlockBucket队列,分别用于存放Single、Multi和InMemory类Block Cache,其中每个BlockBucket维护了一个CachedBlockQueue,按LRU淘汰算法维护该BlockBucket中的所有CachedBlock对象;

4) 遍历记录所有Block Cache的全局ConcurrentHashMap,加入到相应的BlockBucket队列中;

5) 将以上三个BlockBucket队列加入到一个优先级队列中,按照各个BlockBucket超出bucketSize的大小顺序排序(见BlockBucket的compareTo方法);

6) 遍历优先级队列,对于每个BlockBucket,通过Math.min(overflow, (bytesToFree – bytesFreed) / remainingBuckets)计算出需要释放的空间大小,这样做可以保证尽可能平均地从三个BlockBucket中释放指定的空间;具体实现过程详见BlockBucket的free方法,从其CachedBlockQueue中取出即将被淘汰掉的CachedBlock对象:

    public long free(long toFree) {

      CachedBlock cb;

      long freedBytes = 0;

      while ((cb = queue.pollLast()) != null) {

        freedBytes += evictBlock(cb);

        if (freedBytes >= toFree) {

          return freedBytes;

        }

      }

      return freedBytes;

    }

7) 进一步调用了LruBlockCache的evictBlock方法,从全局ConcurrentHashMap中移除该CachedBlock对象,同时更新相关计数:

  protected long evictBlock(CachedBlock block) {

    map.remove(block.getCacheKey());

    updateSizeMetrics(block, true);

    elements.decrementAndGet();

    stats.evicted();

    return block.heapSize();

  }

8) 释放锁,完成善后工作。

3. 总结

以上关于Block Cache的实现机制,核心思想是将Cache分级,这样的好处是避免Cache之间相互影响,尤其是对HBase来说像Meta表这样的Cache应该保证高优先级。

 

十九、 HBase解决Region Server Compact过程占用大量网络出口带宽的问题

 

HBase 0.92版本之后,Region Server的Compact过程根据待合并的文件大小分为small compaction和large compaction两种,由此可能导致在集群写入量大的时候Compact占用过多的网络出口带宽。本文将详细描述集群使用过程中遇到这一问题的排查过程及其解决方法。

1. 发现问题

HBase集群(版本为0.94.0)运行过程中,发现5台Region Server的网络出口带宽经常维持在100MB/s以上,接近到网卡的极限;同时Region Server的机器load负载也很高,高峰时候能够达到30~50。

2. 排查问题

1、集群实际运行过程中,观察到Region Server服务端的网卡,平均每台写入流量大概60MB/s(此时写入量已经很大了);读出流量90MB/s,有时甚至突破100MB/s(注:每台机器都是千兆网卡);

2、观察实际的写入数据量在每秒5w tps左右,单条记录平均大小为1KB,大概会占用50MB/s左右的网卡入口带宽请求量,和观察到的现象一致;

3、观察查询量在每秒6w qps左右,单条记录平均大小为1KB,大概会占用60MB/s左右的网卡出口带宽请求量,奇怪的是实际观察到有接近甚至超过100MB/s的网络出口带宽请求量,多出了40MB/s左右的网络出口带宽;

4、经分析排查确定导致上述过程的原因,可能是HBase服务端由于写入量过大频繁触发compaction过程,而compaction是需要读HBase数据的,因此占据了相当部分的网络出口带宽;

5、结合对相关源码org/apache/hadoop/hbase/regionserver/CompactSplitThread.java的分析,决定对HBase集群配置做出变更(具体见下一小节),主要目的是减少compaction的发生;

6、接下来,观察到Region Server的网络利用率明显降低,一般进出口带宽能维持在70MB/s以下。

3. 解决问题

HBase 0.92版本之后增加了关于compact的配置选项,compact分为small compaction和large compaction两个线程池执行(默认都是各有1个线程,具体源代码见:org/apache/hadoop/hbase/regionserver /CompactSplitThread.java),由于compact过程需要从HBase集群读取数据,因此实际运行中导致了compact占用大 量网络出口流量,解决方案为选择性地关闭small compaction或large compaction。有以下两种变更方式均可解决:

1)方案一

(1) 修改hbase.regionserver.thread.compaction.throttle为一个很大的值(如50GB),强制让所有compact都变为small compaction,减少compact的压力;

(2) 将small compaction和large compaction线程数均设置为1,减少compact的压力(可不配置,系统默认也会将其初始化为1)。

操作步骤:

准备hbase-site.xml文件,添加或修改如下选项:

<property>

    <name>hbase.regionserver.thread.compaction.throttle</name>

    <value>53687091200</value>

</property>

<property>

    <name>hbase.regionserver.thread.compaction.small</name>

    <value>1</value>

</property>

<property>

    <name>hbase.regionserver.thread.compaction.large</name>

    <value>1</value>

</property>

重启集群使配置生效。

2)方案二

将small compaction线程数均设置为0,从而关闭small compaction,只剩下large compaction,也可减少compact的压力。

操作步骤:

准备hbase-site.xml文件,添加或修改如下选项:

<property>

    <name>hbase.regionserver.thread.compaction.small</name>

    <value>0</value>

</property>

重启集群使配置生效。

二十、 HBase集群出现NotServingRegionException问题的排查及解决方法

 

HBase集群在读写过程中,可能由于Region Split或Region Blance等导致Region的短暂下线,此时客户端与HBase集群进行RPC操作时会抛出NotServingRegionException异常,从而导致读写操作失败。这里根据实际项目经验,详细描述这一问题的发现及排查解决过程。

1. 发现问题

在对HBase集群进行压力测试过程中发现,当实际写入HBase和从HBase查询的量是平时的若干倍时(集群规模10~20台,每秒读写数据量在几十万条记录的量级),导致集群的读写出现一定程度的波动。具体如下:

1)写端抛出以下异常信息:

org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException: Failed 150 actions: NotServingRegionException: 150 times, servers with issues: my161208.cm6:60020,at org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation.processBatchCallback(HConnectionManager.java:1600)at org.apache.hadoop.hbase.client.HConnectionManager$HConnectionImplementation.processBatch(HConnectionManager.java:1376)at org.apache.hadoop.hbase.client.HTable.flushCommits(HTable.java:916)

2)读端也抛出类似异常信息:

org.apache.hadoop.hbase.client.RetriesExhaustedException: Failed after attempts=10, exceptions:Mon Oct 29 14:03:09 CST 2012, org.apache.hadoop.hbase.client.ScannerCallable@3740fb20, org.apache.hadoop.hbase.NotServingRegionException: org.apache.hadoop.hbase.NotServingRegionException: xxxxxx,\x0FP\x8D\xC3\xDB1053223266:\x00\x00V6,1351490475989.bd68113129f07163dc25e78fba17ad6c. is closing

以上异常,在压测期间周期性地出现,HBase集群由此出现了短暂的不可服务期。

2. 排查问题

通 过查看HBase Master运行日志,结合客户端抛出异常的时刻,发现当时HBase集群内正在进行Region的Split和不同机器之间的Region Balance,那么,为什么会周期性频繁触发以上过程呢?而且是发生在压测期间(数据量与平时相比大几倍)。下面结合表的设计来分析一下:

1) 由于表中rowkey有时间字段,因此每天都需要新创建Region,同时由于写入数据量大,进一步触发了HBase的Region Split操作,这一过程一般耗时较长(测试时从线上日志来看,平均为10秒左右,Region大小为4GB),且Region Split操作触发较为频繁;

2)同时由于Region Split操作导致Region分布不均匀,进而触发HBase自动做Region Balance操作,Region迁移过程中也会导致Region下线,这一过程耗时较长(测试时从线上日志来看,平均为20秒左右)。

3. 解决问题

首先,从客户端考虑,其实就是要保证Region下线不可服务期间,读写请求能够在集群恢复后继续,具体可以采取如下措施:

1) 对于写端,可以将未写入成功的记录,添加到一个客户端缓存中,隔一段时间后交给一个后台线程统一重新提交一次;也可以通过 setAutoFlush(flase, false)保证提交失败的记录不被抛弃,留在客户端writeBuffer中等待下次writeBuffer满了后再次尝试提交,直到提交成功为止。

2)对于读端,捕获异常后,可以采取休眠一段时间后进行重试等方式。

3)当然,还可以根据实际情况合理调整hbase.client.retries.number和hbase.client.pause配置选项。

然后,从服务端考虑,需要分别针对Region Split和Region Balance进行解决:

1) 由于建表时,我们已经考虑到了数据在不同Region Server上的均匀分布,而且预先在不同Region Server上创建并分配了相同数目的Region,那么考虑到为了集群能够在实际线上环境下提供稳定的服务,可以选择关掉HBase的Region自动 Balance功能,当然关掉后可以选择在每天读写压力小的时候(如凌晨后)触发执行一次Balance操作即可。

2)接下来,Region总是被创建,不能被复用的问题该如何解决呢?根本原因是rowkey中包含了timestamp字段,而每时每刻timestamp总是 向上增长的。但是,使用方确实需要能够根据timestamp字段进行顺序scan操作,因此,timestamp字段必须保留。据此,这里给出两种解决 思路:

· 一种常用方法是将表按照时间分表,例如按天进行分表,这样可以通过预先建表创建好Region分区,避免实际读写过程中频 繁触发Region Split等过程,但是这一方法的缺点是每天需要预先建好表,而这一DDL过程可能出现问题进而导致读写出现问题,同时跨天时读写端也需要做出适应,调整 为读写新创建的表。

· 其实,我们可以换一种思路,通过修改表的rowkey结构,将timestamp字段改成一个周期循环的 timestamp,如取timestamp % TS_MODE后的值,其中TS_MODE须大于等于表的TTL时间周期,这样才能保证数据不会被覆盖掉。经过这样改造后,即可实现Region的复用, 避免Region的无限上涨。对于读写端的变更也较小,读写端操作时只需将timestamp字段取模后作为rowkey进行读写,另外,读端需要考虑能 适应scan扫描时处理[startTsMode, endTsMode]和[endTsMode, startTsMode]两种情况。

4. 总结的话

以上仅是本人结合实际项目中遇到的问题进行了概括总结,仅供参考。欢迎讨论交流。

二十一、 hbase java编程实例

 

HBase提供了java api来对HBase进行一系列的管理涉及到对表的管理、数据的操作等。常用的API操作有:

  1、  对表的创建、删除、显示以及修改等,可以用HBaseAdmin,一旦创建了表,那么可以通过HTable的实例来访问表,每次可以往表里增加数据。

  2、  插入数据

    创建一个Put对象,在这个Put对象里可以指定要给哪个列增加数据,以及当前的时间戳等值,然后通过调用HTable.put(Put)来提交操作,子猴在这里提请注意的是:在创建Put对象的时候,你必须指定一个行(Row)值,在构造Put对象的时候作为参数传入。

  3、  获取数据

    要获取数据,使用Get对象,Get对象同Put对象一样有好几个构造函数,通常在构造的时候传入行值,表示取第几行的数据,通过HTable.get(Get)来调用。

  4、  浏览每一行

    通过Scan可以对表中的行进行浏览,得到每一行的信息,比如列名,时间戳等,Scan相当于一个游标,通过next()来浏览下一个,通过调用HTable.getScanner(Scan)来返回一个ResultScanner对象。HTable.get(Get)HTable.getScanner(Scan)都是返回一个ResultResult是一个

KeyValue的链表。

  5、  删除

    使用Delete来删除记录,通过调用HTable.delete(Delete)来执行删除操作。(注:删除这里有些特别,也就是删除并不是马上将数据从表中删除。)

  6、  

    新增、获取、删除在操作过程中会对所操作的行加一个锁,而浏览却不会。

  7、  簇的访问

    客户端代码通过ZooKeeper来访问找到簇,也就是说ZooKeeper quorum将被使用,那么相关的类(包)应该在客户端的类(classes)目录下,即客户端一定要找到文件hbase-site.xml

  以下是一个完整的代码示例,基于hbase-0.90.3编写(hbase的基本概念可参考我博客的前一篇文章):

?

import java.io.IOException; 

import java.util.ArrayList; 

import java.util.List; 

  

import org.apache.hadoop.conf.Configuration; 

import org.apache.hadoop.hbase.HBaseConfiguration; 

import org.apache.hadoop.hbase.HColumnDescriptor; 

import org.apache.hadoop.hbase.HTableDescriptor; 

import org.apache.hadoop.hbase.KeyValue; 

import org.apache.hadoop.hbase.MasterNotRunningException; 

import org.apache.hadoop.hbase.ZooKeeperConnectionException; 

import org.apache.hadoop.hbase.client.Delete; 

import org.apache.hadoop.hbase.client.Get; 

import org.apache.hadoop.hbase.client.HBaseAdmin; 

import org.apache.hadoop.hbase.client.HTable; 

import org.apache.hadoop.hbase.client.Result; 

import org.apache.hadoop.hbase.client.ResultScanner; 

import org.apache.hadoop.hbase.client.Scan; 

import org.apache.hadoop.hbase.client.Put; 

import org.apache.hadoop.hbase.util.Bytes; 

  

public class HBaseTest { 

     

    private static Configuration conf = null; 

      

    /** 

     * 初始化配置 

     */

    static { 

        Configuration HBASE_CONFIG = new Configuration(); 

        //与hbase/conf/hbase-site.xml中hbase.zookeeper.quorum配置的值相同   

        HBASE_CONFIG.set("hbase.zookeeper.quorum", "10.1.1.1"); 

        //与hbase/conf/hbase-site.xml中hbase.zookeeper.property.clientPort配置的值相同  

        HBASE_CONFIG.set("hbase.zookeeper.property.clientPort", "2181"); 

        conf = HBaseConfiguration.create(HBASE_CONFIG); 

    } 

     

    /** 

     * 创建一张表 

     */

    public static void creatTable(String tableName, String[] familys) throws Exception { 

        HBaseAdmin admin = new HBaseAdmin(conf); 

        if (admin.tableExists(tableName)) { 

            System.out.println("table already exists!"); 

        } else { 

            HTableDescriptor tableDesc = new HTableDescriptor(tableName); 

            for(int i=0; i<familys.length; i++){ 

                tableDesc.addFamily(new HColumnDescriptor(familys[i])); 

            } 

            admin.createTable(tableDesc); 

            System.out.println("create table " + tableName + " ok."); 

        }   

    } 

     

    /** 

     * 删除表 

     */

    public static void deleteTable(String tableName) throws Exception { 

       try { 

           HBaseAdmin admin = new HBaseAdmin(conf); 

           admin.disableTable(tableName); 

           admin.deleteTable(tableName); 

           System.out.println("delete table " + tableName + " ok."); 

       } catch (MasterNotRunningException e) { 

           e.printStackTrace(); 

       } catch (ZooKeeperConnectionException e) { 

           e.printStackTrace(); 

       } 

    } 

      

    /** 

     * 插入一行记录 

     */

    public static void addRecord (String tableName, String rowKey, String family, String qualifier, String value)  

            throws Exception{ 

        try { 

            HTable table = new HTable(conf, tableName); 

            Put put = new Put(Bytes.toBytes(rowKey)); 

            put.add(Bytes.toBytes(family),Bytes.toBytes(qualifier),Bytes.toBytes(value)); 

            table.put(put); 

            System.out.println("insert recored " + rowKey + " to table " + tableName +" ok."); 

        } catch (IOException e) { 

            e.printStackTrace(); 

        } 

    } 

  

    /** 

     * 删除一行记录 

     */

    public static void delRecord (String tableName, String rowKey) throws IOException{ 

        HTable table = new HTable(conf, tableName); 

        List list = new ArrayList(); 

        Delete del = new Delete(rowKey.getBytes()); 

        list.add(del); 

        table.delete(list); 

        System.out.println("del recored " + rowKey + " ok."); 

    } 

      

    /** 

     * 查找一行记录 

     */

    public static void getOneRecord (String tableName, String rowKey) throws IOException{ 

        HTable table = new HTable(conf, tableName); 

        Get get = new Get(rowKey.getBytes()); 

        Result rs = table.get(get); 

        for(KeyValue kv : rs.raw()){ 

            System.out.print(new String(kv.getRow()) + " " ); 

            System.out.print(new String(kv.getFamily()) + ":" ); 

            System.out.print(new String(kv.getQualifier()) + " " ); 

            System.out.print(kv.getTimestamp() + " " ); 

            System.out.println(new String(kv.getValue())); 

        } 

    } 

      

    /** 

     * 显示所有数据 

     */

    public static void getAllRecord (String tableName) { 

        try{ 

             HTable table = new HTable(conf, tableName); 

             Scan s = new Scan(); 

             ResultScanner ss = table.getScanner(s); 

             for(Result r:ss){ 

                 for(KeyValue kv : r.raw()){ 

                    System.out.print(new String(kv.getRow()) + " "); 

                    System.out.print(new String(kv.getFamily()) + ":"); 

                    System.out.print(new String(kv.getQualifier()) + " "); 

                    System.out.print(kv.getTimestamp() + " "); 

                    System.out.println(new String(kv.getValue())); 

                 } 

             } 

        } catch (IOException e){ 

            e.printStackTrace(); 

        } 

    } 

     

    public static void  main (String [] agrs) { 

        try { 

            String tablename = "scores"; 

            String[] familys = {"grade", "course"}; 

            HBaseTest.creatTable(tablename, familys); 

              

            //add record zkb 

            HBaseTest.addRecord(tablename,"zkb","grade","","5"); 

            HBaseTest.addRecord(tablename,"zkb","course","","90"); 

            HBaseTest.addRecord(tablename,"zkb","course","math","97"); 

            HBaseTest.addRecord(tablename,"zkb","course","art","87"); 

            //add record  baoniu 

            HBaseTest.addRecord(tablename,"baoniu","grade","","4"); 

            HBaseTest.addRecord(tablename,"baoniu","course","math","89"); 

              

            System.out.println("===========get one record========"); 

            HBaseTest.getOneRecord(tablename, "zkb"); 

              

            System.out.println("===========show all record========"); 

            HBaseTest.getAllRecord(tablename); 

              

            System.out.println("===========del one record========"); 

            HBaseTest.delRecord(tablename, "baoniu"); 

            HBaseTest.getAllRecord(tablename); 

              

            System.out.println("===========show all record========"); 

            HBaseTest.getAllRecord(tablename); 

        } catch (Exception e) { 

            e.printStackTrace(); 

        } 

    } 

}

  程序编译为一个jarhbtest.jar,执行脚本:

?

#!/bin/sh 

  

source ~/.bash_profile 

export HADOOP_CLASSPATH=/home/admin/hadoop/hadoop-core-0.20.2-CDH3B4.jar:/home/admin/hbase/hbase-0.90.3.jar:/home/admin/zookeeper/zookeeper-3.3.2.jar  

  

hadoop jar hbtest.jar

  输出结果是:

?

create table scores ok. 

11/09/16 00:45:39 INFO zookeeper.ZooKeeper: Initiating client connection, connectString=10.1.1.1:2181 sessionTimeout=180000 watcher=hconnection 

11/09/16 00:45:39 INFO zookeeper.ClientCnxn: Opening socket connection to server /10.1.1.1:2181

11/09/16 00:45:39 INFO zookeeper.ClientCnxn: Socket connection established to search041134.sqa.cm4.tbsite.net/10.1.1.1:2181, initiating session 

11/09/16 00:45:39 INFO zookeeper.ClientCnxn: Session establishment complete on server search041134.sqa.cm4.tbsite.net/10.1.1.1:2181, sessionid = 0x132614b5411007f, negotiated timeout = 180000

insert recored zkb to table scores ok. 

insert recored zkb to table scores ok. 

insert recored zkb to table scores ok. 

insert recored zkb to table scores ok. 

insert recored baoniu to table scores ok. 

insert recored baoniu to table scores ok. 

===========get one record======== 

zkb course: 1316105139153 90

zkb course:art 1316105139156 87

zkb course:math 1316105139154 97

zkb grade: 1316105139149 5

===========show all record======== 

baoniu course:math 1316105139159 89

baoniu grade: 1316105139158 4

zkb course: 1316105139153 90

zkb course:art 1316105139156 87

zkb course:math 1316105139154 97

zkb grade: 1316105139149 5

===========del one record======== 

del recored baoniu ok. 

zkb course: 1316105139153 90

zkb course:art 1316105139156 87

zkb course:math 1316105139154 97

zkb grade: 1316105139149 5

===========show all record======== 

zkb course: 1316105139153 90

zkb course:art 1316105139156 87

zkb course:math 1316105139154 97

zkb grade: 1316105139149 5

 

 

 

二十二、 java实现hbase表创建、数据插入、删除表 

 

近日查看了相关资料后,梳理了一下用java实现hbase的表创建、数据插入、删除表,代码如下:

1、需要的jar包:

commons-codec-1.4.jar

commons-logging-1.0.4.jar

hadoop-0.20.2-core.jar

hbase-0.20.6.jar

log4j-1.2.15.jar

zookeeper-3.2.2.jar

 

2、代码:

[java:nogutter] view plaincopyprint?

1. package org.myhbase;  

2.   

3. import java.io.IOException;  

4.   

5. import org.apache.hadoop.conf.Configuration;  

6. import org.apache.hadoop.hbase.HBaseConfiguration;  

7. import org.apache.hadoop.hbase.HColumnDescriptor;  

8. import org.apache.hadoop.hbase.HTableDescriptor;  

9. import org.apache.hadoop.hbase.KeyValue;  

10. import org.apache.hadoop.hbase.client.HBaseAdmin;  

11. import org.apache.hadoop.hbase.client.HTable;  

12. import org.apache.hadoop.hbase.client.Result;  

13. import org.apache.hadoop.hbase.client.ResultScanner;  

14. import org.apache.hadoop.hbase.client.Scan;  

15. import org.apache.hadoop.hbase.io.BatchUpdate;  

16.   

17. public class HBaseBasic02 {  

18.     static HBaseConfiguration hbaseConfig=null;  

19.     static{  

20.         Configuration config=new Configuration();  

21.         config.set("hbase.zookeeper.quorum","192.168.10.149,192.168.10.44,192.168.10.49");  

22.         config.set("hbase.zookeeper.property.clientPort""2181");  

23.         hbaseConfig=new HBaseConfiguration(config);  

24.     }  

25.       

26.     /** 

27.      * 创建一张表 

28.      * @throws IOException  

29.      */  

30.     public static void createTable(String tablename) throws IOException{  

31.         HBaseAdmin admin = new HBaseAdmin(hbaseConfig);  

32.         if(admin.tableExists(tablename)){  

33.             System.out.println("table Exists!!!");  

34.         }else{  

35.             HTableDescriptor tableDesc = new HTableDescriptor(tablename);  

36.             tableDesc.addFamily(new HColumnDescriptor("name:"));  

37.             admin.createTable(tableDesc);  

38.             System.out.println("create table ok.");  

39.         }  

40.     }  

41.       

42.     /** 

43.      * 删除一张表 

44.      * @throws IOException  

45.      */  

46.     public static void dropTable(String tablename) throws IOException{  

47.         HBaseAdmin admin = new HBaseAdmin(hbaseConfig);  

48.         admin.disableTable(tablename);  

49.         admin.deleteTable(tablename);  

50.         System.out.println("drop table ok.");  

51.     }  

52.       

53.       

54.     /** 

55.      * 添加一条数据 

56.      * @throws IOException  

57.      */  

58.     public static void addData(String tablename) throws IOException{  

59.         HTable table=new HTable(hbaseConfig,tablename);  

60.         BatchUpdate update=new BatchUpdate("Huangyi");  

61.         update.put("name:java","http://www.sun.com".getBytes());  

62.         table.commit(update);  

63.         System.out.println("add data ok.");  

64.     }  

65.       

66.       

67.     /** 

68.      * 显示所有数据 

69.      * @throws IOException  

70.      */  

71.     public static void getAllData(String tablename) throws IOException{  

72.         HTable table=new HTable(hbaseConfig,tablename);  

73.         Scan s=new Scan();  

74.         ResultScanner rs=table.getScanner(s);  

75.         for(Result r:rs){  

76.             for(KeyValue kv:r.raw()){  

77.                 System.out.println("rowkey : "+new String(kv.getRow()));  

78.                 System.out.println(new String(kv.getColumn())+" = "+new String(kv.getValue()));  

79.             }  

80.         }  

81.     }  

82.       

83.     public static void main(String [] args) throws IOException{  

84.             String tablename="table_1";  

85.             HBaseBasic02.createTable(tablename);  

86.             HBaseBasic02.addData(tablename);  

87.             HBaseBasic02.getAllData(tablename);  

88.             HBaseBasic02.dropTable(tablename);  

89.     }  

90.       

91.       

92. }  

二十三、 Java操作Hbase进行建表、删表以及对数据进行增删改查,条件查询

 

1、搭建环境

  新建JAVA项目,添加的包有:

   有关Hadoop的hadoop-core-0.20.204.0.jar

   有关Hbase的hbase-0.90.4.jar、hbase-0.90.4-tests.jar以及Hbase资源包中lib目录下的所有jar包

 

2、主要程序

 

Java代码  

1. package com.wujintao.hbase.test;   

2.   

3. import java.io.IOException;   

4. import java.util.ArrayList;   

5. import java.util.List;   

6.   

7. import org.apache.hadoop.conf.Configuration;   

8. import org.apache.hadoop.hbase.HBaseConfiguration;   

9. import org.apache.hadoop.hbase.HColumnDescriptor;   

10. import org.apache.hadoop.hbase.HTableDescriptor;   

11. import org.apache.hadoop.hbase.KeyValue;   

12. import org.apache.hadoop.hbase.MasterNotRunningException;   

13. import org.apache.hadoop.hbase.ZooKeeperConnectionException;   

14. import org.apache.hadoop.hbase.client.Delete;   

15. import org.apache.hadoop.hbase.client.Get;   

16. import org.apache.hadoop.hbase.client.HBaseAdmin;   

17. import org.apache.hadoop.hbase.client.HTable;   

18. import org.apache.hadoop.hbase.client.HTablePool;   

19. import org.apache.hadoop.hbase.client.Put;   

20. import org.apache.hadoop.hbase.client.Result;   

21. import org.apache.hadoop.hbase.client.ResultScanner;   

22. import org.apache.hadoop.hbase.client.Scan;   

23. import org.apache.hadoop.hbase.filter.Filter;   

24. import org.apache.hadoop.hbase.filter.FilterList;   

25. import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;   

26. import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;   

27. import org.apache.hadoop.hbase.util.Bytes;   

28.   

29. public class JinTaoTest {   

30.   

31.     public static Configuration configuration;   

32.     static {   

33.         configuration = HBaseConfiguration.create();   

34.         configuration.set("hbase.zookeeper.property.clientPort", "2181");   

35.         configuration.set("hbase.zookeeper.quorum", "192.168.1.100");   

36.         configuration.set("hbase.master", "192.168.1.100:600000");   

37.     }   

38.   

39.     public static void main(String[] args) {   

40.         // createTable("wujintao");   

41.         // insertData("wujintao");   

42.         // QueryAll("wujintao");   

43.         // QueryByCondition1("wujintao");   

44.         // QueryByCondition2("wujintao");   

45.         //QueryByCondition3("wujintao");   

46.         //deleteRow("wujintao","abcdef");   

47.         deleteByCondition("wujintao","abcdef");   

48.     }   

49.   

50.     /**  

51.      * 创建表  

52.      * @param tableName  

53.      */  

54.     public static void createTable(String tableName) {   

55.         System.out.println("start create table ......");   

56.         try {   

57.             HBaseAdmin hBaseAdmin = new HBaseAdmin(configuration);   

58.             if (hBaseAdmin.tableExists(tableName)) {// 如果存在要创建的表,那么先删除,再创建   

59.                 hBaseAdmin.disableTable(tableName);   

60.                 hBaseAdmin.deleteTable(tableName);   

61.                 System.out.println(tableName + " is exist,detele....");   

62.             }   

63.             HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);   

64.             tableDescriptor.addFamily(new HColumnDescriptor("column1"));   

65.             tableDescriptor.addFamily(new HColumnDescriptor("column2"));   

66.             tableDescriptor.addFamily(new HColumnDescriptor("column3"));   

67.             hBaseAdmin.createTable(tableDescriptor);   

68.         } catch (MasterNotRunningException e) {   

69.             e.printStackTrace();   

70.         } catch (ZooKeeperConnectionException e) {   

71.             e.printStackTrace();   

72.         } catch (IOException e) {   

73.             e.printStackTrace();   

74.         }   

75.         System.out.println("end create table ......");   

76.     }   

77.   

78.     /**  

79.      * 插入数据  

80.      * @param tableName  

81.      */  

82.     public static void insertData(String tableName) {   

83.         System.out.println("start insert data ......");   

84.         HTablePool pool = new HTablePool(configuration, 1000);   

85.         HTable table = (HTable) pool.getTable(tableName);   

86.         Put put = new Put("112233bbbcccc".getBytes());// 一个PUT代表一行数据,再NEW一个PUT表示第二行数据,每行一个唯一的ROWKEY,此处rowkey为put构造方法中传入的值   

87.         put.add("column1".getBytes(), null, "aaa".getBytes());// 本行数据的第一列   

88.         put.add("column2".getBytes(), null, "bbb".getBytes());// 本行数据的第三列   

89.         put.add("column3".getBytes(), null, "ccc".getBytes());// 本行数据的第三列   

90.         try {   

91.             table.put(put);   

92.         } catch (IOException e) {   

93.             e.printStackTrace();   

94.         }   

95.         System.out.println("end insert data ......");   

96.     }   

97.   

98.     /**  

99.      * 删除一张表  

100.      * @param tableName  

101.      */  

102.     public static void dropTable(String tableName) {   

103.         try {   

104.             HBaseAdmin admin = new HBaseAdmin(configuration);   

105.             admin.disableTable(tableName);   

106.             admin.deleteTable(tableName);   

107.         } catch (MasterNotRunningException e) {   

108.             e.printStackTrace();   

109.         } catch (ZooKeeperConnectionException e) {   

110.             e.printStackTrace();   

111.         } catch (IOException e) {   

112.             e.printStackTrace();   

113.         }   

114.   

115.     }   

116.     /**  

117.      * 根据 rowkey删除一条记录  

118.      * @param tablename  

119.      * @param rowkey  

120.      */  

121.      public static void deleteRow(String tablename, String rowkey)  {   

122.         try {   

123.             HTable table = new HTable(configuration, tablename);   

124.             List list = new ArrayList();   

125.             Delete d1 = new Delete(rowkey.getBytes());   

126.             list.add(d1);   

127.                

128.             table.delete(list);   

129.             System.out.println("删除行成功!");   

130.                

131.         } catch (IOException e) {   

132.             e.printStackTrace();   

133.         }   

134.            

135.   

136.     }   

137.   

138.      /**  

139.       * 组合条件删除  

140.       * @param tablename  

141.       * @param rowkey  

142.       */  

143.      public static void deleteByCondition(String tablename, String rowkey)  {   

144.             //目前还没有发现有效的API能够实现 根据非rowkey的条件删除 这个功能能,还有清空表全部数据的API操作   

145.   

146.     }   

147.   

148.   

149.     /**  

150.      * 查询所有数据  

151.      * @param tableName  

152.      */  

153.     public static void QueryAll(String tableName) {   

154.         HTablePool pool = new HTablePool(configuration, 1000);   

155.         HTable table = (HTable) pool.getTable(tableName);   

156.         try {   

157.             ResultScanner rs = table.getScanner(new Scan());   

158.             for (Result r : rs) {   

159.                 System.out.println("获得到rowkey:" + new String(r.getRow()));   

160.                 for (KeyValue keyValue : r.raw()) {   

161.                     System.out.println("列:" + new String(keyValue.getFamily())   

162.                             + "====值:" + new String(keyValue.getValue()));   

163.                 }   

164.             }   

165.         } catch (IOException e) {   

166.             e.printStackTrace();   

167.         }   

168.     }   

169.   

170.     /**  

171.      * 单条件查询,根据rowkey查询唯一一条记录  

172.      * @param tableName  

173.      */  

174.     public static void QueryByCondition1(String tableName) {   

175.   

176.         HTablePool pool = new HTablePool(configuration, 1000);   

177.         HTable table = (HTable) pool.getTable(tableName);   

178.         try {   

179.             Get scan = new Get("abcdef".getBytes());// 根据rowkey查询   

180.             Result r = table.get(scan);   

181.             System.out.println("获得到rowkey:" + new String(r.getRow()));   

182.             for (KeyValue keyValue : r.raw()) {   

183.                 System.out.println("列:" + new String(keyValue.getFamily())   

184.                         + "====值:" + new String(keyValue.getValue()));   

185.             }   

186.         } catch (IOException e) {   

187.             e.printStackTrace();   

188.         }   

189.     }   

190.   

191.     /**  

192.      * 单条件按查询,查询多条记录  

193.      * @param tableName  

194.      */  

195.     public static void QueryByCondition2(String tableName) {   

196.   

197.         try {   

198.             HTablePool pool = new HTablePool(configuration, 1000);   

199.             HTable table = (HTable) pool.getTable(tableName);   

200.             Filter filter = new SingleColumnValueFilter(Bytes   

201.                     .toBytes("column1"), null, CompareOp.EQUAL, Bytes   

202.                     .toBytes("aaa")); // 当列column1的值为aaa时进行查询   

203.             Scan s = new Scan();   

204.             s.setFilter(filter);   

205.             ResultScanner rs = table.getScanner(s);   

206.             for (Result r : rs) {   

207.                 System.out.println("获得到rowkey:" + new String(r.getRow()));   

208.                 for (KeyValue keyValue : r.raw()) {   

209.                     System.out.println("列:" + new String(keyValue.getFamily())   

210.                             + "====值:" + new String(keyValue.getValue()));   

211.                 }   

212.             }   

213.         } catch (Exception e) {   

214.             e.printStackTrace();   

215.         }   

216.   

217.     }   

218.   

219.     /**  

220.      * 组合条件查询  

221.      * @param tableName  

222.      */  

223.     public static void QueryByCondition3(String tableName) {   

224.   

225.         try {   

226.             HTablePool pool = new HTablePool(configuration, 1000);   

227.             HTable table = (HTable) pool.getTable(tableName);   

228.   

229.             List<Filter> filters = new ArrayList<Filter>();   

230.   

231.             Filter filter1 = new SingleColumnValueFilter(Bytes   

232.                     .toBytes("column1"), null, CompareOp.EQUAL, Bytes   

233.                     .toBytes("aaa"));   

234.             filters.add(filter1);   

235.   

236.             Filter filter2 = new SingleColumnValueFilter(Bytes   

237.                     .toBytes("column2"), null, CompareOp.EQUAL, Bytes   

238.                     .toBytes("bbb"));   

239.             filters.add(filter2);   

240.   

241.             Filter filter3 = new SingleColumnValueFilter(Bytes   

242.                     .toBytes("column3"), null, CompareOp.EQUAL, Bytes   

243.                     .toBytes("ccc"));   

244.             filters.add(filter3);   

245.   

246.             FilterList filterList1 = new FilterList(filters);   

247.   

248.             Scan scan = new Scan();   

249.             scan.setFilter(filterList1);   

250.             ResultScanner rs = table.getScanner(scan);   

251.             for (Result r : rs) {   

252.                 System.out.println("获得到rowkey:" + new String(r.getRow()));   

253.                 for (KeyValue keyValue : r.raw()) {   

254.                     System.out.println("列:" + new String(keyValue.getFamily())   

255.                             + "====值:" + new String(keyValue.getValue()));   

256.                 }   

257.             }   

258.             rs.close();   

259.   

260.         } catch (Exception e) {   

261.             e.printStackTrace();   

262.         }   

263.   

264.     }   

265.   

266. }  

package com.wujintao.hbase.test;

 

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;

 

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.hbase.HBaseConfiguration;

import org.apache.hadoop.hbase.HColumnDescriptor;

import org.apache.hadoop.hbase.HTableDescriptor;

import org.apache.hadoop.hbase.KeyValue;

import org.apache.hadoop.hbase.MasterNotRunningException;

import org.apache.hadoop.hbase.ZooKeeperConnectionException;

import org.apache.hadoop.hbase.client.Delete;

import org.apache.hadoop.hbase.client.Get;

import org.apache.hadoop.hbase.client.HBaseAdmin;

import org.apache.hadoop.hbase.client.HTable;

import org.apache.hadoop.hbase.client.HTablePool;

import org.apache.hadoop.hbase.client.Put;

import org.apache.hadoop.hbase.client.Result;

import org.apache.hadoop.hbase.client.ResultScanner;

import org.apache.hadoop.hbase.client.Scan;

import org.apache.hadoop.hbase.filter.Filter;

import org.apache.hadoop.hbase.filter.FilterList;

import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;

import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;

import org.apache.hadoop.hbase.util.Bytes;

 

public class JinTaoTest {

 

public static Configuration configuration;

static {

configuration = HBaseConfiguration.create();

configuration.set("hbase.zookeeper.property.clientPort", "2181");

configuration.set("hbase.zookeeper.quorum", "192.168.1.100");

configuration.set("hbase.master", "192.168.1.100:600000");

}

 

public static void main(String[] args) {

// createTable("wujintao");

// insertData("wujintao");

// QueryAll("wujintao");

// QueryByCondition1("wujintao");

// QueryByCondition2("wujintao");

//QueryByCondition3("wujintao");

//deleteRow("wujintao","abcdef");

deleteByCondition("wujintao","abcdef");

}

 

/**

 * 创建表

 * @param tableName

 */

public static void createTable(String tableName) {

System.out.println("start create table ......");

try {

HBaseAdmin hBaseAdmin = new HBaseAdmin(configuration);

if (hBaseAdmin.tableExists(tableName)) {// 如果存在要创建的表,那么先删除,再创建

hBaseAdmin.disableTable(tableName);

hBaseAdmin.deleteTable(tableName);

System.out.println(tableName + " is exist,detele....");

}

HTableDescriptor tableDescriptor = new HTableDescriptor(tableName);

tableDescriptor.addFamily(new HColumnDescriptor("column1"));

tableDescriptor.addFamily(new HColumnDescriptor("column2"));

tableDescriptor.addFamily(new HColumnDescriptor("column3"));

hBaseAdmin.createTable(tableDescriptor);

} catch (MasterNotRunningException e) {

e.printStackTrace();

} catch (ZooKeeperConnectionException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

System.out.println("end create table ......");

}

 

/**

 * 插入数据

 * @param tableName

 */

public static void insertData(String tableName) {

System.out.println("start insert data ......");

HTablePool pool = new HTablePool(configuration, 1000);

HTable table = (HTable) pool.getTable(tableName);

Put put = new Put("112233bbbcccc".getBytes());// 一个PUT代表一行数据,再NEW一个PUT表示第二行数据,每行一个唯一的ROWKEY,此处rowkey为put构造方法中传入的值

put.add("column1".getBytes(), null, "aaa".getBytes());// 本行数据的第一列

put.add("column2".getBytes(), null, "bbb".getBytes());// 本行数据的第三列

put.add("column3".getBytes(), null, "ccc".getBytes());// 本行数据的第三列

try {

table.put(put);

} catch (IOException e) {

e.printStackTrace();

}

System.out.println("end insert data ......");

}

 

/**

 * 删除一张表

 * @param tableName

 */

public static void dropTable(String tableName) {

try {

HBaseAdmin admin = new HBaseAdmin(configuration);

admin.disableTable(tableName);

admin.deleteTable(tableName);

} catch (MasterNotRunningException e) {

e.printStackTrace();

} catch (ZooKeeperConnectionException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

 

}

/**

 * 根据 rowkey删除一条记录

 * @param tablename

 * @param rowkey

 */

 public static void deleteRow(String tablename, String rowkey)  {

try {

HTable table = new HTable(configuration, tablename);

List list = new ArrayList();

Delete d1 = new Delete(rowkey.getBytes());

list.add(d1);

table.delete(list);

System.out.println("删除行成功!");

} catch (IOException e) {

e.printStackTrace();

}

 

}

 

 /**

  * 组合条件删除

  * @param tablename

  * @param rowkey

  */

 public static void deleteByCondition(String tablename, String rowkey)  {

//目前还没有发现有效的API能够实现 根据非rowkey的条件删除 这个功能能,还有清空表全部数据的API操作

 

}

 

 

/**

 * 查询所有数据

 * @param tableName

 */

public static void QueryAll(String tableName) {

HTablePool pool = new HTablePool(configuration, 1000);

HTable table = (HTable) pool.getTable(tableName);

try {

ResultScanner rs = table.getScanner(new Scan());

for (Result r : rs) {

System.out.println("获得到rowkey:" + new String(r.getRow()));

for (KeyValue keyValue : r.raw()) {

System.out.println("列:" + new String(keyValue.getFamily())

+ "====值:" + new String(keyValue.getValue()));

}

}

} catch (IOException e) {

e.printStackTrace();

}

}

 

/**

 * 单条件查询,根据rowkey查询唯一一条记录

 * @param tableName

 */

public static void QueryByCondition1(String tableName) {

 

HTablePool pool = new HTablePool(configuration, 1000);

HTable table = (HTable) pool.getTable(tableName);

try {

Get scan = new Get("abcdef".getBytes());// 根据rowkey查询

Result r = table.get(scan);

System.out.println("获得到rowkey:" + new String(r.getRow()));

for (KeyValue keyValue : r.raw()) {

System.out.println("列:" + new String(keyValue.getFamily())

+ "====值:" + new String(keyValue.getValue()));

}

} catch (IOException e) {

e.printStackTrace();

}

}

 

/**

 * 单条件按查询,查询多条记录

 * @param tableName

 */

public static void QueryByCondition2(String tableName) {

 

try {

HTablePool pool = new HTablePool(configuration, 1000);

HTable table = (HTable) pool.getTable(tableName);

Filter filter = new SingleColumnValueFilter(Bytes

.toBytes("column1"), null, CompareOp.EQUAL, Bytes

.toBytes("aaa")); // 当列column1的值为aaa时进行查询

Scan s = new Scan();

s.setFilter(filter);

ResultScanner rs = table.getScanner(s);

for (Result r : rs) {

System.out.println("获得到rowkey:" + new String(r.getRow()));

for (KeyValue keyValue : r.raw()) {

System.out.println("列:" + new String(keyValue.getFamily())

+ "====值:" + new String(keyValue.getValue()));

}

}

} catch (Exception e) {

e.printStackTrace();

}

 

}

 

/**

 * 组合条件查询

 * @param tableName

 */

public static void QueryByCondition3(String tableName) {

 

try {

HTablePool pool = new HTablePool(configuration, 1000);

HTable table = (HTable) pool.getTable(tableName);

 

List<Filter> filters = new ArrayList<Filter>();

 

Filter filter1 = new SingleColumnValueFilter(Bytes

.toBytes("column1"), null, CompareOp.EQUAL, Bytes

.toBytes("aaa"));

filters.add(filter1);

 

Filter filter2 = new SingleColumnValueFilter(Bytes

.toBytes("column2"), null, CompareOp.EQUAL, Bytes

.toBytes("bbb"));

filters.add(filter2);

 

Filter filter3 = new SingleColumnValueFilter(Bytes

.toBytes("column3"), null, CompareOp.EQUAL, Bytes

.toBytes("ccc"));

filters.add(filter3);

 

FilterList filterList1 = new FilterList(filters);

 

Scan scan = new Scan();

scan.setFilter(filterList1);

ResultScanner rs = table.getScanner(scan);

for (Result r : rs) {

System.out.println("获得到rowkey:" + new String(r.getRow()));

for (KeyValue keyValue : r.raw()) {

System.out.println("列:" + new String(keyValue.getFamily())

+ "====值:" + new String(keyValue.getValue()));

}

}

rs.close();

 

} catch (Exception e) {

e.printStackTrace();

}

 

}

 

}

 注意:可能大家没看到更新数据的操作,其实更新的操作跟添加完全一致,只不过是添加呢rowkey不存在,更新呢rowkey已经存在,并且timstamp相同的情况下,还有就是目前好像还没办法实现hbase数据的分页查询,不知道有没有人知道怎么做

 

HBase性能优化建议:

 针对前面的代码,有很多不足之处,在此我就不修改上面的代码了,只是提出建议的地方,大家自己加上

   1)配置

  当你调用create方法时将会加载两个配置文件:hbase-default.xml and hbase-site.xml,利用的是当前的java类路径, 代码中configuration设置的这些配置将会覆盖hbase-default.xml和hbase-site.xml中相同的配置,如果两个配置文件都存在并且都设置好了相应参上面的属性下面的属性即可

 

 2)关于建表

   

public void createTable(HTableDescriptor desc)

 

HTableDescriptor 代表的是表的schema, 提供的方法中比较有用的有

setMaxFileSize,指定最大的region size

setMemStoreFlushSize 指定memstore flush到HDFS上的文件大小

增加family通过 addFamily方法

  

public void addFamily(final HColumnDescriptor family)

 

HColumnDescriptor 代表的是column的schema,提供的方法比较常用的有

setTimeToLive:指定最大的TTL,单位是ms,过期数据会被自动删除。

setInMemory:指定是否放在内存中,对小表有用,可用于提高效率。默认关闭

setBloomFilter:指定是否使用BloomFilter,可提高随机查询效率。默认关闭

setCompressionType:设定数据压缩类型。默认无压缩。

setMaxVersions:指定数据最大保存的版本个数。默认为3。

 

注意的是,一般我们不去setInMemory为true,默认是关闭的

3)关于入库

   官方建议

 table.setAutoFlush(false); //数据入库之前先设置此项为false

 table.setflushCommits();//入库完成后,手动刷入数据

注意:

  在入库过程中,put.setWriteToWAL(true/flase);

  关于这一项如果不希望大量数据在存储过程中丢失,建议设置为true,如果仅是在测试演练阶段,为了节省入库时间建议设置为false

4)关于获取表实例

HTablePool pool = new HTablePool(configuration, Integer.MAX_VALUE);

HTable table = (HTable) pool.getTable(tableName);

建议用表连接池的方式获取表,具体池有什么作用,我想用过数据库连接池的同学都知道,我就不再重复

不建议使用new HTable(configuration,tableName);的方式获取表

5)关于查询

 建议每个查询语句都放入try catch语句块,并且finally中要进行关闭ResultScanner实例以及将不使用的表重新放入到HTablePool中的操作,具体做法如下

Java代码  

1. public static void QueryAll(String tableName) {   

2.         HTablePool pool = new HTablePool(configuration, Integer.MAX_VALUE);   

3.         HTable table = null;   

4.         ResultScanner rs = null;   

5.         try {   

6.             Scan scan = new Scan();   

7.             table = (HTable) pool.getTable(tableName);   

8.             rs = table.getScanner(scan);   

9.             for (Result r : rs) {   

10.                 System.out.println("获得到rowkey:" + new String(r.getRow()));   

11.                 for (KeyValue keyValue : r.raw()) {   

12.                     System.out.println("列:" + new String(keyValue.getFamily())   

13.                             + "====值:" + new String(keyValue.getValue()));   

14.                 }   

15.             }   

16.         } catch (IOException e) {   

17.             e.printStackTrace();   

18.         }finally{   

19.             rs.close();// 最后还得关闭   

20.             pool.putTable(table); //实际应用过程中,pool获取实例的方式应该抽取为单例模式的,不应在每个方法都重新获取一次(单例明白?就是抽取到专门获取pool的逻辑类中,具体逻辑为如果pool存在着直接使用,如果不存在则new)   

21.         }   

22.     }  

public static void QueryAll(String tableName) {

HTablePool pool = new HTablePool(configuration, Integer.MAX_VALUE);

HTable table = null;

ResultScanner rs = null;

try {

Scan scan = new Scan();

table = (HTable) pool.getTable(tableName);

rs = table.getScanner(scan);

for (Result r : rs) {

System.out.println("获得到rowkey:" + new String(r.getRow()));

for (KeyValue keyValue : r.raw()) {

System.out.println("列:" + new String(keyValue.getFamily())

+ "====值:" + new String(keyValue.getValue()));

}

}

} catch (IOException e) {

e.printStackTrace();

}finally{

rs.close();// 最后还得关闭

pool.putTable(table); //实际应用过程中,pool获取实例的方式应该抽取为单例模式的,不应在每个方法都重新获取一次(单例明白?就是抽取到专门获取pool的逻辑类中,具体逻辑为如果pool存在着直接使用,如果不存在则new)

}

}

 

 所以,以上代码有缺陷的地方,感兴趣的同学可以针对优化建议作出相应修改

二十四、 基于Hbase存储的分布式消息(IM)系统-JABase

 

 

 前段日子看了在highscalability.com上一篇介绍facebook消息系统的文章,一夜之后被无数个网站无数次的转载,现如今facebook的任何一个话题都会引起很多人的关注,但我对只对这文章里面没有说明的部分比较感兴趣,系统里是怎么获得即时消息的?PHP监听器?开启很多个监听器服务?推模式?拉模式?跟Twitter一样采用消息中间件?对此有了些疑问,期待日后有人能给出明确解答。
 
    这个叫JABase的 东东 能给我一些启示,JABase是应用在分布式IM系统中的中间件,可以支持大规模的集群环境的伸缩性架构,并采用Java语言来实现的,JABase是介于分布式数据存储(HBase/HDFS)和即时消息收发(Erlbase/XMPP Server)的一个中介体,JABaseXMPP Server中的收/发消息存放在HBase中,这点与facebook消息系统文章中提到的极为相似。另外提一下,JABase给出的方案中的IM消息服务器(XMPP Server) Erlbase是采用 Erlang 语言编写,有点像Twitter

整个系统架构由HBase/HDFSJABaseErlBase 3大部分组成,整体架构如图所示:

 

除了需要安装HBaseHDFS一些主要部件以外,还需要有以下一些组件支持:
   – ejabberd-2.0.3.tar.gz    (Erlang)
   – erlbase-1.0.tar.gz         (Erlang)
   – jabase-1.0.tar.gz           (Java)

JABase项目的了解,结合目前采用的架构引发出2个的设想:
    1XMPPMQ 可扩展性的比较,有时间需要坐下来跟大家讨论一下。
    2MQ的性能扩展 a)前端集成缓存 b)后端消息存储,看看这么加才对我们有真正的帮助。

 

 

二十五、 HBase入门篇

 HBase是什么? 
HBaseApache Hadoop中的一个子项目,Hbase依托于HadoopHDFS作为最基本存储基础单元,通过使用hadoopDFS工具就可以看到这些这些数据 存储文件夹的结构,还可以通过Map/Reduce的框架(算法)HBase进行操作,如右侧的图所示:

HBase在产品中还包含了Jetty,在HBase启动时采用嵌入式的方式来启动Jetty,因此可以通过web界面对HBase进行管理和查看当前运行的一些状态,非常轻巧。

为什么采用HBase
HBase 不同于一般的关系数据库,它是一个适合于非结构化数据存储的数据库.所谓非结构化数据存储就是说HBase是基于列的而不是基于行的模式,这样方面读写你的大数据内容。

HBase是介于Map Entry(key & value)DB Row之间的一种数据存储方式。就点有点类似于现在流行的Memcache,但不仅仅是简单的一个key对应一个 value,你很可能需要存储多个属性的数据结构,但没有传统数据库表中那么多的关联关系,这就是所谓的松散数据。

简单来说,你在HBase中的表创建的可以看做是一张很大的表,而这个表的属性可以根据需求去动态增加,在HBase中没有表与表之间关联查询。你只需要 告诉你的数据存储到Hbase的那个column families 就可以了,不需要指定它的具体类型:char,varchar,int,tinyint,text等等。但是你需要注意HBase中不包含事务此类的功 能。

Apache HBase Google Bigtable 有非常相似的地方,一个数据行拥有一个可选择的键和任意数量的列。表是疏松的存储的,因此用户可以给行定义各种不同的列,对于这样的功能在大项目中非常实用,可以简化设计和升级的成本。

如何运行HBase
从 ApacheHBase的镜像网站上下载一个稳定版本的HBase http://mirrors.devlib.org/apache/hbase/stable/hbase-0.20.6.tar.gz, 下载完成后,对其进行解压缩。确定你的机器中已经正确的安装了Java SDKSSH,否则将无法正常运行。

$ cd /work/hbase
进入此目录

$ vim conf/hbase-env.sh
export JAVA_HOME=/JDK_PATH
编辑 conf/hbase-env.sh 文件,JAVA_HOME修改为你的JDK安装目录

$ vim conf/regionservers
输入你的所有HBase服务器名,localhost,或者是ip地址

$ bin/start-hbase.sh
启动hbase, 中间需要你输入两次密码,也可以进行设置不需要输入密码,启动成功,如图所示:

$ bin/hbase rest start
启动hbase REST服务后就可以通过对uri: http://localhost:60050/api/ 的通用REST操作(GET/POST/PUT/DELETE)实现对hbaseREST形式数据操作.

也可以输入以下指令进入HQL指令模式
$ bin/hbase shell

$ bin/stop-hbase.sh
关闭HBase服务

启动时存在的问题

由于linux系统的主机名配置不正确,在运行HBase服务器中可能存在的问题,如图所示:

2010-11-05 11:10:20,189 ERROR org.apache.hadoop.hbase.master.HMaster: Can not start master
java.net.UnknownHostException: ubuntu-server216: ubuntu-server216
表示你的主机名不正确,你可以先查看一下 /etc/hosts/中名称是什么,再用 hostname 命令进行修改, hostname you_server_name

查看运行状态
1、如果你需要对HBase的日志进行监控你可以查看 hbase.x.x./logs/下的日志文件,可以使用tail -f 来查看。
2、通过 web方式查看运行在 HBase 下的zookeeper  http://localhost:60010/zk.jsp
3、如果你需要查看当前的运行状态可以通过web的方式对HBase服务器进行查看,如图所示:

扩展阅读1
    Apach 的 Hadoop的项目中包含了那些产品,如图所示:
    Pig 是在MapReduce上构建的查询语言(SQL-like),适用于大量并行计算。
    Chukwa 是基于Hadoop集群中监控系统,简单来说就是一个看门狗” (WatchDog)
    Hive DataWareHouse 和 Map Reduce交集,适用于ETL方面的工作。
    HBase 是一个面向列的分布式数据库。
    Map Reduce 是Google提出的一种算法,用于超大型数据集的并行运算。
    HDFS 可以支持千万级的大型分布式文件系统。
    Zookeeper  提供的功能包括:配置维护、名字服务、分布式同步、组服务等,用于分布式系统的可靠协调系统。
    Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。

扩展阅读2
    什么是列存储?列存储不同于传统的关系型数据库,其数据在表中是按行存储的,列方式所带来的重要好处之一就是,由于查询中的选择规则是通过列来定义的,因 此整个数据库是自动索引化的。按列存储每个字段的数据聚集存储,在查询只需要少数几个字段的时候,能大大减少读取的数据量,一个字段的数据聚集存储,那就 更容易为这种聚集存储设计更好的压缩/解压算法。这张图讲述了传统的行存储和列存储的区别:

 
扩展阅读3
    对系统海量的Log4J日志可以存放在一个集中式的机器上,在此机器上安装 splunk 可以方便对所有日志查看,安装方法可以参考:
    http://www.splunk.com/base/Documentation/latest/Installation/InstallonLinux

二十六、 HBase入门篇2-Java操作HBase例子

 

本篇文章讲述用HBase Shell命令 和 HBase Java API HBase 服务器 进行操作。在此之前需要对HBase的总体上有个大概的了解。比如说HBase服务器内部由哪些主要部件构成?HBase的内部工作原理是什么?我想学习任何一项知识、技术的态度不能只是知道如何使用,对产品的内部构建一点都不去关心,那样出了问题,很难让你很快的找到答案,甚至我们希望最后能对该项技术的领悟出自己的心得,为我所用,借鉴该项技术其中的设计思想创造出自己的解决方案,更灵活的去应对多变的计算场景与架构设计。以我目前的对HBase的了解还不够深入,将来不断的学习,我会把我所知道的点滴分享到这个Blog上。

     先来看一下读取一行记录HBase是如何进行工作的,首先HBase Client端会连接Zookeeper Qurom(从下面的代码也能看出来,例如:HBASE_CONFIG.set("hbase.zookeeper.quorum", "192.168.50.216") )。通过Zookeeper组件Client能获知哪个Server管理-ROOT- Region。那么Client就去访问管理-ROOT-Server,在META中记录了HBase中所有表信息,(你可以使用  scan '.META.' 命令列出你创建的所有表的详细信息),从而获取Region分布的信息。一旦Client获取了这一行的位置信息,比如这一行属于哪个RegionClient将会缓存这个信息并直接访问HRegionServer。久而久之Client缓存的信息渐渐增多,即使不访问.META.表也能知道去访问哪个HRegionServerHBase中包含两种基本类型的文件,一种用于存储WALlog,另一种用于存储具体的数据,这些数据都通过DFS Client和分布式的文件系统HDFS进行交互实现存储。

如图所示:



查看大图请点击这里

再来看看HBase的一些内存实现原理:    
    * HMaster— HBase中仅有一个Master server
    * HRegionServer—负责多个HRegion使之能向client端提供服务,在HBase cluster中会存在多个HRegionServer
    * ServerManager—负责管理Region server信息,如每个Region serverHServerInfo(这个对象包含HServerAddressstartCode),load Region个数,死亡的Region server列表
    * RegionManager—负责将region分配到region server的具体工作,还监视rootmeta 2个系统级的region状态。
    * RootScanner—定期扫描root region,以发现没有分配的meta region
    * MetaScanner—定期扫描meta region,以发现没有分配的user region

HBase基本命令
下面我们再看看看HBase的一些基本操作命令,我列出了几个常用的HBase Shell命令,如下:

名称

命令表达式

创建表

create '表名称', '列名称1','列名称2','列名称N'

添加记录      

put '表名称', '行名称', '列名称:', ''

查看记录

get '表名称', '行名称'

查看表中的记录总数

count  '表名称'

删除记录

delete  '表名' ,'行名称' , '列名称'

删除一张表

先要屏蔽该表,才能对该表进行删除,第一步 disable '表名称第二步  drop '表名称'

查看所有记录

scan "表名称"  

查看某个表某个列中所有数据

scan "表名称" , ['列名称:']

更新记录 

就是重写一遍进行覆盖

  如果你是一个新手队HBase的一些命令还不算非常熟悉的话,你可以进入 hbase shell 模式中你可以输入 help 命令查看到你可以执行的命令和对该命令的说明,例如对scan这个命令,help中不仅仅提到有这个命令,还详细的说明了scan命令中可以使用的参数和作用,例如,根据列名称查询的方法和带LIMIT STARTROW的使用方法:
scan   Scan a table; pass table name and optionally a dictionary of scanner specifications.  Scanner specifications may include one or more of  the following: LIMIT, STARTROW, STOPROW, TIMESTAMP, or COLUMNS.  If no columns are specified, all columns will be scanned.  To scan all members of a column family, leave the qualifier empty as in  'col_family:'.  Examples:
            hbase> scan '.META.'
            hbase> scan '.META.', {COLUMNS => 'info:regioninfo'}
            hbase> scan 't1', {COLUMNS => ['c1', 'c2'], LIMIT => 10, STARTROW => 'xyz'}


使用Java APIHBase服务器进行操作

需要下列jar
     hbase-0.20.6.jar
     hadoop-core-0.20.1.jar
     commons-logging-1.1.1.jar
     zookeeper-3.3.0.jar
     log4j-1.2.91.jar

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.io.BatchUpdate;

@SuppressWarnings("deprecation")
public class HBaseTestCase {
    
    static HBaseConfiguration cfg = null;
    static {
        Configuration HBASE_CONFIG = new Configuration();
        HBASE_CONFIG.set("hbase.zookeeper.quorum", "192.168.50.216");
        HBASE_CONFIG.set("hbase.zookeeper.property.clientPort", "2181");
        cfg = new HBaseConfiguration(HBASE_CONFIG);
    }
    
    
    /**
     * 创建一张表
     */
    public static void creatTable(String tablename) throws Exception {
        HBaseAdmin admin = new HBaseAdmin(cfg);
        if (admin.tableExists(tablename)) {
            System.out.println("table   Exists!!!");
        }
        else{
            HTableDescriptor tableDesc = new HTableDescriptor(tablename);
            tableDesc.addFamily(new HColumnDescriptor("name:"));
            admin.createTable(tableDesc);
            System.out.println("create table ok .");
        }
    
         
    }
    
    /**
     * 添加一条数据
     */
    public static void addData (String tablename) throws Exception{
         HTable table = new HTable(cfg, tablename);
             BatchUpdate update = new BatchUpdate("Huangyi");  
             update.put("name:java", "http://www.javabloger.com".getBytes());  
             table.commit(update);  
         System.out.println("add data ok .");
    }
    
    /**
     * 显示所有数据
     */
    public static void getAllData (String tablename) throws Exception{
         HTable table = new HTable(cfg, tablename);
         Scan s = new Scan();
         ResultScanner ss = table.getScanner(s);
         for(Result r:ss){
             for(KeyValue kv:r.raw()){
                System.out.print(new String(kv.getColumn()));
                System.out.println(new String(kv.getValue()    ));
             }

         }
    }
    
    
    public static void  main (String [] agrs) {
        try {
                String tablename="tablename";
                HBaseTestCase.creatTable(tablename);
                HBaseTestCase.addData(tablename);
                HBaseTestCase.getAllData(tablename);
            } 
        catch (Exception e) {
            e.printStackTrace();
        }
        
    }
    
}

二十七、 HBase入门篇3

 

前两篇文件分别说到了我在学习HBase中的一些入门经验,而《HBase 入门3》这篇文章浅显的从几个方面谈谈HBase的一些优化技巧,只能作为我学习笔记的一部分,因为学多了怕忘,留给自己以后看看。

修改 linux 系统参数 
    Linux系统最大可打开文件数一般默认的参数值是1024,如果你不进行修改并发量上来的时候会出现“Too Many Open Files”的错误,导致整个HBase不可运行,你可以用ulimit -n 命令进行修改,或者修改/etc/security/limits.conf /proc/sys/fs/file-max  的参数,具体如何修改可以去Google 关键字 “linux limits.conf ”

2JVM 配置  
修改 hbase-env.sh 文件中的配置参数,根据你的机器硬件和当前操作系统的JVM(32/64)配置适当的参数
    HBASE_HEAPSIZE 4000   HBase使用的 JVM 堆的大小
    HBASE_OPTS  "server XX:+UseConcMarkSweepGC"JVM GC 选项
    HBASE_MANAGES_ZKfalse   是否使用Zookeeper进行分布式管理

3 HBase持久化
重启操作系统后HBase中数据全无,你可以不做任何修改的情况下,创建一张表,写一条数据进行,然后将机器重启,重启后你再进入HBaseshell中使用 list 命令查看当前所存在的表,一个都没有了。是不是很杯具?没有关系你可以在hbase/conf/hbase-default.xml中设置hbase.rootdir的值,来设置文件的保存位置指定一个文件夹 ,例如:<value>file:///you/hbase-data/path</value>,你建立的HBase中的表和数据就直接写到了你的磁盘上,如图所示:
同样你也可以指定你的分布式文件系统HDFS的路径例如: hdfs://NAMENODE_SERVER:PORT/HBASE_ROOTDIR,这样就写到了你的分布式文件系统上了。

4配置HBase运行参数 
其次就需要对hbase/conf/hbase-default.xml 文件进行配置,以下是我认为比较重要的配置参数

hbase.client.write.buffer    
描述:这个参数可以设置写入数据缓冲区的大小,当客户端和服务器端传输数据,服务器为了提高系统运行性能开辟一个写的缓冲区来处理它, 这个参数设置如果设置的大了,将会对系统的内存有一定的要求,直接影响系统的性能。

hbase.master.meta.thread.rescanfrequency  
描述:多长时间 HMaster对系统表 root 和 meta 扫描一次,这个参数可以设置的长一些,降低系统的能耗。

hbase.regionserver.handler.count  
描述:由于HBase/HadoopServer是采用Multiplexed, non-blocking I/O方式而设计的,所以它可以透过一个Thread来完成处理,但是由于处理Client端所呼叫的方法是Blocking I/O,所以它的设计会将Client所传递过来的物件先放置在Queue,并在启动Server时就先产生一堆Handler(Thread),该Handler会透过Polling的方式来取得该物件并执行对应的方法,默认为25,根据实际场景可以设置大一些。
 
hbase.regionserver.thread.splitcompactcheckfrequency 
描述:这个参数是表示多久去RegionServer服务器运行一次split/compaction的时间间隔,当然split之前会先进行一个compact操作.这个compact操作可能是minor compact也可能是major compact.compact,会从所有的Store下的所有StoreFile文件最大的那个取midkey.这个midkey可能并不处于全部数据的mid.一个row-key的下面的数据可能会跨不同的HRegion

hbase.hregion.max.filesize
描述:HRegion中的HStoreFile最大值,任何表中的列族一旦超过这个大小将会被切分,而HStroeFile的默认大小是256M

hfile.block.cache.size   
描述:指定 HFile/StoreFile 缓存在JVM堆中分配的百分比,默认值是0.2,意思就是20%,而如果你设置成0,就表示对该选项屏蔽。

hbase.zookeeper.property.maxClientCnxns     
描述: 这项配置的选项就是从zookeeper中来的,表示ZooKeeper客户端同时访问的并发连接数,ZooKeeper对于HBase来说就是一个入口这个参数的值可以适当放大些。

hbase.regionserver.global.memstore.upperLimit
描述:在Region Server中所有memstores占用堆的大小参数配置,默认值是0.4,表示40%,如果设置为0,就是对选项进行屏蔽。

hbase.hregion.memstore.flush.size 
描述:Memstore中缓存的内容超过配置的范围后将会写到磁盘上,例如:删除操作是先写入MemStore里做个标记,指示那个value, column 或 family等下是要删除的,HBase会定期对存储文件做一个major compaction,在那时HBase会把MemStore刷入一个新的HFile存储文件中。如果在一定时间范围内没有做major compaction,而Memstore中超出的范围就写入磁盘上了。

5 HBaselog4j的日志 
HBase中日志输出等级默认状态下是把debug、 info 级别的日志打开的,可以根据自己的需要调整log级别,HBaselog4j日志配置文件在 hbase\conf\log4j.properties 目录下

二十八、 HBase入门篇4–存储

   前几篇文章讲述了 HBase的安装Hbase命令和API的使用HBase简单的优化技巧,《HBase入门篇4这篇文章是讲述把HBase的数据放在HDFS上的点滴过程。目前对与HBase我是一个绝对的新手,如果在文章中有任何我理解有错误的地方请各位指正,谢谢。

Ok,进行正题 ………

   HBase中创建的一张表可以分布在多个Hregion,也就说一张表可以被拆分成多块,每一块称我们呼为一个Hregion。每个Hregion会保 存一个表里面某段连续的数据,用户创建的那个大表中的每个Hregion块是由Hregion服务器提供维护,访问Hregion块是要通过 Hregion服务器,而一个Hregion块对应一个Hregion服务器,一张完整的表可以保存在多个Hregion 上。HRegion Server Region的对应关系是一对多的关系。每一个HRegion在物理上会被分为三个部分:Hmemcache(缓存)Hlog(日志)HStore(持久层)
上述这些关系在我脑海中的样子,如图所示:

1.HRegionServerHRegionHmemcacheHlogHStore之间的关系,如图所示:

 

2.HBase表中的数据与HRegionServer的分布关系,如图所示:

 

 

HBase读数据
HBase读取数据优先读取HMemcache中的内容,如果未取到再去读取Hstore中的数据,提高数据读取的性能。

HBase写数据
HBase写入数据会写到HMemcacheHlog中,HMemcache建立缓存,Hlog同步HmemcacheHstore的事务日志,发起Flush Cache时,数据持久化到Hstore中,并清空HMemecache

    客户端访问这些数据的时候通过Hmaster ,每个 Hregion 服务器都会和Hmaster 服务器保持一个长连接,Hmaster HBase分布式系统中的管理者,他的主要任务就是要告诉每个Hregion 服务器它要维护哪些Hregion。用户的这些都数据可以保存在Hadoop 分布式文件系统上。 如果主服务器Hmaster死机,那么整个系统都会无效。下面我会考虑如何解决HmasterSPFO的问题,这个问题有点类似HadoopSPFO 问题一样只有一个NameNode维护全局的DataNodeHDFS一旦死机全部挂了,也有人说采用Heartbeat来解决这个问题,但我总想找出 其他的解决方案,多点时间,总有办法的。

昨天在hadoop-0.21.0hbase-0.20.6的环境中折腾了很久,一直报错,错误信息如下:
Exception in thread "main" java.io.IOException: Call to localhost/serv6:9000 failed on local exception: java.io.EOFException
10/11/10 15:34:34 ERROR master.HMaster: Can not start master
java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
        at org.apache.hadoop.hbase.master.HMaster.doMain(HMaster.java:1233)
        at org.apache.hadoop.hbase.master.HMaster.main(HMaster.java:1274)

死活连接不上HDFS,也无法连接HMaster,郁闷啊。
我想想啊,慢慢想,我眼前一亮  java.io.EOFException 这个异常,是不是有可能是RPC 协定格式不一致导致的?也就是说服务器端和客户端的版本不一致的问题?换了一个HDFS的服务器端以后,一切都好了,果然是版本的问题,最后采用 hadoop-0.20.2 搭配hbase-0.20.6 比较稳当。
最后的效果如图所示:

 

查看大图请点击这里, 上图的一些文字说明:
   1.hadoop版本是0.20.2 ,
   2.hbase版本是0.20.6,
   3.hbase中创建了一张表 tab1,退出hbase shell环境,
   4.hadoop命令查看,文件系统中的文件果然多了一个刚刚创建的tab1目录,
以上这张图片说明HBase在分布式文件系统Apache HDFS中运行了。

二十九、 HBase入门5(集群) -压力分载与失效转发

 

 上一篇关于HBase的文章中曾经讲述过HBase在分布式中的架构,这篇文章将会讲述HBase在分布式环境中是如何排除单点故障的(SPFO),做一个小实验讲述HBase在分布式环境中的高可用性,亲眼看到一些现象,延伸一些思考的话题。

先来回顾一下HBase主要部件:
   1.HBaseMaster  
   2.HRegionServer 
   3.HBase Client
   4.HBase Thrift Server
   5.HBase REST Server

HBaseMaster
    HMaster 负责给HRegionServer分配区域,并且负责对集群环境中的HReginServer进行负载均衡,HMaster还负责监控集群环境中的HReginServer的运行状况,如果某一台HReginServer down机,HBaseMaster将会把不可用的HReginServer来提供服务的HLog和表进行重新分配转交给其他HReginServer来提供,HBaseMaster还负责对数据和表进行管理,处理表结构和表中数据的变更,因为在 META 系统表中存储了所有的相关表信息。并且HMaster实现了ZooKeeperWatcher接口可以和zookeeper集群交互。

HRegionServer
    HReginServer负责处理用户的读和写的操作。HReginServer通过与HBaseMaster通信获取自己需要服务的数据表,并向HMaster反馈自己的运行状况。当一个写的请求到来的时候,它首先会写到一个叫做HLogwrite-ahead log中。HLog被缓存在内存中,称为Memcache,每一个HStore只能有一个Memcache。当Memcache到达配置的大小以后,将会创建一个MapFile,将其写到磁盘中去。这将减少HReginServer的内存压力。当一起读取的请求到来的时候,HReginServer会先在Memcache中寻找该数据,当找不到的时候,才会去在MapFiles 中寻找。

HBase Client
    HBase Client负责寻找提供需求数据的HReginServer。在这个过程中,HBase Client将首先与HMaster通信,找到ROOT区域。这个操作是ClientMaster之间仅有的通信操作。一旦ROOT区域被找到以后,Client就可以通过扫描ROOT区域找到相应的META区域去定位实际提供数据的HReginServer。当定位到提供数据的HReginServer以后,Client就可以通过这个HReginServer找到需要的数据了。这些信息将会被Client缓存起来,当下次请求的时候,就不需要走上面的这个流程了。

HBase服务接口
    HBase Thrift ServerHBase REST Server是通过非Java程序对HBase进行访问的一种途径。
 

进入正题

先来看一个HBase集群的模拟环境,此环境中一共有4台机器,分别包含 zookeeperHBaseMasterHReginServerHDSF 4个服务,为了展示失效转发的效果HBaseMasterHReginServer各有2台,只是在一台机器上即运行了HBaseMaster,也运行了HReginServer
注意,HBase的集群环境中HBaseMaster只有失效转发没有压力分载的功能,而HReginServer即提供失效转发也提供压力分载。

服务器清单如下:
    1zookeeper               192.168.20.214
    2HBaseMaster         192.168.20.213/192.168.20.215
    3HReginServer         192.168.20.213/192.168.20.215
    4HDSF                         192.168.20.212

整个模拟环境的架构如图所示:

注意,这里只是做了一个模拟环境,因为这个环境的重点是HBase,所以zookeeperHDFS服务都是单台。 

虽然说在整个HBase的集群环境中只能有一个HMaster,可是在集群环境中HMaster可以启动多个,但真正使用到的HMaster Server只有一个,他不down掉的时候,其他启动的HMaster Server并不会工作,直到与ZooKeeper服务器判断与当前运行的HMaster通讯超时,认为这个正在运行的HMaster服务器down掉了,Zookeeper才会去连接下一台HMaster Server

简单来说,如果运行中HMaster服务器down掉了,那么zookeeper会从列表中选择下一个HMaster 服务器进行访问,让他接管down掉的HMaster任务,换而言之,用Java客户端对HBase进行操作是通过ZooKeeper的,也就是说如果zookeeper集群中的节点全挂了 那么HBase的集群也挂了。本身HBase并不存储中的任何数据 真正的数据是保存在HDFS上,所以HBase的数据是一致的,但是HDFS文件系统挂了,HBase的集群也挂。

在一台HMaster失败后,客户端对HBase集群环境访问时,客户端先会通过zookeeper识别到HMaster运行异常,直到确认多次后,才连接到下一个HMaster,此时,备份的HMaster服务才生效,在IDE环境中的效果,如图所示:

 

上图中能看见抛出的一些异常和name:javahttp://www.javabloger.comname:javahttp://www.javabloger.com1的结果集,因为我在serv215机器上用killall java命令把 HMasterHReginServer都关掉,并且立刻用Java客户端对HBase的集群环境进行访问有异常抛出,但是retry到一定次数后查询出结果,前面已经说了访问HBase是通过zookeeper再和真正的数据打交道,也就是说zookeeper接管了一个standby 的 HMaster,让原先StandbyHMaster接替了失效的HMaster任务,而被接管的HBaseMaster再对HReginServer的任务进行分配,当 HReginServer失败后zookeeper会通知 HMasterHReginServer的任务进行分配。这样充分的说明了HBase做到了实效转发的功能。
如图所示:

 

口水:
1HBase的失效转发的效率比较慢了,不指望能在1-2秒切换和恢复完毕,也许是我暂时没有发现有什么参数可以提高失效转发和恢复过程的速度,将来会继续关注这个问题。
2、在官方网站上看见HBase0.89.20100924的版本有篇讲述关于数据同步的文章,我尝试了一下在一台机器上可以运行所谓的HBase虚拟集群环境,但是切换到多台机器的分布式环境中,单点失效转发的速度很慢比HBase0.20.6还要慢,我又检查了是否存在网络的问题,目前尚未找到正确的答案,对与HBase0.89.20100924 新版中的数据同步的原理,如图所示:(更多信息)


可以留言或者发邮件与我交流,我的联系方式是:njthnet  # gmail.com

三十、 HBase入门篇2-Java操作HBase例子

      本篇文章讲述用HBase Shell命令 和 HBase Java API HBase 服务器 进行操作。在此之前需要对HBase的总体上有个大概的了解。比如说HBase服务器内部由哪些主要部件构成?HBase的内部工作原理是什么?我想学习任何一项知识、技术的态度不能只是知道如何使用,对产品的内部构建一点都不去关心,那样出了问题,很难让你很快的找到答案,甚至我们希望最后能对该项技术的领悟出自己的心得,为我所用,借鉴该项技术其中的设计思想创造出自己的解决方案,更灵活的去应对多变的计算场景与架构设计。以我目前的对HBase的了解还不够深入,将来不断的学习,我会把我所知道的点滴分享到这个Blog上。

     先来看一下读取一行记录HBase是如何进行工作的,首先HBase Client端会连接Zookeeper Qurom(从下面的代码也能看出来,例如:HBASE_CONFIG.set("hbase.zookeeper.quorum", "192.168.50.216") )。通过Zookeeper组件Client能获知哪个Server管理-ROOT- Region。那么Client就去访问管理-ROOT-Server,在META中记录了HBase中所有表信息,(你可以使用  scan '.META.' 命令列出你创建的所有表的详细信息),从而获取Region分布的信息。一旦Client获取了这一行的位置信息,比如这一行属于哪个RegionClient将会缓存这个信息并直接访问HRegionServer。久而久之Client缓存的信息渐渐增多,即使不访问.META.表也能知道去访问哪个HRegionServerHBase中包含两种基本类型的文件,一种用于存储WALlog,另一种用于存储具体的数据,这些数据都通过DFS Client和分布式的文件系统HDFS进行交互实现存储。

如图所示:



查看大图请点击这里

再来看看HBase的一些内存实现原理:    
    * HMaster— HBase中仅有一个Master server
    * HRegionServer—负责多个HRegion使之能向client端提供服务,在HBase cluster中会存在多个HRegionServer
    * ServerManager—负责管理Region server信息,如每个Region serverHServerInfo(这个对象包含HServerAddressstartCode),load Region个数,死亡的Region server列表
    * RegionManager—负责将region分配到region server的具体工作,还监视rootmeta 2个系统级的region状态。
    * RootScanner—定期扫描root region,以发现没有分配的meta region
    * MetaScanner—定期扫描meta region,以发现没有分配的user region

HBase基本命令
下面我们再看看看HBase的一些基本操作命令,我列出了几个常用的HBase Shell命令,如下:

名称

命令表达式

创建表

create '表名称', '列名称1','列名称2','列名称N'

添加记录      

put '表名称', '行名称', '列名称:', ''

查看记录

get '表名称', '行名称'

查看表中的记录总数

count  '表名称'

删除记录

delete  '表名' ,'行名称' , '列名称'

删除一张表

先要屏蔽该表,才能对该表进行删除,第一步 disable '表名称第二步  drop '表名称'

查看所有记录

scan "表名称"  

查看某个表某个列中所有数据

scan "表名称" , ['列名称:']

更新记录 

就是重写一遍进行覆盖

  如果你是一个新手队HBase的一些命令还不算非常熟悉的话,你可以进入 hbase shell 模式中你可以输入 help 命令查看到你可以执行的命令和对该命令的说明,例如对scan这个命令,help中不仅仅提到有这个命令,还详细的说明了scan命令中可以使用的参数和作用,例如,根据列名称查询的方法和带LIMIT STARTROW的使用方法:
scan   Scan a table; pass table name and optionally a dictionary of scanner specifications.  Scanner specifications may include one or more of  the following: LIMIT, STARTROW, STOPROW, TIMESTAMP, or COLUMNS.  If no columns are specified, all columns will be scanned.  To scan all members of a column family, leave the qualifier empty as in  'col_family:'.  Examples:
            hbase> scan '.META.'
            hbase> scan '.META.', {COLUMNS => 'info:regioninfo'}
            hbase> scan 't1', {COLUMNS => ['c1', 'c2'], LIMIT => 10, STARTROW => 'xyz'}


使用Java APIHBase服务器进行操作

需要下列jar
     hbase-0.20.6.jar
     hadoop-core-0.20.1.jar
     commons-logging-1.1.1.jar
     zookeeper-3.3.0.jar
     log4j-1.2.91.jar

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.io.BatchUpdate;

@SuppressWarnings("deprecation")
public class HBaseTestCase {
    
    static HBaseConfiguration cfg = null;
    static {
        Configuration HBASE_CONFIG = new Configuration();
        HBASE_CONFIG.set("hbase.zookeeper.quorum", "192.168.50.216");
        HBASE_CONFIG.set("hbase.zookeeper.property.clientPort", "2181");
        cfg = new HBaseConfiguration(HBASE_CONFIG);
    }
    
    
    /**
     * 创建一张表
     */
    public static void creatTable(String tablename) throws Exception {
        HBaseAdmin admin = new HBaseAdmin(cfg);
        if (admin.tableExists(tablename)) {
            System.out.println("table   Exists!!!");
        }
        else{
            HTableDescriptor tableDesc = new HTableDescriptor(tablename);
            tableDesc.addFamily(new HColumnDescriptor("name:"));
            admin.createTable(tableDesc);
            System.out.println("create table ok .");
        }
    
         
    }
    
    /**
     * 添加一条数据
     */
    public static void addData (String tablename) throws Exception{
         HTable table = new HTable(cfg, tablename);
             BatchUpdate update = new BatchUpdate("Huangyi");  
             update.put("name:java", "http://www.javabloger.com".getBytes());  
             table.commit(update);  
         System.out.println("add data ok .");
    }
    
    /**
     * 显示所有数据
     */
    public static void getAllData (String tablename) throws Exception{
         HTable table = new HTable(cfg, tablename);
         Scan s = new Scan();
         ResultScanner ss = table.getScanner(s);
         for(Result r:ss){
             for(KeyValue kv:r.raw()){
                System.out.print(new String(kv.getColumn()));
                System.out.println(new String(kv.getValue()    ));
             }

         }
    }
    
    
    public static void  main (String [] agrs) {
        try {
                String tablename="tablename";
                HBaseTestCase.creatTable(tablename);
                HBaseTestCase.addData(tablename);
                HBaseTestCase.getAllData(tablename);
            } 
        catch (Exception e) {
            e.printStackTrace();
        }
        
    }
    
}

 

 

 

 

;