MapReduce介绍

场景:比如有海量的文本文件,如订单,页面点击事件的记录,量特别大,单机版很难搞定。

怎样解决海量数据的计算?

求和: 1 + 5 +7 + 3 +4 +9 +3 + 5 +6
在这里插入图片描述

MapReduce产生背景

如果让你统计日志里面的出现的某个URL的总次数,让你自己去写个单机版的程序,写个逻辑:无非就是读这个文件一行,然后把那个地方截取出来,截取出来之后,然后可以把它放到一个HashMap里面,用Map去重,看到一条新的URL ,就把它put进去,然后+1,如果下次看到再有就直接+1,没有就put进去,单机版的话逻辑是很好实现,但是数据量一大,你觉得单机版本还能搞定吗?

首先2T的文件,你放在单机上可能存不下来,如果再他多一点呢?比如几千个文件,几十个T,单机存都存不下,那么存在哪里-------hdfs上。

因为放在HDFS上可以放很多很多,比如说HDFS上有100个节点,每个节点上能耐挂载8T的硬盘,那就有800T,800T,你每个文件存3个副本的话,耗费了大概6个T的空间.

但是你一旦放到HDFS上就有一个问题:你的文件就会被切散了,被切散到很多的机器上,这个时候,你再对它们进行统计,这个时候,按照原来的逻辑,会不会出现问题?

你的任何一个节点上存的是某个文件的某些块,假设你是在那台机器上去做统计的话,你统计到的永远是局部的数据,那你专门写一个客户端,我的程序运行在这个客户端上,我去读数据,读一点统计一点,到把整个文件都读完了,统计结果也就出来了,问题是那样的话,你的程序又变成了一个单机版的,那你的内存也就不够,因为你要第一点进来统计一点,是不是要保存一些中间数据,那你可能内存也不够了,而且你因为是一个单机版的程序,所以你的速度是不是也很慢,而且你还要不断的从网络里面去拿那些数据,也会很慢,所以这个时候呢?你专门写一个客户端去做统计,肯定是不合适。

那你是不是应该把你的程序分发到集群的每一台DN上去做统计,也就是把运算往数据去移动,而不是把数据移动到运算,把我的运算逻辑移动到数据那端去,数据在哪里,我就在哪里运算,但是这也有一个问题,因为运算也变成了一个分布式的了,你的每一份运算结果都只是局部的结果,那么这个时候也存在问题:

1.你的代码怎么实现分发到很多机器上去运行,这件事情谁帮你做,如果是要你自己写程序的话,你是不是得有个U盘拷你的jar包,一个计算一个计算的去拷,拷完之后再启动,每个机器都启动jar,等你启动最后一台的时候,前面那台已经运行完了,最后一台才刚开始启动,这个工作你用手动去做是不是不合适啊?所以当你把一个简单的逻辑,变成这种分布式运行的时候,你发现很多问题就来了

1)我的代码怎么分发,怎么配置启动环境,怎么启动起来,这个是不是得有一个庞大的系统去做,也就是说你应该额外开发这么一个叫做资源分发和Java启动程序这么一个配置 的系统,这个系统你会吗?写得出来吗?你要是写的话,是不是还得花很多时间,你还要写很多东西,因为你现在的Java不一定是擅长那个领域的 ,那这个耗费的代价就很大了

2)那个数据,比如刚才那个日志数据,是放到HDFS上面去了,但是不代表HDFS上的每一台DN上面都有这一部分数据里面的内容,因为我们这个集群很大,你这个文件存进去的时候可能只占了其中的30台节点,其中某30台节点上有你这些文件,其他节点上个根本就没有你这些文件,那我们的代码,运行逻辑,最好是放到那30台上面去做统计,你放到其他的那些机器上,它可以运行,但是它的数据必须来源于网络,是不是效率会比较低,也就是说你的代码究竟分发到哪些机器上去运行,是不是也要一个策略的问题,那么这个策略是不是也有一定的算法,那么这个时候,你为了实现你那个简单的逻辑,就再去开发这样一个系统出来是不是也是很大的开发量,再考虑一个问题,假如刚才那两个工作你都做完了,你的代码真的成功的在那30台机器上跑起来了,跑起来之后,其中假设有一台机器宕机了,那么你统计的那一部分局部数据是不是也就没有了,那个局部结果也就没有了,那个局部结果没有,假设你有个汇总的结果还正确吗?没有意义了,也就是说你还得解决一个问题,就是你时时刻刻得却监控着你的程序运行情况,那个节点正常,哪个节点不正常,这个问题也是很复杂的,假设这个问题 你也解决了,还有一个问题:

