环境
本教程在 Hadoop 2.6.0 下验证通过,适用于 Ubuntu/CentOS 系统,理论上可用于任何原生 Hadoop 2 版本,如 Hadoop 2.4.1,Hadoop 2.7.1。
本教程主要测试环境:
- Ubuntu 14.04
- Hadoop 2.6.0(伪分布式)
- Eclipse 3.8
此外,本教材在 CentOS 6.4 系统中也验证通过,对 Ubuntu 与 CentOS 的不同配置之处有作出了注明。
安装 Eclipse
在 Ubuntu 和 CentOS 中安装 Eclipse 的方式有所不同,但之后的配置和使用是一样的。
在 Ubuntu 中安装 Eclipse,可从 Ubuntu 的软件中心直接搜索安装,在桌面左侧任务栏,点击“Ubuntu软件中心”。
Ubuntu软件中心
在右上角搜索栏中搜索 eclipse,在搜索结果中单击 eclipse,并点击安装。
安装Eclipse
等待安装完成即可,Eclipse 的默认安装目录为:/usr/lib/eclipse。
在 CentOS 中安装 Eclipse,需要下载安装程序,我们选择 Eclipse IDE for Java Developers 版:
- 32位: http://eclipse.bluemix.net/packages/mars.1/?JAVA-LINUX32
- 64位: http://eclipse.bluemix.net/packages/mars.1/?JAVA-LINUX64
下载后执行如下命令,将 Eclipse 安装至 /usr/lib 目录中:
- sudo tar -zxf ~/下载/eclipse-java-mars-1-linux-gtk*.tar.gz -C /usr/lib
Shell 命令
解压后即可使用。在 CentOS 中可以为程序创建桌面快捷方式,如下图所示,点击桌面右键,选择创建启动器,填写名称和程序位置(/usr/lib/eclipse/eclipse):
安装Eclipse
安装 Hadoop-Eclipse-Plugin
要在 Eclipse 上编译和运行 MapReduce 程序,需要安装 hadoop-eclipse-plugin,可下载 Github 上的 hadoop2x-eclipse-plugin(备用下载地址:http://pan.baidu.com/s/1i4ikIoP)。
下载后,将 release 中的 hadoop-eclipse-kepler-plugin-2.6.0.jar (还提供了 2.2.0 和 2.4.1 版本)复制到 Eclipse 安装目录的 plugins 文件夹中,运行 eclipse -clean 重启 Eclipse 即可(添加插件后只需要运行一次该命令,以后按照正常方式启动就行了)。
- unzip -qo ~/下载/hadoop2x-eclipse-plugin-master.zip -d ~/下载 # 解压到 ~/下载 中
- sudo cp ~/下载/hadoop2x-eclipse-plugin-master/release/hadoop-eclipse-plugin-2.6.0.jar /usr/lib/eclipse/plugins/ # 复制到 eclipse 安装目录的 plugins 目录下
- /usr/lib/eclipse/eclipse -clean # 添加插件后需要用这种方式使插件生效
Shell 命令
配置 Hadoop-Eclipse-Plugin
在继续配置前请确保已经开启了 Hadoop。
启动 Eclipse 后就可以在左侧的Project Explorer中看到 DFS Locations(若看到的是 welcome 界面,点击左上角的 x 关闭就可以看到了。CentOS 需要切换 Perspective 后才能看到,即接下来配置步骤的第二步)。
安装好Hadoop-Eclipse-Plugin插件后的效果
插件需要进一步的配置。
第一步:选择 Window 菜单下的 Preference。
打开Preference
此时会弹出一个窗体,窗体的左侧会多出 Hadoop Map/Reduce 选项,点击此选项,选择 Hadoop 的安装目录(如/usr/local/hadoop,Ubuntu不好选择目录,直接输入就行)。
选择 Hadoop 的安装目录
第二步:切换 Map/Reduce 开发视图,选择 Window 菜单下选择 Open Perspective -> Other(CentOS 是 Window -> Perspective -> Open Perspective -> Other),弹出一个窗体,从中选择 Map/Reduce 选项即可进行切换。
切换 Map/Reduce 开发视图
第三步:建立与 Hadoop 集群的连接,点击 Eclipse软件右下角的 Map/Reduce Locations 面板,在面板中单击右键,选择 New Hadoop Location。
建立与 Hadoop 集群的连接
在弹出来的 General 选项面板中,General 的设置要与 Hadoop 的配置一致。一般两个 Host 值是一样的,如果是伪分布式,填写 localhost 即可,另外我使用的Hadoop伪分布式配置,设置 fs.defaultFS 为 hdfs://localhost:9000,则 DFS Master 的 Port 要改为 9000。Map/Reduce(V2) Master 的 Port 用默认的即可,Location Name 随意填写。
最后的设置如下图所示:
Hadoop Location 的设置
Advanced parameters 选项面板是对 Hadoop 参数进行配置,实际上就是填写 Hadoop 的配置项(/usr/local/hadoop/etc/hadoop中的配置文件),如我配置了 hadoop.tmp.dir ,就要进行相应的修改。但修改起来会比较繁琐,我们可以通过复制配置文件的方式解决(下面会说到)。
总之,我们只要配置 General 就行了,点击 finish,Map/Reduce Location 就创建好了。
在 Eclipse 中操作 HDFS 中的文件
配置好后,点击左侧 Project Explorer 中的 MapReduce Location (点击三角形展开)就能直接查看 HDFS 中的文件列表了(HDFS 中要有文件,如下图是 WordCount 的输出结果),双击可以查看内容,右键点击可以上传、下载、删除 HDFS 中的文件,无需再通过繁琐的 hdfs dfs -ls 等命令进行操作了。
以下output/part-r-00000文件记录了输出结果。点击查看HDFS文件系统数据的三种方法可进行扩展学习。
使用Eclipse查看HDFS中的文件内容
如果无法查看,可右键点击 Location 尝试 Reconnect 或重启 Eclipse。
Tips
HDFS 中的内容变动后,Eclipse 不会同步刷新,需要右键点击 Project Explorer中的 MapReduce Location,选择 Refresh,才能看到变动后的文件。
在 Eclipse 中创建 MapReduce 项目
点击 File 菜单,选择 New -> Project…:
创建Project
选择 Map/Reduce Project,点击 Next。
创建MapReduce项目
填写 Project name 为 WordCount 即可,点击 Finish 就创建好了项目。
填写项目名
此时在左侧的 Project Explorer 就能看到刚才建立的项目了。
项目创建完成
接着右键点击刚创建的 WordCount 项目,选择 New -> Class
新建Class
需要填写两个地方:在 Package 处填写 org.apache.hadoop.examples;在 Name 处填写 WordCount。
创建 Class 完成后,在 Project 的 src 中就能看到 WordCount.java 这个文件。将如下 WordCount 的代码复制到该文件中。
package org.apache.hadoop.examples;
import java.io.IOException;
import java.util.Iterator;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class WordCount {
public WordCount() {
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = (new GenericOptionsParser(conf, args)).getRemainingArgs();
if(otherArgs.length < 2) {
System.err.println("Usage: wordcount <in> [<in>...] <out>");
System.exit(2);
}
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(WordCount.TokenizerMapper.class);
job.setCombinerClass(WordCount.IntSumReducer.class);
job.setReducerClass(WordCount.IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
for(int i = 0; i < otherArgs.length - 1; ++i) {
FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
}
FileOutputFormat.setOutputPath(job, new Path(otherArgs[otherArgs.length - 1]));
System.exit(job.waitForCompletion(true)?0:1);
}
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public IntSumReducer() {
}
public void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum = 0;
IntWritable val;
for(Iterator i$ = values.iterator(); i$.hasNext(); sum += val.get()) {
val = (IntWritable)i$.next();
}
this.result.set(sum);
context.write(key, this.result);
}
}
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
private static final IntWritable one = new IntWritable(1);
private Text word = new Text();
public TokenizerMapper() {
}
public void map(Object key, Text value, Mapper<Object, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while(itr.hasMoreTokens()) {
this.word.set(itr.nextToken());
context.write(this.word, one);
}
}
}
}
通过 Eclipse 运行 MapReduce
在运行 MapReduce 程序前,还需要执行一项重要操作(也就是上面提到的通过复制配置文件解决参数设置问题):将 /usr/local/hadoop/etc/hadoop 中将有修改过的配置文件(如伪分布式需要 core-site.xml 和 hdfs-site.xml),以及 log4j.properties 复制到 WordCount 项目下的 src 文件夹(~/workspace/WordCount/src)中:
- cp /usr/local/hadoop/etc/hadoop/core-site.xml ~/workspace/WordCount/src
- cp /usr/local/hadoop/etc/hadoop/hdfs-site.xml ~/workspace/WordCount/src
- cp /usr/local/hadoop/etc/hadoop/log4j.properties ~/workspace/WordCount/src
Shell 命令
没有复制这些文件的话程序将无法正确运行,本教程最后再解释为什么需要复制这些文件。
复制完成后,务必右键点击 WordCount 选择 refresh 进行刷新(不会自动刷新,需要手动刷新),可以看到文件结构如下所示:
WordCount项目文件结构
点击工具栏中的 Run 图标,或者右键点击 Project Explorer 中的 WordCount.java,选择 Run As -> Run on Hadoop,就可以运行 MapReduce 程序了。不过由于没有指定参数,运行时会提示 “Usage: wordcount “,需要通过Eclipse设定一下运行参数。
右键点击刚创建的 WordCount.java,选择 Run As -> Run Configurations,在此处可以设置运行时的相关参数(如果 Java Application 下面没有 WordCount,那么需要先双击 Java Application)。切换到 “Arguments” 栏,在 Program arguments 处填写 “input output” 就可以了。
设置程序运行参数
或者也可以直接在代码中设置好输入参数。可将代码 main() 函数的 String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); 改为:
- // String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
- String[] otherArgs=new String[]{"input","output"}; /* 直接设置输入参数 */
Java
设定参数后,再次运行程序,可以看到运行成功的提示,刷新 DFS Location 后也能看到输出的 output 文件夹。
WordCount 运行结果
至此,你就可以使用 Eclipse 方便的进行 MapReduce程序的开发了。
在 Eclipse 中运行 MapReduce 程序
程序实现
首先我们实现基于共现次数的单词共现矩阵的MapReduce实现。
Pair的方式
自定义Pair类:
package mp.co_occurrence_matrix;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
import org.apache.hadoop.io.WritableUtils;
/**
*
* @author liupenghe
* 自定义TextPair Writable类型
*
*/
public class TextPair implements WritableComparable<TextPair>{
private Text first;
private Text second;
/**
* 默认的构造函数,这样MapReduce方法才能创建对象,然后通过readFeilds方法从序列化数据流中独处进行赋值
*/
public TextPair() {
set (new Text(), new Text());
}
public TextPair(String first, String second) {
set(new Text(first), new Text(second));
}
public TextPair(Text first, Text second) {
set(first, second);
}
public void set(Text first, Text second) {
// TODO Auto-generated method stub
this.first = first;
this.second = second;
}
public Text getFirst() {
return first;
}
public Text getSecond() {
return second;
}
/**
* 通过成员对象本身的readFeilds方法,从输入流中反序列化每一个成员对象
* @param arg0
* @throws IOException
*/
@Override
public void readFields(DataInput arg0) throws IOException {
// TODO Auto-generated method stub
first.readFields(arg0);
second.readFields(arg0);
}
/**
* 通过成员对象本身的write方法,序列化每一个成员对象到输出流中
* @param arg0
* @throws IOException
*/
@Override
public void write(DataOutput arg0) throws IOException {
// TODO Auto-generated method stub
first.write(arg0);
second.write(arg0);
}
/**
* 实现WritableComparable必须要实现的方法,用语比较排序
* @param TextPair
* @return
*/
@Override
public int compareTo(TextPair tp) {
// TODO Auto-generated method stub
int cmp = first.compareTo(tp.first);
if(cmp != 0) {
return cmp;
}
return second.compareTo(tp.second);
}
/**
* 就像针对java语言构造任何值的对象,需要重写java.lang.Object中的hashCode(), equals()和toString()方法
*/
/**
* MapReduce需要一个Partitioner把map的输出作为输入分成一块块喂给多个reduce
* 默认的是HashPartitioner,它是通过对象的hashCode函数进行分割,所以hashCode的好坏决定了分割是否均匀,它是一个关键的方法
* @return
*/
//当不使用reletive frequency时采用该hashCode求值方式
@Override
public int hashCode() {
return first.hashCode() * 163 + second.hashCode();
}
@Override
public boolean equals(Object o) {
if(o instanceof TextPair) {
TextPair tp = (TextPair) o;
return first.equals(tp.first) && second.equals(tp.second);
}
return false;
}
/**
* 重写toString方法,作为TextOutputFormat输出格式的输出
* @return
*/
@Override
public String toString() {
return first + "," + second;
}
/**
* 当Textpair被用作健时,需要将数据流反序列化为对象,然后再调用compareTo()方法进行比较。
* 为了提升效率,可以直接对数据的序列化表示来进行比较
*/
public static class Comparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public Comparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
try {
/**
* Text对象的二进制表示是一个长度可变的证书,包含字符串之UTF-8表示的字节数以及UTF-8字节本身。
* 读取该对象的起始长度,由此得知第一个Text对象的字节表示有多长;然后将该长度传给Text对象RawComparator方法
* 最后通过计算第一个字符串和第二个字符串恰当的偏移量,从而实现对象的比较
* decodeVIntSize返回变长整形的长度,readVInt表示文本字节数组的长度,加起来就是某个成员的长度
*/
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
//先比较first
int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
if(cmp != 0) {
return cmp;
}
//再比较second
return TEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1, b2, s2 + firstL2, l2 - firstL2);
} catch (IOException e) {
throw new IllegalArgumentException();
}
}
}
//注册RawComparator, 这样MapReduce使用TextPair时就会直接调用Comparator
static {
WritableComparator.define(TextPair.class, new Comparator());
}
}
Mapper的实现
/**
* 使用pair的方式,使用自定义了TextPiar Writable对象
*
*/
public static class Co_OccurrenceMatrixMapperWithPair extends Mapper<LongWritable, Text, TextPair, DoubleWritable> {
@Override
public void map(LongWritable inputKey, Text inputValue, Context context)
throws IOException, InterruptedException {
String doc = inputValue.toString();
//这里只是简单的根据正则分词,如果希望准确分词,请使用相关分词包
String reg = "[\\p{P}\\s]";
String[] allTerms = doc.split(reg);
for(int i = 0; i < allTerms.length; i++) {
if((!"".equals(allTerms[i])) && allTerms[i] != null) {
//考虑in-mapper combining
Map<String, Integer> pairMap = new HashMap<String, Integer>();
//取出该单词对应的一定窗口大小内的共现词
String[] termNeighbors = neighborsOfTerm(allTerms[i], i, allTerms, 3);
for(String nbTerm : termNeighbors) {
if((!"".equals(nbTerm)) && nbTerm != null) {
String textPairStr = allTerms[i] + "," + nbTerm;
//in-mapper combining
if(!pairMap.containsKey(textPairStr)) {
pairMap.put(textPairStr, 1);
} else {
pairMap.put(textPairStr, pairMap.get(textPairStr) + 1);
}
}
}
for(Entry<String, Integer> entry: pairMap.entrySet()) {
String[] pairStrs = entry.getKey().split(",");
TextPair textPair = new TextPair(pairStrs[0], pairStrs[1]);
context.write(textPair, new DoubleWritable(entry.getValue()));
}
}
}
}
/**
* 计算某个词在某窗口大小内的共现词
* @param term
* @param allterms
* @return
*/
public String[] neighborsOfTerm(String term, int pos, String[] allterms, int windowSize) {
String[] neighbors = new String[windowSize];
int count = allterms.length;
int j = 0;
int leftOffSet = 0;
int rightOffSet = 0;
if(pos < windowSize / 2) {
leftOffSet = pos;
rightOffSet = windowSize - leftOffSet;
} else if (pos >= count - 1 - windowSize / 2) {
rightOffSet = count - 1 - pos;
leftOffSet = windowSize - rightOffSet;
} else {
leftOffSet = windowSize / 2;
rightOffSet = windowSize - leftOffSet;
}
for(int i = pos - leftOffSet; i <= pos + rightOffSet && i >=0 && i < count; i++) {
if(term != allterms[i] ) {
neighbors[j] = allterms[i];
j ++;
}
}
return neighbors;
}
}
---------------------
Reducer的实现
public static class Co_OccurrenceMatrixReducerWithPair extends Reducer<TextPair, DoubleWritable, TextPair, DoubleWritable> {
@Override
public void reduce(TextPair inputKey, Iterable<DoubleWritable> inputValues, Context context)
throws IOException, InterruptedException {
int sum = 0;
for(DoubleWritable inC : inputValues) {
sum += inC.get();
}
context.write(inputKey, new DoubleWritable(sum));
}
}
Stripe方式实现
自定义Stripe类
package mp.co_occurrence_matrix;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.MapWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
/**
*
* @author liupenghe
* 自定义TextStripe类型,其实是继承自Hadoop提供的MapWritable
*
*/
public class TextStripe extends MapWritable {
public TextStripe(){
super();
}
/**
* 重写格式化输出函数,否则以TextOutputFormat输出,文件只会显示该对象的信息
* 如:TextStripe@75123
*/
@Override
public String toString(){
String res = "";
for(Entry<Writable, Writable> entry : this.entrySet()) {
Text key = (Text) entry.getKey();
DoubleWritable value = (DoubleWritable) entry.getValue();
res += key.toString()+ ":" + value.get() + ";";
}
return res;
}
/**
* 添加另一个putAll方法,实现两个TextStripe的相加
* 自带的不知为何不能叠加,没办法,只能自己动手实现了
* @param ts
*/
public void putAll(TextStripe ts) {
for(Entry<Writable, Writable> entry : ts.entrySet()) {
Text tsKey = (Text)entry.getKey();
DoubleWritable tsValue = (DoubleWritable)entry.getValue();
//如果已包含该健,累加值
if(this.containsKey(tsKey)) {
double newValue = ((DoubleWritable)this.get(tsKey)).get() + tsValue.get();
this.put(tsKey, new DoubleWritable(newValue));
} else { //如果不包含该健,则加上
this.put(tsKey, tsValue);
}
}
}
Mapper的实现
/**
* 使用stripe方式,使用自定义的TextStripe Writable对象
*
*/
public static class Co_OccurrenceMatrixMapperWithStripe extends Mapper<LongWritable, Text, Text, TextStripe> {
@Override
public void map(LongWritable inputKey, Text inputValue, Context context)
throws IOException, InterruptedException {
String doc = inputValue.toString();
//这里只是简单的根据正则分词,如果希望准确分词,请使用相关分词包
String reg = "[\\p{P}\\s]";
String[] allTerms = doc.split(reg);
for (int i = 0; i < allTerms.length; i++) {
if((!"".equals(allTerms[i])) && allTerms[i] != null) {
Text outputKey = new Text(allTerms[i]);
//定义一TextStripe存储与该单词共现的词以及频率
TextStripe termTS = new TextStripe();
//取出该单词对应的一定窗口大小内的共现词
String[] termNeighbors = neighborsOfTerm(allTerms[i], i, allTerms, 3);
for(String nbTerm : termNeighbors) {
if((!"".equals(nbTerm)) && nbTerm != null) {
Text co_term = new Text(nbTerm);
//这里其实是做了in-mapper combining
if(!termTS.containsKey(co_term)) {
termTS.put(co_term, new DoubleWritable(1));
} else {
DoubleWritable lastValue = (DoubleWritable) termTS.get(co_term);
double newValue = lastValue.get() + 1.0;
termTS.put(co_term, new DoubleWritable(newValue));
}
}
}
context.write(outputKey, termTS);
}
}
}
/**
* 计算某个词在某窗口大小内的共现词
* @param term
* @param allterms
* @return
*/
public String[] neighborsOfTerm(String term, int pos, String[] allterms, int windowSize) {
String[] neighbors = new String[windowSize];
int count = allterms.length;
int j = 0;
int leftOffSet = 0;
int rightOffSet = 0;
if(pos < windowSize / 2) {
leftOffSet = pos;
rightOffSet = windowSize - leftOffSet;
} else if (pos >= count - 1 - windowSize / 2) {
rightOffSet = count - 1 - pos;
leftOffSet = windowSize - rightOffSet;
} else {
leftOffSet = windowSize / 2;
rightOffSet = windowSize - leftOffSet;
}
for(int i = pos - leftOffSet; i <= pos + rightOffSet && i >=0 && i < count; i++) {
if(term != allterms[i] ) {
neighbors[j] = allterms[i];
j ++;
}
}
return neighbors;
}
}
Reducer的实现
public static class Co_OccurrenceMatrixReducerWithStripe extends Reducer<Text, TextStripe, Text, TextStripe> {
@Override
public void reduce(Text inputKey, Iterable<TextStripe> inputValues, Context context)
throws IOException, InterruptedException {
//创建一表示总和的TexStripe
TextStripe sumStripe = new TextStripe();
for(TextStripe ts : inputValues) {
//将对应的列表加入总列表里
sumStripe.putAll(ts);
}
context.write(inputKey, sumStripe);
}
}