1、Hadoop序列化
1.1序列化概述
1.1.1什么是序列化 ?
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输;反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象
1.1.2为什么要序列化?
一般来说,“"活的”对象只生存在内存里,关机断电就没有了。而且“活的对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。然而序列化可以存储"活的”对象,可以将"活的”对象发送到迒程计算机。
1.1.3为什么不用Java的序列化
Java的序列化是一个車量级序列化框架( Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息, Header,继承体系等),不便于在网络中高效传输。所以, Hadoop自己开发了一套序列化机制( Writable )。
1.1.4Hadoop序列化特点
(1)紧凑:高效使用存储空间。
(2)快速ε读写数据的额外开销小。
(3)可扩展:随着通信协议的升级而可升级
(4)互操作:支持多语言的交互
1.2自定义bean对象实现序列化接口(Writable)
1.2.1具体实现bean对象序列化步骤如下7步。
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
(3)重写序列化方法
(4)重写反序列化方法
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString(),可用”t”分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。详见后面排序案例。
1.2.2统计每一个手机号耗费的总上行流量、下行流量、总流量
(2)输入数据格式:
7 13560436666 120.196.100.99 1116 954 200
id 手机号码 网络ip 上行流量 下行流量 网络状态码
(3)期望输出数据格式
13560436666 1116 954 2070
手机号码 上行流量 下行流量 总流量
编写流量统计的Bean对象
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sunFlow;
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 getSunFlow() {
return sunFlow;
}
public void setSunFlow(long sunFlow) {
this.sunFlow = sunFlow;
}
@Override
public String toString() {
return upFlow + "t" + downFlow + "t" + sunFlow;
}
public void set(long upFlow,long downFlow){
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sunFlow=upFlow+downFlow;
}
//序列化方法
//out:框架提供的数据出口
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sunFlow);
}
//反序列化方法(必须与序列化的顺序一样)
//in:框架提供的数据来源
@Override
public void readFields(DataInput in) throws IOException {
upFlow=in.readLong();
downFlow=in.readLong();
sunFlow=in.readLong();
}
}
编写Mapper类
public class FlowMapper 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[] fields = value.toString().split("t");
//System.out.println(fields[1]);
phone.set(fields[1]);
long upFlow = Long.parseLong(fields[fields.length - 3]);
long downFlow = Long.parseLong(fields[fields.length - 2]);
flow.set(upFlow,downFlow);
//将获取到的数据交给框架
context.write(flow,phone);
}
}
编写Reducer类
public class FlowReduce extends Reducer<Text,FlowBean,Text,FlowBean> {
private FlowBean flow=new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
long sumUpFlow=0;
long sumDownFlow=0;
for (FlowBean value : values) {
sumUpFlow +=value.getUpFlow();
sumDownFlow +=value.getDownFlow();
}
flow.set(sumUpFlow,sumDownFlow);
//输出结果
context.write(key,flow);
}
}
编写Driver驱动类
public class FlowDriver {
public static void main(String[] args) throws Exception {
//获取jion实例
Job job = Job.getInstance(new Configuration());
//设置类路径
job.setJarByClass(FlowDriver.class);
//设置Mapper和Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
//设置输入输出类型
job.setMapOutputKeyClass(Text.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);
}
}
2、MapReduce框架原理
2.1InputFormat数据输入
2.1.1 切片与MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
2.1.2 切片源码详解
2.1.3 FileInputFormat切片机制
FileInputFormat切片大小的参数配置
2.1.4 CombineTextInputFormat切片机制
(1)应用场景:
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
(2)虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
(3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
①虚拟存储过程:
将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
②切片过程:
(a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
(b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
(c)测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:
1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)
最终会形成3个切片,大小分别为:
(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
2.1.5 FileInputFormat实现类
2.1.6 自定义InputFormat
自定义 InputFormat步骤如下
①自定义一个类继承 FileInput Format
②改写 Recordreader,实现一次读取一个完整文件封装为KV
③在输出时使用SequenceFile OutPutFormat输出合并文件。
2.1.7 自定义InputFormat案例实操
(1)需求
将多个小文件合并成一个SequenceFile文件(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式),SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value。
①输入数据
②期望输出文件格式
(2)需求分析
(3)程序实现
自定义InputFromat
public class WholeFileInputFormat extends FileInputFormat<Text, BytesWritable> {
@Override
public RecordReader<Text, BytesWritable> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
return new WholeFileRecordReader();
}
//判断某个文件能否被切片,false:不被切片
@Override
protected boolean isSplitable(JobContext context, Path filename) {
return false;
}
}
自定义RecordReader类
//自定义RecordReader处理一个文件,把这个文件直接读取成一个kv值
public class WholeFileRecordReader extends RecordReader<Text, BytesWritable> {
//定义文件读取标记,未读为true
private boolean notRead=true;
//定义kv变量
private Text key=new Text();
private BytesWritable value=new BytesWritable();
//设置流
private FSDataInputStream inputStream;
//切片
private FileSplit fs;
//初始化方法,框架会在开始的时候调用一次
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
//转换切片类型到文件切片
fs = (FileSplit) split;
//通过切片获取路径
Path path = fs.getPath();
//通过路径获取文件系统
FileSystem fileSystem = path.getFileSystem(context.getConfiguration());
//开流
inputStream = fileSystem.open(path);
}
//读取下一组kv值,可以读到值返回true,读不到返回false
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (notRead) {
//读取文件
//读key
key.set(fs.getPath().toString());
//读value
byte[] buf = new byte[(int) fs.getLength()];
inputStream.read(buf);
value.set(buf,0,buf.length);
notRead=false;
return true;
} else {
return false;
}
}
//获取当前读到的key
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return key;
}
//获取当前读到的value
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
//当前数据读取的进度
@Override
public float getProgress() throws IOException, InterruptedException {
//未读返回0,已读返回1
return notRead ? 0 : 1;
}
//关闭资源
@Override
public void close() throws IOException {
IOUtils.closeStream(inputStream);
}
}
编写SequenceFileDriver类处理流程
public class WholeFileDriver {
public static void main(String[] args) throws Exception{
//获取job实例
Job job = Job.getInstance(new Configuration());
//设置类路径
job.setJarByClass(WholeFileDriver.class);
//设置Mapper和Reducer的输出数据
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
//设置数据输入输出
job.setInputFormatClass(WholeFileInputFormat.class);
job.setOutputValueClass(SequenceFileOutputFormat.class);
//设置输入输出数据
FileInputFormat.setInputPaths(job,new Path("d:input"));
FileOutputFormat.setOutputPath(job,new Path("d:output"));
//提交job
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}
2.2 MapReduce工作流程
2.2.1.流程示意图
2.2.2.流程详解
上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:
1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中
2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
3)多个溢出文件会被合并成大的溢出文件
4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
6)ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)
2.2.3.注意
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。