1. 为什么用MapReduce?
小案例:
统计HDFS的/wordcount/input 目录下所有文件中的每个单词出现的次数——wordcount
这个wordcount程序可以在任何地方运行,访问HDFS上的文件并进行统计运算,并且可以把统计的结果写回HDFS的结果文件中;
但是,进一步思考:如果文件又多又大,用上面那个程序有什么弊端?
慢!因为只有一台机器在进行运算处理!
如何变得更快?
核心思想:让我们的运算程序并行在多台机器上执行!
2. MapReduce工作流程
MapReduce是一种分布式离线计算框架,主要分为MapTask 和 ReduceTask两部分。
以wordcount为例:
Input
文件可以存储在HDFS上,也可以存储在本地磁盘上,因为job客户端提交的程序可以运行在本地,但是是单机模式(测试专用),实际生产是运行在yarn集群上;
Split
如果输入文件存储在HDFS上,默认情况下,split的切片数就是block的个数;通常情况下,安装HDFS的data node的机器和安装yarn的node manager的机器是同一台,这样node manager的map task读数据时就是访问本地,速度会比访问其他机器要快(访问其他机器要通过网络流来传输数据);
Map
map task的个数由 split 的个数决定。
假如某个文件大小是500M,分成四块切片,第一到第三片是128M,第四片是116M;在Yarn集群上有两台node manager和一台resource manager,当job客户端提交时,resource manager会分配node manager启动map task的数量;在这里是每台node manager各启动两个map task去运算。
map中输出的内容 以 一种 key , value的形式输出。
当然map task会将该task中的所有数据做分区排序,分区规则默认按key的hascode来划分,排序则是按key的compareTo(),区号小的在前面,并且同区中按key升序排列;
Shuffle
在map中,每个 map 函数会输出一组 key/value对, Shuffle 阶段需要从所有 map主机上把相同的 key 的 key value对组合在一起,组合后传给 reduce主机, 作为输入进入 reduce函数里。
Reduce
reduce task的个数由自己决定,默认1个。Reduce() 函数以 key 及对应的 value 列表作为输入,按照用户自己的程序逻辑,经合并 key 相同的 value 值后,产 生另外一系列 key/value 对作为最终输出写入 HDFS。
3. MapReduce核心工作机制
3.1 map task
当某一个map task被启动后,yarnchild(map task)这个进程会调用TextInputFormat类中的LineRecordReader()方法,这方法里面有next()方法,这个方法会去读某个切片(一个切片给一个map task)并且按行读,产生一对<key,value>,key:行起始偏移量,value:行内容;
接着会调用我们写的类的map方法,数据处理的逻辑在这个map方法中实现,接着调用context.write(key,value)方法,将处理后的key,value方法写出去。
每调用一次next()就会调用一次map(),就会持续地产生新的<key,value>,而在context.write()后,会将一对对<key,value>放入环形缓存区;当环形缓存区的<k,v>占了大约80%的空间后,Spiller就会把缓存区的<k,v>取出来并进行分区排序,分区规则默认按key的hascode来划分,排序则是按key的compareTo(),区号小的在前面,并且同区中按key升序排列,这样会产生溢出文件;
循环往复上述过程,就会产生多个溢出文件,最后当next()方法被最后一次调用(即该切片被读完了),map()方法也随之会产生最后一对<k,v>,当所有溢出文件生成后,map task会再一次将溢出文件合并(在这里还可以调用Combiner组件进行局部聚合,数据倾斜的案例可以应用到该组件),将所有溢出文件合并成一个溢出文件并再次分区排序,最后再将这个溢出文件与分区索引文件(即哪一部分属于哪一个区的索引文件)一起纳入NodeManager 的web 程序的document目录中,map task就完成了。
3.2 reduce task
reduce task启动后,会通过http下载属于同一个区的数据,并合并排序成一个文件;然后会调用我们写的类中的reduce();在reduce()中,value是通过一个迭代器获取的,而判断合并文件中相邻key是否属于同一组key会调用GroupingComparator.compare(o1,o2),最后会返回一对<k,v>,该文件中有多少组就会返回多少对<k,v>,最终将文件输出到定义的输出目录中,通常取名为part-r-0000x
4. MapReduce 在 Yarn上的全流程
5. 编程案例
需求:
对以下订单数据实现:输出每一类订单中总金额最高的前三的订单信息
同一类订单中排序规则:先按总金额降序,若总金额相等则按商品名字升序
订单号 | 用户Id | 商品名 | 单价 | 数量 |
---|---|---|---|---|
order001 | u001 | 小米6 | 1999.9 | 2 |
order001 | u001 | 雀巢咖啡 | 99.0 | 2 |
order001 | u001 | 安慕希 | 250.0 | 2 |
order001 | u001 | 经典红双喜 | 200.0 | 4 |
order001 | u001 | 防水电脑包 | 400.0 | 2 |
order002 | u002 | 小米手环 | 199.0 | 3 |
order002 | u002 | 榴莲 | 15.0 | 10 |
order002 | u002 | 苹果 | 4.5 | 20 |
order002 | u002 | 肥皂 | 10.0 | 40 |
order003 | u001 | 小米6 | 1999.9 | 2 |
order003 | u001 | 雀巢咖啡 | 99.0 | 2 |
order003 | u001 | 安慕希 | 250.0 | 2 |
order003 | u001 | 经典红双喜 | 200.0 | 4 |
order003 | u001 | 防水电脑包 | 400.0 | 2 |
输出结果如下:
order001,u001,小米6,1999.9,2
order001,u001,防水电脑包,400.0,2
order001,u001,经典红双喜,200.0,4
order002,u002,小米手环,199.0,3
order002,u002,肥皂,10.0,40
order002,u002,榴莲,15.0,10
order003,u001,小米6,1999.9,2
order003,u001,经典红双喜,200.0,4
order003,u001,防水电脑包,400.0,2
分析:
有多种做法,例如在map阶段,将订单号作为key,整个订单信息(OrderBean对象)作为value输出给reduce task;在reduce阶段,整个订单信息作为key,value为Null作为最终的输出;迭代value将对象add到ArrayList中,并且重载OrderBean的compareTo方法,使之按照排序规则进行排序,然后再对ArrayList进行排序就会按照compareTo()的逻辑进行排序,最后输出ArrayList前3个对象即可;
当然,在了解到 3. MapReduce核心工作机制 后,因为map reduce本身也会存在排序机制,那么我们可以利用这个机制实现,而不必在reduce task中先把对象存入list中排序
关键点:
- 在map task中就将整个OrderBean对象作为key,Null作为value,然后在OrderBean中重载compareTo()方法
- 往前一步map task,在分区时要保证<(o1,u1,小米6,1999.9,2),null>,<(o1,u1,防水电脑包,400.0,2),null>分给同一个reduce task。因为此时OrderBean作为key,如果按照默认情况,分区时会按key的hashcode进行分区,从而会认为<(o1,…,2),null>,<(o1,…,2),null>不属于同一个区;
需要修改partitioner(), 按照 key.getorderId().hashCode()分区, 使之按照相同的orderId分给同一个reduce task - 在reduce task接收到数据后,例如某一个task收到:<(o1,…,3),null>,<(o1,…,2),null>,<(o3,…,3),null>
这里需保证<(o1,…,3),null>,<(o1,…,2),null>是一组,而<(o3,3),null>属于另一组
因为map task是按照hashcode分区,在reduce task需要进一步分组
在这里有WirtableComparator父类,重载比较方法即可
代码
OrderBean类
package ordergrouping;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
// 实现hadoop序列化和可比较的接口 WritableComparable<OrderBean>
public class OrderBean implements WritableComparable<OrderBean>{
private String uId;
private String pId;
private String pName;
private float price;
private int num;
private float amountPrice;
public void set(String uId, String pId, String pName, float price,int num) {
this.uId = uId; // 这里应该是orderId 不管了,就当uId代表订单号吧
this.pId = pId;
this.pName = pName;
this.price = price;
this.num = num;
this.amountPrice = price*num;
}
public String getuId() {
return uId;
}
public void setuId(String uId) {
this.uId = uId;
}
public String getpId() {
return pId;
}
public void setpId(String pId) {
this.pId = pId;
}
public String getpName() {
return pName;
}
public void setpName(String pName) {
this.pName = pName;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public float getAmountPrice() {
return amountPrice;
}
public void setAmountPrice(float amountPrice) {
this.amountPrice = amountPrice;
}
@Override
public String toString() {
return this.uId + ","+this.pId + ","+this.pName + ","+this.price + "," + this.num + "," + this.amountPrice;
}
@Override
public void readFields(DataInput in) throws IOException {
this.uId = in.readUTF();
this.pId = in.readUTF();
this.pName = in.readUTF();
this.price = in.readFloat();
this.num = in.readInt();
this.amountPrice = this.price*this.num;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.uId);
out.writeUTF(this.pId);
out.writeUTF(this.pName);
out.writeFloat(this.price);
out.writeInt(this.num);
out.writeFloat(this.amountPrice);
}
// 先比较订单id(升序),再比较总金额(降序)
@Override
public int compareTo(OrderBean o) {
return this.getuId().compareTo(o.getuId())==0?Float.compare(o.getAmountPrice(), this.amountPrice):this.getuId().compareTo(o.getuId());
}
}
主类
package ordergrouping;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
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;
/**
* 利用reduce task本身存在的对key升序排序机制,例如<o1,3>,<o1,2>,<o2,3>,<o3,3>
* 如果我们要实现对每类订单输出总金额前三的订单信息就可以利用这个机制来实现,而不必在reduce task中先把对象存入list中排序
* 关键点:
* 1. 将整个OrderBean对象作为key,然后在OrderBean中重载compareTo()方法
* 2. 往前一步map task,在分区时要保证<(o1,3),null>,<(o1,2),null>分给同一个reduce task;
* 需要修改partitioner(), 按照 key.getuId().hashCode()分区, 使之按照相同的uId分给同一个reduce task
* 3. 在reduce task接收到数据后,例如某一个task收到:<(o1,3),null>,<(o1,2),null>,<(o3,3),null>
* 这里需保证<(o1,3),null>,<(o1,2),null>是一组,而<(o3,3),null>属于另一组
* 因为map task是按照hashcode分区,在reduce task需要进一步分组,按照平时可以用equal() 比较两个对象的值
* 在这里有WirtableComparator父类,重载比较方法即可
*
*/
public class OrderGrouping {
// map方法
public static class OrderGroupingMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable>{
OrderBean bean = new OrderBean();
NullWritable n = NullWritable.get();
@Override
protected void map(LongWritable key,Text value,Context context)
throws IOException, InterruptedException {
String[] split = value.toString().split(",");
bean.set(split[0], split[1], split[2], Float.parseFloat(split[3]), Integer.parseInt(split[4]));
context.write(bean, n);
}
}
// reduce方法
public static class OrderGroupingReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable>{
@Override
protected void reduce(OrderBean key,Iterable<NullWritable> values,
Reducer<OrderBean, NullWritable, OrderBean, NullWritable>.Context context)
throws IOException, InterruptedException {
int i=0;
for (NullWritable v : values) {
context.write(key, v);
if(++i==3) return;
}
}
}
// job提交客户端
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(OrderGrouping.class);
job.setPartitionerClass(OrderPartitioner.class);
job.setGroupingComparatorClass(OrderGroupingComparator.class);
job.setMapperClass(OrderGroupingMapper.class);
job.setReducerClass(OrderGroupingReducer.class);
job.setNumReduceTasks(2);
job.setMapOutputKeyClass(OrderBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path("e:/ordercount/input"));
FileOutputFormat.setOutputPath(job, new Path("e:/ordercount/groupoutput"));
job.waitForCompletion(true);
}
}
分区逻辑
package ordergrouping;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;
// 按照订单中的uId分区
public class OrderPartitioner extends Partitioner<OrderBean, NullWritable>{
@Override
public int getPartition(OrderBean key, NullWritable value, int numPartitions) {
return (key.getuId().hashCode() & Integer.MAX_VALUE)% numPartitions;
}
}
GroupingComparator
package ordergrouping;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
public class OrderGroupingComparator extends WritableComparator{
public OrderGroupingComparator() {
// 告诉他下面要比较的是哪一种对象(OrderBean),并且在构造函数中实例化对象
super(OrderBean.class,true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
OrderBean o1 = (OrderBean)a;
OrderBean o2 = (OrderBean)b;
return o1.getuId().compareTo(o2.getuId());
}
}