2017-06-20 55 views
18

我有一个要求写入记录到一个文件,其中的数据写入文件位置(即寻找位置)取决于数字键的值。例如,如果密钥是100,我可能在位置400写入。爪哇 - 如何有效地写入一个连续的文件,其中有偶尔的洞

记录包含数字键和一段数据。记录不会很大(几个字节)。但是,可能有很多记录(百万)。

有两种可能的情况:

  1. 键是单调递增。在这种情况下,最好的方法是使用包装BufferedOutputStreamDataOutputStream进行写入,将缓冲区大小设置为某个数字(例如64k)以最大化I/O吞吐量。

  2. 关键在增加,但可能存在很大差距。在这种情况下,使用OutputStream将需要在文件的间隙中写入零。为了避免这种情况,RandomAccessFile会更好,因为它可以寻找差距,如果有可能寻找整个区块,节省空间。缺点是,据我所知,RandomAccessFile不缓冲,所以这种方法对于顺序键将会很慢。

但是,可能的情况是该文件有点两者。有单调递增的密钥序列。有一些间隙很小的间隙和其他间隙很大的间隙。

我在寻找的是一个能够给两全其美的解决方案。如果检测到键之间的间隙,可能是我在两种I/O模式之间切换。但是,如果有一个可以完成这两件事的标准Java类,那将会更好。我见过FileImageOutputStream,但我不确定这是如何工作的。

请注意,我不是在寻找代码示例(虽然这对于演示复杂解决方案会有所帮助),但只是一个常规策略。最好知道顺序数据的最佳大小缓冲区大小以及您需要从顺序策略切换到随机访问策略的哪个点(间隙大小)。

编辑:

对于要接受一个答案,我想一些保证所提出的方案可以同时处理,而不仅仅是它可能。这将需要:

  • 确认顺序模式被缓冲。
  • 确认随机访问模式在文件中留下空洞。

此外,该解决方案需要具有高效的内存,因为可能会有许多这些文件同时打开。

EDIT 2

这些文件可能是在NAS。这不是通过设计,而是简单地认识到,在企业环境中,这种架构被大量使用,解决方案应该可能会处理它(可能不是最佳),并且不会阻止它的使用。 AFAIK,这应该不会影响基于write()lseek()的解决方案,但可能会使一些更深奥的解决方案失效。 

+0

文件大小是否固定?还是需要根据密钥增长?我会简单地使用'MappedByteBuffer'来进行写入操作。如果文件太大或需要增长,我会将其封装在一个映射为“块”的类中,然后在您写入时将块移动。此算法相当简单..只需选择一个对您正在编写的数据有意义的块大小即可。 – Nim

+0

该文件的大小并未提前知道。该文件可能位于网络驱动器上 - 我不确定这是否会影响您的解决方案 – rghome

+0

查看'java.nio.channels'。你可以用'FileChannel'进行随机访问,并写入缓冲数据。 – teppic

回答

-1

我已经改变了我的想法。你应该使用MappedByteBuffer。它由操作系统作为虚拟内存子系统的一部分进行分页,这可满足您的缓冲需求;它在写入时与写入内存一样快;并且在编写满足该要求的文件时,它受操作系统的行为支配。

+0

是的 - 我在我的问题中提到了RandomAccessFile - 我知道如何使用它。但是,写入是无缓冲的,因此与使用缓冲区顺序写入相比非常缓慢。请记住,记录很小。我想要的是缓冲和随机访问(我想让我的蛋糕吃掉它)。 – rghome

+0

那么你会映射整个文件一次?你如何处理需要进一步写入文件的结尾?我想这需要重新映射......然后,我们遇到了你提到的有关我的答案的相同细节......或者我错过了什么? –

1

编辑/警告:有这个解决方案潜在的陷阱,因为它大量使用了MappedByteBuffer,以及如何/时,相应的资源被释放,目前尚不清楚。见this Q&A & JDK-4724038 : (fs) Add unmap method to MappedByteBuffer

话虽这么说,也请看到这个帖子的末尾


我会做什么Nim suggested

包裹这其中映射在“块”和一类然后按照你写的方向移动块。这种算法相当简单..只需选择一个块大小,这对你正在编写的数据有意义。

事实上,我确实做到了年前刚刚挖出来的代码,它是这样(脱光了最低限度的演示,有一个方法来写数据):

import java.io.IOException; 
import java.io.RandomAccessFile; 
import java.nio.MappedByteBuffer; 
import java.nio.channels.FileChannel; 
import java.nio.file.Path; 

public class SlidingFileWriterThingy { 

    private static final long WINDOW_SIZE = 8*1024*1024L; 
    private final RandomAccessFile file; 
    private final FileChannel channel; 
    private MappedByteBuffer buffer; 
    private long ioOffset; 
    private long mapOffset; 

    public SlidingFileWriterThingy(Path path) throws IOException { 
     file = new RandomAccessFile(path.toFile(), "rw"); 
     channel = file.getChannel(); 
     remap(0); 
    } 

