在对HBase集群进行调整之前,你需要先知道它的性能如何。因此,我们将使用Yahoo! Cloud Serving Benchmark(YCSB,雅虎云服务基准)来测量HBase集群的性能(基准性能测试)。
HBase集群基本上可按负载类型分为两类:写密集的集群和读密集的集群。每种类型的调优选项不同。许多调优方案都需要在写性能和读性能之间作出权衡。我们将介绍几种调整HBase集群以获得更好写入性能的方法,同时我们也将介绍几种调整读密集HBase集群的方法。这些方法包括服务器端配置、客户端配置和表模式选择。
没有一种调整能够适用于所有的情形。你需要仔细考虑你的系统的性能要求,然后调整集群以获得写入性能和读取性能之间的最佳平衡。
一、使用YCSB对HBase集群进行基准测试
测量HBase集群的性能(或对集群进行基准性能测试)与集群调优本身一样重要。HBase集群需要测量的性能特性至少包括以下这些内容:
- 集群的总体吞吐量(每秒的操作数)
- 集群的平均延迟时间(每个操作的平均时间)
- 最小延迟
- 最大延迟
- 操作延迟的分布情况
YCSB是一个很棒的工具,可对HBase集群进行基准性能测试。YCSB支持以并行方式运行可变的负载测试,可以评估系统的插入、更新、删除和读取性能。因此,对于写密集和读密集HBase集群都可以使用YCSB来进行基准性能测试。每个测试都可以配置其要加载的记录数、要执行的操作次数、读写比以及许多其他属性,这样就可以很容易地使用YCSB来测试集群的不同负载情形。
YCSB也可以用于对许多别的键-值存储进行性能评估。YCSB的常见用途就是对多个系统进行基准测试,然后对它们的性能进行比较。
HBase带有一个自己的性能评价(PE)的工具,它也可以用来对HBase进行基准测试。
二、增加RegionServer的处理线程数
RegionServer要保持一定数量的线程处于运行状态,以此来响应传入的对于用户表的请求。为了防止RegionServer在运行时耗尽内存,该线程数默认都设得非常低。在很多情况下,尤其是在有大量并发客户端的时候,为了能处理更多请求,你需要将该值调高。
vi $HBASE_HOME/conf/hbase-site.xml
<property>
<name>hbase.regionserver.handler.count</name>
<value>40</value>
</property>
hbase.regionserver.handler.count属性可控制RPC侦听程序的线程数。该属性的默认值为10。这是一个相当低的值,这样设置的目的是防止RegionServer在某些情况下出现耗尽内存的情况。
如果RegionServer上的可用内存较少,就应该将该属性设为一个较低的值。较低的值也适用于需要大量内存才能对请求进行处理的情形(比如将大值写入HBase或在大型缓存中扫描数据)。将hbase.regionserver.handler.count属性设为较高值意味着可以有更多的并发客户端,这可能会消耗掉很多RegionServer的内存,甚至有可能会用光所有的内存。
如果每个请求只需要一点点内存,但每秒的交易次数(TPS)却很高,那么就应该将该属性设为一个较大的值,以使RegionServer可以处理更多的并发请求。
在调整该属性的同时,建议启用RPC级的日志记录功能,然后监控每个RPC请求的内存使用情况和GC状态。
三、使用自定义算法预创建区域
当我们在HBase中创建一张表时,该表一开始只有一个区域。插入该表的所有数据会保存在这个区域中。随着数据的不断增加,当该区域的大小达到一定阀值时,就会发生区域分割(Region Splitting)。这个区域会分成两半,以便使该表可以处理更多的数据。
在写密集的HBase集群中,区域分割会带来以下几个需要解决的问题。
如果数据均匀地增长,大部分区域就会在同一时间发生分割,从而导致大量的磁盘I/O和网络流量。
尤其是在表刚创建时,所有请求都会发给首个区域所在的那台RegionServer。
对于第一个问题,可以参考“HBase基本性能调整”的“管理区域分割”一节,可通过手动分割的方法来解决。
对于第二个问题,可以参考“在数据移入HBase前预创建区域”一节,可通过在创建表时预先创建好一些区域的方法来避免这一问题。使用HBase的RegionSplitter工具来预创建区域的方法。
在默认情况下,RegionSplitter实用工具会使用MD5算法来生成MD5校验和,以此来作为区域的开始键。键值的范围是“00000000”到“7FFFFFFF”。这种算法(HexStringSplit)适用于很多情况,但在有些情况下,你可能更需要使用一种自己的算法来生成键值,以使负载能在集群中分散开来。
HBase的行键完全由向HBase写入数据的应用程序所控制,在很多情况下,(由于这样或那样的原因)行键的范围和分布情况是可以预测的。因此,可以先计算出各区域的分割键,然后再使用这些分割键来创建预分割区域。
我们将使用一组记录在文本文件中的区域开始键来创建表的预定义区域。
1.创建一个split-keys文件。在该文件中输入一组区域分割键,每个键一行。假设该文件包含有如下一些开始键:
$ cat split-keys
a0000
affff
b0000
bffff
2.创建Java类FileSplit,用它来实现org.apache.hadoop.hbase.util.RegionSplitter.SplitAlgorithm接口。
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.RegionSplitter.SplitAlgorithm;
public class FileSplit implements SplitAlgorithm {
// public static final String SPLIT_KEY_FILE = "split-keys";
private String splitKeyFile;
public FileSplit (String splitKeyFile) {
this.splitKeyFile = splitKeyFile;
}
@Override
public byte[] split(byte[] start, byte[] end) {
return null;
}
@Override
public byte[][] split(int numberOfSplits) {
BufferedReader reader=null;
try {
List<byte[]> regions = new ArrayList<byte[]>();
//一行一行读
reader = new BufferedReader(new FileReader(splitKeyFile));
String line;
while ((line = reader.readLine()) != null) {
// System.out.println(line);
if (line.trim().length() > 0) {
regions.add(Bytes.toBytes(line));
}
}
return regions.toArray(new byte[0][]);
} catch (IOException e) {
throw new RuntimeException("Error reading splitting keys from " + splitKeyFile, e);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (reader != null) reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public byte[] firstRow() {
return null;
}
@Override
public byte[] lastRow() {
return null;
}
@Override
public byte[] strToRow(String input) {
return null;
}
@Override
public String rowToStr(byte[] row) {
return null;
}
@Override
public String separator() {
return null;
}
}
3.编译该Java文件。
$ javac -classpath $HBASE_HOME/hbase-0.92.0.jar FileSplit.java
4.将包含有分割键信息的split-keys文件复制到编译生成FileSplit类的目录下。
5.运行如下脚本来在创建表的时候预创建一些区域。
$ export HBASE_CLASSPATH=$HBASE_CLASSPATH:./
$ $HBASE_HOME/bin/hbase org.apache.hadoop.hbase.util.RegionSplitter -D split.algorithm=FileSplit -c 2 -f f1 test_table
6.通过HBase的Web用户界面确认表和预定义区域是否已正确创建。
HBase带有一个名为RegionSplitter的实用工具,该类可以用于:
- 创建带预分割区域的HBase表
- 对现有的表中的所有区域进行滚动分割
- 使用自定义算法来分割区域
为了能在创建表时分割区域,只需要像第2步那样实现一下split()方法。RegionSplitter类会调用该方法来对整张表进行分割。在我们的实现中,该方法会从我们准备好的文件中读取分割键(每个键一行),然后将它们转换并保存在一个存储该表所有初始区域的分割键的byte[]数组中。
为了能使用该类来分割区域,我们将FileSplit类加入到了HBASE_CLASSPATH中,然后以带如下参数的方式调用了RegionSplitter实用程序。
- -D split.algorithm=FileSplit:指定分割算法。
- -c 2:表分割的区域数。在本实现中没有用到。
- -f f1:创建一个名为“f1”的列族。
- test_table:待创建的表的名称。
正如你在第6步中看到那样,我们用4个分割键将test_table分成了5个区域。这4个分割键正是我们写在split-keys文件中的内容。
该表的其他属性都使用了默认值,你可以在HBase Shell中使用alter命令来修改其中的一些属性。
请注意,即便是在对区域进行过分割之后,也仍然需要在应用层对行键进行设计,以避免将过多连续的行键写入同一个区域。你需要仔细地选择,找到一种适合你的数据访问模式的分割算法。
该方法的另一个适用场景是用来提高将HBase表导出备份的数据导入回HBase的速度。
你可以使用一个简单的脚本来备份各区域的开始键。有了FileSplit类,我们可以使用原来的这些键来恢复HBase表的区域边界,然后再以使用导出数据文件进行导入的方式来恢复数据。
与将数据导入到同一个区域相比,这种方法能够显著提高数据恢复的速度,因为它会提前在多台RegionServer上恢复好多个区域,并且让这些区域均衡分布在多台RegionServer上。
四、避免写密集集群中的更新阻塞
在写密集的HBase集群中,你可能会发现有写入速度不稳定的现象。大多数写操作都非常快,但有些写操作却非常慢。对于一个在线系统来说,无论平均速度如何,这种写入速度的不稳定都是不可接受的。
导致这种情况的原因可能有两个:
- 分割/合并使得集群的负荷非常高
- 更新操作被RegionServer给阻塞住了
正如我们在“HBase基本性能调整”中描述的那样,你可以通过“禁用自动分割/合并,然后在低负载时手动进行分割/合并”的方式来避免分割/合并问题。
要在RegionServer日志中进行一下查找,如果发现很多带“Blocking updates for ...”字样的消息,说明很可能就有很多更新都被阻塞住了,这样的更新操作的响应时间可能就是非常差。
若要解决这类问题,必须同时对服务器端和客户端的配置作出调整,才能获得稳定的写入速度。
vi $HBASE_HOME/conf/hbase-site.xml
<property>
<name>hbase.hregion.memstore.block.multiplier</name>
<value>8</value>
</property>
<property>
<name>hbase.hstore.blockingStoreFiles</name>
<value>20</value>
</property>
HBase写入操作的工作流程。
数据的修改首先会被写入到了RegionServer的HLog(WAL预写日志)中,以此方式来持久化在HDFS中。然后,该修改被传给了宿主HRegion,然后传入到其所属列族的HStore的内存空间里(即MemStore)。当MemStore的大小达到一定阀值时,数据修改会被写入到HDFS上的StoreFile中。StoreFiles在内部使用HFile文件格式来存储数据。
HBase是一种多版本并发控制(MVCC,Multiversion Concurrency Control)的架构。在更新/删除任何旧数据时,HBase都不会进行覆盖,而是给数据增加一个新版本。这使得HBase的写速度非常快,因为所有写操作都是顺序化的操作。
如果HDFS上的小StoreFile太多,HBase就会启动合并操作来将它们重写为几个大一些的StoreFile,同时减少StoreFile文件的数量。当一个区域的大小达到一定阀值时,它又会被分成两个部分。
为了防止合并/分割的时间过长并导致内存耗尽的错误,在某一区域的MemStore的大小达到一定阀值时,HBase会对更新进行阻塞。该阀值的定义为:
hbase.hregion.memstore.flush.size 乘以hbase.hregion.memstore.block.multiplier
hbase.hregion.memstore.flush.size属性指定了MemStore在大小达到何值时会被写入到磁盘中。其默认值是128MB(0.90版本是64MB)。
hbase.hregion.memstore.block.multiplier属性的默认值是2,这意味着,如果MemStore的大小达到了256MB(128 * 2),该区域上的更新将被阻塞。对于写密集集群的更新高峰期来说,这个值太小了,所以我们要调高该阻塞阀值。
我们通常会让hbase.hregion.memstore.flush.size属性保持为默认值,然后将hbase.hregion.memstore.block.multiplier属性调整为一个较大的值(比如8),以此来提高MemStore阻塞阀值,减少更新阻塞发生的次数。
请注意,调高hbase.hregion.memstore.block.multiplier会增大写盘时出现合并/分割问题的可能性,所以调整时一定要小心。
第2步适用于另一种阻塞情况。如果任何一个Store的StoreFiles数(每个写盘MemStore一个StoreFile)超过了hbase.hstore.blockingStoreFiles(默认为7),那么该区域的更新就会被阻塞,直到合并完成或超过hbase.hstore.blockingWaitTime(默认90秒)所指定的时间为止。我们将它加大到了20,这对于写密集集群来说是一个相当大的值了。这样做的副作用是:会有更多的文件需要合并。
这种调整通常会减少更新阻塞的发生机会。然而,正如我们刚才提到的那样,它也有副作用。建议在调整这些设置时务必小心,调整期间务必时刻观察写操作的吞吐量和延迟时间,这样才能找到最佳的配置值。
五、调整MemStore内存大小
HBase的写操作会首先在所属区域的MemStore中生效,然后要等到MemStore的大小达到一定阀值之后才会被写入HDFS,从而省出一部分内存空间。MemStore的写盘操作以后台线程的方式运行,使用的是MemStore的快照。因此,即便是在MemStore写盘时,HBase仍可以一直处理写操作。这使得HBase的写入速度非常快。如果写操作的高峰值高到MemStore写盘都跟不上趟的程度,那么写操作填满MemStore的速度就会不断提高,而MemStore的总内存使用量也将继续升高。如果某一RegionServer中所有MemStore的总大小达到了某一可配置的阀值,更新又会被阻塞,并且强制进行写盘。
调整MemStore总体内存的大小以避免发生更新阻塞。
vi $HBASE_HOME/conf/hbase-site.xml
<property>
<name>hbase.regionserver.global.memstore.upperLimit</name>
<value>0.45</value>
</property>
<property>
<name>hbase.regionserver.global.memstore.lowerLimit</name>
<value>0.4</value>
</property>
hbase.regionserver.global.memstore.upperLimit属性控制了一台RegionServer中所有MemStore的总大小的最大值,超过该值后,新的更新就会被阻塞,并且强制进行写盘。这是一项可防止HBase在写入高峰时耗尽内存的配置信息。该属性默认为0.4,这意味着RegionServer堆大小的40%。
该默认值在很多情况下都可以很好地发挥作用。但是,如果RegionServer日志中出现了许多含有Flush of region xxxx due to global heap pressure的日志,那么可能就需要调整该属性来处理这种高写入速度了。
在第2步中,我们调整了hbase.regionserver.global.memstore.lowerLimit属性,它指定了何时对MemStores进行强制写盘,系统会一直进行写盘直到MemStore所占用的总内存大小低于该属性的值为止。其默认值是RegionServer堆大小的35%。
在写密集集群中,调高这两个值有助于减少更新因MemStore大小限制而被阻塞的机会。另一方面,调整时必须十分小心,避免引发全垃圾回收或出现内存耗尽的错误。
通常情况下,写密集集群上的读性能不像写性能那么重要。因此,我们可以为了优化写操作而调整集群。优化方法之一就是:减少分配给HBase块缓存的内存空间,将空间留给MemStore。关于如何调整块缓存的大小,请参见“调高读密集集群的块缓存大小”一节。
六、低延迟系统的客户端调节
前面两节介绍的是避免服务器端阻塞的方法。可以帮助集群稳定、高性能地运行。服务器端的调整可以显著改善集群的吞吐量和平均延迟时间。
然而,在低延迟系统和实时系统中,只进行服务器端的调优还不够。在低延迟系统中,长时间的停顿即使只是偶尔发生一两次也不可以接受。
为了避免长时间的停顿,还要对客户端进行一些调节。
在写密集集群中进行客户端调整的步骤如下。
vi $HBASE_HOME/conf/hbase-site.xml
<property>
<name>hbase.client.pause</name>
<value>20</value>
</property>
<property>
<name>hbase.client.retries.number</name>
<value>11</value>
</property>
<property>
<name>hbase.ipc.client.tcpnodelay</name>
<value>true</value>
</property>
<property>
<name>ipc.ping.interval</name>
<value>4000</value>
</property>
在第1步和第2步中调整hbase.client.pause和hbase.client.retries.number属性的目的是:让客户端在连接集群失败时能在很短的时间内迅速重试。
hbase.client.pause属性控制的是让客户端在两次重试之间休眠多久。其默认值是1000毫秒(1秒)。hbase.client.retries.number属性用来指定最大重试次数。其默认值是10。
每两次重试之间的休眠时间可按下面这个公式计算得出。
pause_time =hbase.client.pause * RETRY_BACKOFF[retries]
其中,RETRY_BACKOFF是一个重试系数表,其定义如下。
public static intRETRY_BACKOFF[] = { 1,1,1,2,2,4,4,8,16,32};
在重试10次以后,HBase就会一直使用最后一个系数(32)来计算休眠时间。
如果将暂停时间设为20毫秒,最大重试次数设为11,每两次连接集群重试之间的暂停时间将依次为:
{ 20, 20, 20, 40, 40, 80, 80, 160, 320, 640, 640}
这就意味着客户端将在2060毫秒内重试11次,然后放弃连接到集群。
在第3步中,我们将hbase.ipc.client.tcpnodelay设为了true。此设置将禁止使用Nagle算法来进行客户端和服务器之间的套接字传输。
Nagle算法是一种提高网络效率的手段,它会将若干较小的传出消息存在缓冲区中,然后再将它们一次全都发送出去。Nagle算法默认是启用的。低延迟系统应该将hbase.ipc.client.tcpnodelay设置为true,从而禁用Nagle算法。
在第4步中,我们将ipc.ping.interval设为了4000毫秒(4秒),这样客户端和服务器之间的套接字传输就不会超时。ipc.ping.interval的默认值是1分钟,对于低延迟系统来说,这有点太长了。
你也可以在客户端代码中使用org.apache.hadoop.hbase.HBaseConfiguration类来覆盖之前在hbase-site.xml中设置的各属性的值。下面代码可以实现与前面的第1步和第2步设定同样的效果。
Configuration conf = HBaseConfiguration.create();
conf.setInt("hbase.client.pause", 20);
conf.setInt("hbase.client.retries.number", 11);
HTable table = new HTable(conf, "tableName");