Hadoop(十二):多数据源Join统计
定义:在一个MR任务中可能需要访问和处理两个甚至更多个的数据集。Hadoop由于没有像关系型数据库那样join的连接处理操作,所以多数据源的连接处理会比较复杂,需要程序员自己实现。
-
Map端数据连接
-
Reduce端数据连接
-
总共包含了四种小的连接方式。
Map端数据连接(map side join)
-
一个大文件(file1)和一个小文件(file2)进行join操作的情况,file2的内容可以放到内存中。
-
将小表复制多份,让每个map task内存中存在一份(比如存放到hash table/hash map中).
-
然后只扫描大表:对于大表中的每一条记录key/value,在hash table中查找是否有相同的key的记录,如果有,则连接后输出即可。
使用方式概述:
-
直接使用job.addCacheFile(URI)进行添加file2的文件地址。
-
在map中使用context.getCacheFiles()来获取缓存文件的地址信息
-
指定的文件地址要求是hdfs上的文件地址,比如:hdfs://hh:8020/beifeng/cache.file
优点:实现简单,shuffle数据量少。
缺点:如果小文件的内容太多,无法存放到内存中,就无法实现,对环境要求比较严格。
package com.beifeng; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.HashMap; import java.util.Map; 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.Mapper; /** * 演示map端数据join操作<br/> * file1是一个大表,file2是一个小表 * * @author gerry * */ public class Demo1 { public static void main(String[] args) throws Exception { //正常配置信息 Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "test"); job.setJarByClass(Demo1.class); job.setMapperClass(DemoMapper.class); //省略其他配置... //添加文件到内存中 job.addCacheFile(new Path("hdfs://hh:8020/beifeng/file2.txt").toUri()); job.waitForCompletion(true); } /** * 自定义的mapper类 * * @author gerry * */ public static class DemoMapper extends Mapper<Object, Text, Object, Object> { // 保存file2的map对象,key是join操作的时候对应的字段信息,value是file2中其他字段信息 private Map<Object, Object> cache = new HashMap<>(); @Override protected void setup(Mapper<Object, Text, Object, Object>.Context context) throws IOException, InterruptedException { super.setup(context); URI[] uris = context.getCacheFiles();// 获取所有配置的cache file uri信息 // 循环处理每个uris for (URI uri : uris) { Path path = new Path(uri); // 拿到文件输入流 InputStream is = path.getFileSystem(context.getConfiguration()).open(path); // TODO 进行文件内容的解析操作,将解析结果保存到cache中 } } @Override protected void map(Object key, Text value, Mapper<Object, Text, Object, Object>.Context context) throws IOException, InterruptedException { String line = value.toString(); // 拿到file1(大文件)中对应行的信息 // 把文件中的key写道joinkey中 Object joinKey = null; if (this.cache.containsKey(joinKey)) { // 表示file2中存在这个joinKey,表示数据连接成功,进行输出操作 Object outputKey = null; // 定义根据业务逻辑来 Object outputValue = null; // 根据逻辑来 context.write(outputKey, outputValue); } } } }
Reduce端数据连接(reduce side join)
-
在map阶段,map函数同时读取两个文件File1和File2,为了区分两种来源的key/value数据对.
-
对每条数据打一个标签(tag),比如:tag=1表示来自文件File1,tag=2表示来自文件File2。
-
即:map阶段的主要任务是对不同文件中的数据打标签。
-
-
在reduce阶段,reduce函数获取key相同的来自File1和File2文件的value list, 然后对于同一个key,对File1和File2中的数据进行join(笛卡尔乘积)。
-
即:reduce阶段进行实际的连接操作。
优点:实现简单。
缺点:shuffle数据量大,对网络要求比较严格。对reduce端的内存有要求。会涉及到二次排序(可选)。
-
package com.beifeng; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.Writable; import org.apache.hadoop.io.WritableComparable; 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.FileSplit; /** * 演示reduce端数据join操作 * * @author gerry * */ public class Demo2 { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "test"); job.setJarByClass(Demo1.class); // 其他配置信息 job.setMapperClass(DemoMapper.class); // 其他配置信息 // 设置二次排序的相关参数配置 job.waitForCompletion(true); } /** * 自定义的mapper类 * * @author gerry * */ public static class DemoMapper extends Mapper<Object, Text, MapperOutputKey, MapperOutputValue> { private MapperOutputValue ov = new MapperOutputValue(); private MapperOutputKey ok = new MapperOutputKey(); @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); // 拿到file1(大文件)中对应行的信息 FileSplit fs = (FileSplit) context.getInputSplit(); if (fs.getPath().toString().contains("file1")) { // file1文件的处理方式 // 采用二次排序 this.ok.tag = 1; this.ov.tag = 0; // 不采用二次排序 this.ok.tag = 0; this.ov.tag = 1; // 公用部分 this.ok.joinKey = null; // TODO: 根据line信息获取join字段的对应信息 this.ov.file1Value = line; // 设置file1的value this.ov.file2Value = null; // 覆盖 } else { // file2文件的处理方式 // 使用二次排序 this.ok.tag = 2; this.ov.tag = 0; // 不使用二次排序 this.ok.tag = 0; this.ov.tag = 2; // 都需要的部分 this.ok.joinKey = null; // TODO: 根据line信息获取join字段的对应信息 this.ov.file2Value = line; // 设置file2的value this.ov.file1Value = null; // 覆盖 } context.write(ok, ov); } } /** * 输入到reduce的结果是按照joinKey进行分组分区的,按照joinKey+tag进行排序的,排序规则:先按照joinKey排序, * 然后在按照tag排序,tag为1的在前,为2的在后。 * * @author gerry * */ public static class DemoReducer extends Reducer<MapperOutputKey, MapperOutputValue, Object, Object> { // 假设file1和file2之间是一对多的join操作,或者file1中几条数据对应file2中很多条数据join private Set<Object> file1Cache = new HashSet<>(); @Override protected void reduce(MapperOutputKey key, Iterable<MapperOutputValue> values, Context context) throws IOException, InterruptedException { this.file1Cache.clear(); // 清空操作 for (MapperOutputValue value : values) { if (key.tag == 1) { // 表示还出于刚刚开始阶段 this.file1Cache.add(value.file1Value); } else { // 如果tag不为1,那么表示file1的文件内容以及完全读取完成,开始和file2的文件内容进行join操作 Object file2v = value.file2Value; for (Object file1v : this.file1Cache) { // 对应file1v和file2v进行join操作,然后将结果进行输出操作即可 // 正式join操作的地方 } } } } } /** * 不使用二次排序,那么key中的tag标签在map的时候全部设置为0,在value中添加一个tag,含义和之前的一样 * * @author gerry * */ public static class DemoReducer2 extends Reducer<MapperOutputKey, MapperOutputValue, Object, Object> { private Set<Object> file1Cache = new HashSet<>(); private Set<Object> file2Cache = new HashSet<>(); @Override protected void reduce(MapperOutputKey key, Iterable<MapperOutputValue> values, Context context) throws IOException, InterruptedException { this.file1Cache.clear(); // 清空操作 this.file2Cache.clear(); for (MapperOutputValue value : values) { if (value.tag == 1) { // 表示还出于刚刚开始阶段 this.file1Cache.add(value.file1Value); } else { this.file2Cache.add(value.file2Value); } } // 进行join操作 for (Object v1 : this.file1Cache) { for (Object v2 : this.file2Cache) { // 针对v1和v2进行join操作并输出 } } } } public static class MapperOutputValue implements Writable { public Object file1Value; public Object file2Value; public int tag; // 当不适用二次排序的时候使用该值 @Override public void write(DataOutput out) throws IOException { // TODO Auto-generated method stub } @Override public void readFields(DataInput in) throws IOException { // TODO Auto-generated method stub } } public static class MapperOutputKey implements WritableComparable<MapperOutputKey> { public Object joinKey; public int tag; // 标签 @Override public void write(DataOutput out) throws IOException { // TODO Auto-generated method stub } @Override public void readFields(DataInput in) throws IOException { // TODO Auto-generated method stub } @Override public int compareTo(MapperOutputKey o) { // TODO Auto-generated method stub return 0; } } }
半连接 Semi join
-
为了解决reduce join高网络传输量的一种解决方案。目标是能够在map端过滤掉不会参加join操作的数据,则可以大大节省网络IO。
-
选取一个小表,假设是File1,将其参与join的key抽取出来,保存到文件File3中,File3文件一般很小,可以放到内存中。
-
在map阶段,使用job.addCacheFile方法将File3复制到各个执行Task节点上,然后将File2中不在File3中的key对应的记录过滤掉,剩下的reduce阶段的工作与reduce side join相同。
其实这种方式就是结合map side join和reduce sid join的一个连接方式。
优点:减低shuffle网络传输量。
缺点:增加了编程复杂性,增加了对map端内存的压力。
package com.beifeng; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.HashSet; import java.util.Set; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.Writable; import org.apache.hadoop.io.WritableComparable; 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.FileSplit; /** * 演示半连接<br/> * file1和file2进行join操作,然后file1中的joinKey提出出来形成file3<br/> * 一般情况下选择file1和file2文件中,文件数据相对而言少的文件进行joinKey提出形成file3文件的方式。 * * @author gerry * */ public class Demo3 { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "test"); job.setJarByClass(Demo1.class); // 其他配置信息 job.setMapperClass(DemoMapper.class); // 其他配置信息 // 配置reduce side join时候需要的二次排序 // 添加分布式缓存 job.addCacheFile(new Path("hdfs://hh:8020/beifeng/file3.txt").toUri()); job.waitForCompletion(true); } /** * 自定义的mapper类 * * @author gerry * */ public static class DemoMapper extends Mapper<Object, Text, MapperOutputKey, MapperOutputValue> { private MapperOutputValue ov = new MapperOutputValue(); private MapperOutputKey ok = new MapperOutputKey(); // 保存file3的map对象,key是join操作的时候对应的字段信息 private Set<Object> joinKeySet = new HashSet<>(); @Override protected void setup(Context context) throws IOException, InterruptedException { URI[] uris = context.getCacheFiles();// 获取所有配置的cache file uri信息 // 循环处理每个uris for (URI uri : uris) { Path path = new Path(uri); // 拿到文件输入流 InputStream is = path.getFileSystem(context.getConfiguration()).open(path); // 解析文件输入流,然后进行文件内容的解析操作,将解析结果保存到joinKeySet中 } } @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); // 拿到file1(大文件)中对应行的信息 FileSplit fs = (FileSplit) context.getInputSplit(); if (fs.getPath().toString().contains("file1")) { // file1文件的处理方式 // 采用二次排序 this.ok.tag = 1; this.ov.tag = 0; // 不采用二次排序 this.ok.tag = 0; this.ov.tag = 1; // 公用部分 this.ok.joinKey = null; // TODO: 根据line信息获取join字段的对应信息 this.ov.file1Value = line; // 设置file1的value this.ov.file2Value = null; // 覆盖 context.write(ok, ov); } else { // file2文件的处理方式 Object joinKey = null;// TODO: 根据line信息获取join字段的对应信息 if (this.joinKeySet.contains(joinKey)) { // 如果存在,表示可以进行join操作,否则不会jion操作,直接过滤掉数据 // 使用二次排序 this.ok.tag = 2; this.ov.tag = 0; // 不使用二次排序 this.ok.tag = 0; this.ov.tag = 2; // 都需要的部分 this.ok.joinKey = joinKey; this.ov.file2Value = line; // 设置file2的value this.ov.file1Value = null; // 覆盖 context.write(ok, ov); } } } } /** * 输入到reduce的结果是按照joinKey进行分组分区的,按照joinKey+tag进行排序的,排序规则:先按照joinKey排序, * 然后在按照tag排序,tag为1的在前,为2的在后。 * * @author gerry * */ public static class DemoReducer extends Reducer<MapperOutputKey, MapperOutputValue, Object, Object> { // 假设file1和file2之间是一对多的join操作,或者file1中几条数据对应file2中很多条数据join private Set<Object> file1Cache = new HashSet<>(); @Override protected void reduce(MapperOutputKey key, Iterable<MapperOutputValue> values, Context context) throws IOException, InterruptedException { this.file1Cache.clear(); // 清空操作 for (MapperOutputValue value : values) { if (key.tag == 1) { // 表示还出于刚刚开始阶段 this.file1Cache.add(value.file1Value); } else { // 如果tag不为1,那么表示file1的文件内容以及完全读取完成,开始和file2的文件内容进行join操作 Object file2v = value.file2Value; for (Object file1v : this.file1Cache) { // 对应file1v和file2v进行join操作,然后将结果进行输出操作即可 // 正式join操作的地方 } } } } } /** * 不使用二次排序,那么key中的tag标签在map的时候全部设置为0,在value中添加一个tag,含义和之前的一样 * * @author gerry * */ public static class DemoReducer2 extends Reducer<MapperOutputKey, MapperOutputValue, Object, Object> { private Set<Object> file1Cache = new HashSet<>(); private Set<Object> file2Cache = new HashSet<>(); @Override protected void reduce(MapperOutputKey key, Iterable<MapperOutputValue> values, Context context) throws IOException, InterruptedException { this.file1Cache.clear(); // 清空操作 this.file2Cache.clear(); for (MapperOutputValue value : values) { if (value.tag == 1) { // 表示还出于刚刚开始阶段 this.file1Cache.add(value.file1Value); } else { this.file2Cache.add(value.file2Value); } } // 进行join操作 for (Object v1 : this.file1Cache) { for (Object v2 : this.file2Cache) { // 针对v1和v2进行join操作并输出 } } } } public static class MapperOutputValue implements Writable { public Object file1Value; public Object file2Value; public int tag; // 当不适用二次排序的时候使用该值 @Override public void write(DataOutput out) throws IOException { // TODO Auto-generated method stub } @Override public void readFields(DataInput in) throws IOException { // TODO Auto-generated method stub } } public static class MapperOutputKey implements WritableComparable<MapperOutputKey> { public Object joinKey; public int tag; // 标签 @Override public void write(DataOutput out) throws IOException { // TODO Auto-generated method stub } @Override public void readFields(DataInput in) throws IOException { // TODO Auto-generated method stub } @Override public int compareTo(MapperOutputKey o) { // TODO Auto-generated method stub return 0; } } }
Reduce端连接+BloomFilter过滤(reduce side join + BloomFilter)
-
某些情况下,Semi Join抽取出来的小表的key集合在内存中仍然存放不下,这时候可以使用BloomFilter以节省空间。
-
BloomFilter的主要作用是判断元素是否在一个集合中。BloomFilter不会出现false negative(返回false,但是结果存在),会出现少量的true negative(返回true,但是结果不存在)。
-
因而可能有一些不在小表中的记录没有过滤掉(但是在小表中的记录一定不会过滤掉),这没什么关系,不会影响最终运算结果,只不过增加了少量的网络IO而已。其他处理方式同半连接。
-
其实和半连接一样,只是在map阶段不是把Join key放在文件中,而是放在BloomFilter。
优点:减低shuffle网络传输量,降低对map端内存的压力。
缺点:增加了编程复杂性。
package com.beifeng; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.HashSet; import java.util.Set; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.Writable; import org.apache.hadoop.io.WritableComparable; 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.FileSplit; import org.apache.hadoop.util.bloom.BloomFilter; import org.apache.hadoop.util.bloom.Key; /** * 演示BloomFilter应用于半连接的方式<br/> * 采用方式基本和半连接类型,除了在maper端进行过滤的方式不一样外 * * @author gerry * */ public class Demo4 { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "test"); job.setJarByClass(Demo1.class); // 其他配置信息 job.setMapperClass(DemoMapper.class); // 其他配置信息 // 配置reduce side join时候需要的二次排序 // 添加分布式缓存 job.addCacheFile(new Path("hdfs://hh:8020/beifeng/file3.txt").toUri()); job.waitForCompletion(true); } /** * 自定义的mapper类 * * @author gerry * */ public static class DemoMapper extends Mapper<Object, Text, MapperOutputKey, MapperOutputValue> { private MapperOutputValue ov = new MapperOutputValue(); private MapperOutputKey ok = new MapperOutputKey(); // 保存file3的map对象,key是join操作的时候对应的字段信息 private BloomFilter bf = null; @Override protected void setup(Context context) throws IOException, InterruptedException { URI[] uris = context.getCacheFiles();// 获取所有配置的cache file uri信息 int line = 0; // 循环处理每个uris,获得文件的总体的行数 for (URI uri : uris) { Path path = new Path(uri); // 拿到文件输入流 InputStream is = path.getFileSystem(context.getConfiguration()).open(path); } this.bf = new BloomFilter(line, 2, 0); // 第三个参数必须为0或者1 // 循环处理结果 for (URI uri : uris) { Path path = new Path(uri); // 拿到文件输入流 InputStream is = path.getFileSystem(context.getConfiguration()).open(path); // 对is进行数据处理操作,获取每一个值都添加到bf中去 String le = null; // 循环处理le this.bf.add(new Key(le.getBytes())); // 添加每一个joinKey } } @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); // 拿到file1(大文件)中对应行的信息 FileSplit fs = (FileSplit) context.getInputSplit(); if (fs.getPath().toString().contains("file1")) { // file1文件的处理方式 // 采用二次排序 this.ok.tag = 1; this.ov.tag = 0; // 不采用二次排序 this.ok.tag = 0; this.ov.tag = 1; // 公用部分 this.ok.joinKey = null; // TODO: 根据line信息获取join字段的对应信息 this.ov.file1Value = line; // 设置file1的value this.ov.file2Value = null; // 覆盖 context.write(ok, ov); } else { // file2文件的处理方式 Object joinKey = null;// TODO: 根据line信息获取join字段的对应信息 if (this.bf.membershipTest(new Key(joinKey.toString().getBytes()))) { // 如果存在,表示可以进行join操作,否则不会jion操作,直接过滤掉数据 // 使用二次排序 this.ok.tag = 2; this.ov.tag = 0; // 不使用二次排序 this.ok.tag = 0; this.ov.tag = 2; // 都需要的部分 this.ok.joinKey = joinKey; this.ov.file2Value = line; // 设置file2的value this.ov.file1Value = null; // 覆盖 context.write(ok, ov); } } } } /** * 输入到reduce的结果是按照joinKey进行分组分区的,按照joinKey+tag进行排序的,排序规则:先按照joinKey排序, * 然后在按照tag排序,tag为1的在前,为2的在后。 * * @author gerry * */ public static class DemoReducer extends Reducer<MapperOutputKey, MapperOutputValue, Object, Object> { // 假设file1和file2之间是一对多的join操作,或者file1中几条数据对应file2中很多条数据join private Set<Object> file1Cache = new HashSet<>(); @Override protected void reduce(MapperOutputKey key, Iterable<MapperOutputValue> values, Context context) throws IOException, InterruptedException { this.file1Cache.clear(); // 清空操作 for (MapperOutputValue value : values) { if (key.tag == 1) { // 表示还出于刚刚开始阶段 this.file1Cache.add(value.file1Value); } else { // 如果tag不为1,那么表示file1的文件内容以及完全读取完成,开始和file2的文件内容进行join操作 Object file2v = value.file2Value; for (Object file1v : this.file1Cache) { // 对应file1v和file2v进行join操作,然后将结果进行输出操作即可 // 正式join操作的地方 } } } } } /** * 不使用二次排序,那么key中的tag标签在map的时候全部设置为0,在value中添加一个tag,含义和之前的一样 * * @author gerry * */ public static class DemoReducer2 extends Reducer<MapperOutputKey, MapperOutputValue, Object, Object> { private Set<Object> file1Cache = new HashSet<>(); private Set<Object> file2Cache = new HashSet<>(); @Override protected void reduce(MapperOutputKey key, Iterable<MapperOutputValue> values, Context context) throws IOException, InterruptedException { this.file1Cache.clear(); // 清空操作 this.file2Cache.clear(); for (MapperOutputValue value : values) { if (value.tag == 1) { // 表示还出于刚刚开始阶段 this.file1Cache.add(value.file1Value); } else { this.file2Cache.add(value.file2Value); } } // 进行join操作 for (Object v1 : this.file1Cache) { for (Object v2 : this.file2Cache) { // 针对v1和v2进行join操作并输出 } } } } public static class MapperOutputValue implements Writable { public Object file1Value; public Object file2Value; public int tag; // 当不适用二次排序的时候使用该值 @Override public void write(DataOutput out) throws IOException { // TODO Auto-generated method stub } @Override public void readFields(DataInput in) throws IOException { // TODO Auto-generated method stub } } public static class MapperOutputKey implements WritableComparable<MapperOutputKey> { public Object joinKey; public int tag; // 标签 @Override public void write(DataOutput out) throws IOException { // TODO Auto-generated method stub } @Override public void readFields(DataInput in) throws IOException { // TODO Auto-generated method stub } @Override public int compareTo(MapperOutputKey o) { // TODO Auto-generated method stub return 0; } } }