本文已参与「新人创作礼」活动,一起开启掘金创作之路。
配置HDFS的StaticUser
配置完成之后,可以在浏览器上实现对集群的管理(创建文件夹,删除文件夹等)
cd /opt/module/hadoop-3.1.3/etc/hadoop/
vim core-site.xml
<property>
<name>hadoop.http.staticuser.user</name>
<value>hike</value>
</property>
xsync core-site.xml
stop-dfs.sh
start-dfs.sh
1、框架原理回顾
配置HDFS的StaticUser
修改core-site.xml文件的配置。
InputFormat模块: 负责把输入数据变为KV值,第一个工作负责数据的切片,考虑将数据分为几份,分完份之后,每个切片会启动一个MapTask并行处理(分布式),处理完成之后,对于每个MapTask,又会调用InputFormat把每一个切片的数据编程KV值,把KV值输入到Mapper中。其中有很多的InputFormat,也自定义了一个InputFormat。
job提交流程: job在提交之前,在客户端需要做出一些准备,重点是向临时文件夹提交了三个文件,第一个是需要使用到的jar包,第二个是切片信息,第三个是job的配置XML文件。
几个概念辨析:
Map阶段: 是一个概念性的描述,在这个阶段,实际执行的MapTask,Map阶段的流程是由MapTask控制的(MapTask.run)。
MapTask.run: 执行的Map阶段,在run中会创建Mapper的对象,执行Mapper的run方法。
Mapper: 在MapTask中会调用自定义Mapper的map方法,也就是Mapper.map。
Mapper.map: 一个方法。
二、shuflle机制
****从Mapper将数据写出去一直到Reducer将数据写进来之间的阶段都是Shuffle,也就是MapTask的后半部分和ReducerTask的前半部分。在这期间,数据发生了非常关键的变化,Mapper出去的数据是无序的,而在Reducer的KV值有分组的,也就是说,在shuffle阶段,其将数据进行了归纳整理,分好组了,分组通过排序实现。快速排序是最快的,但其要求数据全部都在内存中一次排完,对于大数据环境下是不现实的。框架的排序将待排序的数据分成很多份,按份进行排序,份内有序,称为局部排序。块内使用快排,接下来需要归并(将多个有序的块合成一块有序的块),归并并不需要将所有排序的数据都放到内存中,只需要一块很小的缓冲区就可以完成归并过程。
核心为三次排序:在内存中的快排,之后的归并,最后的归并,一次做的事情分三次做,花费的时间更多,使用的资源更少,空间换时间,永远不变的定理。
可能存在的问题: 在map处理完成数据之后,之后需要将数据汇总到reduce中进行处理,在汇总过程中,reduce会把所有的数据放到本地进行一次归并排序,reduce要处理的数据量十分之大,并行处理是解决此问题的一个方法,也就是启动多个reduce来处理数据。
那么reduce的数量如何决定呢: 在map中,数据被切成几片,就有几个map来处理数据,但reduce的数量是通过setNumReduceTask()方法,人为设定的,具体设置几个,根据实际情况灵活变动。
每一个reduce处理哪几个map文件:
默认的分区规则: (key.hashcode() & Integer.MAX_VALUE) % numReduceTasks。相同的key一定会进入到相同的reduce,& Integer.MAX_VALUE的目的是去负号,&运算规则:全是1为1,其余为0。
此做法有一个隐患: 数据倾斜(如以A开头的字母数量非常多,交给第一个reduce,B开头的一个,交给第二个reduce,C开头的一个,交给第三个reduce)会造成第一reduce的任务十分之重。可以使用自定义倾斜的方式解决数据倾斜的问题。
如何自定义分区:
****需求:将统计结果按照手机归属地不同省份输出到不同文件中 , 136/137/128/139开头的手机号分到独立的4个文件中,其他开头的放到一个文件中
package com.hike.mr.partitioner;
import com.hike.mr.flow.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* 需求:将统计结果按照手机归属地不同省份输出到不同文件中
* 136/137/128/139开头的手机号分到独立的4个文件中,其他开头的放到一个文件中
*/
public class MyPartitioner extends Partitioner<Text, FlowBean> {
/**
* 为每一条KV对,返回它们对应的分区号
* @param text 手机号
* @param flowBean 流量
* @param numPartitions
* @return
*/
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
switch (text.toString().substring(0,3)){
case "136":
return 0;
case "137":
return 1;
case "138":
return 2;
case "139":
return 3;
default:
return 4;
}
}
}
新建Driver类,添加以下两行语句,其余部分与其他Driver一样。
//设置reduce分区数量
job.setNumReduceTasks(5);
//使用自定义的组件而不是默认的组件
job.setPartitionerClass(MyPartitioner.class);
WritableComparable自定义排序: 框架会自动按照key进行排序,现在想将其他数据(FlowBean)放到key的位置进行排序,需要实现WritableComparable接口,让默认的Comparator实现compareTo方法来进行比较。
FlowBean实现接口,实现方法,Mapper封装数据,Reducer写出数据。
需求:将输出数据再按照总流量将需进行排序。
FlowBean类
//想要在框架中使用,需要实现接口
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
@Override
public String toString(){
return upFlow + "/t" + downFlow + "/t" + sumFlow;
}
public void set(long upFlow,long downFlow){
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
/**
* 将对象数据写出到框架指定地方 序列化
* 当map端处理完数据之后,通过网络发送给reduce,需要经过序列化,当map端序列化时需要调用write方法,同时传一个容器
* @param dataOutput 数据的容器
* @throws IOException
*/
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
/**
* 从数据指定地方读取数据填充对象 反序列化
* 当map端序列化完成之后,将容器发送给reduce端,先进先出,通过反序列化读取数据
* @param dataInput 数据的容器
* @throws IOException
*/
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
/**
* 按照总流量的降序进行排序
* @param o
* @return
*/
@Override
public int compareTo(FlowBean o) {
// if(this.sumFlow < o.sumFlow){
// return 1;
// }else if(this.sumFlow == o.sumFlow){
// return 0;
// }else{
// return -1;
// }
return Long.compare(o.sumFlow,this.sumFlow);
}
}
Mapper类
//默认根据key进行排序,将FlowBean放在key的位置上
public class CompareMapper extends Mapper<LongWritable,Text, FlowBean,Text> {
private Text phone = new Text();
private FlowBean flow = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] fields = line.split("\t");
phone.set(fields[0]);
flow.setUpFlow(Long.parseLong(fields[1])); //将字符串转成整数
flow.setDownFlow(Long.parseLong(fields[2]));
flow.setSumFlow(Long.parseLong(fields[3]));
}
}
Reducer类
public class CompareReducer extends Reducer<FlowBean, Text,Text,FlowBean> {
/**
* Reduce收到的数据已经完成排序,直接输出即可
* @param key
* @param values
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
for (Text value : values) {
//先写value,再写key,因为reduce收到数据是,流量在前,手机号在后,反过来输出
context.write(value,key);
}
}
}
Driver类
public class CompareDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(CompareDriver.class);
job.setMapperClass(CompareMapper.class);
job.setReducerClass(CompareReducer.class);
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
combiner合并:
Combiner默认不开启,因为可能导致Mapper端和Reducer端的最终结果不一致。
Combiner就是Reducer,对输入的数据有要求,数据一定是要有序的。
在数据从环形缓冲区出来之前,会经过combiner合并一次,归并完成之后会合并一次,最终将某一个map中的数据写到磁盘上,局部汇总。
最终的数据汇总还是会由Reduce完成,Reduce负责全局汇总。
在Driver类中添加job.setConbinerClass(Reducer类名.class)即可开启局部汇总功能,Conbiner功能可以大幅度提升系统的性能。
GroupingComparator分组比较器: 在reduce中的数据是一个key有序的数据,进一步需要将数据按照组输入到reduce中,在最后的分组过程中,会比较key的值,相同分为一组,不同分为两组,在这时候,会使用到分组比较器,这就是分组比较器的作用:判断两组数据key的值相不相同。
自定义GroupingComparator分组比较器:
public class OrderBean implements WritableComparable<OrderBean> {
private String orderId;
private String productId;
private double price;
@Override
public String toString() {
return "OrderBean{" +
"orderId='" + orderId + '\'' +
", productId='" + productId + '\'' +
", price=" + price +
'}';
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
/**
* 先按照订单排序,订单相同按照价格降序排序
* @param o
* @return
*/
@Override
public int compareTo(OrderBean o) {
int compare = this.orderId.compareTo(o.orderId);
if(compare != 0){
return compare;
}else {
return Double.compare(o.price,this.price);
}
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(orderId);
out.writeUTF(productId);
out.writeDouble(price);
}
@Override
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.productId = in.readUTF();
this.price = in.readDouble();
}
}
/**
* 封装OrderBean
*
*/
public class OrderMapper extends Mapper<LongWritable,Text,OrderBean,NullWritable> {
private OrderBean order = new OrderBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//拆分数据
String[] fields = value.toString().split("\t");
//封装数据
order.setOrderId(fields[0]);
order.setProductId(fields[1]);
order.setPrice(Double.parseDouble(fields[2]));
//写出数据
//mapper写出去的数据,最终会进入环形缓冲区,缓冲区会将数据写出来,到reduce之前会得到一个有序的数据
//进一步要对这个数据进行分组,如果没有重写分组比较器,会自动调用默认的WritableComparator,调用OrderBean
//会按照订单和价格两个因素进行分组,所以下一步需要重写分组比较器
context.write(order,NullWritable.get());
}
}
/**
* 按照订单编号对订单数据进行分组
*/
public class OrderComparator extends WritableComparator {
//初始化,提前将类创建好
//数据在进入环形缓冲区(byte字节数组)时,是要序列化之后,才能够进入的
//写入字节数组,排序时需要将数据反序列化,在本地准备几个空对象,通过空对象的readFields方法实现反序列化
//然后再进行比较,以下告诉它需要创建哪两个空对象
protected OrderComparator() {
super(OrderBean.class,true);
}
/**
* 分组比较器,使得相同订单进入一组进行比较
* 只要订单相同就分为一组
* @param a
* @param b
* @return
*/
@Override
public int compare(WritableComparable a, WritableComparable b) {
OrderBean oa = (OrderBean) a;
OrderBean ob = (OrderBean) b;
return oa.getOrderId().compareTo(ob.getOrderId());
}
}
/**
* 取每个订单的最高价格
*/
public class OrderReducer extends Reducer<OrderBean, NullWritable,OrderBean, NullWritable> {
/**
*
* @param key 订单信息
* @param values 空
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key, NullWritable.get());
}
}
public class OrderDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(OrderDriver.class);
//设置分组比较器
job.setGroupingComparatorClass(OrderComparator.class);
job.setMapperClass(OrderMapper.class);
job.setReducerClass(OrderReducer.class);
job.setMapOutputKeyClass(OrderBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}