客戶端在請求資源時,請求會發送到服務端的業務程序,而後業務程序負責把資源返回給客戶端。在這個過程當中,若是咱們要對服務端的程序進行優化,那麼分析服務端的日誌是必不可少的。而分析服務端的日誌,首先就須要把服務端的日誌收集起來。那麼如何實現一個高效的本地日誌收集程序,是本文要討論的內容。java
"高效" 應該怎麼理解呢?本文把"高效"定義爲:在保證讀取日誌吞吐量的同時,儘量少地佔用服務器資源。更具體的說,"高效"包含的指標有:CPU消耗、內存佔用、可靠性、耗時、吞吐量等因素。git
本文將介紹使用 Java 編程語言實現的讀取本地文件的幾種方式,並分析每種方式的優缺點。最後給出筆者實踐獲得的最高效的本地日誌收集程序,供讀者參考。另外因爲筆者水平有限,文中有誤之處,歡迎指正。github
讀取本地文件,咱們最經常使用的方式是構建一個 BufferedReader 類,經過它的 readLine() 方法讀取每一行日誌。編程
若是咱們要實現可靠的文件傳輸功能,就須要定時保存文件的當前讀取位置。 這樣能夠保證,程序即便在讀文件過程當中中止,程序重啓後,依然能夠從文件上次讀取的位置繼續消費日誌,保障日誌不會被重複或遺漏消費。代碼以下:數組
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath))); while ((line = reader.readLine()) != null){ recordPosition(); // 記錄讀取位置 process(line); // 處理每一行的內容 }
BufferedReader
方式的優勢是:讀取日誌消耗的內存和CPU比較小,吞吐量高。緩存
BufferedReader
方式的缺點是:不支持隨機讀取,在一些場景下耗時比較高。服務器
BufferedReader
方式會從頭開始掃描文件,直到找到上次文件讀取的位置,在繼續消費日誌。而查找文件某個位置的時間複雜度爲O(n)
,這樣若是文件很大(超過1GB),且重啓操做比較頻繁,那麼程序會消耗不少無用的操做在掃描日誌上,從而增長日誌處理的耗時。基於上述BufferedReader
樸素方式的缺點,咱們但願實現隨機讀取日誌的方式。所以咱們考慮使用 RandomAccessFile
類,經過它的 readLine()
方法來讀取每一行日誌。網絡
一樣,要實現高可靠的文件傳輸的功能,也須要定時保存文件的當前讀取位置。 實現代碼以下:app
RandomAccessFile raf = new RandomAccessFile(file, "r"); raf.seek(position); // 定位到文件的讀取位置 while ((line = raf.readLine()) != null) { process(line); // 處理每一行的內容 }
RandomAccessFile
方式的優勢是:支持隨機讀取,讀取日誌消耗內存少。dom
O(1)
。RandomAccessFile
方式的缺點是:CPU佔用高、吞吐量低。
RandomAccessFile
隨機讀取方式須要按字節讀取文件,這樣讀取文件的吞吐量會很低。而 BufferedReader
的數據塊緩存機制能提升文件的讀取吞吐量,所以考慮爲 RandomAccessFile
添加緩存。調研發現 MappedByteBuffer
內存映射文件方式提供了緩存機制。
一樣,要實現可靠的文件傳輸的功能,也須要定時保存文件的當前讀取位置。下面代碼展現了核心的讀文件處理流程,考慮到更清晰地展現核心處理流程,去掉了保存文件的當前讀取位置的邏輯。實現代碼以下:
RandomAccessFile raf = new RandomAccessFile(file, "r"); FileChannel channel = raf.getChannel(); MappedByteBuffer out = channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); byte[] buf = new byte[count]; // buf 數組用來存儲每一行 while(out.remaining()>0){ // 解析出每一行 byte by = out.get(); ch =(char)by; switch(ch){ case '\n': flag = true; break; case '\r': flag = true; break; default: buf[j++] = by; break; } // 讀取的字符超過了buf 數組的大小,須要動態擴容 if(flag ==false && j>=count){ count = count + extra; buf = copyOf(buf,count); } // 處理每一行並初始化環境 if(flag==true){ String line = new String(buf, 0, j, StandardCharsets.UTF_8); process(line); // 處理每一行 flag = false; count = extra; buf = new byte[count]; j =0; } }
MappedByteBuffer
內存映射文件方式的優勢:CPU消耗低、吞吐量高、支持隨機讀取。
RandomAccessFile
,支持文件內容的隨機讀取,查找文件讀取位置的時間複雜度爲 O(1)
.MappedByteBuffer
方式內存佔用高,且映射的文件有文件大小限制。
這種方式須要把文件內容所有讀入內存,這樣會消耗服務器的大量內存,內存佔用高。
另外這種方式最大映射的文件大小爲 Integer的最大值,即最大支持映射 2GB 的文件,也就是說只能處理2GB如下的文件,沒法處理超過 2GB 的文件。
MappedByteBuffer
內存映射文件方式,須要把文件內容所有寫入內存,並且沒法應對傳輸文件大小超過2GB大小的場景。因而可知,MappedByteBuffer
方式的核心缺點在於內存佔用的問題。
針對上述缺點,筆者設計了一種 ByteBuffer
數據塊緩存方式的解決方案:申請一個數據塊緩存,把文件相應大小的內容裝入緩存,該數據塊的緩存被消費完後,在往數據塊緩存裝入下一部分的文件內容,而後繼續消費數據塊緩存中的數據;如此循環,直到把文件內容所有讀完爲止。
一樣,要實現可靠的文件傳輸的功能,也須要定時保存文件的當前讀取位置。具體實現代碼以下:
RandomAccessFile raf = new RandomAccessFile(filePath, "r"); FileChannel fc = raf.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(bufferSize); // 讀取一批日誌申請的字節緩存空間大小 ByteBuffer lineBuffer = ByteBuffer.allocate(lineBufferSize); //每行日誌申請的字節緩存空間大小 int bytesRead = fc.read(buffer); while (bytesRead != -1 && !fileReaderClosed.get()) { currPos = fc.position() - bytesRead; buffer.flip(); // 切換爲讀模式 while (buffer.hasRemaining()) { byte b = buffer.get(); currPos++; if (b == '\n' || b == '\r') { sendLine(lineBuffer); // 處理日誌 } else { // 若空間不夠則擴容 if (!lineBuffer.hasRemaining()) { lineBuffer = reAllocate(lineBuffer); } lineBuffer.put(b); } } buffer.clear(); // 清除緩存 bytesRead = fc.read(buffer); // 寫入緩存 }
ByteBuffer
數據塊緩存方式的優勢是:支持隨機讀取,CPU消耗少,內存佔用低,吞吐量高。
RandomAccessFile
作文件掃描,查找指定位置的字符串時間複雜度O(1)
。本文由淺入深地介紹了四種讀取本地文件的方式,並分析了每種方式存在的優缺點。經過對每種方式存在的缺點進行探索式改進,最後實現了一種高效的收集本地日誌文件的方案——ByteBuffer
數據塊緩存方式。這四種方式的優缺點對比彙總以下:
吞吐量 | CPU消耗 | 內存佔用 | 時間複雜度(理論值) | |
---|---|---|---|---|
BufferedReader 樸素方式 |
高 | 低 | 低 | O(n) |
RandomAccessFile 隨機讀取方式 |
低 | 高 | 低 | O(1) |
MappedByteBuffer 內存映射文件方式 |
高 | 低 | 高 | O(1) |
ByteBuffer 數據塊緩存方式 |
高 | 低 | 低 | O(1) |
這裏須要說明一點,文中提到的可靠性是指在正常狀況下的操做,如:啓動、中止操做,日誌消費可作到Exactly-once。可是在異常狀況下,如:網絡抖動或服務被強制kill,日誌消費可能會出現少許的日誌重複或丟失現象。服務還有待向高可靠的方向演進。
ByteBuffer
數據塊緩存方式已應用到本部門的開源項目 Databus
的日誌推送端業務中,其具體的實現爲FileSource
,代碼地址:https://github.com/weibodip/databus/blob/master/src/main/java/com/weibo/dip/databus/source/FileSource.java ,有興趣的同窗能夠查閱。