前言
近日工作中發(fā)現(xiàn)應用容器的磁盤空間打滿,執(zhí)行完日志清理后磁盤空間依然無法釋放。懷疑有文件只是釋放了鏈接,但是沒有實際刪除。排查后發(fā)現(xiàn),應用容器上有個日志訂閱工具,導致log4j日志文件沒有辦法真正執(zhí)行刪除,解除訂閱之后,空間得到釋放。
通過出現(xiàn)的現(xiàn)象可以猜測,在寫日志達到觸發(fā)歸檔條件時會對日志文件執(zhí)行了刪除操作。由此對log4j2日志歸檔的源碼產(chǎn)生了興趣,所以閱讀源碼,寫下此筆記。
RollingFileAppender
RollingFileAppender 負責寫入日志到指定文件,并根據(jù)TriggeringPolicy和RolloverPolicy滾動文件。所以我們從它的源碼入手。關鍵代碼如下:
//Writes the log entry rolling over the file when required.
@Override
public void append(final LogEvent event) {
//我的注釋:檢查并觸發(fā)Rollover
getManager().checkRollover(event);
super.append(event);
}
可以看出,在append()執(zhí)行第一步就是檢查是否需要rollover。這個行為是通過RollingFileManager執(zhí)行的。下面讓我們看下它的代碼。
RollingFileManager
/**
* Determines if a rollover should occur.
* @param event The LogEvent.
*/
public synchronized void checkRollover(final LogEvent event) {
//我的注釋:符合條件則執(zhí)行rollover
if (triggeringPolicy.isTriggeringEvent(event)) {
rollover();
}
}
符合條件,則執(zhí)行rollover。至于如何判斷的細節(jié)我們暫不關注,目前只需要了解這取決于我們配置的TriggeringPolicies。其中常見的就是:TimeBasedTriggeringPolicy 和 SizeBasedTriggeringPolicy。
下面讓我進入 rollover():
public synchronized void rollover() {
if (!hasOutputStream()) {
return;
}
if (rollover(rolloverStrategy)) {
try {
size = 0;
initialTime = System.currentTimeMillis();
createFileAfterRollover();
} catch (final IOException e) {
logError("Failed to create file after rollover", e);
}
}
}
private boolean rollover(final RolloverStrategy strategy) {
boolean releaseRequired = false;
try {
// Block until the asynchronous operation is completed.
semaphore.acquire();
releaseRequired = true;
} catch (final InterruptedException e) {
logError("Thread interrupted while attempting to check rollover", e);
return false;
}
boolean success = true;
try {
//我的注釋:
final RolloverDescription descriptor = strategy.rollover(this);
if (descriptor != null) {
writeFooter();
closeOutputStream();
if (descriptor.getSynchronous() != null) {
LOGGER.debug("RollingFileManager executing synchronous {}", descriptor.getSynchronous());
try {
success = descriptor.getSynchronous().execute();
} catch (final Exception ex) {
success = false;
logError("Caught error in synchronous task", ex);
}
}
if (success && descriptor.getAsynchronous() != null) {
LOGGER.debug("RollingFileManager executing async {}", descriptor.getAsynchronous());
asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(), this));
releaseRequired = false;
}
return true;
}
return false;
} finally {
if (releaseRequired) {
semaphore.release();
}
}
}
rollover()總結(jié)一下:
- 獲取鎖
- 獲取RolloverDescription。descriptor中包含了一系列rollover需要執(zhí)行的行為。它是通過strategy.rollover創(chuàng)建的。這里的strategy我們以DefaultRolloverStrategy的實現(xiàn)為例往下分析。
- 寫頁腳,writeFooter()。
- 關閉寫入流,closeOutputStream()。
- 執(zhí)行descriptor中的一系列操作。
- 返回并釋放鎖。
所以關鍵就在于strategy.rollover()定義了那些行為。似乎離真相越來越近了,讓我們繼續(xù)。
DefaultRolloverStrategy
注意:這里我們使用DefaultRolloverStrategy 來分析,這也是默認和最常用的。
直接上代碼,礦就都在這塊了 :
/**
* Performs the rollover.
*
* @param manager The RollingFileManager name for current active log file.
* @return A RolloverDescription.
* @throws SecurityException if an error occurs.
*/
@Override
public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
//我的注釋:算fileIndex,為歸檔文件命名作準備。
int fileIndex;
if (minIndex == Integer.MIN_VALUE) {
final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
} else {
if (maxIndex < 0) {
return null;
}
final long startNanos = System.nanoTime();
fileIndex = purge(minIndex, maxIndex, manager);
if (fileIndex < 0) {
return null;
}
if (LOGGER.isTraceEnabled()) {
final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
}
}
//我的注釋:拼裝歸檔文件名
final StringBuilder buf = new StringBuilder(255);
manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
final String currentFileName = manager.getFileName();
String renameTo = buf.toString();
final String compressedName = renameTo;
Action compressAction = null;
//我的注釋:獲取歸檔壓縮文件擴展名類的實例,并依據(jù)不同的壓縮文件類型創(chuàng)建壓縮行為。
final FileExtension fileExtension = manager.getFileExtension();
if (fileExtension != null) {
final File renameToFile = new File(renameTo);
renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
if (tempCompressedFilePattern != null) {
buf.delete(0, buf.length());
tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
final String tmpCompressedName = buf.toString();
final File tmpCompressedNameFile = new File(tmpCompressedName);
final File parentFile = tmpCompressedNameFile.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
compressAction = new CompositeAction(
Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName,
true, compressionLevel),
new FileRenameAction(tmpCompressedNameFile,
renameToFile, true)),
true);
} else {
compressAction = fileExtension.createCompressAction(renameTo, compressedName,
true, compressionLevel);
}
}
if (currentFileName.equals(renameTo)) {
LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
return new RolloverDescriptionImpl(currentFileName, false, null, null);
}
if (compressAction != null && manager.isAttributeViewEnabled()) {
// Propagate posix attribute view to compressed file
// @formatter:off
final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
.withBasePath(compressedName)
.withFollowLinks(false)
.withMaxDepth(1)
.withPathConditions(new PathCondition[0])
.withSubst(getStrSubstitutor())
.withFilePermissions(manager.getFilePermissions())
.withFileOwner(manager.getFileOwner())
.withFileGroup(manager.getFileGroup())
.build();
// @formatter:on
compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
}
//我的注釋:創(chuàng)建文件重命名行為
final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
manager.isRenameEmptyFiles());
//我的注釋:合并壓縮行為和文件重命名行為,創(chuàng)建RolloverDescription并返回
final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
}
總結(jié)一下:
- 計算歸檔文件的文件名。
- 根據(jù)歸檔文件擴展類型,創(chuàng)建壓縮行為 fileExtension.createCompressAction()。FileExtension是一組枚舉,包含支持的文件壓縮類型及壓縮行為。
- 判斷是否需要 AttributeView,并添加posixAttributeViewAction。這里我們不關心,也就不問了。
- 創(chuàng)建文件重命名行為renameAction。FileRenameAction.excute()代碼在這就不貼了,就是復制文件到指定位置。
- 合并所有歸檔需要的行為,并創(chuàng)建RolloverDescription實例,最后返回。
總結(jié)
至此,log4j2 的歸檔實現(xiàn)主要流程,我們大致了解了。文字描述一下就是:
- 停止寫入原日志文件。
- 復制源文件到指定位置。
- 執(zhí)行壓縮。
- 創(chuàng)建新的日志文件,繼續(xù)寫入。
參考:
Log4j – Log4j 2 Appenders
log4j2 版本:log4j-core-2.9.1