单机多线程模拟 MapReduce 思想完成超大文件的 WordCount 计算

最近面试遇到了一个问题,就是有个 100G 的文件,里面的内容都是单词,请问在单机笔记本的情况下,怎么使用 MapReduce 的思想完成 WordCount 的计算。

其实这个问题,就是让我给出多线程模拟 MapReduce 进行 WordCount 计算的思路。之前看过一些 MapReduce 的源码,所以按照源码中的思路进行了回答,感觉还不错,于是回来后尝试写了代码。

1)首先,需要模拟出对应的数据,我这里模拟了 1G 左右的数据测试,代码如下

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @author xu
 * @desc 生成 word 的模拟数据,这里生成 1G 用来测试,需要注意的是这里生成的文件大小不是一个绝对准确的值。
 */
public class GenWordData {
    private static final int TARGET_FILE_SIZE = 1024 * 1024 * 1024; // 1G
    private static final int BUFFER_SIZE = 1024 * 1024 * 10; // 10MB
    private static final File file = new File("data/words.txt");
    private static final BlockingQueue<String> QUEUE = new ArrayBlockingQueue<>(10);
    private static final String[] words = new String[]{"hive", "spark", "flink", "clickhouse", "doris", "hadoop", "redis", "kafka"};

    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        long begin = System.currentTimeMillis();

        if (!file.exists()) {
            file.getParentFile().mkdirs();
            file.createNewFile();
        }

        CompletableFuture<Void> producerTask = CompletableFuture.runAsync(() -> {
            try {
                generateData();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        CompletableFuture<Void> consumerTask = CompletableFuture.runAsync(() -> {
            try {
                writeToFile();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        });
        CompletableFuture.allOf(producerTask, consumerTask).join();

        System.out.println(System.currentTimeMillis() - begin);
    }

    private static void generateData() throws InterruptedException {
        Random r = new Random();
        while (file.length() < TARGET_FILE_SIZE) {
            StringBuilder builder = new StringBuilder();
            while (builder.length() < BUFFER_SIZE){
                builder.append(words[r.nextInt(words.length)]).append(" ");
            }
            builder.append("\n");
            QUEUE.put(builder.toString());
        }
    }

    private static void writeToFile() throws IOException, InterruptedException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(file, true))) {
            while (file.length() < TARGET_FILE_SIZE) {
                String content = QUEUE.take();
                writer.write(content);
            }
        }
    }
}

2)接下来编写多线程处理的代码

public class DiyMapReduce {
    public static final File file = new File("data/words.txt");
    public static final long segment = 1024 * 1024 * 10; // 10M

    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        long begin = System.currentTimeMillis();

        // 记录每次移动的位置点,首坐标设置为零
        final List<Long> pos = new ArrayList<>();
        pos.add(0L);

        // 给出每个线程处理的范围大小,游标不断前移,但不能超过文件总长度
        BufferedReader br = new BufferedReader(new FileReader(file));
        long currPos = 0;
        while ((currPos + segment) < file.length()) {
            // 游标移动到理论值,同时跳过理论值游标字节
            currPos += segment;
            br.skip(segment);
            // 读取游标后的一个字符串,如果是空格(32)、回车(13)、结束符(-1)就结束,否则继续向后找。
            int chr = br.read();
            currPos++;
            while (chr != 32 && chr != 13 && chr != -1) {
                chr = br.read();
                currPos++;
            }
            pos.add(currPos);
        }
        // 把文件总字节数存放到集合中
        pos.add(file.length());

        // 使用固定线程池,因为 word count 算是 cpu 密集型,所以这里线程数就等于 cpu 核数
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        ArrayList<CompletableFuture<HashMap<String, Integer>>> futureList = new ArrayList<>();
        for (int i = 0; i < pos.size() - 1; i++) {
            Compute compute = new Compute((pos.get(i + 1) - pos.get(i)), pos.get(i));
            CompletableFuture<HashMap<String, Integer>> future = CompletableFuture.supplyAsync(compute, executor);
            futureList.add(future);
        }
        CompletableFuture<Void> allOf = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
        allOf.join();
        executor.shutdown();

        // 最后合并所有集合中的数组
        Map<String, Integer> res = futureList.stream()
                .map(CompletableFuture::join)
                .flatMap(map -> map.entrySet().stream())
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
        System.out.println(res);

