在运行hadoop程序时,从hdfs上读取数据,可能会由于小文件过多而影响内存资源大量被占用,从而导致hadoop集群崩溃,或者程序执行耗时过长。(在Hadoop的世界中,小文件是指文件大小远远小于HDFS块大小的文件,Hadoop2.0中,HDFS默认的块大小是128MB,所以,比如2MB,7MB或9MB的文件就认为是小文件。)
Hadoop的应用中,Hadoop可以很好的处理大文件,不过当文件很多,并且文件很小时,Hadoop会把每一个小文件传递给map()函数,而Hadoop在调用map()函数时会创建一个映射器,这样就会创建了大量的映射器,应用的运行效率并不高。例如,如果有2000个文件,每一个文件的大小约为2-3MB,在处理这一批文件时,就需要2000个映射器,将每一个文件发送到一个映射器,效率会非常低的。所以,在Hadoop的环境环境中,要解决这个问题,就需要把多个文件合并为一个文件,然后在进行处理。Hadoop主要设计批处理大量数据的大文件,不是很多小文件。解决小文件问题的主要目的就是通过合并小文件为更大的文件来加快Hadoop的程序的执行,解决小文件问题可以减少map()函数的执行次数,相应地提高hadoop作业的整体性能。
解决方法:
将小文件提交到MapReduce/Hadoop之前,需要先把这些小文件合并到大文件中,再把合并的大文件提交给MapReduce驱动器程序。
定义一个SmallFilesConsolidator类接受一组小文件,然后将这些小文件合并在一起,生成更大的Hadoop文件,这些文件的大小接近于HDFS块大小(dfs.block.size),最优的解决方案便是尽可能创建少的文件。
定义一个BucketThread类,这个类把小文件合并为一个大小于或接近于HDFS块大小的大文件。BucketThread是一个实现了Runable接口的独立线程,通过提供copyMerge()方法,把小文件合并为一个大文件。由于BucketThread是一个线程,所有的BucketThread对象可以并发的合并小文件。copyMerge()是BucketThread类的核心方法,它会把一个桶中的所有小文件合并为为一个临时的HDFS文件。例如,如果一个同种包含小文件{file1,file2,file3,file4,file5},那么合并得到的文件如下图所示:
SmallFilesConsolidator类的实现
- /**
- * 接受一组小Hadoop文件,然后将这些小文件合并在一起,构成更大的hadoop文件
- * @author hujie.hu
- *
- */
- public class SmallFilesConsolidator {
- /**
- * 创建空桶数组
- * @param totals 小文件总文件数
- * @param available map允许数量
- * @param pers 每一个Bucket的最大文件数
- * @return
- */
- public static BucketThread[] createBuckets(int totals, int available, int pers) {
- // 计算需要桶数量
- int nums = getNumberOfBuckets(totals, available, pers);
- BucketThread[] buckets = new BucketThread[nums];
- return buckets;
- }
- /**
- * 获取桶的数量
- * @param totals 小文件总文件数
- * @param available map允许数量
- * @param pers 每一个Bucket的最大文件数
- * @return
- */
- private static int getNumberOfBuckets(int totals,int available, int pers) {
- // 如果文件数量小于pers * available
- if (totals <= (pers * available)) {
- return available;
- } else {
- int nums = totals / pers;
- int remainder = totals % pers;
- if (remainder == 0) {
- return nums;
- } else {
- return nums + 1;
- }
- }
- }
- /**
- * 填充bucket(将所有的小文件分区并填充到bucket中)
- * @param buckets 所有Bucket列表
- * @param smallFiles 小文件路径集合
- * @param dir 拷贝文件输出目录文件夹
- * @param pers 每一个Bucket的最大文件数
- * @throws Exception
- */
- public static void fillBuckets(BucketThread[] buckets,Configuration conf, List<Path> smallFiles, int pers,String dir)throws Exception {
- // 获得桶的数量
- int bucketSize = buckets.length;
- // 总文件数
- int totals = smallFiles.size();
- // 获得每个桶需要文件数量
- int num = totals / bucketSize;
- // 如果每个桶的文件数量小于每一个Bucket的最大文件数,且还有多余文件没有装到桶内,则向每个桶内再添加一个文件
- if (num < pers) {
- int remainder = totals % bucketSize;
- if (remainder != 0) {
- num++;
- }
- }
- // 使用Bucket的序号定义Bucket的i(范围是从0到numberOfBuckets-1)
- int i = 0;
- int index = 0;
- // 标识小文件是否全部填充到桶内
- boolean done = false;
- while ((!done) & (i < bucketSize)) {
- // 创建一个Bucket对象
- buckets[i] = new BucketThread(dir,conf, i);
- // 使用小文件填充Bucket
- for (int b = 0; b < num; b++) {
- buckets[i].add(smallFiles.get(index));
- index++;
- if (index == totals) {
- done = true;
- break;
- }
- }
- i++;
- }
- }
- /**
- * 逐个桶合并文件
- * @param buckets 桶对象数组
- * @return
- * @throws Exception
- */
- public static List<Path> mergeEachBucket(BucketThread[] buckets) throws Exception {
- // 用于记录合并之后的文件路径
- List<Path> list = new ArrayList<>();
- // 桶对象数组为空或数组长度为0时,直接返回
- if (buckets == null || buckets.length < 1) {
- return list;
- }
- // 遍历线程数组,逐个桶合并每个桶内文件
- for (int i = 0; i < buckets.length; i++) {
- if (buckets[i] != null) {
- buckets[i].start();
- }
- }
- // 等待所有线程完成
- for (int i = 0; i < buckets.length; i++) {
- if (buckets[i] != null) {
- buckets[i].join();
- }
- }
- // 将每个桶内合并后的文件路径保存在集合中
- for (int i = 0; i < buckets.length; i++) {
- if (buckets[i] != null) {
- Path biosetPath = buckets[i].getTargetDir();
- list.add(biosetPath);
- }
- }
- return list;
- }
- }
BucketThread类的实现
- /**
- * 把小文件连接为一个小于hdfs块大小的大文件
- * @author hujie.hu
- *
- */
- public class BucketThread implements Runnable {
- private static Logger logger = Logger.getLogger(BucketThread.class);
- private static final Path NULL_PATH = new Path("/tmp/angle/null");
- private Thread runner = null;
- private List<Path> bucket = null;
- private Configuration conf = null;
- private FileSystem fs = null;
- private String targetDir = null;
- private String targetFile = null;
- /**
- * 创建一个新的Bucket线程对象
- * @param parentDir 父目录
- * @param id 每一个Bucket都有一个唯一的ID
- */
- public BucketThread(String parentDir,Configuration conf, int id) {
- try {
- // 桶的存储路径
- this.targetDir = parentDir + File.separator + id;
- // 合并之后生成的大文件路径
- this.targetFile = targetDir + File.separator + id + ".lzo";
- this.conf = conf;
- // 当前线程执行对象
- this.runner = new Thread(this);
- this.fs = FileSystem.get(URI.create("hdfs://ns1"),conf);
- this.bucket = new ArrayList<>();
- } catch (Exception e) {
- logger.error("创建bucket对象错误,父目录/bucketID:"+parentDir+"/"+id, e);
- }
- }
- /**
- * 线程执行
- */
- @Override
- public void run() {
- try {
- copyMerge();
- } catch (Exception e) {
- logger.error("run(): copyMerge() failed.", e);
- }
- }
- /**
- * 逐个桶和并文件
- * @throws IOException
- */
- private void copyMerge() throws IOException {
- // 如果bucket中只有一个路径/dir,则不需要合并它
- if (bucket.size() < 2) {
- return;
- }
- Path hdfsTargetFile = new Path(targetFile);
- LzopCodec lzo=new LzopCodec();
- lzo.setConf(conf);
- // 创建输出流,合并为大文件
- OutputStream out = lzo.createOutputStream(fs.create(hdfsTargetFile));
- for (int i = 0; i < bucket.size(); i++) {
- FileStatus[] contents = fs.listStatus(bucket.get(i));
- for (int k = 0; k < contents.length; k++) {
- if (!contents[k].isDirectory()) {
- InputStream in = lzo.createInputStream(fs.open(contents[k].getPath()));
- IOUtils.copyBytes(in, out, this.conf, false);
- in.close();
- }
- }
- }
- out.close();
- }
- /**
- * 添加一个文件到Bucket中
- * @param path
- * @throws Exception
- */
- public void add(Path path) throws Exception {
- if (path == null) {
- return;
- }
- if (fs.exists(path)) {
- bucket.add(path);
- }
- }
- /**
- * 启动线程
- */
- public void start() {
- runner.start();
- }
- /**
- * 连接并等待其他线程
- */
- public void join() {
- try {
- runner.join();
- } catch (InterruptedException e) {
- logger.error("线程连接发生错误", e);
- }
- }
- /**
- * 获得合并后桶的路径
- * @return
- */
- public Path getTargetDir() {
- if (bucket.isEmpty()) {
- // 没有文件的空目录
- return NULL_PATH;
- } else if (bucket.size() == 1) {
- return bucket.get(0);
- } else {
- // bucket有两个或更多的文件,并且已经被合并
- return new Path(targetDir);
- }
- }
- }
调用方法
- // 小文件合并成大文件
- int available = 15;
- int pers = 10;
- List<Path> validlist = new ArrayList<>();
- validlist = LoadHdfs.readFile2List(ReadConf.jobsvalidPath,month,validlist);
- BucketThread[] buckets = SmallFilesConsolidator.createBuckets(validlist.size(), available, pers);
- SmallFilesConsolidator.fillBuckets(buckets, conf, validlist, pers, ReadConf.mixValidPath);
- List<Path> validBucket = SmallFilesConsolidator.mergeEachBucket(buckets);