编写大量文件的最佳方法

时间:2015-03-12 07:23:07

标签: java nio java-io

我正在写很多像贝娄这样的文件。

public void call(Iterator<Tuple2<Text, BytesWritable>> arg0)
        throws Exception {
    // TODO Auto-generated method stub

    while (arg0.hasNext()) {
        Tuple2<Text, BytesWritable> tuple2 = arg0.next();
        System.out.println(tuple2._1().toString());
        PrintWriter writer = new PrintWriter("/home/suv/junk/sparkOutPut/"+tuple2._1().toString(), "UTF-8");
        writer.println(new String(tuple2._2().getBytes()));
        writer.close();
    }
}

有没有更好的方法来编写文件..无需每次关闭或创建printwriter。

2 个答案:

答案 0 :(得分:2)

没有明显更好的方法来编写大量文件。你正在做的事情本身就是I / O密集型。

更新 - @Michael Anderson是对的,我想。使用多个线程来编写文件(可能)会大大加快速度。但是,I / O仍然是几个方面的最终瓶颈:

  • 创建,打开和关闭文件涉及文件&amp;目录元数据访问和更新。这需要非常重要的CPU。

  • 需要将文件数据和元数据更改写入光盘。这可能是多次光盘写入。

  • 每个文件至少有3个系统调用。

  • 然后是线程拼接开销。

除非写入每个文件的数据量很大(每个文件多个千字节),否则我怀疑使用NIO,直接缓冲区,JNI等技术是值得的。真正的瓶颈将出现在内核中:文件系统操作和低级磁盘I / O.


  

...每次都不关闭或创建版画。

没有。您需要为每个文件创建一个新的PrintWriter(或WriterOutputStream)。

然而,这......

  writer.println(new String(tuple2._2().getBytes()));

......看起来很奇怪。你似乎是:

  • getBytes()(?),
  • 上调用String
  • 将字节数组转换为String
  • 调用将复制它的println()上的String方法,并在最终输出之前将其转换回字节。

是什么给出的?字符串有什么意义 - &gt; bytes - &gt;字符串转换?

我只是这样做:

  writer.println(tuple2._2());

这应该更快,但我不希望百分比加速到那么大。

答案 1 :(得分:1)

我假设你是追求最快的方式。因为每个人都知道最快最好;)

一种简单的方法是使用一堆线程为您写作。 但是,除非你的文件系统能够很好地扩展,否则你不会从中获得太多好处。 (我在基于Lustre的集群系统上使用这种技术,并且在“大量文件”可能意味着10k的情况下 - 在这种情况下,许多写入将转到不同的服务器/磁盘)

代码看起来像这样:(注意我认为这个版本不适合填充工作队列的少量文件 - 但无论如何都要查看下一个版本的版本......)

public void call(Iterator<Tuple2<Text, BytesWritable>> arg0) throws Exception {
    int nThreads=5;
    ExecutorService threadPool = Executors.newFixedThreadPool(nThreads);
    ExecutorCompletionService<Void> ecs = new ExecutorCompletionService<>(threadPool);

    int nJobs = 0;

    while (arg0.hasNext()) {
        ++nJobs;
        final Tuple2<Text, BytesWritable> tuple2 = arg0.next();
        ecs.submit(new Callable<Void>() {
          @Override Void call() {
             System.out.println(tuple2._1().toString());
             String path = "/home/suv/junk/sparkOutPut/"+tuple2._1().toString();
             try(PrintWriter writer = new PrintWriter(path, "UTF-8") ) {
               writer.println(new String(tuple2._2().getBytes()))
             }
             return null;
          }
       });
    }
    for(int i=0; i<nJobs; ++i) {
       ecs.take().get();
    }
}

更好的是,只要有第一个数据就开始编写文件,而不是在获得所有文件的数据时 - 并且写入时不会阻止计算线程。

要执行此操作,请将应用程序拆分为多个通过(线程安全)队列进行通信的部分。

然后代码看起来更像是这样:

public void main() {
  SomeMultithreadedQueue<Data> queue = ...;

  int nGeneratorThreads=1;
  int nWriterThreads=5;
  int nThreads = nGeneratorThreads + nWriterThreads;

  ExecutorService threadPool = Executors.newFixedThreadPool(nThreads);
  ExecutorCompletionService<Void> ecs = new ExecutorCompletionService<>(threadPool);

  AtomicInteger completedGenerators = new AtomicInteger(0);

  // Start some generator threads.
  for(int i=0; ++i; i<nGeneratorThreads) {
    ecs.submit( () -> { 
      while(...) { 
        Data d = ... ;
        queue.push(d);
      }
      if(completedGenerators.incrementAndGet()==nGeneratorThreads) {
        queue.push(null);
      }
      return null;
   });
  }

  // Start some writer threads
  for(int i=0; i<nWriterThreads; ++i) {
    ecs.submit( () -> { 
      Data d
      while((d = queue.take())!=null) {
        String path = data.path();
        try(PrintWriter writer = new PrintWriter(path, "UTF-8") ) {
           writer.println(new String(data.getBytes()));
        }
        return null;
      }
    });
  }

  for(int i=0; i<nThreads; ++i) {
    ecs.take().get();
  }
}

注意我没有提供队列类的实现,你可以轻松地包装标准的java线程类,以获得你需要的东西。

还有很多可以做的事情来减少延迟等等 - 这是我用来减少时间的一些其他事情......

  1. 甚至不等待为给定文件生成所有数据。传递包含要写入的字节数据包的另一个队列。

  2. 注意分配 - 您可以重复使用某些缓冲区。

  3. nio中存在一些延迟 - 使用C写入和JNI以及直接缓冲区可以提高性能。

  4. 线程切换可能会受到影响,并且队列中的延迟可能会受到影响,因此您可能需要稍微批量处理数据。将此与1平衡可能会非常棘手。