用Node.js實現檔案迴圈覆寫
這次編寫Node.js專案的時候用到了日誌模組,其中碰到了一個小問題。 這是一個定時執行可配置自動化任務的專案,所以輸出資訊會不斷增加,也就意味著日誌檔案會隨時間不斷增大。 如果對日誌檔案大小不加以控制,那麼伺服器的磁碟遲早會被撐滿。所以限制檔案大小是有必要的。 最理想的控制方式就是當檔案大小超過限制時,清除最先記錄的資料。類似一個FIFO的佇列。
# 刪除前面的資料 - 1 xxx ...... 100 abc # 檔案末尾追加資料 + 101 xxxx 複製程式碼
log4js的file rolling
一提到記錄日誌很多Node.js開發者肯定會找到log4js,先來看看log4js是怎麼處理這個問題的。
log4js分為很多appenders(可以理解為記錄日誌的媒介),file rolling功能可以通過函式來進行配置。
file rolling功能有兩種方式:日期和檔案大小。 要控制檔案大小,當然選擇後者。 為了測試這個功能是否滿足我們要求,寫一段迴圈程式碼來寫日誌。
const log4js = require('log4js') // 配置log4js log4js.configure({ appenders: { everything: { type: 'file', filename: 'a.log', maxLogSize: 1000, backups: 0 }, }, categories: { default: { appenders: ['everything'], level: 'debug' } } }); const log = log4js.getLogger(); for (let i = 0; i < 41; i++) { const str = i.toString().padStart(6, '000000'); log.debug(str); } 複製程式碼
執行之後生成兩個檔案a.log
和a.log.1
。
其中a.log.1
有20行資料,實際大小1kb,a.log
只有1行資料。
雖然確實控制了檔案大小,但是會帶來兩個問題:
- 額外產生一個備份檔案,總佔用磁碟空間會超過檔案限制。
- 日誌檔案內容的大小是變動的,查詢日誌的時候很可能需要聯合備份檔案進行查詢(比如上面的情況日誌檔案只有1行資料)。
推測log4js的實現邏輯可能是下面這樣:
- 檢查日誌檔案是否達到限制大小,如果達到則刪除備份檔案,否則繼續寫入日誌檔案。
- 重新命名日誌檔案為備份檔案。
這顯然不能完全滿足需求。
字串替換?
如果要在記憶體中完成迴圈覆寫操作就比較簡單了,使用字串或Buffer的即可完成。
- 新增字串/Buffer長度,如果超過大小則擷取。
- 寫入並覆蓋日誌檔案。
但是有一個很大的問題:佔用記憶體。
比如限制檔案大小為1GB,有10個日誌檔案同時寫入,那麼至少佔用10GB記憶體空間!
記憶體可是比磁碟空間更寶貴的,如此明顯的效能問題,顯然也不是最優解決方式。
file roll
按照需求可以把實現步驟拆成兩步:
- 追加最新的資料到檔案末尾。(Node.js的fs模組有相應函式)
- 刪除檔案開頭超出限制部分。(Node.js沒有響應函式)
這兩步不分先後順序,但是Node.js沒有提供API來刪除檔案開頭部分,只提供了修改檔案指定位置的函式。
既然無法刪除檔案開頭部分內容,那麼我們就換個思路,只保留檔案末尾部分內容(不超出大小限制)。
什麼?這不是一個意思麼?
略有區別~
刪除是在原有檔案上進行的操作,而保留內容可以藉助臨時檔案來進行操作。
所以思路變成:
- 建立一個臨時檔案,臨時檔案的內容來自於日誌檔案。
- 往臨時檔案中增加資料。
- 將臨時檔案中符合檔案大小限制的內容,從後往前(採取偏移量的形式)進行讀取並複製到日誌檔案進行覆蓋。
- 為了不佔用額外的磁碟空間,寫操作完成後刪除臨時檔案。
這樣就不會出現像log4js一樣日誌檔案內容不全的現象,也不會保留額外的臨時檔案。但是對IO的操作會增加~
對於寫操作可以採取tail
命令來實現,最終實現程式碼如下:
private write(name: string, buf?: Buffer | string) { // append buf to tmp file const tmpName = name.replace(/(.*\/)(.*$)/, '$1_\.$2\.tmp'); if (!existsSync(tmpName)) { copyFileSync(name, tmpName); } buf && appendFileSync(tmpName, buf); // if busy, wait if (this.stream && this.stream.readable) { this.needUpdateLogFile[name] = true; } else { try { execSync(`tail -c ${limit} ${tmpName} > ${name}`); try { if (this.needUpdateLogFile[name]) { this.needUpdateLogFile[name] = false; this.write(name); } else { existsSync(tmpName) && unlinkSync(tmpName); } } catch (e) { console.error(e); } } catch (e) { console.error(e); } } } 複製程式碼