MapReduce模型
MapReduce编程遵循一个特定的流程,首先编写map函数和reduce函数,最好使用单元测试来确保函数的运行符合预期,然后编写一个驱动程序来运行作业,看这个作业是否可以正确运行。当然你也可以使用本地IDE来调试用例,使其能正确处理输入,并得到预期输出。
map函数
要编写map函数,首先需要继承Hadoop提供的Mapper这个类(org.apache.hadoop.mapreduce.Mapper),然后在map中编写自己的业务逻辑。例如下面这段代码,Wordcount的map部分。
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] words = line.split(" ");
for(String word:words){
context.write(new Text(word),new IntWritable(1));
}
}
}
需要解释的是map(LongWritable key, Text value, Context context)函数的三个参数,第一个是偏移量,这个与数据划分有关,第二个参数,MapReduce默认使用的是TextInputFormat输入格式,即按行读取,context则是上下文参数,将map的输出交给这个上下文参数。
因为mapper这个类使用了泛型,而且其中的参数类型都应该是实现了HADOOP序列化框架的类型,而不是java提供的基本数据类型,装换规则如下:
# String比较例外,Hadoop中与之对应的是Text,注意package应该是org.apache.hadoop.io.
String->Text
#其余类型则在后面加上Writable即可
int->IntWritable
Long->LongWritable
除此之外,你还可以指定自定义类型,但是有一些要求,首先需要实现WritableComparable接口,其次指明序列化和反序列化的规则。假设我有一个OderBean,则代码可以这样:其中的write就是序列化,readFields就是反序列化。
public class OrderBean implements WritableComparable<OrderBean>{
private String orderId;
private String userId;
private String pdtName;
private float price;
private int number;
private float amountFee;
// 需要有无参构造方法,不然无法初始化
public FlowBean(){}
//省略了setter和getter方法
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.orderId);
out.writeUTF(this.userId);
out.writeUTF(this.pdtName);
out.writeFloat(this.price);
out.writeInt(this.number);
}
@Override
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.userId = in.readUTF();
this.pdtName = in.readUTF();
this.price = in.readFloat();
this.number = in.readInt();
this.amountFee = this.price * this.number;
}
}
map阶段的到是许多的key-value这样的键值对。
补充一点:map阶段的结果是给reduce的,在reduce阶段我们可以指定输出为几个文件,通过setNumReduceTasks可以指定开启多少个reduce task。这里面就涉及到map的数据按照什么规则划分给reduce task。Mapper任务划分数据的过程就称作Partition。负责实现划分数据的类称作Partitioner。Hadoop内置了分区类,称作HashPartitioner,源码如下:
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
public HashPartitioner() {
}
public void configure(JobConf job) {
}
public int getPartition(K2 key, V2 value, int numReduceTasks) {
return (key.hashCode() & 2147483647) % numReduceTasks;
}
}
参见源码可知,划分数据时默认按照key的hash码对numReduceTasks求余数。至于它与(&)上一个极大的数,是防止key的hash码如果很大(超过int的范围),那就会溢出。如果我们想改变划分数据的规则,可以实现自己的Partitioner,例如下面代码
public class OrderIdPartitioner extends Partitioner<OrderBean, NullWritable>{
@Override
public int getPartition(OrderBean key, NullWritable value, int numPartitions) {
// 按照订单中的orderid来分发数据
return (key.getOrderId().hashCode() & Integer.MAX_VALUE) % numPartitions;
}
}
实现自己的Partitioner之后,需要在驱动程序中指定使用的是自定义Partitioner。
job.setPartitionerClass(OrderIdPartitioner.class);
reduce函数
与map类似,也需要继承Reducer这个类,如下代码是Wordcount的reduce部分
public class WordcountReducer extends Reducer<Text, IntWritable,Text,IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int count=0;
Iterator<IntWritable> iterator = values.iterator();
while (iterator.hasNext()){
IntWritable value = iterator.next();
count+=value.get();
}
context.write(key,new IntWritable(count));
}
}
reduce函数传入的参数中是map阶段Key-Value,并且将相同key的许多value封装到聚合在一起,即reduce函数的values参数可以得到相同Key的多个值,它是一个可迭代对象,并且支持增强for循环。
补充一点:reduce函数会将key相同的聚合在一起,我们怎么判两个key相等呢,例如有些情况下,我们的map函数输出的key不是Hadoop内置的类型,而是自定义的类型,如OderBean,如果我们只需要OderId变量相同的map输出就被规约到一起,如果不自定义分组方式,那么只有所有属性都相等的情况才会被规约到一起。这种情况下,我们就需要使用GroupingComparatorClass来自定义分组方式。我们需要定义一个Comparator函数,令其继承WritableComparator,并重写compare方法。在compare方法方法中,我们定义规约器的key分组方式。通过这种方式,我们就可以将oderId相同的分为同一个组。代码如下:
public class OrderIdGroupingComparator extends WritableComparator{
public OrderIdGroupingComparator() {
super(OrderBean.class,true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
OrderBean o1 = (OrderBean) a;
OrderBean o2 = (OrderBean) b;
return o1.getOrderId().compareTo(o2.getOrderId());
}
}
JobSubmit函数
我们编写好mapper和reducer函数之后,就可以编写驱动程序将MapReduce程序提交运行,驱动程序如下(提交到yarn运行,yarn上需要有相应配置,具体见下面yarn部分):
public class JobSubmit {
public static void main(String[] args) throws Exception{
// 如果有特别的设置,可通过conf配置。
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 1.设置主方法类
job.setJarByClass(JopSubmitter.class);
// 2、封装参数: 本次job所要调用的Mapper实现类、Reducer实现类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 3、封装参数:本次job的Mapper实现类、Reducer实现类产生的结果数据的key、value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 4、封装参数:本次job要处理的输入数据集所在路径、最终结果的输出路径
FileInputFormat.setInputPaths(job, new Path("/wordcount/input"));
// 注意:输出路径必须不存在
FileOutputFormat.setOutputPath(job, new Path("/wordcount/output"));
// 5、封装参数:想要启动的reduce task的数量
job.setNumReduceTasks(2);
// 6、提交job给yarn
boolean res = job.waitForCompletion(true);
// 设置状态码看是否完成
System.exit(res?0:-1);
}
}
如果你只需要验证MapReduce是否符合预期,并且只用少量数据测试,可以在IDE环境下运行,当然需要配置一些本地环境,具体见另外一篇博客:https://blog.csdn.net/wangshun_410/article/details/90681225
在IDE中运行,只需要更改输入路径和输出路径即可
// 本地磁盘读取
FileInputFormat.setInputPaths(job, new Path("F:\\mrdata\\wordcount\\input"));
// 注意:输出路径必须不存在
FileOutputFormat.setOutputPath(job,new Path("F:\\mrdata\\wordcount\\output"));
Yarn快速理解
yarn的基本概念
yarn是一个分布式程序的运行调度平台
yarn中有两大核心角色:
Resource Manager
接受用户提交的分布式计算程序,并为其划分资源
管理、监控各个Node Manager上的资源情况,以便于均衡负载
Node Manager
管理它所在机器的运算资源(cpu + 内存)
负责接受Resource Manager分配的任务,创建容器、回收资源
YARN的安装
node manager在物理上应该跟data node部署在一起
resource manager在物理上应该独立部署在一台专门的机器上
1、修改配置文件:
vi yarn-site.xml
<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>node1</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
- scp这个yarn-site.xml到其他节点
- 启动yarn集群:start-yarn.sh (注:该命令应该在resourcemanager所在的机器上执行)
- 用jps检查yarn的进程,用web浏览器查看yarn的web控制台,网址http://node1:8088