如何读取其他进程当前写入的大日志文件
how to read a large log file which other process current write
按天创建日志文件,一个文件约400MB,JVM内存约2GB。
让一个进程以 'a' 模式写一个大日志文件。
我想读取这个文件并能实现一些功能:
- 追加读取新写入的数据
- jvm重启后我会存储偏移量以恢复读取
这是我的简单实现,不知道时间和内存消耗好不好。我想知道有没有更好的办法解决这个问题
public static void main(String[] args) throws IOException {
String filePath = "D://test.log";
long restoreOffset = resotoreOffset();
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
randomAccessFile.seek(restoreOffset);
while (true) {
String line = randomAccessFile.readLine();
if(line != null) {
// doSomething(line);
restoreOffset = randomAccessFile.getFilePointer();
//storeOffset(restoreOffset);
}
}
}
不幸的是,它不是。
此代码有 2 个主要问题。首先我会解决简单的问题,但最重要的是第二点。
编码问题
String line = randomAccessFile.readLine();
此行将字节隐式转换为字符,这通常不是一个好主意,因为字节不是字符,并且从一种转换为另一种需要字符集编码。
这种方法(readLine()
来自英国皇家空军)是一个奇怪的案例——可能是因为 RandomAccessFile 太旧了 API。使用此方法将应用一些奇怪的 ISO-8859-1 esque 字符集编码:它通过将每个字节作为一个完整的字符将字节转换为字符,假设字节代表列出的 unicode 字符,这实际上不是一个合理的编码,只是一个懒惰的程序员。
你的结果是:除非你能保证这个日志文件永远只包含 ASCII 字符,否则这个代码是错误的,readLine
不能完全用过。相反,您将不得不做更多的工作:读取字节直到遇到换行符,然后使用 new String(byteArray, StandardCharsets.UTF_8)
将如此收集的字节转换为字符串,或者使用 ByteBuffer
并应用类似的策略。但是请继续阅读,因为解决第二个问题会自动解决这个问题。
缓冲
现代计算机系统倾向于 'packeting'。您不能真正对单个字节进行操作。以 SSD 为例(尽管这也适用于旋转盘片):实际的 SSD 硬件无法读取单个字节。它只能读取整个块的数据。
因此,当您明确向 OS 请求单个字节时,最终会引发一系列事件,导致 SSD 读取整个块,然后将整个块传递给操作系统,然后它将忽略除您想要的一个字节之外的所有内容,return就是这样。
如果您的代码随后请求下一个字节,我们将再次执行该例程。
因此,如果您从具有 1024 字节块的 SSD 连续读取 1024 字节,通过调用 read()
1024 次会导致 SSD 执行 1024 次读取,而调用 read(byteArr)
一次,向其传递一个 1024 字节的数组,使 SSD 执行单次读取。
是的,这意味着字节数组解决方案实际上快了 1000 倍。
网络也是如此。发送 1 个字节一千次通常比一次发送 1000 个字节慢近 1000 倍; TCP/IP 数据包可以携带大约 1800 字节的数据,因此发送任何小于此的数据几乎不会给您带来任何好处。
RAF 的 readLine()
与第一个(坏的)场景类似:它一次读取一个字节,直到遇到换行符。因此,要读取 100 个字符的字符串,比只知道需要读取 100 个字符并一次性读取它们慢 100 倍。
解决方案
您可能想要完全放弃 RandomAccessFile,它已经很旧了 API。
缓冲的一个主要问题是它要困难得多,除非您事先知道要读取多少字节。在这里,您不知道:您想继续阅读直到遇到换行符,但您不知道我们到达那里需要多长时间。此外,缓冲 APIs 往往只是 return 方便,因此读取的字节数可能比我们要求的少(不过,除非我们到达文件末尾,否则它总是至少读取 1 个字节)。因此,我们需要编写代码来重复读取整个块的数据价值,分析块中的换行符,如果不存在,则继续阅读。
此外,开通渠道等费用高昂。所以,如果你想挖掘所有日志行,编写每次都打开一个新频道的代码是次优的。
怎么样,使用来自 java.nio.file
的较新文件 API:
public class LogLineReader implements AutoCloseable {
private final byte[] buffer = new byte[1024];
private final ByteBuffer bb = wrap(buffer);
private final SeekableByteChannel channel;
private final Charset charset = StandardCharsets.UTF_8;
public LogLineReader(Path p) {
channel = Files.newByteChannel(p, StandardOpenOption.READ);
channel.position(111L); // you seek to pos 111 in your code...
}
@Override public void close() throws IOException {
channel.close();
}
// This code buffers: First, our internal buffer is scanned
// for a new line. If there is no full line in the buffer,
// we read bytes from the file and check again until we find one.
public String readLine() {
int len = 0;
if (!channel.isOpen()) return null;
int scanStart = 0;
while (true) {
// Scan through the bytes we have buffered for a newline.
for (int i = scanStart; i < buffer.position(); i++) {
if (buffer[i] == '\n') {
// Found it. Take all bytes up to the new line, turn into
// a string.
String res = new String(buffer, 0, i, charset);
// Copy all bytes from _after_ the newline to the front.
System.arraycopy(buffer, i + 1, buffer, 0, buffer.position() - i - 1);
// Adjust the position (which represents how many bytes are buffered).
buffer.position(buffer.position() - i - 1);
return res;
}
}
scanStart = buffer.position();
// If we get here, the buffer is empty or contains no newline.
if (scanStart == buffer.limit()) {
throw new IOException("Log line too long");
}
int read = channel.read(buffer); // let's fetch more bytes!
if (read == -1) {
// we've reached the end of the file.
if (buffer.position() == 0) return null;
return new String(buffer, 0, buffer.position(), charset);
}
}
}
}
为了效率,这段代码无法处理长度超过1024的日志行;随意增加这个数字。如果您希望能够读取无限大小的日志行,那么在某些时候,巨大的缓冲区是一个问题。如果必须,您可以编写代码在达到 1024 时调整缓冲区的大小,或者您可以更新此代码以使其继续读取,但只有 return 是具有前 1024 个字符的截断字符串。我会把它留给你作为练习。
注意:我也没有测试这个,但至少它应该给你使用 SeekableByteChannel 的一般要点,以及缓冲区的概念。
使用:
Path p = Paths.get("D://logfile.txt");
try (LogLineReader reader = new LogLineReader(p)) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
// do something with line
}
}
您必须确保 LLR 对象已关闭,因此请使用 try-with-resources。
按天创建日志文件,一个文件约400MB,JVM内存约2GB。 让一个进程以 'a' 模式写一个大日志文件。 我想读取这个文件并能实现一些功能:
- 追加读取新写入的数据
- jvm重启后我会存储偏移量以恢复读取
这是我的简单实现,不知道时间和内存消耗好不好。我想知道有没有更好的办法解决这个问题
public static void main(String[] args) throws IOException {
String filePath = "D://test.log";
long restoreOffset = resotoreOffset();
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
randomAccessFile.seek(restoreOffset);
while (true) {
String line = randomAccessFile.readLine();
if(line != null) {
// doSomething(line);
restoreOffset = randomAccessFile.getFilePointer();
//storeOffset(restoreOffset);
}
}
}
不幸的是,它不是。
此代码有 2 个主要问题。首先我会解决简单的问题,但最重要的是第二点。
编码问题
String line = randomAccessFile.readLine();
此行将字节隐式转换为字符,这通常不是一个好主意,因为字节不是字符,并且从一种转换为另一种需要字符集编码。
这种方法(readLine()
来自英国皇家空军)是一个奇怪的案例——可能是因为 RandomAccessFile 太旧了 API。使用此方法将应用一些奇怪的 ISO-8859-1 esque 字符集编码:它通过将每个字节作为一个完整的字符将字节转换为字符,假设字节代表列出的 unicode 字符,这实际上不是一个合理的编码,只是一个懒惰的程序员。
你的结果是:除非你能保证这个日志文件永远只包含 ASCII 字符,否则这个代码是错误的,readLine
不能完全用过。相反,您将不得不做更多的工作:读取字节直到遇到换行符,然后使用 new String(byteArray, StandardCharsets.UTF_8)
将如此收集的字节转换为字符串,或者使用 ByteBuffer
并应用类似的策略。但是请继续阅读,因为解决第二个问题会自动解决这个问题。
缓冲
现代计算机系统倾向于 'packeting'。您不能真正对单个字节进行操作。以 SSD 为例(尽管这也适用于旋转盘片):实际的 SSD 硬件无法读取单个字节。它只能读取整个块的数据。
因此,当您明确向 OS 请求单个字节时,最终会引发一系列事件,导致 SSD 读取整个块,然后将整个块传递给操作系统,然后它将忽略除您想要的一个字节之外的所有内容,return就是这样。
如果您的代码随后请求下一个字节,我们将再次执行该例程。
因此,如果您从具有 1024 字节块的 SSD 连续读取 1024 字节,通过调用 read()
1024 次会导致 SSD 执行 1024 次读取,而调用 read(byteArr)
一次,向其传递一个 1024 字节的数组,使 SSD 执行单次读取。
是的,这意味着字节数组解决方案实际上快了 1000 倍。
网络也是如此。发送 1 个字节一千次通常比一次发送 1000 个字节慢近 1000 倍; TCP/IP 数据包可以携带大约 1800 字节的数据,因此发送任何小于此的数据几乎不会给您带来任何好处。
RAF 的 readLine()
与第一个(坏的)场景类似:它一次读取一个字节,直到遇到换行符。因此,要读取 100 个字符的字符串,比只知道需要读取 100 个字符并一次性读取它们慢 100 倍。
解决方案
您可能想要完全放弃 RandomAccessFile,它已经很旧了 API。
缓冲的一个主要问题是它要困难得多,除非您事先知道要读取多少字节。在这里,您不知道:您想继续阅读直到遇到换行符,但您不知道我们到达那里需要多长时间。此外,缓冲 APIs 往往只是 return 方便,因此读取的字节数可能比我们要求的少(不过,除非我们到达文件末尾,否则它总是至少读取 1 个字节)。因此,我们需要编写代码来重复读取整个块的数据价值,分析块中的换行符,如果不存在,则继续阅读。
此外,开通渠道等费用高昂。所以,如果你想挖掘所有日志行,编写每次都打开一个新频道的代码是次优的。
怎么样,使用来自 java.nio.file
的较新文件 API:
public class LogLineReader implements AutoCloseable {
private final byte[] buffer = new byte[1024];
private final ByteBuffer bb = wrap(buffer);
private final SeekableByteChannel channel;
private final Charset charset = StandardCharsets.UTF_8;
public LogLineReader(Path p) {
channel = Files.newByteChannel(p, StandardOpenOption.READ);
channel.position(111L); // you seek to pos 111 in your code...
}
@Override public void close() throws IOException {
channel.close();
}
// This code buffers: First, our internal buffer is scanned
// for a new line. If there is no full line in the buffer,
// we read bytes from the file and check again until we find one.
public String readLine() {
int len = 0;
if (!channel.isOpen()) return null;
int scanStart = 0;
while (true) {
// Scan through the bytes we have buffered for a newline.
for (int i = scanStart; i < buffer.position(); i++) {
if (buffer[i] == '\n') {
// Found it. Take all bytes up to the new line, turn into
// a string.
String res = new String(buffer, 0, i, charset);
// Copy all bytes from _after_ the newline to the front.
System.arraycopy(buffer, i + 1, buffer, 0, buffer.position() - i - 1);
// Adjust the position (which represents how many bytes are buffered).
buffer.position(buffer.position() - i - 1);
return res;
}
}
scanStart = buffer.position();
// If we get here, the buffer is empty or contains no newline.
if (scanStart == buffer.limit()) {
throw new IOException("Log line too long");
}
int read = channel.read(buffer); // let's fetch more bytes!
if (read == -1) {
// we've reached the end of the file.
if (buffer.position() == 0) return null;
return new String(buffer, 0, buffer.position(), charset);
}
}
}
}
为了效率,这段代码无法处理长度超过1024的日志行;随意增加这个数字。如果您希望能够读取无限大小的日志行,那么在某些时候,巨大的缓冲区是一个问题。如果必须,您可以编写代码在达到 1024 时调整缓冲区的大小,或者您可以更新此代码以使其继续读取,但只有 return 是具有前 1024 个字符的截断字符串。我会把它留给你作为练习。
注意:我也没有测试这个,但至少它应该给你使用 SeekableByteChannel 的一般要点,以及缓冲区的概念。
使用:
Path p = Paths.get("D://logfile.txt");
try (LogLineReader reader = new LogLineReader(p)) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
// do something with line
}
}
您必须确保 LLR 对象已关闭,因此请使用 try-with-resources。