刚才你的逻辑只是统计出中间结果,这个时候是不是还得汇总啊,汇总就是意味着,你要在那30台节点之间结果里面才能汇总,要么把它们全部调到一台机器上就能进行汇总,但是你调到一台机器上汇总的话,你那一台机器的负载是不是会很高,对吧?假设我调到多台机器汇总,逻辑就变得复杂了,比如说,只要是你们那30台机器上统计,每台机器上的那个URL有多少条,你又把那个所有那个URL的数据全部汇总到某个汇总节点,假如有两个URL,哪个URL分发到那个节点上进行汇总,这个策略是不是也会变得很复杂了,那你还得去做个中间数据的调度系统,那也很麻烦。

那这样的话,我们就发现,哪怕是一个很简单的东西,你也要把它变成一个分布式运行的程序,是不是面临很多很多其他的问题,跟我们逻辑无关的问题,往往这些问题要比解决那个逻辑要复杂得多,那么这些问题解决不是我们擅长的,我们大量的普通程序员还没达到那些程度,这么复杂的问题要写出来是不是很麻烦呢?你不能要求每个程序员都能达到那个功力把,我们不过就是写个简单的逻辑统计这个文本里面哪个URL出现的总次数,很简单的东西,所以呢,MapReduce才是我们这些普通程序员的福音。

就是说当我们面临海量数据处理的时候,那个逻辑也许很简单,但是面临海量数据处理,要我们这个 逻辑代码变成分布式运行,就会变得很复杂,而那些很复杂的事情又不是我们关心的,我关心的只是那个逻辑,那这个时候,就有人把你不擅长的而且又必须解决的,而且跟你的逻辑关系又不大的那些东西全部给封装起来,那么这个时候,我们是不是就直接写逻辑了,就会MapReduce的框架和Yarn,这两个是不是做运算的,由这两个框架把我们刚才讲的那些东西全部封装起来,这个就是MapReduce产生的背景,就是这个问题。

总结

把我们很简单的运算逻辑很方便的扩展到海量数据的场景下分布式运算,所以MapReduce程序对我们程序员来说很简单,因为它把那些东西都给封装起来了,你只要写业务逻辑,写业务逻辑还不擅长吗?业务逻辑大部分就是处理文本,处理字符串,我们学的大部分逻辑里面,大部分都是在处理这个问题:处理文本、处理字符串、查询数据库是不是得到一些东西啊。查询一下数据库,处理一下字符串,输出结果,而这个逻辑本身 不用你太多的分布式细节 ,你只要把逻辑写出来就可以了,但是你写MapReduce的时候,必须要符合人家编程的规范,你不能你的写法写,他按他的写法写,每个人的写法都不一样,那MapReduce也没法给你去分发和运行,所以你也要符合他的规范,怎样才算符合规范呢?

就说你的代码,你的任意一个逻辑实现都要分成这么两个步骤:
1)Map
2)Reduce

比如说我们统计日志文件里面,相同URL出现的总次数
如下图:
在这里插入图片描述

例子:

在这里插入图片描述

WCMapper.java

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
/**
 * Description: Mapper<br/>
 * Copyright (c) , 2018, xlj <br/>
 * This program is protected by copyright laws. <br/>
 * Program Name:WCMapper.java <br/>
 * 
 * @version : 1.0
 * 
 * Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>
 * 它的这个Mapper让你去定义四个泛型,为什么mapper里面需要四个泛型
 * 其实读文本文件的操作不用你来实现,框架已经帮你实现了,框架可以读这个文件
 * 然后每读一行,就会发给你这个map,让你去运行一次,所以它读一行是不是把数据传给你,
 * 
 * 那他传给map的时候,这个数据就意味着类型的一个协议,我以什么类型的数据给你,我是不是得事先定好啊
 * map接收的数据类型得和框架给他的数据类型一致,不然的话就会出现类型转换异常
 * 所以map里面得定数据类型,前面两个是map拿数据的类型,拿数据是以什么类型拿的,那么框架就是以这个类型传给你
 * 
 * 另外两个泛型是map的输出数据类型,即reduce也得有4个泛型,前面两个是reduce拿数据的泛型得和map输出的泛型类型一致
 * 剩下两个是reduce再输出的结果时的两个数据类型
 */
/*
 * 4个泛型,前两个是指定mapper端输入数据的类型,为什么呢,mapper和reducer都一样
 * 拿数据,输出数据都是以<key,value>的形式进行的--那么key,value都分别有一个数据类型
 * KEYIN:输入的key的类型
 * VALUEIN:输入的value的类型
 * KEYOUT:输出的key的数据类型
 * VALUEOUT:输出的value的数据累心
 * map reduce的数据输入输出都是以key,value对封装的
 * 至于输入的key,value形式我们是不能控制的,是框架传给我们的,
 * 框架传给我们是什么类型,我们这里就写什么数据类型
 * 
 * 默认情况下框架传给我们的mapper的输入数据中,key是要处理的文本中一行的起始偏移量,
 * 因为我们的框架是读一行就调用一次我们的偏移量
 * 那么就把一行的起始偏移量作为key,这一行的内容作为value 
 * 
 * 那么输出端的数据类型是什么,由于我们输出的数<hello,1>
 * 那么它们的数据类型就显而易见了
 * 初步定义为:
 * Mapper<Long, String, String, int>
 * 但是不管是Long还是String,在MapReduce里面运行的时候,这个数据读到网络里面进行传递
 * 即各个节点之间会进行传递,那么要在网络里面传输,那么就意味着这个数据得序列化
 * Long、String对象,内存对象走网络都得序列化,Long、String,int序列化
 * 如果自己实现Serializable接口,那么附加的信息太多了
 * hadoop实现了自己的一套序列化机制
 * 所以就不要用Java里面的数据类型了,而是用它自己的封装一套数据类型
 * 这样就有助于提高效率,实现了自己的序列化接口
 * 在序列化传输的 时候走的就是自己的序列化方法来传递,少了很多负载信息,传递数据精简,
 * Long---LongWritable
 * String也有自己的封装-Text
 * int--IntWritable
 */
public class WCMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
//	MapReduce框架每读一次数据,就会调用一次该方法
	@Override
	protected void map(LongWritable key, Text value, Context context)
			throws IOException, InterruptedException {
		//具体业务逻辑就写在这个方法体中,而且我们业务要处理的数据已经被框架传递进来,在方法参数中
		//key--这一行数据的其实偏移量   value--这一行数据的文本内容
		//1.先把单词拿出来,拿到一行
		String line = value.toString();
		//2.切分单词,这个是按照特定的分隔符 进行切分
		String [] words = line.split(" ");
		//3.把里面的单词发送出去
		/*
		 * 怎么发出去呢?我都不知道reduce在哪里运行
		 * 其实呢,这个不用我们关心
		 * 你只要把你的东西给那个工具就可以了
		 * 剩下的就给那个框架去做
		 * 那个工具在哪-----context
		 * 它把那个工具放到那个context里面去了,即输出的工具
		 * 所以你只要输出到context里面就行了
		 * 剩下的具体往哪里走,是context的事情
		 */
		//遍历单词数组,输出为<K,V>形式 key是单词,value是1
		for (String word : words) {
			//记得把key和value继续封装起来,即下面
			context.write(new Text(word), new IntWritable(1));
		}
		/*
		 * map方法的执行频率:每读一行就调一次
		 * 最后到reduce 的时候,应该是把某个单词里面所有的1都到,才能处理
		 * 而且中间有一个缓存的过程,因为每个map的处理速度都不会完全一致
		 * 等那个单词所有的1都到齐了才传给reduce
		 */
		//每一组key,value都全了,才会去调用一次reduce,reduce直接去处理valuelist
		//接着就是写Reduce逻辑了
		
	}
}

