摘要: 在学习使用Hadoop的时候肯定会遇到以下问题,想当然的定义一些静态全局变量,好让mapper和reducer能够访问到这些变量里边的内容,然后兴高采烈的去写代码,最后得到空指针异常,很懊恼,怎么就不行呢,打破了传统对java的理解。
其实对java的理解没有错,只是对Hadoop架构的理解错了,为什么全局静态变量访问不到,是因为启动job的程序和运行job的程序不在同一个jvm中,你想用jvmb去访问jvma里的数据是不行的,最起码用你常用的方式--全局变量是不行的。
那么,难道就没有办法了吗,当然不是,办法自然有,那就是configuration对象。
为什么会想到这个,先简单分析一下,我们会使用configuration对象去配置job的参数,这些参数在map task和reduce task都要使用到的,所以可以贯穿始终。但是在mapper和reducer里没有发现这个对象啊,发现贯穿mapper和reducer的对象是一个context对象,可喜的是这个context对象包含了configuration对象,根据见名知意的原则,上下文对象报错有贯穿始终的配置信息是非常合理的一种猜测与理解。
先不过多说原理,先看例子然后在慢慢猜测理解其原理。
方式一:Configuration传递参数
configuration类中提供了大量的set和get方法,可以简单的存放于获取基本数据类型和String类型。
那我们就来看一个例子,如何实现:
Configuration conf = new Configuration();
// 传递参数方式一 :简单字符串
conf.set("key1", "value1");
在入口程序中通过conf对象设置一个String类型的值,然后在Mapper和Reducer中分别取获取这个值:
// 获取参数方式一:获取简单字符串
Configuration conf = context.getConfiguration();
String value1 = conf.get("key1");
LOG.info("第一个参数: " + value1);
运行程序后,得到如下结果:
第一个参数: value1
这个是mappe task 和reduce task log打印出来的信息,说明参数传递成功
至于其他的基本类型的设置方式与String类型类似,并且其实还是转化为String类型进行存取,例如int型:
/**
* Set the value of the <code>name</code> property to an <code>int</code>.
*
* @param name property name.
* @param value <code>int</code> value of the property.
*/
public void setInt(String name, int value) {
set(name, Integer.toString(value));
}
第一种方式就不过多介绍,因为很简单,使用起来也很方便,但是有人就有问题了,这个只能传递String类型和基本类型,那么对象怎么传递呢?这个是个好问题,hadoop的工程师不可能想不到,下面就来介绍一个传递对象的方式。
方式二:DefaultStringifier传递参数
继续先查看一下这个类的API,看看有什么接口提供用于传参。
接口也很明显,支持传递对象和对象数组,但是这次先不能着急试,先简单看一下API,比如看一下store:
/**
* Stores the item in the configuration with the given keyName.
*
* @param <K> the class of the item
* @param conf the configuration to store
* @param item the object to be stored
* @param keyName the name of the key to use
* @throws IOException : forwards Exceptions from the underlying
* {@link Serialization} classes.
*/
public static <K> void store(Configuration conf, K item, String keyName)
throws IOException {
DefaultStringifier<K> stringifier = new DefaultStringifier<K>(conf,
GenericsUtil.getClass(item));
conf.set(keyName, stringifier.toString(item));
stringifier.close();
}
这里用了toString方法,并且看到了属性的conf.set()方法,这说明什么,他把对象转换成了字符串,然后使用configuration对象的set方法进行存放,和我们将的第一种方式是一样的。
再来看一下toString方法:
public String toString(T obj) throws IOException {
outBuf.reset();
serializer.serialize(obj);
byte[] buf = new byte[outBuf.getLength()];
System.arraycopy(outBuf.getData(), 0, buf, 0, buf.length);
return new String(Base64.encodeBase64(buf));
}
这里又发现一个现象,它把对象序列化了,其实也不难想象,如果要把对象转换为字符串,序列化是不是一种途径,很正常。可是又有问题了,我们定义一个普通的对象用于传参行吗?答案肯定是不行的,至少要实现Serializable接口吧。不错,思路很对,但是hadoop毕竟是个框架,它本身提供了一种序列化方案,并不是实现Serializable接口,而是Writable接口,所以,在尝试前,定义的用于传递对象的类要实现Writable接口。
我来定义一个简单的类:
package com.darren.hadoop.transfer;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;
public class SimpleParameter implements Writable {
private String name;
private String value;
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.name);
out.writeUTF(this.value);
}
@Override
public void readFields(DataInput in) throws IOException {
this.name = in.readUTF();
this.value = in.readUTF();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Parameter [name=" + name + ", value=" + value + "]";
}
}
这里要实现以下Writable的方法,用于序列化。
然后我们使用一下:
// 传递参数方式二 :简单对象
SimpleParameter param = new SimpleParameter();
param.setName("name");
param.setValue("Darren");
DefaultStringifier.store(conf, param, "key2");
// 传递参数方式三 : 对象数组
SimpleParameter param1 = new SimpleParameter();
param1.setName("name1");
param1.setValue("Darren1");
List<SimpleParameter> list = new ArrayList<>();
list.add(param);
list.add(param1);
DefaultStringifier.storeArray(conf, list.toArray(), "key3");
Mapper中:
// 获取参数方式二: 获取简单对象
SimpleParameter param = DefaultStringifier.load(conf, "key2", SimpleParameter.class);
LOG.info("第二个参数: " + param);
// 获取参数方式三:获取对象数组
SimpleParameter[] params = DefaultStringifier.loadArray(conf, "key3", SimpleParameter.class);
for (SimpleParameter parameter : params) {
LOG.info("第三个参数: " + parameter);
}
运行结果:
第二个参数: Parameter [name=name, value=Darren]
第三个参数: Parameter [name=name, value=Darren]
第三个参数: Parameter [name=name1, value=Darren1]
结果也很正确。
刚才看了store和toString方法,猜测一下load和fromString干了什么事,load肯定是从conf对象中获取传递的字符串,fromString把字符串反序列化成对象,接下来验证一下对不对:
/**
* Restores the object from the configuration.
*
* @param <K> the class of the item
* @param conf the configuration to use
* @param keyName the name of the key to use
* @param itemClass the class of the item
* @return restored object
* @throws IOException : forwards Exceptions from the underlying
* {@link Serialization} classes.
*/
public static <K> K load(Configuration conf, String keyName,
Class<K> itemClass) throws IOException {
DefaultStringifier<K> stringifier = new DefaultStringifier<K>(conf,
itemClass);
try {
String itemStr = conf.get(keyName);
return stringifier.fromString(itemStr);
} finally {
stringifier.close();
}
}
public T fromString(String str) throws IOException {
try {
byte[] bytes = Base64.decodeBase64(str.getBytes("UTF-8"));
inBuf.reset(bytes, bytes.length);
T restored = deserializer.deserialize(null);
return restored;
} catch (UnsupportedCharsetException ex) {
throw new IOException(ex.toString());
}
}
没问题吧,这个用起来是不是也还算方便。
那么问题又来了,如果我想传递一个map怎么办,因为我们常用的是不是都应map这个结果存储啊。
其实,hadoop也提供了解决方案,它提供了MapWritable类,我们直接来看例子吧:
// 传递参数方式四 : 复杂Map
MapWritable map = new MapWritable();
map.put(new Text("param"), param);
map.put(new Text("param1"), param1);
DefaultStringifier.store(conf, map, "key4");
// 获取参数方式四:获取复杂Map
MapWritable map = DefaultStringifier.load(conf, "key4", MapWritable.class);
Set<Entry<Writable, Writable>> entrySet = map.entrySet();
for (Entry<Writable, Writable> entry : entrySet) {
LOG.info("第四个参数: " + entry.getKey().toString() + ": " + entry.getValue().toString());
}
运行结果:
第四个参数: param: Parameter [name=name, value=Darren]
第四个参数: param1: Parameter [name=name1, value=Darren1]
由于map的key和value都要求是Writable类型的,所以你只要实现Writable接口就可以了吧,当然,如果是key的话再重写一下equals和hashcode方法就行了,不过hadoop也提供了依稀作为key的常用类型,String类型的话你可以用Text类,总之各种常用类型都要,请看下图:
value的话,我是用的是自定义的对象,这样不就可以实现一个复杂Map的传递了吗。
如果是复杂的对象呢,比如:
package com.darren.hadoop.transfer;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ComplexParameter implements Serializable {
/**
*
*/
private static final long serialVersionUID = 3689550651827430036L;
private String name;
private SimpleParameter silpleParameter;
private Map<String, String> simpleMap = new HashMap<>();
private Map<String, SimpleParameter> complexMap = new HashMap<>();
private List<SimpleParameter> complexList = new ArrayList<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SimpleParameter getSilpleParameter() {
return silpleParameter;
}
public void setSilpleParameter(SimpleParameter silpleParameter) {
this.silpleParameter = silpleParameter;
}
public Map<String, String> getSimpleMap() {
return simpleMap;
}
public void setSimpleMap(Map<String, String> simpleMap) {
this.simpleMap = simpleMap;
}
public Map<String, SimpleParameter> getComplexMap() {
return complexMap;
}
public void setComplexMap(Map<String, SimpleParameter> complexMap) {
this.complexMap = complexMap;
}
public List<SimpleParameter> getComplexList() {
return complexList;
}
public void setComplexList(List<SimpleParameter> complexList) {
this.complexList = complexList;
}
@Override
public String toString() {
return "ComplexParameter [name=" + name + ", silpleParameter=" + silpleParameter + ", simpleMap=" + simpleMap
+ ", complexMap=" + complexMap + ", complexList=" + complexList + "]";
}
}
这个对象很复杂,实现Writable接口似乎也不便于序列化,那怎么办呢?
方式三:实现Serializable接口,自己序列化
这个方案就是自己序列化的一种,转换为JSON字符串是不是也可以啊,我们在这里就采用java序列化的方式:
这个时候SimpleParameter也要实现Serializable接口:
package com.darren.hadoop.transfer;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
import org.apache.hadoop.io.Writable;
public class SimpleParameter implements Writable, Serializable {
/**
*
*/
private static final long serialVersionUID = 898737795340677656L;
private String name;
private String value;
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.name);
out.writeUTF(this.value);
}
@Override
public void readFields(DataInput in) throws IOException {
this.name = in.readUTF();
this.value = in.readUTF();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Parameter [name=" + name + ", value=" + value + "]";
}
}
// 传递参数方式五 : 复杂对象, 复杂对象需要手动序列化
ComplexParameter complexParameter = new ComplexParameter();
complexParameter.setName("Darren");
complexParameter.setComplexList(list);
complexParameter.setSilpleParameter(param);
Map<String, SimpleParameter> complexMap = new HashMap<>();
complexMap.put("test", param);
complexParameter.setComplexMap(complexMap);
Map<String, String> simpleMap = new HashMap<>();
simpleMap.put("name", "Darren");
complexParameter.setSimpleMap(simpleMap);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(complexParameter);
objOut.flush();
objOut.close();
String complexString = out.toString("ISO-8859-1");
conf.set("key5", URLEncoder.encode(complexString, "UTF-8"));
// 获取参数方式五: 获取复杂对象
String value5 = conf.get("key5");
LOG.info("第五个参数: " + value5);
ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(URLDecoder.decode(value5, "UTF-8").getBytes("ISO-8859-1")));
ComplexParameter complexParameter = null;
try {
complexParameter = (ComplexParameter) objIn.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
LOG.info("第五个参数: " + complexParameter);
运行结果:
第五个参数: %C2%AC%C3%AD%00%05sr%00%2Bcom.darren.hadoop.transfer.ComplexParameter33%C3%AA%C3%85%0E%C3%BC%22%C2%94%02%00%05L%00%0BcomplexListt%00%10Ljava%2Futil%2FList%3BL%00%0AcomplexMapt%00%0FLjava%2Futil%2FMap%3BL%00%04namet%00%12Ljava%2Flang%2FString%3BL%00%0FsilpleParametert%00%2CLcom%2Fdarren%2Fhadoop%2Ftransfer%2FSimpleParameter%3BL%00%09simpleMapq%00%7E%00%02xpsr%00%13java.util.ArrayListx%C2%81%C3%92%1D%C2%99%C3%87a%C2%9D%03%00%01I%00%04sizexp%00%00%00%02w%04%00%00%00%02sr%00*com.darren.hadoop.transfer.SimpleParameter%0Cx%C3%B5C%5D%7F%C2%B2%18%02%00%02L%00%04nameq%00%7E%00%03L%00%05valueq%00%7E%00%03xpt%00%04namet%00%06Darrensq%00%7E%00%08t%00%05name1t%00%07Darren1xsr%00%11java.util.HashMap%05%07%C3%9A%C3%81%C3%83%16%60%C3%91%03%00%02F%00%0AloadFactorI%00%09thresholdxp%3F%40%00%00%00%00%00%0Cw%08%00%00%00%10%00%00%00%01t%00%04testq%00%7E%00%09xq%00%7E%00%0Bq%00%7E%00%09sq%00%7E%00%0F%3F%40%00%00%00%00%00%0Cw%08%00%00%00%10%00%00%00%01q%00%7E%00%0Aq%00%7E%00%0Bx
第五个参数: ComplexParameter [name=Darren, silpleParameter=Parameter [name=name, value=Darren], simpleMap={name=Darren}, complexMap={test=Parameter [name=name, value=Darren]}, complexList=[Parameter [name=name, value=Darren], Parameter [name=name1, value=Darren1]]]
这种方式主要解决更加复杂对象的传递,使用hadoop自带的序列化方式不便于解决的,可以采用这种方式,当然,转化为JSON一样可以达到目的。
总结以上方式:都是采用configuration对象来达到目的的,为什么这种方式可以跨越不同的JVM呢,我们来简单分析一下原来,当然在分析之前,再看一个例子:
Job job = new Job(conf);
conf.set("key7", "test7");
我在new Job之后又设置了一个参数
然后在Mapper接收:
LOG.info("第七个参数: " + conf.get("key7"));
结果:
第七个参数: null
获取不到了,这是为什么呢?
现在来揭晓答案,configuration对象传递参数的原理是因为自己被序列化到HDFS上,然后再被不同的JVM反序列化为对象使用的,所以可以传递参数,可以查看Configuration的源码:
public class Configuration implements Iterable<Map.Entry<String,String>>,
Writable {
也实现了Writable接口。
但是为什么new Job之后就不能传递了呢?是因为初始化Job之后会把configuration对象序列化,并且是只读的,不可再更改。所以初始化Job之后不能再有传递参数的作用。
方式四:DistributedCache
通过上述三种方式,我们发现基本能满足使用需要,但是如果想传递个文件怎么办呢?就使用第四种方式,当然这个类不仅是为了传递文件而写的,它甚至可以直接把jar文件放到相应的classpath下去使用,当然今天在这里只介绍一下如何传递文件。
先来简单了解一下这个类可以做什么事情,以及在什么需求下使用这个类。
DistributedCache是hadoop框架提供的一种机制,可以将job指定的文件,在job执行前,先行分发到task执行的机器上,并有相关机制对cache文件进行管理.
常见的应用场景有:
- 分发第三方库(jar,so等);
- 分发算法需要的词典文件;
- 分发程序运行需要的配置;
- 分发多表数据join时小表数据简便处理等
主要的注意事项有:
- DistributedCache只能应用于分布式的情况,包括伪分布式,完全分布式.有些api在这2种情况下有移植性问题.
- 需要分发的文件,必须提前放到hdfs上.默认的路径前缀是hdfs://的,不是file://
- 需要分发的文件,最好在运行期间是只读的.
- 不建议分发较大的文件,比如压缩文件,可能会影响task的启动速度.
来看例子:
// 传递参数方式六 : 传递文件
DistributedCache.addCacheFile(new Path(args[0]).toUri(), conf);
// 获取参数方式六: 获取文件
Path[] paths = DistributedCache.getLocalCacheFiles(conf);
LOG.info("第六个参数: " + paths[0]);
LOG.info("path=================本地文件系统 ");
for (Path path : paths) {
BufferedReader reader = new BufferedReader(new FileReader(path.toString()));
String line = null;
while((line = reader.readLine()) != null){
LOG.info("第六个参数: " + line);
}
reader.close();
}
URI[] urls = DistributedCache.getCacheFiles(conf);
LOG.info("第六个参数: " + urls[0]);
LOG.info("uri=================HDFS文件系统 ");
//fileSystem = FileSystem.get(conf);
fileSystem = FileSystem.newInstance(conf);
for (URI uri : urls) {
BufferedReader reader = new BufferedReader(new InputStreamReader(fileSystem.open(new Path(uri.toString()))));
String line = null;
while((line = reader.readLine()) != null){
LOG.info("第六个参数: " + line);
}
reader.close();
}
运行结果:
第六个参数: /mnt/dsk/8/yarn/nm/usercache/darren/appcache/application_1489987036629_958673/container_1489987036629_958673_01_000002/wordCount.txt
path=================本地文件系统
第六个参数: I am Darren
第六个参数: /user/darren/darren-hadoop/data/test/darren/wordCount.txt
uri=================HDFS文件系统
第六个参数: I am Darren
我的文件内容就是:
I am Darren
所以得到的结果也是正确的。
这里发现,我用两种方式去获取文件路径,一种获取的是本地文件系统的路径,一种是HDFS文件系统的路径,这个相信大家从方法名上可以看出来。可是我就有一个疑问,返回HDFS文件系统路径的时候是URI,二本地文件系统的是Path,感觉刚好使用反了,不知道工程师是怎么想的。
传递参数就先到这里,以后发现新的东西再补充
代码:
https://github.com/zhangpanfeng/darren-hadoop
参考: