一.前言
分享之前参与过的一个HBase二次开发项目,以及如何使用ITBLL进行集成测试。
1.1 HBase二次开发
-
HBase写操作(Sync模式)性能瓶颈很大一部分在于WAL(Write Ahead Log)写Hlog
-
HBase的一个RegionServer上的所有的Region都共享一个HLog进行WAL
-
二次开发的目的是支持RegionServer多并发写多个HLog,提升HBase写性能
WAL是HBase的RegionServer记录Put,Delete操作内容的一种日志,Client往RegionServer提交数据时,会先写WAL,WAL写成功以后,Client才会返回正常结果,如果写WAL失败会告知Client操作失败,详细参考:www.edureka.co/blog/hbase-…
1.2 面临问题
多HLog改造面临的重要问题是Data Recovery时,WAL数据的保序性。
比如,key00001,cf.f1数据顺序执行put=1,2,3,4
- 1HLog
- 3HLog
- Data Recovery时必须还原出put=1,2,3,4
WAL文件存储了一系列数据操作,每一个操作对应WAL中的一行,新的操作会顺序写在WAL文件的末尾,当RegionServer宕机等异常发生时,会顺序读取WAL并执行恢复操作。
1.3 解决方法
多HLog配置下,使用ITBLL,持续长时间模拟业务操作高并发HBase写过程中,注入Chaos故障事件,最后检查HBase数据的完整性和正确性。
二. ITBLL简介
2.1 ITBLL是什么
ITBLL是IntegrationTestBigLinkList的简称,是hbase源代码hbase-it子项目org.apache.hadoop.hbase.test包下的一个测试类(github.com/apache/hbas… move等各种异常场景下是否存在数据丢失。
2.2 ITBLL原理
顾名思义,ITBLL使用逻辑链表结构来检测一系列操作后是否存在数据丢失,其主要原理如下:
对于一个预先生成的循环链表,从根节点开始分别以parent-node的值为key,child-node的值为value,写入HBase。利用循环链表每个节点的入度和出度应该相等的特点,假设写入HBase的某一个row丢失,则必然有节点的入度不等于出度,来检测是否发生了数据丢失。以循环链表a->b->c->a为例,从节点a开始,写入HBase的将有(a,b)、(b,c)、(c,a)三行数据,如果(b,c)丢失,则链表节点b的入度(1)不等于出度(0)
三.ITBLL原理
实现方面,ITBLL中默认生成1 million个循环链表,每个链表的长度为25,每个链表节点的value大小为16 bytes,即每轮测试写入800MB(32*25M)数据。链表的个数和长度可以通过长度控制。由于每个链表节点的值都是随机生成的,可以保证数据在hbase的表中均匀分布,从而任何一个region发生数据丢失都可以被检测到。
具体到代码上,上述原理主要由以下两个内部类组成:
-
Generator
-
通过runRandomInputGenerator方法,启动MapReduce作业生成待插入HBase的HDFS数据
-
通过runGenerator方法,启动MapReduce作业将数据插入HBase
-
-
Verifier
-
VerifyMapper读取HBase表中的数据,对于每一个key-value对,生成两组数据,分别为:{key, new BytesWritable(new byte[1])}和{value,key}
-
VerifyReducer对于shuffle的每一组数据,分别统计value.length=1和value.length>1的值的数目,如果两个数目相等则verify成功,否则输出当前key并标记verify失败
-
3.1 Generator
3.1.1 runRandomInputGenerator
MapReduce作业(只有Mapper)生成待插入HBase的HDFS文件,文件每行是1条随机生成的value大小为16 bytes
代码实现如下:
public int runRandomInputGenerator(int numMappers, long numNodes, Path tmpOutput, Integer width,
...
job.setJobName("Random Input Generator");
job.setMapperClass(Mapper.class); // identity mapper
job.setNumReduceTasks(0);
job.setInputFormatClass(GeneratorInputFormat.class);
...
}
...
static class GeneratorInputFormat extends InputFormat<BytesWritable, NullWritable> {
static class GeneratorRecordReader extends RecordReader<BytesWritable, NullWritable> {
...
@Override
public BytesWritable getCurrentKey() throws IOException, InterruptedException {
byte[] bytes = new byte[ROWKEY_LENGTH];
rand.nextBytes(bytes);
return new BytesWritable(bytes);
}
...
}
生成HDFS文件格式如下:
$hadoop dfs -cat hdfs://nameservice1:8020/user/zhangfei.t/itbll/generator/output/part-00000
...
LDOAZANDDS3VA1A3
TJ1J4VNIP6V1D86J
MDODC3PNZ7DJD952
5V5G1HEXB0CF0QR8
0ST5E2OQ6W6JZJCT
...
3.1.2 runGenerator
MapReduce作业(只有Mapper)读取runRandomInputGenerator生成的HDFS文件,以循环链表的形式写入HBase
代码实现如下:
public int runGenerator(int numMappers, long numNodes, Path tmpOutput, Integer width,
Integer wrapMultiplier, Integer numWalkers) throws Exception {
...
job.setJobName("Link Generator");
job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job, tmpOutput);
job.setMapperClass(GeneratorMapper.class);
...
}
...
static class GeneratorMapper
extends Mapper<BytesWritable, NullWritable, NullWritable, NullWritable> {
...
@Override
protected void map(BytesWritable key, NullWritable value, Context output) throws IOException {
current[i] = new byte[key.getLength()];
System.arraycopy(key.getBytes(), 0, current[i], 0, key.getLength());
...
persist(output, count, prev, current, id);
...
}
...
protected void persist(Context output, long count, byte[][] prev, byte[][] current, byte[] id)
throws IOException {
for (int i = 0; i < current.length; i++) {
...
Put put = new Put(current[i]);
put.addColumn(FAMILY_NAME, COLUMN_PREV, prev == null ? NO_KEY : prev[i]);
...
mutator.mutate(put);
}
...
}
}
写入HBase数据如下:
# Scan all rows of table 'IntegrationTestBigLinkedList'
hbase> scan 'IntegrationTestBigLinkedList'
ROW COLUMN+CELL
LDOAZANDDS3VA1A3 column=meta:prev, timestamp = xxxxx, value = 1FF5P2OQ6W6JZNAP
TJ1J4VNIP6V1D86J column=meta:prev, timestamp = xxxxx, value = LDOAZANDDS3VA1A3
MDODC3PNZ7DJD952 column=meta:prev, timestamp = xxxxx, value = TJ1J4VNIP6V1D86J
5V5G1HEXB0CF0QR8 column=meta:prev, timestamp = xxxxx, value = MDODC3PNZ7DJD952
0ST5E2OQ6W6JZJCT column=meta:prev, timestamp = xxxxx, value = 5V5G1HEXB0CF0QR8
....
3.2 Verify
MapReduce作业,map阶段读取HBase数据对每行数据写执行context.write(rowkey, rowkey.length),context.write(prev, prev.length),reduce阶段根据聚合后的values长度检查是否有数据丢失。
代码实现如下:
public static class VerifyMapper extends TableMapper<BytesWritable, BytesWritable> {
...
@Override
protected void map(ImmutableBytesWritable key, Result value, Context context)
throws IOException, InterruptedException {
byte[] rowKey = key.get();
row.set(rowKey, 0, rowKey.length);
context.write(row, DEF);
byte[] prev = value.getValue(FAMILY_NAME, COLUMN_PREV);
ref.set(prev, 0, prev.length);
context.write(ref, row);
...
}
...
/**
* Per reducer, we output problem rows as byte arrays so can be used as input for subsequent
* investigative mapreduce jobs. Each emitted value is prefaced by a one byte flag saying what
* sort of emission it is. Flag is the Count enum ordinal as a short.
*/
public static class VerifyReducer
extends Reducer<BytesWritable, BytesWritable, BytesWritable, BytesWritable> {
...
}
}
3.3 Chaos
在生成数据的同时,可以通过参数指定在运行ITBLL的过程中启动的ChaosMonkey类型,默认使用CalmChaosMonkey,即不进行任何异常操作。而我们通常使用“-m slowDeterministic”来使用SlowDeterministicMonkeyFactory类生成的PolicyBasedChaosMonkey,会在测试过程中进行kill RegionServer/Master,Region split/move等异常操作,具体参考SlowDeterministicMonkeyFactory类的build方法(github.com/apache/hbas…
四.ITBLL 使用方法
首先,由于ITBLL需要启动MapReduce作业,因此需要事先搭好Yarn集群(可以部署使用standalone,详细参考Hadoop: Setting up a Single Node Cluster)。
然后,进入hadoop-current目录,通过下述命令启动ITBLL:
./bin/hadoop org.apache.hadoop.hbase.test.IntegrationTestBigLinkedList Loop 1 25 1000000 /tmp/itbll 10
其中参数的含义如下:
Usage: Loop <num iterations> <num mappers> <num nodes per mapper> <output dir> <num reducers> [<width> <wrap multiplier>]