WCReducer.java

import java.io.IOException;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

/**
 * Description: Reducer<br/>
 * Copyright (c) , 2018, xlj <br/>
 * This program is protected by copyright laws. <br/>
 * Program Name:WCReducer.java <br/>
 * 
 * @version : 1.0
 */
/*
 * 类型记得要对应
 */
public class WCReducer extends Reducer<Text, IntWritable, Text, Text> {
	//map处理之后,value传过来的是一个value的集合 
	//框架在map处理完成之后,将所有的KV对保存起来,进行分组,然后传递一个组,调用一次reduce
	//相同的key在一个组
	@Override
	protected void reduce(Text key, Iterable<IntWritable> values, Context context)
			throws IOException, InterruptedException {
		//遍历valuelist,进行了累加
		int count = 0;
		for (IntWritable value : values) {
			//get()方法就能拿到里面的值
			count += value.get();
		}
		//输出一组(一个单词)的统计结果
		//默认输出到HDFS的一个文件上面去,放在HDFS的某个目录下
		context.write(key, new Text(count+""));
		//但是还差一个描述类:用来描述整个逻辑
	
		/*
		 * Map,Reducce都是个分散的,那集群运行的时候不知道运行哪些MapReduce
		 * 
		 * 处理业务逻辑的一个整体,叫做job
		 * 我们就可以把那个job告诉那个集群,我们此次运行的是哪个job,
		 * job里面用的哪个作为Mapper,哪个业务作为Reducer,我们得指定
		 * 
		 * 所以还得写一个类用来描述处理业务逻辑
		 * 把一个特定的业务处理逻辑叫做一个job(作业),我们就可以把这个job告诉那个集群,
		 * 
		 */
	}
}

WCRunner.java

import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;


/**
 * Description: 用来描述一个特定的作业<br/>
 * Copyright (c) , 2018, xlj <br/>
 * This program is protected by copyright laws. <br/>
 * Program Name:WCRunner.java <br/>
 * 
 * 该作业使用哪个类作为逻辑处理的map
 * 哪个作为reduce
 * 还可以指定该作业要处理的数据所在的路径
 * 还可以指定该作业输出的结果放到哪个路径
 * 
 * @version : 1.0
 */

public class WCRunner {
	public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
		//首先要描述一个作业,这些信息是挺多的,哪个是map,哪个是reduce,输入输出路径在哪
		//一般来说这么多信息,就可以把它封装在一个对象里面,那么这个对象呢就是 ----Job对象
		Job job = Job.getInstance(new Configuration());
		
		//job用哪个类作为Mapper 指定输入输出数据类型是什么
		job.setMapperClass(WCMapper.class);
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);
		
		//job用哪个类作为Reducer 指定数据输入输出类型是什么
		job.setReducerClass(WCReducer.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(Text.class);
		
		//指定原始数据存放在哪里
		//参数1:里面是对哪个参数进行指定
		//参数2:文件在哪个路径下,这个路径下的所有文件都会去读的
		FileInputFormat.setInputPaths(job, new Path("input/data1"));
		
		//指定处理结果的数据存放路径
		FileOutputFormat.setOutputPath(job, new Path("output1"));
		
		//提交
		int isok =  job.waitForCompletion(true)?0:-1;
		System.exit(isok);
	}
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 190
    点赞
  • 531
    收藏
    觉得还不错? 一键收藏
  • 36
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值