MapReduce第二节
还记得上一节的一个mr例子吗,对文件进行排序
#bin/hadoop jarcontrib/streaming/hadoop-streaming-1.2.1.jar -D mapred.compress.map.output=truemapred.job.reuse.jvm.num.tasks=4 -input/wolf1 -output /wolf2 -mapper /bin/cat -reducer /bin/cat
这里我们指定了几个参数–input 这里是指定文件输入目录
-output 这里是指定reduce结果输出目录
一、用java写mapreduce
1.1、自定义map函数
Mapreduce框架提供了map接口,实现这个接口,就自定义了map函数
publicclass Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { publicclass Context extends MapContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { public Context(Configuration conf, TaskAttemptID taskid, RecordReader<KEYIN,VALUEIN> reader, RecordWriter<KEYOUT,VALUEOUT> writer, OutputCommitter committer, StatusReporter reporter, InputSplit split) throws IOException, InterruptedException { super(conf, taskid, reader, writer, committer, reporter, split); } } /** * 初始化方法,仅被调用一次,在map函数运行之前 */ protectedvoid setup(Context context); /** * map函数 */ protectedvoid map(KEYIN key, VALUEIN value, Context context){ context.write((KEYOUT) key, (VALUEOUT) value); }
/** *清理函数,map任务运行结束后,仅被调用一次 */ protectedvoid cleanup(Context context)
/** * 运行函数,map任务的入口 */ publicvoid run(Context context) { setup(context); try { while (context.nextKeyValue()) { map(context.getCurrentKey(), context.getCurrentValue(), context); } } finally { cleanup(context); } } } |
1.2、自定义reduce函数
Mapreduce框架提供了reduce接口,实现这个接口,就自定义了reduce函数
publicclass Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {
publicclass Context extends ReduceContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { public Context(Configuration conf, TaskAttemptID taskid, RawKeyValueIterator input, Counter inputKeyCounter, Counter inputValueCounter, RecordWriter<KEYOUT,VALUEOUT> output, OutputCommitter committer, StatusReporter reporter, RawComparator<KEYIN> comparator, Class<KEYIN> keyClass, Class<VALUEIN> valueClass ) throws IOException, InterruptedException { super(conf, taskid, input, inputKeyCounter, inputValueCounter, output, committer, reporter, comparator, keyClass, valueClass); } }
/** *初始化方法,仅被调用一次,在map函数运行之前 */ protectedvoid setup(Context context);
/** * reduce函数 */ protectedvoid reduce(KEYIN key, Iterable<VALUEIN> values, Context context){ for(VALUEIN value: values) { context.write((KEYOUT) key, (VALUEOUT) value); } }
/** * 清理函数,reduce任务运行结束后,仅被调用一次 */ protectedvoid cleanup(Context context;
/** * reduce任务入口 */ publicvoid run(Context context) throws IOException, InterruptedException { setup(context); try { while (context.nextKey()) { reduce(context.getCurrentKey(), context.getValues(), context); } } finally { cleanup(context); } } }
|
1.3、利用InputFormat定义map输入
Map的输入数据从哪儿来呢?答案是可以从任何地方,hdfs文件、oracle、hbase等等。为map变出数据的魔术师正是----InputFormat。
只要实现InputFormat接口,就可以为Map指定输入。
publicabstractclassInputFormat<K, V> {
/** * 将输入数据切分成多个InputSplit */ publicabstract List<InputSplit> getSplits(JobContext context); /** * 为InputSplit创建读取类:RecordReader */ publicabstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context ); } |
1.4、利用outPutFormat定向reduce输出
那么redcue输出结果,放到哪儿呢?可以是任何地方,hdfs、oralcle、hbase等等。
只要实现outPutFormat接口,就可以指定输出。
publicabstractclass OutputFormat<K, V> {
/** * 获取RecordWriter * @param context存储当前任务相关信息. */ publicabstract RecordWriter<K, V> getRecordWriter(TaskAttemptContext context ) throws IOException, InterruptedException;
/** * */ publicabstractvoid checkOutputSpecs(JobContext context ) throws IOException, InterruptedException;
/** * */ publicabstract OutputCommitter getOutputCommitter(TaskAttemptContext context ) throws IOException, InterruptedException; }
|
1.5、现成的InputFormat、outputFormat
编写InputFormat、outputform并不轻松,考虑到多种数据源,工作量可想而知,好在hadoop为我们提供了多种实现。
1.5.1 InputFormat
名称 | 数据来源 | Key | Value |
TextInputFormat | hdfs文件 | LongWritable:行号 | Text:文件里面一行内容 |
OracleDataDrivenDBInputFormat | Oracle表 | LongWritable:行号 | Textends DBWritable(resultset) |
MultiTableInputFormat | 多张hbase表 | ImmutableBytesWritable:行键 | org.apache.hadoop.hbase.client.Result :Result |
1.5.2 OutputFormat
名称 | 数据输出 | Key | Value |
TextOutputFormat | hdfs文件 | Text 或者实现toString方法 | Text 或者实现toString方法 |
DBOutputFormat | Oracle表 | Textends DBWritable | 无意义 |
MultiTableInputFormat | 多张hbase表 | ImmutableBytesWritable:表名 | Put:向hbase写操作 Delete:向hbase删除操作 |
1.6 实现一个简单的MR
1.6.1 功能
从base_netizen_register表提取属性关联关系,对相同网民的属性建立关联关系,输出到hdfs文件中。
陈永鑫 11003 15804250106 11031
表名 | base_netizen_register | ||||
字段名 | 类型 | 中文注释 | 数据1 | 数据2 | 数据3 |
NETIZEN_ID | VARCHAR2(255) | 网民id | chenyongxin0321 | chenyongxin0321 | chenyongxin0321 |
WEBSITE | VARCHAR2(255) | 网站域名 | my.51job.com | my.51job.com | my.51job.com |
ATTRIBUTE_TYPE | INTEGER | 属性类型 | 11003 | 11031 | 11097 |
ATTRIBUTE_VALUE | VARCHAR2(4000) | 属性值 | 陈永鑫 | 15804250106 | 210111198703212537 |
1.6.2实现
1.6.2.1 map函数
package test;
import java.io.IOException; import java.sql.SQLException;
import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text;
publicclass testMap extends org.apache.hadoop.mapreduce.Mapper<LongWritable, DbRow,Text,Text>{ publicvoid map(LongWritable key, DbRow value, Context context) throws IOException, InterruptedException{ try { Text oKey = new Text(value.values.getString("netizen_id") + "_" + value.values.getString("website") ); String oValue = value.values.getString("attribute_type") + "\t" + value.values.getString("attribute_value");
context.write(oKey, new Text(oValue));
} catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); return; } }
}
|
1.6.2.2 reduce函数
package test;
import java.io.IOException; import java.util.ArrayList; import java.util.List;
import org.apache.hadoop.io.Text;
publicclass testReduce extends org.apache.hadoop.mapreduce.Reducer<Text ,Text, Text, Text> { publicvoid reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException{ List<String> attrs = new ArrayList<String>(); for(Text t1:values) attrs.add(t1.toString()); for(String t1:attrs){ String[] attr1 = t1.split("\t",10); Text outkey = new Text(attr1[1] + "_" + attr1[0]); for(String t2:attrs){ if(t1.equals(t2)){ continue; } String[] attr2 = t2.split("\t",10); context.write(outkey, new Text(attr2[1] + "_" + attr2[0])); } } } } |
1.6.2.3 main函数
package test;
import java.io.IOException;
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.filecache.DistributedCache; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.db.DBConfiguration; import org.apache.hadoop.mapreduce.lib.db.DBWritable; import org.apache.hadoop.mapreduce.lib.db.OracleDataDrivenDBInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
publicclass Main { static Log _log = LogFactory.getLog(Main.class);
publicstaticvoid main(String[] args) throws IOException, InterruptedException, ClassNotFoundException{ Configuration conf = new Configuration(); Job job = new Job(conf, "NeEt" ); /* * 设置map */ job.setInputFormatClass(OracleDataDrivenDBInputFormat.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(Text.class); job.setMapperClass(testMap.class);
/* * 设置reduce */ job.setOutputFormatClass(TextOutputFormat.class); FileOutputFormat.setOutputPath(job, new Path("/wolf1/")); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); job.setReducerClass(testReduce.class); job.setNumReduceTasks(1);
/* * 设置oracle相关配置 */ job.getConfiguration().set(DBConfiguration.URL_PROPERTY, "jdbc:oracle:thin:@15.15.17.68:1521:ora11g"); job.getConfiguration().set(DBConfiguration.USERNAME_PROPERTY,"noahdbn"); job.getConfiguration().set(DBConfiguration.PASSWORD_PROPERTY,"noahdbn"); job.getConfiguration().set(DBConfiguration.DRIVER_CLASS_PROPERTY,"oracle.jdbc.driver.OracleDriver" ); job.getConfiguration().set(DBConfiguration.INPUT_TABLE_NAME_PROPERTY, " base_netizen_register "); job.getConfiguration().set(DBConfiguration.INPUT_FIELD_NAMES_PROPERTY, " netizen_id,website,attribute_type,attribute_value,capturetime,datasource "); String cond = " 1 = 1 and rownum < 100 ";
job.getConfiguration().set(DBConfiguration.INPUT_CONDITIONS_PROPERTY, cond); job.getConfiguration().setClass(DBConfiguration.INPUT_CLASS_PROPERTY, DbRow.class, DBWritable.class); _log.info("sql:" + "select " + conf.get(DBConfiguration.INPUT_FIELD_NAMES_PROPERTY) + " from " + conf.get(DBConfiguration.INPUT_TABLE_NAME_PROPERTY) + " where " + cond);
/* * */ job.setJarByClass(test.Main.class); DistributedCache.addArchiveToClassPath(new Path("/wfp/ojdbc5.jar") , job.getConfiguration() , FileSystem.get(job.getConfiguration()));
/* * 运行 */ job.waitForCompletion(true);
} }
|
二、MR调试
2.1 常见错误
错误名称 | 发送时间点 | 错误原因 | 解决办法 |
Exception in thread "main" org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory /wolf1 already exists | 任务启动前(准备期间) | 输出目录已经存在, |
删除输出目录 |
java.lang.RuntimeException: java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver | 任务启动前(准备期间) | 我们自己的类库,没有加入的hadoop安装目录的lib下面 |
将我们自己调用的jar包拷贝到hadoop lib目录下 |
java.lang.RuntimeException: java.lang.RuntimeException: java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver | 任务启动后 |
找不到我们依赖jar包的类 | DistributedCache.addArchiveToClassPath(new Path("/wfp/ojdbc5.jar") , job.getConfiguration() , FileSystem.get(job.getConfiguration()));
|
2.2 调试技巧
2.2.1 通过jobtracker页面查看mr运行情况
http://15.15.36.104:50030/jobtracker.jsp
2.2.1 如何打印调试信息
由于mr程序是分布式运行,查看调试信息比较困难,这里介绍一种方法,
在map、reduce函数,调用Systerm.out打印输出日志。然后可以通过页面查看调试信息
2.3 搭建elipse MR开发环境
1、拷贝Hadoop项目中的eclipse plugin jar文件到eclipse 安装目录plugin目录下
2、重启eclipse,配置hadoop installation directory。
如果安装插件成功,打开Window-->Preferens,你会发现Hadoop Map/Reduce选项,在这个选项里你需要配置Hadoop installation directory。配置完成后退出。
3.配置Map/Reduce Locations。
在Window-->Show View->other...,在MapReduce Tools中选择Map/ReduceLocations。
在Map/Reduce Locations(Eclipse界面的正下方)中新建一个Hadoop Location。在这个View中,点击鼠标右键-->New HadoopLocation。在弹出的对话框中你需要配置Location name,可任意填,如Hadoop,以及Map/ReduceMaster和DFS Master。这里面的Host、Port分别为你在mapred-site.xml、core-site.xml中配置的地址及端口。我的这两个文件中配置如下:
mapred-site.xml
[html] view plaincopy
- <property>
- <name>mapred.job.tracker</name>
- <value>matraxa:9001</value>
- </property>
core-site.xml:
[html] view plaincopy
- <property>
- <name>fs.default.name</name>
- <value>hdfs://matraxa:9000</value>
- </property>
最后的配置截图如下:
设置完成后,点击Finish就应用了该设置。然后,在最左边的Project Explorer中就能看到DFS的目录,如下图所示:
1、新建项目。
File-->New-->Other-->Map/Reduce Project
项目名可以随便取,如hadoopTest。
三 MR优化技巧
3.1 合理设置reduce、map数量
默认情况下,reduce数量为1,所以当reduce运行比较慢的时候,可以多设置reduce任务数量,提高性能
属性名称 | 类型 | 默认值 | 说明 |
mapred.reduce.tasks | Int | 1 | Reduce任务数量 |
mapred.map.tasks | Int | 动态决定 | 可以人为设置 |
3.2 jvm重用
默认情况下,hadoop,每启动一个map或reduce任务,就启动一个新的jvm。
Jvm重用的好处?1、节省启动jvm时间 2、可以充分利用HotSpot JVM所用的运行时优化
属性名称 | 类型 | 默认值 | 说明 |
mapred.job.reuse.jvm.num.tasks | Int | 1 | 在一个tasktracker上面给定作业的每个jvm可以运行的任务最大数 |
3.3推测式执行开关
什么是推测式执行?一个mr任务启动会,会检测map、reduce任务执行情况,如果一个map或reduce任务,运行得异常缓慢,则会开启一个相同的map或reduce任务,作为备份。
推测执行的好处?一个map任务可能由于网络、硬盘老化等问题,执行很慢,拖这个job后腿,推测式执行,开启一个备份,可以避免一个点的异常状况。
坏处?开启备份,会造成资源浪费,有些时候,任务执行慢,无法避免,开启备份还是慢,并且占用宝贵的资源
属性名称 | 类型 | 默认值 | 说明 |
mapred.map.tasks.speculative.execution | Boolean | True | 一个map任务运行缓慢时,是否开启一个备份 |
mapred.reduce.tasks.speculative.execution | Boolean | True | 一个reduce任务运行缓慢时,是否开启一个备份 |
3.4对Map输出数据压缩(数据量大时)
属性名称 | 类型 | 默认值 | 说明 |
mapred.compress.map.output | Boolean | False | 对map输出结果压缩 |
mapred.map.output.compression.codec | String | org.apache.hadoop.io.compress.DefaultCodec | 编码格式 |
3.5Map端可调整的属性
属性名称 | 类型 | 默认值 | 说明 |
io.sort.mb | int | 100 | 对Map端输出进行排序时所使用的内存大小,单位M |
io.sort.record.percent | Float | 0.05 | io.sort.mb内存预留一部分给,来记录map的输出边界 |
io.sort.spill.percent | Float | 0.8 | io.sort.mb内存使用达到多少,将缓存数据写到磁盘 |
Io.sort.factor | int | 10 | 排序文件时,一次合并的最大流数 |
Min.num.spills.for.combine | int | 3 | 运行combiner所需要的最少文件数量(如果设置了combinber) |
tasktracker.http.threads | Int | 40 | Tasktracker开启的数据复制线程数(赋值map数据给reduce) |
3.6 redue端可调整的属性
属性名称 | 类型 | 默认值 | 说明 |
mapred.reduce.parallel.copies | int | 5 | 用于将map输出复制到reducer的线程数量 |
Io.sort.factor | int | 10 | 排序文件时,一次合并的最大流数 |
mapred.job.shuffle.input.buffer.percent
| Float | 0.70 | 整个堆空间的百分比,用于shuffle的赋值阶段 |
mapred.job.shuffle.merge.percent
| Float | 0.66 | map输出缓存,使用达到这个比例,启动合并输出到磁盘 |
mapred.inmem.merge.threshold | Int | 1000 | 启动合并,最大map输出量 |
mapred.job.reduce.input.buffer.percent
| Float | 0 | 在reduce过程中,用来在内存中保存map输出空间占整个堆空间的比例 |
Stream
属性名 | 属性值 | 描述 |
mapred.input.dir
| hdfs://15.15.36.104:9000/wolf1
| 输入文件目录 |
mapred.input.format.class | org.apache.hadoop.mapred.TextInputFormat |
|
mapred.mapper.class | org.apache.hadoop.streaming.PipeMapper |
|
stream.map.streamprocessor | /bin/cat | Stream的map函数 |
mapred.mapoutput.key.class | org.apache.hadoop.io.Text | Map函数输出key类型 |
mapred.mapoutput.value.class
| org.apache.hadoop.io.Text | Map函数输出value类型 |
| ||
mapred.output.dir
| hdfs://15.15.36.104:9000/wolf2
| 输出文件目录 |
mapred.output.format.class | org.apache.hadoop.mapred.TextOutputFormat |
|
mapred.reducer.class | org.apache.hadoop.streaming.PipeReducer |
|
stream.reduce.streamprocessor | /bin/cat | Stream 的 reduce函数 |
mapred.output.key.class | org.apache.hadoop.io.Text | Reduce输出的Key类型 |
mapred.output.value.class | org.apache.hadoop.io.Text
| Reduce输出的value类型 |
Pipes
属性名 | 属性值 | 描述 |
mapred.input.dir | hdfs://15.15.36.104:9000/temp
| 输入文件目录 |
mapred.input.format.class | org.apache.hadoop.mapred.pipes.PipesNonJavaInputFormat |
|
mapred.map.runner.class | org.apache.hadoop.mapred.pipes.PipesMapRunner |
|
mapred.mapoutput.key.class | org.apache.hadoop.io.Text | Map函数输出key类型 |
mapred.mapoutput.value.class
| org.apache.hadoop.io.Text | Map函数输出value类型 |
| ||
mapred.output.dir
| hdfs://15.15.36.104:9000/x | 输出文件目录 |
mapred.output.format.class | org.apache.hadoop.mapred.lib.NullOutputFormat |
|
mapred.reducer.class | org.apache.hadoop.mapred.pipes.PipesReducer |
|
stream.reduce.streamprocessor | /bin/cat | Stream 的 reduce函数 |
mapred.output.key.class | org.apache.hadoop.io.Text | Reduce输出的Key类型 |
mapred.output.value.class | org.apache.hadoop.io.Text
| Reduce输出的value类型 |