程序开发中,经常需要将程序执行中的相关参数进行可配置化,以实现程序的灵活性。在Hadoop环境下编程,也有同样的需求。本文介绍在MapReduce编程中,怎样使用Configuration类读取相关配置。这些配置可能不仅仅在Job配置时需要,有些配置还要在Map或者Reduce编程间传递。
一.读取配置
在以往的编程中,通常将相关参数预先写入相关文件中(如xml,json,Java中的properties,C++中libconfig等),然后在程序初始化时读取该配置项,存入特定的变量中,这种操作在MapReduce中是否可行了?除此之外,是否还有其他方法了?
首先,介绍以往编程中常用的方法,即将配置读取解析后存在一个Singleton对象中,实际运行中会发现这种方法存在局限性。其次,介绍使用Hadoop提供的Configuration类来存取配置项。
代码结构:
//JobConf.java
package com.test.hadoop.conf;
public class JobConf {
private int nAge;
private String strName;
private static JobConf instance = null;
private JobConf(){
}
public static JobConf Instance(){
if(instance != null){
return instance;
}
synchronized (JobConf.class) {
if(instance != null){
return instance;
}
instance = new JobConf();
}
return instance;
}
public void setAge(int age){
this.nAge = age;
}
public void setName(String name){
this.strName = name;
}
public int getAge(){
return this.nAge;
}
public String getName(){
return this.strName;
}
}
//JobTask.java
package com.test.hadoop.conf;
import java.io.IOException;
...
public class JobTask {
private static final Log mLogger = LogFactory.getLog(JobTask.class);
private String MR_NAME = "wordcount";
public boolean execute() throws IOException, ClassNotFoundException, InterruptedException{
Configuration conf = new Configuration();
mLogger.info("JobTask Age:" + JobConf.Instance().getAge() + ", Name:" + JobConf.Instance().getName());
//method 1
conf.addResource("test.xml");
mLogger.info("JobTask flower:" + conf.get("flower")+",color:"+conf.get("color"));
//method 2
conf.set("code.language", "java");
conf.set("compute.method", "mapreduce");
mLogger.info("JobTask language:" + conf.get("code.language")+",method:"+conf.get("compute.method"));
Job job = Job.getInstance(conf, MR_NAME);
...
boolean success = job.waitForCompletion(true);
...
return true;
}
public static void main(String[] args) {
JobTask job = new JobTask();
//bad case
JobConf.Instance().setAge(20);
JobConf.Instance().setName("Coder9527");
try {
if(job.execute()){
mLogger.info(" run success");
}
} catch (Exception e) {
mLogger.error(e);
}
}
}
//JobMap.java
package com.test.hadoop.conf;
import java.io.IOException;
...
public class JobMap extends Mapper<Object, Text, Text, IntWritable>{
private static final Log mLogger = LogFactory.getLog(JobTask.class);
...
protected void setup(Context context){
//bad case
mLogger.info("JobMap Age:" + JobConf.Instance().getAge() + ", Name:" + JobConf.Instance().getName());
//well case
mLogger.info("JobMap flower:" + context.getConfiguration().get("flower")+",color:"+context.getConfiguration().get("color"));
mLogger.info("JobMap language:" + context.getConfiguration().get("code.language")+",method:"+context.getConfiguration().get("compute.method"));
}
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
...
}
}
//JobReduce.java
package com.test.hadoop.conf;
import java.io.IOException;
...
public class JobReduce extends Reducer<Text,IntWritable,Text,IntWritable>{
private static final Log mLogger = LogFactory.getLog(JobTask.class);
...
protected void setup(Context context){
//bad case
mLogger.info("JobReduce Age:" + JobConf.Instance().getAge() + ", Name:" + JobConf.Instance().getName());
//well case
mLogger.info("JobReduce flower:" + context.getConfiguration().get("flower")+",color:"+context.getConfiguration().get("color"));
mLogger.info("JobReduce language:" + context.getConfiguration().get("code.language")+",method:"+context.getConfiguration().get("compute.method"));
}
public void reduce(Text key, Iterable<IntWritable> values,Context context ) throws IOException, InterruptedException {
...
}
}
//test.xml
<configuration>
<property>
<name>flower</name>
<value>rose</value>
<description>flower name</description>
</property>
<property>
<name>color</name>
<value>red</value>
<final>true</final>
<description>flower color,final value,unmodified</description>
</property>
</configuration>
在上述的代码中,以wordcount为例,介绍怎样读取参数,并将参数传递到map和reduce编程中,此处旨在介绍参数的读取等,M/R计算的相关代码在此略过,读者可以参考Hadoop MapReduce-wordcount示例。
JobConf类是一个Singleton,用于保存相关的参数,并且我们期望在其他的类中可以读取到相关参数。main()方法中通过JobTask实例提供的set方法模拟参数的读取。在其他方法中试图通过Singleton提供的get方法获取到该参数。如下:
JobConf.Instance().setAge(20);
JobConf.Instance().getAge();
Configuration类是由Hadoop提供的,通过addResource()方法读取满足特定格式xml文件来读取相关的配置项,读取后的配置项会保存到M/R程序执行的上下文环境中,可以通过Configuration类的get方法获取,也可以在Context对象中获取。
Configuration conf = new Configuration();
conf.addResource("test.xml");
conf.get("flower"); //通过Configuration类的get方法获取
context.getConfiguration().get("flower"); //通过Context对象获取
Configuration类还提供了set方法,直接配置key-value形式的参数,配置后参数同样可以通过get方法和Context对象获取。
那么上述代码的执行结果了?
JobTask中输出:
... INFO conf.JobTask: JobTask Age:20, Name:Coder9527
... INFO conf.JobTask: JobTask flower:rose,color:red
... INFO conf.JobTask: JobTask language:java,method:mapreduce
JobMap中输出:
... INFO [main] com.test.hadoop.conf.JobTask: JobMap Age:0, Name:null
... INFO [main] com.test.hadoop.conf.JobTask: JobMap flower:rose,color:red
... INFO [main] com.test.hadoop.conf.JobTask: JobMap language:java,method:mapreduce
JobReduce中输出:
... INFO [main] com.test.hadoop.conf.JobTask: JobReduce Age:0, Name:null
... INFO [main] com.test.hadoop.conf.JobTask: JobReduce flower:rose,color:red
... INFO [main] com.test.hadoop.conf.JobTask: JobReduce language:java,method:mapreduce
运行结果表明,单例JobConf的结果只能在JobTask中可以获取,而在其他类中没法获取,是一个空指针。事实上在作业提交后,application master就会为该作业所有的map任务和reduce任务向资源管理器请求容器(容器:YARN为资源隔离而提出的框架),每个任务对应一个容器,且只能在该容器中执行。此外在任务执行前将资源本地化,即通过共享文件系统拷贝作业的配置,jar文件和所有来自分布式缓存的文件。由于资源隔离,map任务和reduce任务无法共享task中JobConf之前的set设置。
二、Configuration
通过上面的介绍,在Hadoop环境下的编程更应该使用Configuration类来设置和获取相关配置。下面对于Configuration类进行介绍。
1.Configuration类的配置文件
<configuration>
<property>
<name>flower</name>
<value>rose</value>
<description>flower name</description>
</property>
<property>
<name>color</name>
<value>red</value>
<final>true</final>
<description>flower color,final value,unmodified</description>
</property>
<property>
<name>descript</name>
<value>The color of ${flower} is red</value>
<description>flower color,final value,unmodified</description>
</property>
</configuration>
每个property代表一个配置项,其由属性名称、属性值以及描述定义组成。需要注意的是属性color多了一个final定义,表示此属性不能被覆盖,在第三个属性descript中,其value值被定义为含有可拓展的变量${flower}。
Configuration类addResource方法有多个重载类型,其中addResource(String name),加载的xml文件的路径为{HADOOP_HOME}/etc/hadoop,也就是需要将test.xml放置在此目录下才能读取。如果想加载指定路径下的xml文件配置可以使用addResource(Path file)方法。
下面的代码演示了final定义的作用。
//test.xml
<configuration>
<property>
<name>flower</name>
<value>rose</value>
<description>flower name</description>
</property>
<property>
<name>color</name>
<value>red</value>
<final>true</final>
<description>flower color,final value,unmodified</description>
</property>
</configuration>
//test2.xml
<configuration>
<property>
<name>flower</name>
<value>viole</value>
<description>flower name</description>
</property>
<property>
<name>color</name>
<value>violet</value>
<final>true</final>
<description>flower color,final value,unmodified</description>
</property>
</configuration>
package com.test.hadoop.conf;
import org.apache.hadoop.conf.Configuration;
public class Test {
public static void main(String[] args) {
Configuration conf = new Configuration();
conf.addResource("test.xml");
System.out.println("flower:" + conf.get("flower")+",color:"+conf.get("color"));
conf.addResource("test2.xml");
System.out.println("flower:" + conf.get("flower")+",color:"+conf.get("color"));
}
}
程序输出:
flower:rose,color:red
17/10/12 17:52:58 WARN conf.Configuration: test2.xml:an attempt to override final parameter: color; Ignoring.
flower:viole,color:red
2.属性的覆盖
前面提及Configuraion允许你通过两种方式设置key/value格式的属性,一种是通过set方法,另一种通过addResouce(String name),将一个xml文件加载到Configuration中。那么下面的情况说输出什么?
Configuration conf = new Configuration();
conf.set("flower","viole");
conf.addResource("test.xml"); //flower->rose
System.out.println("flower:" + conf.get("flower"));
其结果是输出”viole”,也就是说”rose”值无法覆盖原来的”viole”。因为当一个配置属性是用户通过set方法设置的时,该属性的来源将被标注为“programatically”,这样的属性是不能被addResource方法覆盖的,必须通过set方法覆盖或修改。事实上在addResource实现中,首先会用指定的xml文件覆盖包含的所有属性,之后再还原“programmatically”来源的那些属性。