在使用slf4j的logback實現時,使用TimeBasedRollingPolicy根據時間滾動日誌策略並使用RollingFileAppender進行日誌滾動,多進程共用同一個日誌文件時,會出現較多xxxxxx.tmp文件未刪除的情況。
出現tmp文件的條件: 使用TimeBasedRollingPolicy/RollingFileAppender配置,並啓用壓縮,並配置的<file></file>標籤名稱與滾動名稱模板不同(如打印日誌時文件名爲demo.log,歸檔時文件名demo.2019-12-12.log.gz),並且單應用啓動多實例共用一個日誌文件作爲輸出,例如:
<appender name="File"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/demo.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/demo.log.%d{yyyy-MM-dd}.log.gz
</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
多進程將日誌輸出到同一個日誌文件logback是允許的,僅輸出也不會出現問題,但是當歸檔時,多個進程同時歸檔,原文件與目標文件(歸檔文件)名稱不同時,會首先關閉輸出流,然後將原日誌文件rename爲xxx.timestamp.tmp,然後讀取tmp文件輸出到目標歸檔文件,此時如果是多進程,其他進程那一時刻很有可能沒有關閉輸出流,所以tmp文件內容一致再增加,並且當其他進程開始歸檔時也會同樣的流程創建tmp文件,然是創建tmp文件後,後續判斷歸檔文件已存在,直接返回了,導致tmp文件未被刪除。
解決方法:
1. 不壓縮(但是多進程也存在問題,日誌輸出混亂,某個時間點的日誌可能出現在上一個時間點日誌文件內)
2. 刪除<file>xxx</file>標籤,此時產生的日誌文件名與歸檔文件名相同(歸檔文件後綴.gz/zip),不需要創建臨時文件,直接壓縮原文件,壓縮完畢會刪除原文件(可能會丟日誌,因爲其他進程還在往裏面寫)
3. 多進程配置不同的logback配置文件,日誌分開存儲
4. appender標籤啓用<prudent>true</prudent>, logback允許多jvm使用同一個log日誌,啓用該標誌會加鎖,在日誌輸出每秒<100條時性能影響不大,但是不使用該功能要比使用該功能日誌性能高3倍左右。侷限性就是不能日誌使用壓縮功能,不能使用<file>標籤指定日誌文件名,詳細參考:prudent prudentWithRolling
源碼解析如下:
//RollingFileAppender
public void rollover() {
//加鎖,統一進程同一時刻只會有一個歸檔操作
lock.lock();
try {
//關閉日誌輸出流
this.closeOutputStream();
//歸檔,刪除過期文件(如保留30天內,則超過30天的文件被刪除)
attemptRollover();
//重新創建或打開日誌文件,並設置輸出流
attemptOpenFile();
} finally {
lock.unlock();
}
}
private void attemptRollover() {
try {
//調用滾動策略滾動歸檔日誌
rollingPolicy.rollover();
} catch (RolloverFailure rf) {
addWarn("RolloverFailure occurred. Deferring roll-over.");
// we failed to roll-over, let us not truncate and risk data loss
this.append = true;
}
}
//TimeBasedRollingPolicy 1.1.7
public void rollover() throws RolloverFailure {
//該方法被執行時,會認爲日誌文件爲已關閉
String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
//壓縮模式,FileNamePattern標籤對應的文件後綴,.gz .zip,否則不壓縮
if (compressionMode == CompressionMode.NONE) {
//獲取file標籤是否配置,如果配置了,則將原文件重命名爲歸檔文件
if (getParentsRawFileProperty() != null) {
renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
} // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty == null }
} else {
//file標籤沒有配置,直接將原文件壓縮爲目標歸檔文件
if (getParentsRawFileProperty() == null) {
compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
} else {
//配置了file標籤則需要先重命名爲tmp,然後讀取tmp輸出到歸檔壓縮文件
compressionFuture = renamedRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
}
}
//刪除過期文件
if (archiveRemover != null) {
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
}
//重命名並壓縮歸檔
Future<?> renamedRawAndAsyncCompress(String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
String parentsRawFile = getParentsRawFileProperty();
//tmp文件名
String tmpTarget = parentsRawFile + System.nanoTime() + ".tmp";
//重命名
renameUtil.rename(parentsRawFile, tmpTarget);
//異步壓縮
return compressor.asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName);
}
//Compressor 異步壓縮
public Future<?> asyncCompress(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
//參數:原文件,歸檔文件
CompressionRunnable runnable = new CompressionRunnable(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
ExecutorService executorService = context.getExecutorService();
//提交線程池
Future<?> future = executorService.submit(runnable);
return future;
}
// 壓縮
public void compress(String nameOfFile2Compress, String nameOfCompressedFile, String innerEntryName) {
//gz zip壓縮
switch (compressionMode) {
case GZ:
gzCompress(nameOfFile2Compress, nameOfCompressedFile);
break;
case ZIP:
zipCompress(nameOfFile2Compress, nameOfCompressedFile, innerEntryName);
break;
case NONE:
throw new UnsupportedOperationException("compress method called in NONE compression mode");
}
}
private void gzCompress(String nameOfFile2gz, String nameOfgzedFile) {
File file2gz = new File(nameOfFile2gz);
//原文件不存在直接返回,注意此時tmp文件沒有被刪除
if (!file2gz.exists()) {
addStatus(new WarnStatus("The file to compress named [" + nameOfFile2gz + "] does not exist.", this));
return;
}
//如果沒有gz後綴,則加個後綴
if (!nameOfgzedFile.endsWith(".gz")) {
nameOfgzedFile = nameOfgzedFile + ".gz";
}
//歸檔文件
File gzedFile = new File(nameOfgzedFile);
//歸檔文件是否存在,已存在直接返回,注意此時tmp文件沒有被刪除
if (gzedFile.exists()) {
addWarn("The target compressed file named [" + nameOfgzedFile + "] exist already. Aborting file compression.");
return;
}
addInfo("GZ compressing [" + file2gz + "] as [" + gzedFile + "]");
createMissingTargetDirsIfNecessary(gzedFile);
BufferedInputStream bis = null;
GZIPOutputStream gzos = null;
//讀取tmp文件輸出到歸檔文件
try {
bis = new BufferedInputStream(new FileInputStream(nameOfFile2gz));
gzos = new GZIPOutputStream(new FileOutputStream(nameOfgzedFile));
byte[] inbuf = new byte[BUFFER_SIZE];
int n;
while ((n = bis.read(inbuf)) != -1) {
gzos.write(inbuf, 0, n);
}
bis.close();
bis = null;
gzos.close();
gzos = null;
//刪除臨時文件,這個地方有個問題如果上面拋異常了,tmp文件依舊刪不掉
//1.3.0版本該部分移到了try-catch後面
if (!file2gz.delete()) {
addStatus(new WarnStatus("Could not delete [" + nameOfFile2gz + "].", this));
}
} catch (Exception e) {
addStatus(new ErrorStatus("Error occurred while compressing [" + nameOfFile2gz + "] into [" + nameOfgzedFile + "].", this, e));
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
// ignore
}
}
if (gzos != null) {
try {
gzos.close();
} catch (IOException e) {
// ignore
}
}
}
}