探索MapReduce过程及分组详解

一直对MapReduce的分区和分组理解的比较模糊和不确定。这次又遇到reduce输出结果跟自己预想的不一样的情况,因此决定深入进去操作一下各种情况的结果,争取理清楚分组、分区的关系。

以前的认识

我一直以来对MapReduce的分区和分组有一个自己的理解。

分区:如果不自定义分区类,而使用默认分区时,采取的是对键进行哈希操作,并与reducetask任务数取模,根据得到的值进行分区。由于默认的reducetask任务数设置为1,因此默认情况下只有1个分区。如果自己重新定义了一个分区类,则会按照自定义的方式进行分区。

分组:分组和分区类似,也是用来划分数据集的,只不过更加细粒度。如果不自定义分组类而使用默认分组的话,跟默认分区相同,也是通过比较键来进行分组。reduce()函数是按照组为操作对象进行统计的。

数据集

摩根士丹利MORGANSTANLEY,美国
工商银行INDUSTRIALCOMMERBANKOFCHINA,中国
TalanxTALANX,德国
华为投资控股HUAWEIINVESTMENTHOLDING,中国
天津物产TEWOOGROUP,中国
阿里巴巴ALIBABAGROUPHOLDING,中国
FomentoEconmicoMexicanoFOMENTOECONMICOMEXICANO,墨西哥
农业银行AGRICULTURALBANKOFCHINA,中国
戴尔科技DELLTECHNOLOGIES,美国
太平洋保险股份CHINAPACIFICINSURANCE,中国
浙江吉利控股ZHEJIANGGEELYHOLDINGGROUP,中国
建设银行CHINACONSTRUCTIONBANK,中国
电子信息产业CHINAELECTRONICS,中国
江苏沙钢JIANGSUSHAGANGGROUP,中国
上海汽车股份SAICMOTOR,中国
厦门建发XIAMENCD,中国
电信CHINATELECOMMUNICATIONS,中国
甲骨文ORACLE,美国
广州汽车工业GUANGZHOUAUTOMOBILEINDUSTRYGROUP,中国
山东能源SHANDONGENERGYGROUP,中国
联想LENOVOGROUP,中国
正威国际AMERINTERNATIONALGROUP,中国
移动通信CHINAMOBILECOMMUNICATIONS,中国
陕西延长石油责任SHAANXIYANCHANGPETROLEUM,中国
航天科工CHINAAEROSPACESCIENCEINDUSTRY,中国
富士通FUJITSU,日本
思科CISCOSYSTEMS,美国
微软MICROSOFT,美国
北京汽车BEIJINGAUTOMOTIVEGROUP,中国

测试一、使用默认分组

贴代码

public class HandleTopAttend {
	public static class CmpnyCutry implements WritableComparable {
		private String company;
		private String country;

		public void set(String first, String second) {
			this.company = first;
			this.country = second;
		}

		public String getCompany() {
			return this.company;
		}

		public String getCountry() {
			return this.country;
		}

		@Override
		public void readFields(DataInput in) throws IOException {
			// TODO Auto-generated method stub
			company = in.readUTF();
			country = in.readUTF();
		}

		@Override
		public void write(DataOutput out) throws IOException {
			// TODO Auto-generated method stub
			out.writeUTF(company);
			out.writeUTF(country);
		}

		@Override
		public int compareTo(CmpnyCutry o) {// 先按照公司比较,再按照所属国家比较
			// TODO Auto-generated method stub
			int i = 0;
			if (o instanceof CmpnyCutry) {
				CmpnyCutry cc = (CmpnyCutry) o;
				i = this.company.compareTo(cc.company);
				if (i == 0) {
					return this.country.compareTo(cc.country);
				}
			}
			return i;
		}

	}

	public static class Mapper1 extends Mapper {
		static int count = 0;
		static Map m;
		CmpnyCutry cmcu = new CmpnyCutry();

		public void setup(Context context) {
			m = new HashMap<>();
		}

		public void map(LongWritable index, Text line, Context context) throws IOException, InterruptedException {
			String[] cc = line.toString().split(",");
			m.put(cc[0], cc[1]);
			count++;//有多少行数据,最后count就为几,代表500强的公司数
		}

		public void cleanup(Context context) throws IOException, InterruptedException {
			Set s = m.entrySet();
			Iterator> it = s.iterator();
			while (it.hasNext()) {
				Entry en = it.next();
				cmcu.set(en.getKey(), en.getValue());
				context.write(cmcu, new IntWritable(count));//map输出的key:(公司名,国家);value:500强公司数
			}
		}
	}

	public static class CountryPartitioner extends Partitioner {
		public int getPartition(CmpnyCutry key, IntWritable count, int numPartitions) {
			if(key.getCountry().equals("中国")){
				return 0;
			}else{
				return 1;
			}
		}
	}


	public static class Reducer1 extends Reducer {
		int groupCount = 0;//统计当前分区中有多少个分组
		public void reduce(CmpnyCutry cc, Iterable count, Context context)
				throws IOException, InterruptedException {
			int sum = 0;//统计当前分组的集合中有多少个元素
			IntWritable iw = new IntWritable();
			Iterator i = count.iterator();
			while (i.hasNext()) {
				iw = i.next();
				sum++;
			}
			//输出:国家,公司数,第几组,500强公司数
			context.write(new Text(cc.getCountry() + "," + sum+","+groupCount++), iw);
		}
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
		Configuration conf = new Configuration();
		conf.set("mapred.textoutputformat.ignoreseparator","true");  
		conf.set("mapred.textoutputformat.separator",","); 
		String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
		if (otherArgs.length != 2) {
			System.err.println("Usage: wordcount  ");
			System.exit(2);
		}

		Job job = Job.getInstance(conf, "topAttend");
		job.setJarByClass(HandleTopAttend.class);
		FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
		FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
		job.setMapperClass(Mapper1.class);
		job.setMapOutputKeyClass(CmpnyCutry.class);
		job.setMapOutputValueClass(IntWritable.class);

		job.setPartitionerClass(CountryPartitioner.class);
		job.setNumReduceTasks(2);
		job.setReducerClass(Reducer1.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);
		System.exit(job.waitForCompletion(true) ? 0 : 1);

	}
}

查看输出日志

探索MapReduce过程及分组详解_第1张图片

因为只有一个输入文件,且文件很小不足一个splits(分片),因此map tasks=1。同时由于设置了两个分区和两个reducetask,因此reduce tasks=2。关键是看reduce input groups的数量,可以看到groups数量为29,也就是原始数据集的记录数。因为自定义了组合键,每条记录的组合键都不相同,因此这个结果证明了分组是按照键进行的。

下面是输出的part-r-00000文件内容

探索MapReduce过程及分组详解_第2张图片

这个结果,证明了reducer类和其中的reduce()函数各自的处理范围。reducer类处理整个分区的数据,其操作对象是区,一个区调用一次reducer类。而reduce()函数的操作对象是组,也就是分区中有 几个分组就调用几次reduce()函数,reduce()函数对分组对应的集合进行处理。结合输出的第二个分区文件part-r-00001可以进一步佐证。

下面是输出的part-r-00001文件内容

探索MapReduce过程及分组详解_第3张图片

测试二、使用自定义分组

现在想要实现的是,将相同国家的数据分到一个组中进行整合,也就是按照“国家”进行分组。因此要自定义分组类。

贴代码

public class HandleTopAttend {
	public static class CmpnyCutry implements WritableComparable {
		private String company;
		private String country;

		public void set(String first, String second) {
			this.company = first;
			this.country = second;
		}

		public String getCompany() {
			return this.company;
		}

		public String getCountry() {
			return this.country;
		}

		@Override
		public void readFields(DataInput in) throws IOException {
			// TODO Auto-generated method stub
			company = in.readUTF();
			country = in.readUTF();
		}

		@Override
		public void write(DataOutput out) throws IOException {
			// TODO Auto-generated method stub
			out.writeUTF(company);
			out.writeUTF(country);
		}

		@Override
		public int compareTo(CmpnyCutry o) {// 先按照公司比较,再按照所属国家比较
			// TODO Auto-generated method stub
			int i = 0;
			if (o instanceof CmpnyCutry) {
				CmpnyCutry cc = (CmpnyCutry) o;
				i = this.company.compareTo(cc.company);
				if (i == 0) {
					return this.country.compareTo(cc.country);
				}
			}
			return i;
		}

	}

	public static class Mapper1 extends Mapper {
		static int count = 0;
		static Map m;
		CmpnyCutry cmcu = new CmpnyCutry();

		public void setup(Context context) {
			m = new HashMap<>();
		}

