MapReduce经典性能优化场景

小文件问题

Hadoop的HDFS和MapReduce都是针对大数据文件来设计的,在小文件的处理上不但效率低下,而且十分消耗内存资源,针对HDFS而言,每一个小文件在NameNode中都会占用150字节的内存空间,最终会导致集群中虽然存储了很多个文件,虽然文件的体积并不大,但没有太大的意义。

同时针对MapReduce而言,每一个小文件都是一个Block,都会产生一个InputSplit,最终每一个小文件都会产生一个map任务,这样会导致同时启动太多的Map任务,Map任务的启动是非常消耗性能的,但是启动了以后执行了很短时间就停止了,因为小文件的数据量太小了,这样就会造成任务执行消耗的时间还没有启动任务消耗的时间多,这样也会影响MapReduce执行的效率。

针对这个问题,解决办法通常是选择一个容器,将这些小文件组织起来统一存储,HDFS提供了两种类型的容器,分别是SequenceFileMapFile

SequenceFile

通过SequenceFile类型将小文件包装起来,可以获得更高效率的存储和处理。

SequenceFile的写操作

    /**
     * 生成SequenceFile文件
     * @param inputDir 输入目录-windows目录
     * @param outputFile 输出文件-hdfs文件
     * @throws Exception
     */
    private static void write(String inputDir,String outputFile)
            throws Exception{
        //创建一个配置对象
        Configuration conf = new Configuration();
        //指定HDFS的地址
        conf.set("fs.defaultFS","hdfs://bigdata01:9000");
        //获取操作HDFS的对象
        FileSystem fileSystem = FileSystem.get(conf);
        //删除输出文件
        fileSystem.delete(new Path(outputFile),true);

        //构造opts数组,有三个元素
        /*
        第一个是输出路径
        第二个是key类型
        第三个是value类型
         */
        SequenceFile.Writer.Option[] opts = new SequenceFile.Writer.Option[]{
                SequenceFile.Writer.file(new Path(outputFile)),
                SequenceFile.Writer.keyClass(Text.class),
                SequenceFile.Writer.valueClass(Text.class)};

        //创建一个writer实例
        SequenceFile.Writer writer = SequenceFile.createWriter(conf, opts);
        //指定要压缩的文件的目录
        File inputDirPath = new File(inputDir);
        if(inputDirPath.isDirectory()){
            File[] files = inputDirPath.listFiles();
            for (File file : files) {
                //获取文件全部内容
                String content = FileUtils.readFileToString(file, "UTF-8");
                //文件名作为key
                Text key = new Text(file.getName());
                //文件内容作为value
                Text value = new Text(content);
                //调用append()方法在文件末尾附加键-值对
                writer.append(key,value);
            }
        }
        //写操作结束
        writer.close();
    }

    /**
     * 读取SequenceFile文件
     * @param inputFile SequenceFile文件路径
     * @throws Exception
     */

SequenceFile的读操作

从头到尾读取顺序文件利用SequenceFile.Reader实例,反复调用next()方法迭代读取记录

 private static void read(String inputFile)
            throws Exception{
        //创建一个配置对象
        Configuration conf = new Configuration();
        //指定HDFS的地址
        conf.set("fs.defaultFS","hdfs://bigdata01:9000");
        //创建阅读器
        SequenceFile.Reader reader = new SequenceFile.Reader(conf, SequenceFile.Reader.file(new Path(inputFile)));
        Text key = new Text();
        Text value = new Text();
        //循环读取数据
        while(reader.next(key,value)){
            //输出文件名称
            System.out.print("文件名:"+key.toString()+",");
            //输出文件的内容
            System.out.println("文件内容:"+value.toString());
        }
        reader.close();
    }

MapFile

MapFile是排序后的SequenceFile,由两部分组成,分别是indexdata。可提供对主数据文件的快速查找,缺点是会消耗一部分内存来存储index数据。

MapFile的写操作

【注意】当使用MapFile.Writer进行写操作时,map条目必须顺序添加,否则会抛出IOException异常。

public class SmallFileMap {
    public static void main(String[] args) throws Exception{
        //生成MapFile文件
        write("D:\\smallFile","/mapFile");
        //读取MapFile文件
        read("/mapFile");
    }
    /**
     * 生成MapFile文件
     * @param inputDir 输入目录-windows目录
     * @param outputDir 输出目录-hdfs目录
     * @throws Exception
     */
    private static void write(String inputDir,String outputDir)
            throws Exception{
        //创建一个配置对象
        Configuration conf = new Configuration();
        //指定HDFS的地址
        conf.set("fs.defaultFS","hdfs://bigdata01:9000");
        //获取操作HDFS的对象
        FileSystem fileSystem = FileSystem.get(conf);
        //删除输出目录
        fileSystem.delete(new Path(outputDir),true);
        
        //构造opts数组,有两个元素        
        /*
        第一个是key类型
        第二个是value类型
         */
        SequenceFile.Writer.Option[] opts = new SequenceFile.Writer.Option[]{
                MapFile.Writer.keyClass(Text.class),
                MapFile.Writer.valueClass(Text.class)};

        //创建一个writer实例
        MapFile.Writer writer = new MapFile.Writer(conf,new Path(outputDir),opts);
        //指定要压缩的文件的目录
        File inputDirPath = new File(inputDir);
        if(inputDirPath.isDirectory()){
	        //获取目录中的文件
            File[] files = inputDirPath.listFiles();
            //对获取到的文件进行排序[如果文件默认无序的情况下,需要先进行排序]
			List<File> fileList = Arrays.asList(files);
			Collections.sort(fileList, new Comparator<File>() {
			   @Override
			   public int compare(File f1, File f2) {
			       return f1.getName().compareTo(f2.getName());
			   }
			});
            for (File file : fileList) {
                //获取文件全部内容
                String content = FileUtils.readFileToString(file, "UTF-8");
                //文件名作为key
                Text key = new Text(file.getName());
                //文件内容作为value
                Text value = new Text(content);
                writer.append(key,value);
            }
        }
        writer.close();
    }

MapFile的读操作

