什么是序列化?
序列化,英文称作Serialization。指的是一个结构化对象可转变为字节流以便在网络上传输或者写到磁盘进行永久存储的过程。
序列化在Hadoop中,主要体现以下两个特点:
1.进程间通信:不同机子可将序列化结果进行相互传递
2.永久存储:一个对象序列化后,可以通过编码永久存储在磁盘中**
在Hadoop中,系统中多个节点上进程间的通信是通过“远程过程调用”(RPC)实现的。RPC协议将消息序列化成流后传递到远程节点,然后远程节点通过反序列化将流转为消息,其序列化机制有如下特征:
1.紧凑:网络带宽是数据中心最稀缺的资源,RPC序列化能够将其充分利用,通俗地讲,就是高效使用存储空间。
2.快速:不同节点之间的进程通信包括处理MR时的数据都会用到序列化,所以需要尽量减少序列化和反序列化的性能开销,可以使读写数据的开销尽可能小。
3.可扩展:为了满足协议的变化(或者类的升级或变化),控制客户端和服务器的过程中,可以直接引进相关协议。也就是说,可以透明地读取旧格式的数据。
4.互操作:支持以不同语言之间的通信,但需要设计一种特定的格式来满足这一需求。
为什么不用JAVA自带的序列化机制?
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。它绝对紧凑,快速,但不太容易用JAVA以外的语言进行使用和扩展。Avro序列化系统可以弥补Writable部分不足,由于没学到,所以就不多说了。
Writable接口:根据 DataInput 和 DataOutput 实现的简单、有效的序列化对象。
MR的任意Key和Value必须实现Writable接口。
MR的任意key必须实现WritableComparable接口。
序列化实例——电话流量统计
首先,我们先看电话信息数据文件:
以上信息我们需要的是第2列,第7列,第8列,分别对应:
电话号码 上传流量 下载流量
我们希望统计总流量,故输出应该为:
电话号码 上传流量 下载流量 总流量
相关分析及代码
1. 创建一个对象:FlowBean
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;
public class FlowBean implements Writable{
public FlowBean() {
// TODO Auto-generated constructor stub
}
public FlowBean(long up_flow, long down_flow) {
this.up_flow = up_flow;
this.down_flow = down_flow;
this.sum_flow = up_flow + down_flow;
}
private String phoneNum;
private long up_flow;
private long down_flow;
private long sum_flow;
public String getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
}
public long getUp_flow() {
return up_flow;
}
public void setUp_flow(long up_flow) {
this.up_flow = up_flow;
}
public long getDown_flow() {
return down_flow;
}
public void setDown_flow(long down_flow) {
this.down_flow = down_flow;
}
public long getSum_flow() {
return sum_flow;
}
public void setSum_flow(long sum_flow) {
this.sum_flow = sum_flow;
}
//通过编写toString方法来显示文本
@Override
public String toString() {
// TODO Auto-generated method stub
return up_flow+"\t"+down_flow+"\t"+sum_flow;
}
//序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(up_flow);
out.writeLong(down_flow);
out.writeLong(sum_flow);
}
//反序列化
@Override
public void readFields(DataInput in) throws IOException {
up_flow = in.readLong();
down_flow = in.readLong();
sum_flow = in.readLong();
}
}
分析
序列化和反序列中,可以看到,两者的顺序是一致的,先序列化up_flow,必定要先反序列化up_flow。这两者顺序一定不能写错,否则反序列化出来的可不是你想要的规格和信息。
为什么要写一个缺省的构造函数呢?因为在反序列化中,需要反复调用空参数函数。
重写toString方法是因为输出格式的需要,否则你会得到电话号码+一串地址。如图:
2.Map阶段
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
//FlowBean类是自定义的对象
public class FlowSumMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
public void map(LongWritable k1, Text v1, Context context) throws IOException, InterruptedException {
//先反序列
String line = v1.toString();
//利用转义字符“\t"来进行分隔
String fields[] = line.split("\t");
String phoneNum = fields[1];
int n = fields.length;
//将字符串转换为long型
long upFlow = Long.parseLong(fields[n-3]);
long downFlow = Long.parseLong(fields[n-2]);
//写入k2,v2
context.write(new Text(phoneNum), new FlowBean(upFlow, downFlow));
}
}
分析
fields[n-3]、field[n-2]分别指的是上传流量和下载流量,数组中的类型是String,故使用parseLong转换为Long型。
新写入的k2,v2,我们分别用Text和一个对象FlowBean进行传输。
3.Reduce阶段
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class FlowSumReducer extends Reducer<LongWritable, Text, Text, FlowBean> {
public void reduce(Text k3, Iterable<FlowBean> v3s, Context context)
throws IOException, InterruptedException {
long sum_upFlow = 0;
long sum_downFlow = 0;
// 遍历FlowBean,将上行流量和下行流量累加
for (FlowBean flowBean : v3s) {
sum_upFlow += flowBean.getUp_flow();
sum_downFlow += flowBean.getDown_flow();
}
//写入k4,v4
context.write(k3,new FlowBean(sum_upFlow,sum_downFlow));
}
}
Driver类
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class FlowBeanRunner {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException{
//1.建立Job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.建立jar路径
job.setJarByClass(FlowBeanRunner.class);
//3.建立mapper/reducer业务
job.setMapperClass(FlowSumMapper.class);
job.setReducerClass(FlowSumReducer.class);
//4.指定mapper输出类型,也就是key/values
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5.指定reducer输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6.选择输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//是否显示处理过程
job.waitForCompletion(true);
}
}
分析
在第6步中,args[0]这个文件夹一定是你输入的文件夹,必须创建。
至于输出则不该创建,否则出现错误。
总结
对于序列化,要明白的是在Hadoop中的相关原理和特征,对于Writeable接口,必须重写,心里要有点数。对于MR,根据业务需求来调整代码,总体来说,序列化对于传输和存储有着重要作用。
PS:我也是初学序列化,上述内容仅为个人理解,若有更好的建议,欢迎留言。