		public void map(LongWritable index, Text line, Context context) throws IOException, InterruptedException {
			String[] cc = line.toString().split(",");
			m.put(cc[0], cc[1]);
			count++;//有多少行数据,最后count就为几,代表500强的公司数
		}

		public void cleanup(Context context) throws IOException, InterruptedException {
			Set s = m.entrySet();
			Iterator> it = s.iterator();
			while (it.hasNext()) {
				Entry en = it.next();
				cmcu.set(en.getKey(), en.getValue());
				context.write(cmcu, new IntWritable(count));//map输出的key:(公司名,国家);value:500强公司数
			}
		}
	}

	public static class CountryPartitioner extends Partitioner {
		public int getPartition(CmpnyCutry key, IntWritable count, int numPartitions) {
			if(key.getCountry().equals("中国")){
				return 0;
			}else{
				return 1;
			}
		}
	}
//自定义分组类,重写compare()方法
	public static class GroupingComparator extends WritableComparator {
		protected GroupingComparator() {
			super(CmpnyCutry.class, true);
		}

		@SuppressWarnings("rawtypes")
		public int compare(WritableComparable cc1, WritableComparable cc2) {
			CmpnyCutry ip1 = (CmpnyCutry) cc1;
			CmpnyCutry ip2 = (CmpnyCutry) cc2;
			String l = ip1.getCountry();
			String r = ip2.getCountry();
			return l.compareTo(r);// 比较两个字符串的大小
		}
	}

	public static class Reducer1 extends Reducer {
		int groupCount = 0;//统计当前分区中有多少个分组
		public void reduce(CmpnyCutry cc, Iterable count, Context context)
				throws IOException, InterruptedException {
			int sum = 0;//统计当前分组的集合中有多少个元素
			IntWritable iw = new IntWritable();
			Iterator i = count.iterator();
			while (i.hasNext()) {
				iw = i.next();
				sum++;
			}
			//输出:国家,公司数,第几组,500强公司数
			context.write(new Text(cc.getCountry() + "," + sum+","+groupCount++), iw);
		}
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
		Configuration conf = new Configuration();
		conf.set("mapred.textoutputformat.ignoreseparator","true");  
		conf.set("mapred.textoutputformat.separator",","); 
		String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
		if (otherArgs.length != 2) {
			System.err.println("Usage: wordcount  ");
			System.exit(2);
		}

		Job job = Job.getInstance(conf, "topAttend");
		job.setJarByClass(HandleTopAttend.class);
		FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
		FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
		job.setMapperClass(Mapper1.class);
		job.setMapOutputKeyClass(CmpnyCutry.class);
		job.setMapOutputValueClass(IntWritable.class);

		job.setPartitionerClass(CountryPartitioner.class);
		job.setGroupingComparatorClass(GroupingComparator.class);//为job配置自定义的分组类
		job.setNumReduceTasks(2);
		job.setReducerClass(Reducer1.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);
		System.exit(job.waitForCompletion(true) ? 0 : 1);

	}
}

查看输出的日志结果

探索MapReduce过程及分组详解_第4张图片

可以看到groups数由之前的29变为了5,说明自定义分组其作用了。进一步分析,自定义分组规则是按照国家进行分组的。此处显示5,说明数据集中总共应该有5个国家,查看上面测试一输出的两个文件,发现果然总共有5个国家。进一步分析,第一个分区是“中国”,并且因为在这里设置了按国家分组,因此输出的part-r-00000文件应该只有一行记录,而part-r-00001应该有4条记录。

查看输出文件part-r-00000

查看输出文件part-r-00001

进一步分析每条记录的信息,可以更加佐证测试一的结论。也就是,关于reducer类和reduce()函数的作用范围的结论。

总结

很高兴的是,实际结果证明自己以前的认识基本是正确的!!!!!

reducer类的操作对象是分区,一个分区初始化一次reducer类。reduce()函数的操作对象是组,一个分区中有几个分组就调用几次reduce()函数。

分组默认采用通过比较键的方式来实现。当自定义组合键时,往往需要根据组合键中的某一个属性进行分组统计,此时就用到自定义分组类。通过重写其中的compare()方法定义分组规则。

不足

由于输出结果中没有将整个组合键输出,因此没有体现出排序的过程。实际上,排序应该是在map端执行溢写的时候进行的操作,操作的代码就是自定义组合键中的compareTo()方法。到达溢写条件时,先锁定要溢写的数据,然后对其进行分区,然后在分区内进行排序。如果设置了combiner类,还会在排序后执行combine操作。最后才将结果写出到临时文件中。