    public void close() throws IOException { 
     file.close(); 
    } 

    public void seek(long offset) { 
     ioOffset = offset; 
    } 

    public void writeBytes(byte[] data) throws IOException { 
     if (data.length > WINDOW_SIZE) { 
      throw new IOException("Data chunk too big, length=" + data.length + ", max=" + WINDOW_SIZE); 
     } 
     boolean dataChunkWontFit = ioOffset < mapOffset || ioOffset + data.length > mapOffset + WINDOW_SIZE; 
     if (dataChunkWontFit) { 
      remap(ioOffset); 
     } 
     int offsetWithinBuffer = (int)(ioOffset - mapOffset); 
     buffer.position(offsetWithinBuffer); 
     buffer.put(data, 0, data.length); 
    } 

    private void remap(long offset) throws IOException { 
     mapOffset = offset; 
     buffer = channel.map(FileChannel.MapMode.READ_WRITE, mapOffset, WINDOW_SIZE); 
    } 

} 

这里是测试片段:

SlidingFileWriterThingy t = new SlidingFileWriterThingy(Paths.get("/tmp/hey.txt")); 
t.writeBytes("Hello world\n".getBytes(StandardCharsets.UTF_8)); 
t.seek(1000); 
t.writeBytes("Are we there yet?\n".getBytes(StandardCharsets.UTF_8)); 
t.seek(50_000_000); 
t.writeBytes("No but seriously?\n".getBytes(StandardCharsets.UTF_8)); 

什么输出文件的样子:

$ hexdump -C /tmp/hey.txt 
00000000 48 65 6c 6c 6f 20 77 6f 72 6c 64 0a 00 00 00 00 |Hello world.....| 
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 
* 
000003e0 00 00 00 00 00 00 00 00 41 72 65 20 77 65 20 74 |........Are we t| 
000003f0 68 65 72 65 20 79 65 74 3f 0a 00 00 00 00 00 00 |here yet?.......| 
00000400 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 
* 
02faf080 4e 6f 20 62 75 74 20 73 65 72 69 6f 75 73 6c 79 |No but seriously| 
02faf090 3f 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |?...............| 
02faf0a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 
* 
037af080 

我希望我没有锐n通过删除不必要的位和重命名...至少偏移量计算看起来正确(0x3e0 + 8 = 1000,0x02faf080 = 50000000)。

数由文件占用的数据块(左列),而同样大小的另一种非稀疏文件:

