logback 多實例 歸檔問題 無法自動刪除.tmp文件問題

在使用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
                }
            }
        }
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章