        System.out.println(System.currentTimeMillis() - begin);
    }

    public static class Compute implements Supplier<HashMap<String, Integer>> {
        private final long readSize;
        private final long skipSize;

        public Compute(long readSize, long skipSize) {
            this.readSize = readSize;
            this.skipSize = skipSize;
        }

        @Override
        public HashMap<String, Integer> get() {
            byte[] bytes = new byte[0];
            try (FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ)) {
                MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, skipSize, readSize);
                bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
            } catch (IOException e) {
                e.printStackTrace();
            }

            // 把字符串组装成 map,key 为单词,value 为出现的次数
            HashMap<String, Integer> map = new HashMap<>();
            String[] words = new String(bytes).split("\\s+");
            for (String word : words) {
                map.compute(word.trim(), (key, value) -> value == null ? 1 : ++value);
            }
            return map;
        }
    }
}

3)优化

上述代码处理 1G 左右的数据耗时大约在 65 秒左右,而且 CPU 和内存占用也挺高的。去看了下火焰图发现,大部分耗时都在 split 函数上。
在这里插入图片描述

由于都是空格,也不涉及什么正则啥的,于是换成 StringTokenizer 类来切割,代码如下

StringTokenizer tokenizer = new StringTokenizer(new String(bytes), " ");
while (tokenizer.hasMoreTokens()) {
    map.compute(tokenizer.nextToken().trim(), (key, value) -> value == null ? 1 : ++value);
}

大概耗时变成了 15 秒左右,大约快了 3 倍左右。当然这也是业务比较简单,StringTokenizer 是没法做一些正则的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个使用Java语言基于MapReduce计算框架完成wordcount的例子: Mapper类实现: ```java import java.io.IOException; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; public class WordCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> { // 定义key变量,存储每个单词 private Text key = new Text(); // 定义value变量,存储每个单词的词频 private LongWritable value = new LongWritable(1); public void map(LongWritable k, Text v, Context context) throws IOException, InterruptedException { // 将每一行文本转换成字符串 String line = v.toString(); // 将字符串按照空格切分成单词 String[] words = line.split(" "); // 遍历每个单词,输出<word, 1> for (String word : words) { key.set(word); context.write(key, value); } } } ``` Reducer类实现: ```java import java.io.IOException; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> { // 定义key变量,存储每个单词 private Text key = new Text(); // 定义value变量,存储每个单词的总词频 private LongWritable value = new LongWritable(); public void reduce(Text k, Iterable<LongWritable> v, Context context) throws IOException, InterruptedException { // 定义一个计数器,用于统计每个单词的词频 long count = 0; // 遍历每个value,累加每个单词的词频 for (LongWritable val : v) { count += val.get(); } // 将词频累加结果输出为<word, count> key.set(k); value.set(count); context.write(key, value); } } ``` Driver类实现: ```java import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.LongWritable; 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; public class WordCountDriver { public static void main(String[] args) throws Exception { // 创建一个配置对象 Configuration conf = new Configuration(); // 创建一个Job对象 Job job = Job.getInstance(conf, "word count"); // 设置job的主类 job.setJarByClass(WordCountDriver.class); // 设置Mapper类 job.setMapperClass(WordCountMapper.class); // 设置Reducer类 job.setReducerClass(WordCountReducer.class); // 设置Mapper的输出key类型 job.setMapOutputKeyClass(Text.class); // 设置Mapper的输出value类型 job.setMapOutputValueClass(LongWritable.class); // 设置Reducer的输出key类型 job.setOutputKeyClass(Text.class); // 设置Reducer的输出value类型 job.setOutputValueClass(LongWritable.class); // 设置输入路径 FileInputFormat.addInputPath(job, new Path(args[0])); // 设置输出路径 FileOutputFormat.setOutputPath(job, new Path(args[1])); // 等待job完成 System.exit(job.waitForCompletion(true) ? 0 : 1); } } ``` 运行该代码需要在Hadoop集群中进行,可以使用Hadoop单节点伪分布式模式进行测试。您需要创建一个文本文件作为输入,将其上传到HDFS中,并将该文件的HDFS路径作为参数传递给上述Driver的main()函数。输出将保存在另一个HDFS目录中,您可以使用Hadoop命令将其下载到本地进行查看。 希望这个例子可以帮助您理解如何使用Java语言基于MapReduce计算框架完成wordcount

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值