java io顺序
许多应用程序将一系列事件记录到基于文件的存储中,以供以后使用。 从日志记录和审核,直到在事件源设计或其紧密相关的CQRS中保留事务重做日志,这都可以是任何东西。
Java具有多种方法,可以通过这些方法将文件顺序写入或重新读取。 本文探讨了其中一些机制,以了解其性能特征。 对于本文的范围,我将使用预分配的文件,因为我想关注性能。 不断扩展文件会带来很大的性能开销,并给应用程序增加抖动,从而导致高度可变的延迟。 “为什么预分配的文件性能更好?”,我听到您问。 好吧,在磁盘上,文件是由一系列包含数据的块/页面组成的。 首先,重要的是这些块是连续的,以提供快速的顺序访问。 其次,必须分配元数据来描述此文件在磁盘上并保存在文件系统中。 典型的大文件将分配许多“间接”块,以描述包含组成此元数据一部分的文件内容的数据块链。 我将其留给读者或以后的文章来练习,以探讨不预先分配数据文件对性能的影响。 如果您使用过数据库,则可能已经注意到它预先分配了所需的文件。
考试
我想尝试2种文件大小。 一个足够大,可以测试顺序访问,但可以轻松放入文件系统缓存中;另一个很大,可以使缓存子系统被迫退出页面,以便可以加载新页面。 对于这两种情况,我将分别使用400MB和8GB。 我还将遍历文件多次,以显示预热和预热特性。
我将测试4种顺序写入和读取文件的方式:
- 使用页大小的普通字节[]的RandomAccessFile 。
- 缓冲的FileInputStream和FileOutputStream 。
- 具有页面大小的ByteBuffer的NIO FileChannel 。
- 内存使用NIO和直接MappedByteBuffer映射文件。
这些测试在具有8GB RAM的2.0Ghz Sandybridge CPU,具有ext4文件系统的Fedora Core 15 64位Linux上的Intel 320 SSD和Oracle JDK 1.6.0_30上运行。
代码
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.System.out;
import static java.nio.channels.FileChannel.MapMode.READ_ONLY;
import static java.nio.channels.FileChannel.MapMode.READ_WRITE;
public final class TestSequentialIoPerf
{
public static final int PAGE_SIZE = 1024 * 4;
public static final long FILE_SIZE = PAGE_SIZE * 2000L * 1000L;
public static final String FILE_NAME = "test.dat";
public static final byte[] BLANK_PAGE = new byte[PAGE_SIZE];
public static void main(final String[] arg) throws Exception
{
preallocateTestFile(FILE_NAME);
for (final PerfTestCase testCase : testCases)
{
for (int i = 0; i < 5; i++)
{
System.gc();
long writeDurationMs = testCase.test(PerfTestCase.Type.WRITE,
FILE_NAME);
System.gc();
long readDurationMs = testCase.test(PerfTestCase.Type.READ,
FILE_NAME);
long bytesReadPerSec = (FILE_SIZE * 1000L) / readDurationMs;
long bytesWrittenPerSec = (FILE_SIZE * 1000L) / writeDurationMs;
out.format("%s\twrite=%,d\tread=%,d bytes/sec\n",
testCase.getName(),
bytesWrittenPerSec, bytesReadPerSec);
}
}
deleteFile(FILE_NAME);
}
private static void preallocateTestFile(final String fileName)
throws Exception
{
RandomAccessFile file = new RandomAccessFile(fileName, "rw");
for (long i = 0; i < FILE_SIZE; i += PAGE_SIZE)
{
file.write(BLANK_PAGE, 0, PAGE_SIZE);
}
file.close();
}
private static void deleteFile(final String testFileName) throws Exception
{
File file = new File(testFileName);
if (!file.delete())
{
out.println("Failed to delete test file=" + testFileName);
out.println("Windows does not allow mapped files to be deleted.");
}
}
public abstract static class PerfTestCase
{
public enum Type { READ, WRITE }
private final String name;
private int checkSum;
public PerfTestCase(final String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public long test(final Type type, final String fileName)
{
long start = System.currentTimeMillis();
try
{
switch (type)
{
case WRITE:
{
checkSum = testWrite(fileName);
break;
}
case READ:
{
final int checkSum = testRead(fileName);
if (checkSum != this.checkSum)
{
final String msg = getName() +
" expected=" + this.checkSum +
" got=" + checkSum;
throw new IllegalStateException(msg);
}
break;
}
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
return System.currentTimeMillis() - start;
}
public abstract int testWrite(final String fileName) throws Exception;
public abstract int testRead(final String fileName) throws Exception;
}
private static PerfTestCase[] testCases =
{
new PerfTestCase("RandomAccessFile")
{
public int testWrite(final String fileName) throws Exception
{
RandomAccessFile file = new RandomAccessFile(fileName, "rw");
final byte[] buffer = new byte[PAGE_SIZE];
int pos = 0;
int checkSum = 0;
for (long i = 0; i < FILE_SIZE; i++)
{
byte b = (byte)i;
checkSum += b;
buffer[pos++] = b;
if (PAGE_SIZE == pos)
{
file.write(buffer, 0, PAGE_SIZE);
pos = 0;
}
}
file.close();
return checkSum;
}
public int testRead(final String fileName) throws Exception
{
RandomAccessFile file = new RandomAccessFile(fileName, "r");
final byte[] buffer = new byte[PAGE_SIZE];
int checkSum = 0;
int bytesRead;
while (-1 != (bytesRead = file.read(buffer)))
{
for (int i = 0; i < bytesRead; i++)
{
checkSum += buffer[i];
}
}
file.close();
return checkSum;
}
},
new PerfTestCase("BufferedStreamFile")
{
public int testWrite(final String fileName) throws Exception
{
int checkSum = 0;
OutputStream out =
new BufferedOutputStream(new FileOutputStream(fileName));
for (long i = 0; i < FILE_SIZE; i++)
{
byte b = (byte)i;
checkSum += b;
out.write(b);
}
out.close();
return checkSum;
}
public int testRead(final String fileName) throws Exception
{
int checkSum = 0;
InputStream in =
new BufferedInputStream(new FileInputStream(fileName));
int b;
while (-1 != (b = in.read()))
{
checkSum += (byte)b;
}
in.close();
return checkSum;
}
},
new PerfTestCase("BufferedChannelFile")
{
public int testWrite(final String fileName) throws Exception
{
FileChannel channel =
new RandomAccessFile(fileName, "rw").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(PAGE_SIZE);
int checkSum = 0;
for (long i = 0; i < FILE_SIZE; i++)
{
byte b = (byte)i;
checkSum += b;
buffer.put(b);
if (!buffer.hasRemaining())
{
buffer.flip();
channel.write(buffer);
buffer.clear();
}
}
channel.close();
return checkSum;
}
public int testRead(final String fileName) throws Exception
{
FileChannel channel =
new RandomAccessFile(fileName, "rw").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(PAGE_SIZE);
int checkSum = 0;
while (-1 != (channel.read(buffer)))
{
buffer.flip();
while (buffer.hasRemaining())
{
checkSum += buffer.get();
}
buffer.clear();
}
return checkSum;
}
},
new PerfTestCase("MemoryMappedFile")
{
public int testWrite(final String fileName) throws Exception
{
FileChannel channel =
new RandomAccessFile(fileName, "rw").getChannel();
MappedByteBuffer buffer =
channel.map(READ_WRITE, 0,
Math.min(channel.size(), MAX_VALUE));
int checkSum = 0;
for (long i = 0; i < FILE_SIZE; i++)
{
if (!buffer.hasRemaining())
{
buffer =
channel.map(READ_WRITE, i,
Math.min(channel.size() - i , MAX_VALUE));
}
byte b = (byte)i;
checkSum += b;
buffer.put(b);
}
channel.close();
return checkSum;
}
public int testRead(final String fileName) throws Exception
{
FileChannel channel =
new RandomAccessFile(fileName, "rw").getChannel();
MappedByteBuffer buffer =
channel.map(READ_ONLY, 0,
Math.min(channel.size(), MAX_VALUE));
int checkSum = 0;
for (long i = 0; i < FILE_SIZE; i++)
{
if (!buffer.hasRemaining())
{
buffer =
channel.map(READ_WRITE, i,
Math.min(channel.size() - i , MAX_VALUE));
}
checkSum += buffer.get();
}
channel.close();
return checkSum;
}
},
};
}
结果
400MB file =========== RandomAccessFile write=379,610,750 read=1,452,482,269 bytes/sec
BufferedStreamFile写入= 98,178,331读取= 286,433,566字节/秒 BufferedStreamFile写入= 100,244,738读取= 288,857,545字节/秒 BufferedStreamFile写入= 82,948,562读取= 154,100,827字节/秒 BufferedStreamFile写入= 108,503,311读取= 153,869,271字节/秒 BufferedStreamFile写入= 113,055,478读取= 152,608,047字节/秒
BufferedChannelFile写入= 228,443,948读取= 356,173,913字节/秒 BufferedChannelFile写入= 265,629,053读取= 374,063,926字节/秒 BufferedChannelFile写= 223,825,136读= 1,539,849,624字节/秒BufferedChannelFile写= 232,992,036读= 1,539,849,624字节/秒BufferedChannelFile写= 212,779,220读= 1,534,082,397字节/秒 MemoryMappedFile写入= 300,955,180读取= 305,899,925字节/秒 MemoryMappedFile写入= 313,149,847读取= 310,538,286字节/秒 MemoryMappedFile写入= 326,374,501读取= 303,857,566字节/秒 MemoryMappedFile写入= 327,680,000读取= 304,535,315字节/秒 MemoryMappedFile写入= 326895450读取= 303632320字节/秒
8GB文件 ============ RandomAccessFile写入= 167,402,321读取= 251,922,012字节/秒 RandomAccessFile写入= 193,934,802读取= 257,052,307字节/秒 RandomAccessFile写入= 192,948,159读取= 248,460,768字节/秒 RandomAccessFile写入= 191,814,180读取= 245,225,408字节/秒 RandomAccessFile写入= 190,635,762读取= 275,315,073字节/秒
BufferedStreamFile写入= 154,823,102读取= 248,355,313字节/秒 BufferedStreamFile写入= 152,083,913读取= 253,418,301字节/秒 BufferedStreamFile写入= 133,099,369读取= 146,056,197字节/秒 BufferedStreamFile write = 131,065,708 read = 146,217,827字节/秒 BufferedStreamFile写入= 132694052读取= 148116004字节/秒
BufferedChannelFile写入= 186,703,740读取= 215,075,218字节/秒 BufferedChannelFile写入= 190,591,410读取= 211,030,680字节/秒BufferedChannelFile写入= 187,220,038读取= 223,087,606字节/秒 BufferedChannelFile写入= 191,585,397读取= 221,297,747字节/秒 BufferedChannelFile写入= 192,653,214读取= 211,789,038字节/秒
MemoryMappedFile写入= 123,023,322读取= 231,530,156字节/秒 MemoryMappedFile写入= 121,961,023读取= 230,403,600字节/秒 MemoryMappedFile写入= 123,317,778读取= 229,899,250字节/秒 MemoryMappedFile写入= 121,472,738读取= 231,739,745字节/秒 MemoryMappedFile写入= 120,362,615读取= 231,190,382字节/秒
分析
多年来,我一直是直接使用RandomAccessFile的忠实拥护者 ,因为它提供了控制和可预测的执行。 从性能的角度来看,我从来没有发现使用缓冲流会很有用,而且情况似乎仍然如此。
在最近的测试中,我发现使用NIO FileChannel和ByteBuffer做得更好。 使用Java 7,此编程方法的灵活性已得到改善,可以使用SeekableByteChannel进行随机访问。
似乎在某些情况下,读取RandomAccessFile和NIO可以很好地使Memory Mapped文件赢得写操作。
您的里程可能会有所不同……
在推动最大吞吐量时,应特别注意使用内存映射的大文件。 我经常发现操作系统可能由于虚拟内存子系统上的压力而变得无响应。
结论
从Java执行顺序文件IO的不同方法在性能上存在显着差异。 并非所有方法都遥遥相等。 对于大多数IO,我发现使用ByteBuffers和Channels是IO库中最优化的部分。 如果缓冲流是您的IO库的选择,那么值得进行分支并熟悉Channel和Buffer的实现,甚至可以使用旧的RandomAccessFile进行回退。
参考: Mechanical Sympathy博客上的JCG合作伙伴 Martin Thompson提供的Java顺序IO性能 。
翻译自: https://www.javacodegeeks.com/2012/07/java-sequential-io-performance.html
java io顺序