$ head -c 58388608 /dev/zero > /tmp/not_sparse.txt 
$ ls -ls /tmp/*.txt 
    8 -rw-r--r-- 1 nug nug 58388608 Jul 19 00:50 /tmp/hey.txt 
57024 -rw-r--r-- 1 nug nug 58388608 Jul 19 00:58 /tmp/not_sparse.txt 

块(和实际的“稀疏”)的数量将取决于OS &文件系统,上面是Debian Buster,ext4 - HFS + for macOS不支持稀疏文件,而在Windows上,它们需要程序去做一些我不太了解的东西,但这似乎并不容易,甚至不可行来自Java,不确定。

我没有新的数字,但当时这个“滑动-MappedByteBuffer技术”非常快,正如你在上面看到的,它确实在文件中留下了漏洞。
你需要适应WINDOW_SIZE的东西,有意义的你,也许通过包装writeBytes,只要适合你添加的所有writeThingy方法需要。此外,在这种状态下,它会根据需要增大文件大小,但也可能由WINDOW_SIZE组成,您可能还需要进行修改。

除非有很好的理由,否则它可能是最好的保持它的简单与此单一的机制,而不是保持一个复杂的双模系统。


关于脆弱性和内存消耗,我和内存800GB以下运行在Linux上的压力测试没有一个小时的任何问题,一台机器上,并在另一个非常谦虚VM配备1G的RAM 。系统看起来非常健康,java进程不会使用任何大量的堆内存。

String path = "/tmp/data.txt"; 
    SlidingFileWriterThingy w = new SlidingFileWriterThingy(Paths.get(path)); 
    final long MAX = 5_000_000_000L; 
    while (true) { 
     long offset = 0; 
     while (offset < MAX) { 
      offset += Math.pow(Math.random(), 4) * 100_000_000; 
      if (offset > MAX/5 && offset < 2*MAX/5 || offset > 3*MAX/5 && offset < 4*MAX/5) { 
       // Keep 2 big "empty" bands in the sparse file 
       continue; 
      } 
      w.seek(offset); 
      w.writeBytes(("---" + new Date() + "---").getBytes(StandardCharsets.UTF_8)); 
     } 
     w.seek(0); 
     System.out.println("---"); 
     Scanner output = new Scanner(new ProcessBuilder("sh", "-c", "ls -ls " + path + "; free") 
       .redirectErrorStream(true).start().getInputStream()); 
     while (output.hasNextLine()) { 
      System.out.println(output.nextLine()); 
     } 
     Runtime r = Runtime.getRuntime(); 
     long memoryUsage = (100 * (r.totalMemory() - r.freeMemory()))/r.totalMemory(); 
     System.out.println("Mem usage: " + memoryUsage + "%"); 
     Thread.sleep(1000); 
    } 

所以,是的这就是经验,也许它只是正常工作在最近的Linux系统,也许它只是与特定的工作量运气......但我开始认为这是对一些系统和有效的解决方案工作量,它可以是有用的。

+0

这将会在每次重新映射时创建一个新的映射字节缓冲区。没有明确的时间来释放这些时间,因此很容易导致内存不足。 – EJP

+0

这是真的,它依赖于垃圾收集器和可能的操作系统机制。对于Linux上的大文件我们已经足够了,我会检查SCM历史记录和应用程序使用情况,看看我是否找到可能导致问题的技巧或信息 –

+0

它不依赖于垃圾回收器。阅读我写的内容。 “MappedByteBuffers”可以被垃圾收集的时间没有明确的时间。因此,他们不是负责*而不是*垃圾收集。这会导致内存耗尽。这是'MappedByteBuffers'的一个众所周知的问题。 – EJP

0

你说几百万字节的记录。因此我们假设它是10百万个10字节,这意味着要写入的文件将有大约100 MB。在我们这个时代,这并不多。

我只是创建一个地图,其中存储了所有的键值对。然后编写一个函数,将地图的内容序列化为byte[]。然后简单地将Files.write()的字节写入磁盘。 然后用新文件替换旧文件。或者,最好先移动旧文件,然后移动新文件。

+0

将数字映射到其他数字的映射效率极低。你可以使用一个自定义地图Colt或Trove,但即使如此,仍然不是很好。 – rghome

0

我假设当你的按键之后按顺序递增然后出现间隙时,不会有另一个按键添加到“完成”序列。如果这是正确的话,我会sujest以下解决方案

只要你的钥匙一直在逐渐增加的保持与你的第一个方法工作:

写使用DataOutputStream包装一BufferedOutputStream,缓冲区的大小设置为一些数量(例如64k)以最大化I/O吞吐量。

将您的数据写入临时文件。一旦发生差距,开始写入下一个临时文件并保存临时文件的记录。通过这种方式,您可以无差距地获取每个记录序列的文件。一旦你完成了你的主文件的数据处理,然后有一个单独的方法,可以巧妙地将你的临时文件合并为一个最终文件。这将是一件容易的事情,因为你知道每个临时文件没有任何间隙

+0

我认为这里的缺点是,你最终会写两次文件。 – rghome

+0

你是对的,但是连接任务可以在稍后阶段完成,并且在系统繁忙时不占用关键资源。优点是在编写顺序块时,您将非常高效地工作(性能明智),而且逻辑非常简单。 –

0

我的第一个努力就是简单地使用RandomAccessFile,看看它是否足够快。如果它很慢,我会感到惊讶 - 尽管Java不会缓冲它,但文件系统实现将会如此。


如果真的有性能问题,我的下一个努力将包裹RandomAccessFile在缓冲门面,与沿(Java的ISH伪代码)行写逻辑:

void write(record, location) { 
    if(location != lastLocation + recordLength) { 
      flushBufferToRandomAccessFile(); 
    ) 
    addToBuffer(record); 
    flushBufferToRandomAccessFileIfFull(); 
    lastLocation = location; 
} 

的缓冲区将是一个byte[]。这里的潜在胜利是你做的更少randomAccessFile.write(buffer, 0, longLength)而不是更多randomAccessFile.write(record, 0, shortLength)

您可以通过封装关于Buffer类中的缓冲块的所有必需信息(字节,起始位置,结束位置)来整理这一点。您还需要刷新缓冲区以便在close()方法中存档)。

也就是说,你在堆内存中收集的记录块,刷新到RandomAccessFile

当你达到你的缓冲区的大小
  • 当记录位置是不邻接,当前缓冲块
  • 的最后一个记录后

我明白,你不想浪费内存 - 但不管它是在堆或其他地方,备忘录ry是记忆,没有它就不能缓冲。有了这个解决方案,您可以调整缓冲区的大小 - 即使只有两个记录,它也可以减少写入次数。

如果您想对内存使用情况持狂热态度,请使用错误的语言。


如果这还不够快,我会考虑将写入移动到另一个线程。因此,将您的记录写入队列,并让文件写入线程从队列中消耗。这不会使文件本身写得更快,而是意味着消费者可以在制作人做不同的工作时赶上积压 - 所以它的效用取决于制作人是否有其他工作要做。

+0

我认为这是一个可行的解决方案,但如果只有一个小差距,我不会冲洗整个缓冲区。为缓冲区分配几个K可用于内存使用。我不得不说,我希望有一个标准的Java类可以做到这一点,而不必写一个。 – rghome

+0

当然,您可以在缓冲区中包含空的空白块 - 但是您正在追逐微观优化,并且收益递减。 – slim