     private static void read(String inputDir)
            throws Exception{
        //创建一个配置对象
        Configuration conf = new Configuration();
        //指定HDFS的地址
        conf.set("fs.defaultFS","hdfs://bigdata01:9000");
        //创建阅读器
        MapFile.Reader reader = new MapFile.Reader(new Path(inputDir),conf);
        Text key = new Text();
        Text value = new Text();
        //循环读取数据
        while(reader.next(key,value)){
            //输出文件名称
            System.out.print("文件名:"+key.toString()+",");
            //输出文件的内容
            System.out.println("文件内容:"+value.toString());
        }
        reader.close();
    }

能不能使用zip或者rar文件解决HDFS中的小文件问题?

不可以,其原因如下:

  1. zip和rar压缩文件允许不必读取另外的数据而检索独立的文件,而这与使用HDFS的目的相悖。
  2. zip和rar只是压缩了存储的空间,并不是实际解决小文件的内存损耗的问题。
  3. zip和rar是解决大文件存储问题,而SquenceFile和Mapfile解决的是运行内存损耗问题,根本的解决的问题就不同。
  4. zip及rar压缩文件只能用于存储,而无法读取数据内容进行处理分析

数据倾斜问题

假设场景

假设我们有一个文件,有1000W条数据,这里面的值主要都是数字:1,2,3,4,5,6,7,8,9,10,我们希望统计出来每个数字出现的次数。这里面这1000w条数据,值为5的数据有910w条左右,剩下的9个数字一共只有90w条,那也就意味着,这份数据中,值为5的数据比较集中,或者说值为5的数据属于倾斜的数据,在这一整份数据中,它占得比重比其他的数据多得多。

解决方法

假设这1000W条数据的文件有3个Block,会产生3个InputSplt,最终会产生3个Map任务,默认情况下只有一个Reduce任务,这个Reduce压力肯定很大,大量的时间都消耗在了这里,可以增加Reduce任务的数量,我们把Reduce任务的数量调整到10个,这个时候就会把1000w条数据让这10 个Reduce任务并行处理了,这个时候效率肯定会有一定的提升,但是最后我们会发现,这种方法带来的性能提升是有限的,并没有达到质的提升,其原因在于1个Reduce任务处理值为5的数据有910w条,这就占了整份数据的90%了。
假设是Reduce5处理值为5的数据,因为reduce5中处理的数据量和其他Reduce中处理的数据量规模相差太大了,所以最终reduce5拖了后腿。而MapReduce任务执行消耗的时间是一直统计到最后一个执行结束的Reduce任务,所以就算其他Reduce任务早都执行结束了也没有用,整个MapReduce任务是没有执行结束的。

最好的办法是把这个值为5的数据尽量打散,把这个倾斜的数据分配到其他reduce任务中去计算,这样才能从根本上解决问题。

根据刚才的分析,有两种方案:

  1. 增加reduce任务个数,这个属于治标不治本,针对倾斜不是太严重的数据是可以解决问题的,针对倾斜严重的数据,这样是解决不了根本问题的
  2. 把倾斜的数据打散 这种可以根治倾斜严重的数据

只需要在map中把k2的值修改一下就可以了,这样就可以把值为5的数据打散了。

public class WordCountJobSkewRandKey {
    /**
     * Map阶段
     */
    public static class MyMapper extends Mapper<LongWritable, Text,Text,LongWritable>{
        Logger logger = LoggerFactory.getLogger(MyMapper.class);
        Random random = new Random();
        /**
         * 需要实现map函数
         * 这个map函数就是可以接收<k1,v1>,产生<k2,v2>
         * @param k1
         * @param v1
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void map(LongWritable k1, Text v1, Context context)
                throws IOException, InterruptedException {
            //输出k1,v1的值            
            //k1 代表的是每一行数据的行首偏移量,v1代表的是每一行内容
            //对获取到的每一行数据进行切割,把单词切割出来
            String[] words = v1.toString().split(" ");
            //把单词封装成<k2,v2>的形式
            String key = words[0];
            if("5".equals(key)){
                //把倾斜的key打散,分成10份
                key = "5"+"_"+random.nextInt(10);
            }
            Text k2 = new Text(key);
            LongWritable v2 = new LongWritable(1L);
            //把<k2,v2>写出去
            context.write(k2,v2);
        }
    }

但是这个时候我们获取到的最终结果是一个半成品,还需要进行一次加工,其实我们前面把这个倾斜的数据打散之后相当于做了一个局部聚合,现在还需要再开发一个mapreduce任务再做一次全局聚合,其实也很简单,获取到上一个map任务的输出,在map端读取到数据之后,对数据先使用空格分割,然后对第一列的数据再使用下划线分割,分割之后总是取第一列,这样就可以把值为5的数据还原出来了

参考文献

慕课网 https://www.imooc.com/wiki/BigData

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是希望

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值