[英]how to read a large log file which other process current write
每天創建一個日志文件,一個文件400MB左右,JVM memory 2GB左右。讓一個進程用'a'模式寫一個大日志文件。我想讀這個文件並能實現一些功能:
這是我的簡單實現,不知道時間和memory消耗好不好。 我想知道是否有更好的方法來解決這個問題
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);
}
}
}
不幸的是,事實並非如此。
這段代碼有兩個主要問題。 首先,我將解決簡單的問題,但最重要的是第二點。
String line = randomAccessFile.readLine();
這一行將字節隱式轉換為字符,這通常是個壞主意,因為字節不是字符,從一個字節轉換到另一個字節需要字符集編碼。
這種方法(來自 RAF 的readLine()
)是一個奇怪的案例 - 可能是因為 RandomAccessFile 是非常古老的 API。 使用這種方法將應用一些奇怪的 ISO-8859-1 esque 字符集編碼:它通過將每個字節作為一個完整的字符來將字節轉換為字符,假設該字節表示 unicode 字符,這實際上不是一個理智的編碼,只是一個懶惰的程序員。
你的結果是:除非你能保證這個日志文件永遠只包含 ASCII 字符,否則這個代碼會被破壞,並且readLine
根本不能使用。 相反,您將不得不做更多的工作:讀取字節直到換行,然后使用new String(byteArray, StandardCharsets.UTF_8)
將如此收集的字節轉換為字符串,或者使用ByteBuffer
並應用類似的策略。 但是請繼續閱讀,因為解決第二個問題會自動解決這個問題。
現代計算機系統傾向於喜歡“打包”。 您不能真正對單個字節進行操作。 以 SSD 為例(盡管這也適用於旋轉盤片):實際的 SSD 硬件無法讀取單個字節。 它只能讀取整個塊的數據。
因此,當您明確向操作系統詢問單個字節時,最終會引發一系列事件,導致 SSD 讀取整個塊,然后將整個塊傳遞給操作系統,然后操作系統將忽略除一個字節之外的所有內容你想要的,然后返回。
如果您的代碼隨后請求下一個字節,我們將再次執行該例程。
因此,如果您從具有 1024 字節塊的 SSD 連續讀取 1024 字節,則通過調用read()
1024 次會導致 SSD 執行 1024 次讀取,而調用read(byteArr)
一次,將其傳遞給 1024 字節數組, 使 SSD 執行單次讀取。
是的,這意味着字節數組解決方案實際上快了 1000 倍。
這同樣適用於網絡。 一千次發送 1 字節通常比發送 1000 字節一次慢近 1000 倍; TCP/IP 數據包可以攜帶大約 1800 字節的數據,因此發送少於該數據的數據幾乎不會為您帶來任何好處。
RAF 的readLine()
就像第一個(壞的)場景一樣工作:它一次讀取一個字節,直到遇到換行符。 因此,要讀取 100 個字符的字符串,它比只知道需要讀取 100 個字符並在一個 go 中讀取它們要慢 100 倍。
您可能想完全放棄 RandomAccessFile,它是相當古老的 API。
緩沖的一個主要問題是,除非您事先知道要讀取多少字節,否則它要困難得多。 在這里,您不知道:您想繼續閱讀,直到遇到換行符,但您不知道要多久才能到達那里。 此外,緩沖 API 往往只返回方便的內容,因此可能讀取的字節數比我們要求的要少(但它總是至少讀取 1,除非我們到達文件末尾)。 所以,我們需要編寫代碼來重復讀取整個塊的數據,分析塊中的換行符,如果不存在,繼續閱讀。
此外,打開通道等是昂貴的。 因此,如果您想挖掘所有日志行,編寫每次都打開一個新通道的代碼是次優的。
這個怎么樣,使用來自java.nio.file
:
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 時調整緩沖區大小,或者您可以更新此代碼以使其繼續讀取,但僅返回前 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 object 已關閉,因此,請使用 try-with-resources。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.