不知道有没有不对的地方,因为都是自己的认识,不是正规军。希望各位大佬指正!互相学习!

写博客不容易,转载请注明出处,谢谢!!!https://blog.csdn.net/ASN_forever/article/details/81778972

 

进一步补充

真的是刚做完上面的实验,就又发现问题了。。。。。

虽然上面通过自定义分组类之后,貌似数据集的输出结果确实是实现分组了。但当数据集换成下面这个时,情况就又变啦。。。

新数据集

华为投资控股HUAWEIINVESTMENTHOLDING,中国
天津物产TEWOOGROUP,中国
日本瑞穗金融MIZUHOFINANCIALGROUP,日本
阿里巴巴ALIBABAGROUPHOLDING,中国
英国葛兰素史克GLAXOSMITHKLINE,英国
戴尔科技DELLTECHNOLOGIES,美国
腾讯控股TENCENTHOLDINGS,中国
浙江吉利控股ZHEJIANGGEELYHOLDINGGROUP,中国
通用汽车GENERALMOTORS,美国
国家电网STATEGRID,中国
电子信息产业CHINAELECTRONICS,中国
美国银行BANKOFAMERICACORP,美国
江苏沙钢JIANGSUSHAGANGGROUP,中国
上海汽车股份SAICMOTOR,中国
厦门建发XIAMENCD,中国
电信CHINATELECOMMUNICATIONS,中国
OrangeORANGE,法国
广州汽车工业GUANGZHOUAUTOMOBILEINDUSTRYGROUP,中国
联合信贷UNICREDITGROUP,意大利
山东能源SHANDONGENERGYGROUP,中国
兴业银行INDUSTRIALBANK,中国
SAPSAP,德国
富士通FUJITSU,日本
微软MICROSOFT,美国
北京汽车BEIJINGAUTOMOTIVEGROUP,中国

问题

当用测试二中的代码处理这个新数据集时,得到的打印日志和输出文件信息分别如下:

打印日志

探索MapReduce过程及分组详解_第5张图片

part-r-00000文件

part-r-00001文件

探索MapReduce过程及分组详解_第6张图片

分析这些信息,首先是groups=10.新数据集中明明总共只有7个不同的国家,按道理来说,根据国家分组后应该得到groups=7才对。先不管,接着看输出的两个文件。第一个文件是中国区的统计数据,15表示这个分区中总共有15条记录,0表示这个分区中只有一个分组,都跟预计的结果一致。再看第二个文件,发现问题了!竟然没有将相同国家的聚合到相同分组中???虽然不知道哪里出了问题,但是也会发现一点猫腻。就是第一个美国分组中,显示有2条记录。而后接着就是日本分组了。因此做如下猜想:因为reduce端是先将所有map端对应的分区数据copy过来,然后合并成一个大文件。此后对这个大文件中的记录进行排序操作,而这个排序规则也是根据自定义键类中的compareTo()方法实现的,而这个比较规则是先比较公司后比较国家。也就是说,相同国家的公司中间很可能被其他国家的公司隔开了。而分组操作是从上往下一条一条记录进行比对来进行分组的。如果上下两条记录对应的组合键中的国家相同,则合并到一个组,如果不同,则上下各分一个组。这样就能解释part-r-00001的结果了。

为了进一步证明猜想,需要在输出中增加公司名,并且取消分组,看看输出结果跟猜想的符不符合。

下面是增加公司名,且取消分组时的输出文件part-r-00001:

探索MapReduce过程及分组详解_第7张图片

完美有没有!!!完美证明了猜想的正确性!

再总结

分组详解:分组前先对合并后的分区文件中的记录进行排序,排序后再进行分组。分组是通过对排序后的记录从上往下遍历比对进行的。如果上下两个比对结果为0,则分到同一个组,否则各分一个组。后面的分组与前面的分组无关,只与紧挨着的上一条记录有关。也就是说,就算前面有一个“美国”分组,但是中间隔了一个“日本”分组,则后面再出现“美国”时也不会合并到前面的分组中!!!因此,如果想按照国家分组的话,应该将国家作为组合键的第一个属性,这样在reduce端排序后得到的就是相同的国家上下挨着了。

关于MapReduce的shuffle过程,请看这篇文章:https://mp.csdn.net/postedit/81233547

你可能感兴趣的:(hadoop)