背景
从使用hadoop的第一天开始,就一直没有离开过对Hadoop自身功能的开发以及hadoop本身bug的修复的相关开发。这样的开发模式已经持续了好几年,但是可以从中发现的一个现象:对于我们修复的bug或者开发的功能,一直都没有一种很规范,很统一,高效,好管理,并且一目了然的测试的方式。常常的现象是:开发了一个功能或修复了一个bug后,就针对该修改进行一些人为手动的环境模拟和测试,然后测试确认没有问题以后,就合入基线版本进行打包上线。这种模式的缺点是:
1. 没有统一的测试框架和测试规范,很容易导致做一次修改,就需要认为的搭建或模拟一些针对该修改的现象进行测试,而这次测试完成后,环境撤掉,又没有形成相应的文档,导致后续进行版本发布的时候,无法进行回归测试。
2. 测试环境的模拟取决于进行测试的人员自身的考虑。所以对同一个修改的测试,隔一段时间以后换一个人测,方式和结果可能大相径庭。
3. 对于一些性能相关的测试,没有相应的性能测试工具,这样很容易导致对于运行了很长时间的集群,没有人能够准确的说出集群的各种性能指标。对于以后的改进和优化,也无法量化优化和改进的效果数据,很难进行评估。
4. 开发人员跟测试人员通常是同一个人。
其实,hadoop每个版本都包含有很多功能和改进所对应的测试代码,每个版本发布,或者每个patch合入的时候,都会将所有的测试用例进行一次完整的回归,这样可以确保修改不会影响到其他的模块或其他的功能。所以对hadoop测试相关的了解其实跟对hadoop本身代码的了解以及后续的开发,其实同样重要。
本文接下来的部分会对目前hadoop中测试相关的框架和结构进行简单介绍,并会在介绍后提供一些例子,以展示如何对hadoop的内核开发进行测试代码的编写。
0.21以前的版本
目录结构
在hadoop 0.21以前的版本中(这里拿0.20为例,其他版本可能有少许不同),所有的测试相关代码都是放置在${HADOOP_HOME}/src/test下,在该目录下,是按照不同的目录来区分针对不同模块的测试代码。这里需要了解的是:对于相应的hadoop代码和class的包结构,在test中也是以相同的包结构来管理。比如,对于org.apache.hadoop.hdfs.server.namenode中的代码,其源码在src/hdfs/org/apache/hadoop/hdfs/server/namenode中,其测试用例的代码就位于:/src/test/org/apache/hadoop/hdfs/server/namenode内。其他模块以此类推。
测试用例结构
以hdfs为例,对于不需要集群环境的测试,其测试代码就跟寻常的单元测试代码一样,无非是程序级别的一些验证和assert,跟一般的测试用例代码没有什么区别。
MiniDFSCluster
若需要模拟HDFS集群环境,但有没有真是的集群情况下,hadoop测试代码中提供了一个MiniDFSCluster的类,这个类提供了一个本机单进程的hdfs集群环境,用来模拟对hdfs集群环境的模拟。在对该类进行初始化时,程序会根据构造函数参数来设置集群环境下相应的关键配置和参数设置,比如:dfs.name.dir,dfs.data.dir,fs.checkpoint.dir,fs.default.name(这里会设置为hdfs://localhost:port,相当于一个一台机器的hdfs分布式环境),同时会根据从参数获取的datanode数来初始化一些datanode,这样一个真实的分布式环境就能构建出来,如果对某项功能的测试中需要设置相应的namenode或datanode配置参数,只需对构造函数参数中的conf对象进行set即可。
Example
这里拿TestDFSRename用例来做example:
- 构建MiniDFSCluster环境,所以在testcase的setup()中如下初始化hdfs cluster
- 获取DistributeFileSystem实例:
- 编写自己的testRename()方法。
运行testcase
在IDE中调试运行
通常对于这种单元测试的testcase,都是可以在开发环境的IDE中直接运行,如下如所示:
回归
当要发布版本,或者编译新的hadoop版本的时候,可以在编译的同时进行回归测试,将所有相关的testcase全部运行一遍,看某些改动会不会影响到其他的模块的逻辑。这种情况下,可以通过ant来运行相应的target,以运行所有的用例测试。如下图所示:
这样,对target为test-core的所有用例,就会进行一次全部的回归,当所有的用例全部通过,就至少能够保证在已经预料到的情形下,目前的代码版本不会有什么问题了。每个case都会记录其全部的日志,日志记录的路径在: ${HADOOP_HOME}/build/test中,如下图所示:
这样对于出错的testcase,就可以找到相应的出错日志,查看为什么case会失败,进而发现代码修改引发的其他问题。
随着testcase的增多,运行一次完整的回归可能需要花费很长的时间,长的话可能需要好几个小时。所以如果希望在ant模式下运行单独的testcase,也是可以的,可以通过
ant -Dtestcase=${casename} test-core
就可以了,其中casename为测试用例的name,例如TestDFSRename。
一些测试工具
我们常常需要对HDFS或者mapreduce进行一些性能方面的测试,比如测试rpc的性能,测试DFS的IO读写性能,测试DFS的吞吐率性能,测试namenode的benchmark性能,mapreduce的sort性能等等。在hadoop的发行版中,其实已经提供了许多类似的工具,并已经打包成jar,供我们使用。以下是0.20.2中自带的一系列工具列表:
DFSCIOTest | Distributed i/o benchmark of libhdfs. |
DistributedFSCheck | Distributed checkup of the file system consistency. |
MRReliabilityTest | A program that tests the reliability of the MR framework by injecting faults/failures |
TestDFSIO | Distributed i/o benchmark. |
dfsthroughput | measure hdfs throughput |
filebench: | Benchmark SequenceFile(Input|Output)Format (block,record compressed and uncompressed), Text(Input|Output)Format (compressed and uncompressed) |
loadgen | Generic map/reduce load generator |
mapredtest | A map/reduce test check. |
minicluster | Single process HDFS and MR cluster. |
nnbench | A benchmark that stresses the namenode. |
testbigmapoutput | A map/reduce program that works on a very big non-splittable file and does identity map/reduce |
testfilesystem | A test for FileSystem read/write. |
testrpc | A test for rpc |
testsequencefile | A test for flat files of binary key value pairs. |
threadedmapbench: | A map/reduce benchmark that compares the performance of maps with multiple spills over maps with 1 spill |
单独运行每个工具,都会有详细的帮助信息输出到命令行,根据命令行提示,就可以对很多想要进行性能测试的模块进行压力和性能测试。每个工具最终都会输出一个统计结果。若要定制自定义的压力测试工具,可以自己动手编写相应的压力测试程序,然后注册到org.apache.hadoop.test.AllTestDriver中。如想要了解每个benchmark工具的细节,可以从AllTestDriver中找到相应的测试工具的代码。
总结
可以看出,hadoop发行版中的测试环境已经非常丰富,对于模拟集群环境的类,工具等都已经有不少了。这些代码和工具对于程序开发者来说,非常有用。要编写新的测试代码,添加新的测试用例,也都非常方便。对于避免程序修改引起其他的相关问题等,都非常有效。
但是,可以看出,除了一些测试工具外,很多的测试用例都是运行在模拟环境中,并没有针对真实的集群环境进行的相关测试框架。这种缺陷的原因在于,许多的测试用例是需要对hadoop集群的daemon进程进行个性化的设置,这样就造成对集群需要个性化的启停,重启操作,而0.20之前还没有通过java API来实现在测试用例中方便的对真实的集群进行reconfiguration,restart的功能,必须要有外围的人工和脚本的介入。而一旦需要外围人工和脚本的介入,许多的测试就无法达到自动化的效果。所以,从0.21开始,hadoop发行版中引入了一个新的Large-Scale Automated Test Framework(HADOOP-6332)。
0.21开始的版本
从0.21开始,hadoop发行版中引入了一个新的测试框架,Large-Scale Automated Test Framework,该框架跟以前的测试框架不同之处在于,基于它之上的测试的开发是基于真正的集群环境的系统层面的,取名叫做Herriot。
Herriot测试框架最大的特点在于,可以通过Herriot中提供的对HDFS或者MR系统的API,来直接启动,停止,重启一个真实的hadoop集群,并能够保证每次case的运行都是在一个全新的集群执行环境中。这样能够达到的效果就是,通过java代码的testcase,就能够完成所有真实集群环境的自动化测试,而不需要额外的人工和外围脚本的介入。
目录结构
Herriot使用的是JUnit4的框架,JUnit关键的一些fixtures都会在Herriot框架中被用到。如@Before, @After等。所以对于测试的开发人员而言,Herriot测试框架其实就是JUnit的测试用例编程。所以,熟悉JUnit测试用例开发的人,使用Herriot框架都不存在问题。
在新的测试框架中,测试代码被放置在:
src/
test/
system/
test/
[org.apache.hadoop.hdfs|org.apache.hadoop.mapred]
中,而跟Framework相关的代码都位于org.apache.hadoop.test.system中,而HDFS和MR相关的Herriot测试代码,则分别位于org.apache.hadoop.hdfs.test.system, org.apache.hadoop.mapreduce.test.system中。
Example
这里以Herriot系统中真实的case src/test/system/test/org/apache/hadoop/mapred/TestCluster.java为例。
该用例中,从@BeforeClass开始,该before会创建一个cluster proxy的instance(在这里为一个mapreduce cluster),这个proxy能够让程序直接访问MapReduce的daemons进程(JT和TTs)。程序的第二行创建了mapreduce所有的daemon proxies,并通过Herriot library API让这些daemons进程都对测试程序可用。Herriot会保证测试环境是完全clean并且所有的内部daemons的状态都已经reset。不仅如此,所有daemons进程的log也都会被保存下来。这些log非常有用,可以让开发和测试人员方便定位问题。@BeforeClass会确保所有的testcase在运行时,只有一个cluster proxy 的instance在服务。以避免冲突。
新的JT API调用submitAndVerifyJob(Configuration conf)来检查提交的job是否已经成功完成。并会跟踪job运行的details(比如运行了多少map多少reduce),监控job的progress和成功与否,并执行相应的cleanup。如果任意一个过程中发生异常,测试框架将会抛出异常。
下列代码演示了如何修改一个集群的配置并将之重启的过程。随后又使用之前的配置再次进行重启。
1.1 测试用例执行环境
执行测试用例前,测试的client端需要满足如下条件:
l 能够访问某个已有的支持Herriot的hadoop集群
l 相应的hadoop配置文件目录(通常在$HADOOP_CONF_DIR下)
运行testcase的客户端并不需要hadoop的binary包,Herriot test是使用source code直接通过运行如下命令来执行。
ant test-system -Dhadoop.conf.dir.deployed=${HADOOP_CONF_DIR}
这样运行test-system,会将有的testcase全部执行一遍。如果只想运行其中某一个testcase,只需要运行时加上选项 -Dtestcase=testname 就可以了。
当test执行完以后,执行结果和日志都可以从 build-fi/system/test目录下找到。
通常,test client都是在cluster的gateway上,但执行test的client同样也可以是一台slave,笔记本等其他有权访问cluster的机器。