mr程序的执行过程
MapReduce程序的主体思想是分而治之。构建抽象模型:Map和Reduce
MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现:
map: (k1; v1) → [(k2; v2)] 分,可以高度并行
reduce: (k2; [v2]) → [(k3; v3)] 合,将同一个分区的数据拉到一起处理
MapReduce处理的数据类型是<key,value>键值对。
编写mr程序的代码片段如下
public class PartitionerMain extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
String jobName = PartitionerMain.class.getSimpleName();
Job job = Job.getInstance(super.getConf(),jobName);
job.setJarByClass(PartitionerMain.class);
//第一步:读取 文件,解析成key,value对
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path(args[0]));
//第二步:设置mapper类
job.setMapperClass(PartitionerMapper.class);
job.setMapOutputKeyClass(PairSort.class);//PairSort通过compareTo可以自定义排序,分组时默认也是通过compareTo来确定是否相等,toString可以自定义输出字符串形式
job.setMapOutputValueClass(Text.class);
//第三步:设置分区类,以及reducetask的个数,注意reduceTask的个数要与分区的个数一致
job.setPartitionerClass(PartitionerOwn.class);
job.setNumReduceTasks(2);
//第五步:设置combiner类
job.setCombinerClass(MyCombiner.class);
//设置自定义分组器
//覆写无参构造,将自定义的JavaBean(如PairSort)注册到自定义的分组器中
//通过重写compare方法可以重写分组逻辑
job.setGroupingComparatorClass(GroupingComparator.class);
//第三至六步都可省略,用框架的默认值
//第七步:设置reducer类
job.setReducerClass(PartitionerReducer.class);
job.setOutputKeyClass(PairSort.class);
job.setOutputValueClass(NullWritable.class);
//第八步:设置输出类及输出的路径
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new Path(args[1]));
boolean b = job.waitForCompletion(true);
return b?0:1;
}
public static void main(String[] args) throws Exception {
int run = ToolRunner.run(new Configuration(), new PartitionerMain(), args);
System.exit(run);
}
}
粗略来说,mapTask的数量是与输入文件在hdfs上的block数相同。
更准确的来说,读取数据组件InputFormat(默认TextInputFormat)会通过getSplits方法对输入目录中文件进行逻辑切片规划得到splits,bytesRemaining < splitSize * 1.1 时为一块,然后有多少个split就对应启动多少个MapTask。split与block的对应关系默认是一对一。
在重写的map函数执行完之后,每条数据写入内存前会默认使用HashPartitioner进行分区,内存中的环形缓存区写满后就会溢写到磁盘(spill)。溢写时会进行排序和规约。数据处理结束后合并溢写的临时文件,最终一个mapTask只会生成一个磁盘文件,并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量。
reduceTask的数据与分区数一致。可通过job.setNumReduceTasks(2)来设置。
每个reduceTask会根据索引文件拉取属于自己分区的数据。当copy到内存的数据达到一定阈值时,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,溢写时会排序,如果设置有Combiner,也是会启用的,然后合并溢写文件,一个reduceTask最终只生成一个磁盘文件。数据合并时会进行排序,排序后调用用户定义的reduce方法,然后把输出写入到HDFS中。
nameNode管理元数据
nameNode在内存中会保存全部的完整的元数据信息,如何保证内存中的元数据0丢失?
如果是正常命令停止nameNode,会直接把元数据信息刷到磁盘里。如果是非正常关机是如何保证不丢失元数据信息的呢?
fsimage : 镜像文件,存的就是元数据信息,非常容易恢复,更新不及时
edits : 操作日志文件,即时存储除了查询之外的所有操作日志,恢复成元数据时效率低
secondarynameNode会定期拉取fsimage和edits合并成新的fsimage文件,拉取后nameNode会写新的edits文件,这样edits不会越来越大,且合并完成后的fsimage是非常接近内存中的真实元数据信息的。当重启时,会在根据fsimage和edits恢复出元数据存到内存里。
hive中的优化
存储文件的压缩比总结:
ORC > Parquet > textFile
建表语句中 create table ...(...) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' STORED AS orc tblproperties ("orc.compress"="SNAPPY"); 可指定文件存储格式,orc默认的压缩是ZLIB,比SNAPPY压缩的还要小。在实际的项目开发当中,hive表的数据存储格式一般选择列式存储格式:orc或parquet。压缩方式一般选择snappy,压缩效率与读取效率相对比较平衡。
执行顺序
在 hive 和 mysql 中都可以通过 explain+sql 语句,来查看执行顺序。对于一条标准 sql 语句,它的书写顺序是这样的:
select … from … where … group by … having … order by … limit …
(1)mysql 语句执行顺序:
from... where...group by... having.... select ... order by... limit …
(2)hive 语句执行顺序:
FROM —> WHERE —> GROUP BY—> 聚合函数 —> HAVING—> SELECT —> ORDER BY —> LIMIT
Hive 的顺序
hive 基于 MapReduce 程序,它的执行顺序决定了 hive 语句的执行顺序,Map 阶段:
- 执行 from 加载,进行表的查找与加载
- 执行 where 过滤,进行条件过滤与筛选
- 执行 select 查询:进行输出项的筛选
- 执行 group by 分组:描述了分组后需要计算的函数
- map 端文件合并:map 端本地溢出写文件的合并操作,每个 map 最终形成一个临时文件。
然后按列映射到对应的 reduceReduce 阶段:
- group by:对map端发送过来的数据进行分组并进行计算。
- select:最后过滤列用于输出结果
- limit:排序后进行结果输出到HDFS文件
优化要点
根据执行顺序,我们平时编写时需要记住以下几点:
- 使用分区剪裁、列剪裁,分区一定要加
- 少用 COUNT DISTINCT,group by 代替 distinct
- 是否存在多对多的关联
- 连接表时使用相同的关键词,这样只会产生一个 job
- 减少每个阶段的数据量,只选出需要的,在 join 表前就进行过滤
- 大表放后面
- 谓词下推:where 谓词逻辑都尽可能提前执行,减少下游处理的数据量
- sort by 代替 order by,用同一个key进行cluster by 后的两张表join时可以不用全表join.
当使用外关联时,如果将副表的过滤条件写在Where后面,那么就会先全表关联,之后再过滤。
with temp as() 这种语句并不是真正的建立了临时表,sql中每次用到temp表,都会把所有的逻辑都执行一遍,并不是直接读取缓存的结果。所以如果temp的逻辑复杂,最好是落成一张hive表,这也是数据仓库分层清晰的意义所在。(数仓分层,维度建模等内容待记录)
数据倾斜优化:空key过滤,空key转换随机数;map端join;把一个job拆分成多个job逐层聚合执行,例如想把热点key均匀分到1000个分区,就在key前拼接一个0-1000的随机数据后再聚合;手动把数据分成多份分别处理后再合并结果;手动把倾斜的key专门拿出来做map端join等。
hive常用参数
-----------hive参数-------------------------------
-----------------------------------------------------
--Map端运行环境的内存,例子中为4G。在解决GC overhead limit exceeded报错时调此参数有效果。
set mapreduce.map.java.opts=-Xmx4096m;
--一个 Map Task 可使用的内存上限(单位:MB),默认为 1024。如果 Map Task 实际使用的资源量超过该值,则会被强制杀死。
set mapreduce.map.memory.mb=5120;
--reduce端运行环境的内存
set mapreduce.reduce.java.opts=-Xmx3276m;
--一个 Reduce Task 可使用的内存上限(单位:MB),默认为 1024。如果 Reduce Task 实际使用的资源量超过该值,则会被强制杀死。
set mapreduce.reduce.memory.mb=4096;
-------------------------------------------------
--启动压缩
hive本身的参数 set hive.exec.compress.output=true;
对应mr参数 set mapreduce.output.fileoutputformat.compress=true;
set mapreduce.output.fileoutputformat.compress.codec; --查看使用的压缩算法
使用默认的org.apache.hadoop.io.compress.DefaultCodec压缩算法,就不支持文件切分,
此时降低mapred.max.split.size的值并不能增加mapTask的数量。
--控制map数量-- mr程序执行时,mapTask的个数默认是由文件在hdfs里的block数决定的
SET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat; --防止map端输入合并小文件(当文件体积很小但是行数很多时。实践中很少用到)
此时有以下三个属性值来确定InputSplit的个数:
set mapred.map.tasks=2;
set mapred.min.split.size=10000000;
set dfs.block.size=134217728 --128Mb
1.goalSize:该值由 totalSize/numSplits totlasize文件块大小,numslplits=mapred.map.tasks=2
2.minSize:由配置参数 mapred.min.split.size(或者新版本的mapreduce.input.fileinputformat.split.minsize)mapred.min.split.size=10000000决定
3.blockSize:HDFS 中的文件存储块block的大小,默认为128MB。
这三个参数决定一个 InputFormat 分片的最终的长度,计算方法如下:
splitSize = max{minSize,min{goalSize,blockSize}}
--在执行mapTask之前进行小文件的合并
set hive.input.format=org.apache.Hadoop.hive.ql.io.CombineHiveInputFormat; #执行Map前进行小文件合并,默认开启
如果Hive处理的的文件为非压缩格式或者压缩可切分,且inputFormat为CombineHiveInputFormat时,则控制map个数是由以下四个参数起作用
set mapred.max.split.size=256000000; #每个Map最大输入大小,不设置,则所有输入只启动一个map任务。对应新版本的mapreduce.input.fileinputformat.split.minsize
set mapred.min.split.size=10000000; #每个Map最小输入大小。默认值1
set mapred.min.split.size.per.node=100000000; #一个节点上split的至少的大小
set mapred.min.split.size.per.rack=100000000; #一个交换机下split的至少的大小
在开启了org.apache.hadoop.hive.ql.io.CombineHiveInputFormat后,一个dataNode节点上多个小文件会进行合并,合并文件数由mapred.max.split.size限制的大小决定。
mapred.min.split.size.per.node决定了多个data node上的文件是否需要合并
mapred.min.split.size.per.rack决定了多个交换机上的文件是否需要合并
注意
1.一般来说这四个参数的配置结果大小要满足如下关系。
max.split.size >= min.split.size >= min.size.per.node >= min.size.per.rack
2.这四个参数发生矛盾时作用的优先级分别如下
max.split.size <= min.split.size <= min.size.per.node <= min.size.per.rack
---------------------------------------------------
--输出合并
MR作业结束后,判断生成文件的平均大小,如果小于阀值,就再启动一个job来合并文件
对应参数:
set hive.merge.mapfiles = true; #在Map-only的任务结束时合并小文件
set hive.merge.mapredfiles = true; #在Map-Reduce的任务结束时合并小文件
set hive.merge.size.per.task = 256*1000*1000; #合并文件的大小
set hive.merge.smallfiles.avgsize=16000000; #当输出文件的平均大小小于该值时,启动一个独立的map-reduce任务进行文件merge。这个值只有当hive.merge.mapfiles或hive.merge.mapredfiles设定为true时,才有效。
--控制reduce个数--
不指定reduce个数的情况下,hive会猜测确定一个reduce个数
参数1 :set hive.exec.reducers.bytes.per.reducer=500000000; (500M) #每个reduce任务处理的数据量,默认为1000^3=1G
参数2 :set hive.exec.reducers.max = 888;(每个任务最大的reduce数,默认为999)
计算reducer数的公式很简单 N=min(参数2,总输入数据量/参数1)
也可以直接指定reduce的数量
set mapred.reduce.tasks = 15;
--启用map端join的参数
set hive.mapjoin.smalltable.filesize=50000000;
--动态分区,并行执行
set hive.exec.dynamici.partition=true;
set hive.exec.parallel=true;
--查看集群block块大小set dfs.block.size;