Ответ 1
Изменить/предупреждение: есть потенциальные проблемы с этим решением, потому что он сильно использует MappedByteBuffer
, и неясно, как/когда соответствующие ресурсы будут выпущены. См. этот Q & A и JDK-4724038: (fs) Добавить метод unmap в MappedByteBuffer.
При этом, пожалуйста, также см. конец этого сообщения
Я бы сделал именно то, что предложил Ним:
оберните это в класс, который отображается в "блоках", а затем перемещает блок по мере написания. Алгоритм для этого достаточно прост. Просто выберите размер блока, который имеет смысл для данных, которые вы пишете..
На самом деле, я сделал именно то, что лет назад, и просто выкопал код, он выглядит следующим образом (разделяется до минимума для демонстрации, с единственным методом для записи данных):
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
Надеюсь, я не повредил все, удалив ненужные биты и переименовав... По крайней мере, вычисление смещения выглядит корректно (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
Количество блоков (и фактическая "разреженность" ) будет зависеть от ОС и файловой системы, выше было в Debian Buster, ext4 - разреженные файлы не поддерживаются в HFS + для macOS, а в Windows им требуется, чтобы программа что-то делала я не знаю достаточно, но это не кажется легким или даже выполнимым с Java, но не уверен.
У меня нет свежих чисел, но в то время эта "техника скольжения MappedByteBuffer
" была очень быстрой, и, как вы можете видеть выше, она оставляет дыры в файле.
Вам нужно будет адаптировать WINDOW_SIZE
к чему-то, что имеет смысл для вас, добавить все методы writeThingy
, которые вам нужны, возможно, обернув writeBytes
, что вам подходит. Кроме того, в этом состоянии он будет увеличивать файл по мере необходимости, но кусками WINDOW_SIZE
, которые также могут потребоваться для адаптации.
Если нет очень веской причины не делать этого, лучше всего держать его простым с этим единственным механизмом, а не поддерживать сложную двухрежимную систему.
О хрупкости и потреблении памяти, я провел стресс-тест ниже на Linux без каких-либо проблем в течение часа, на машине с 800 ГБ ОЗУ, а также на другой очень скромной виртуальной машине с 1 ГБ ОЗУ. Система выглядит совершенно здоровой, 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-системах, возможно, это просто удача в этой конкретной рабочей нагрузке... но я начинаю думать, что это действительное решение для некоторых систем и рабочих нагрузок, может быть полезно.