前言
近日工作中发现应用容器的磁盘空间打满,执行完日志清理后磁盘空间依然无法释放。怀疑有文件只是释放了链接,但是没有实际删除。排查后发现,应用容器上有个日志订阅工具,导致log4j日志文件没有办法真正执行删除,解除订阅之后,空间得到释放。
通过出现的现象可以猜测,在写日志达到触发归档条件时会对日志文件执行了删除操作。由此对log4j2日志归档的源码产生了兴趣,所以阅读源码,写下此笔记。
RollingFileAppender
RollingFileAppender 负责写入日志到指定文件,并根据TriggeringPolicy和RolloverPolicy滚动文件。所以我们从它的源码入手。关键代码如下:
//Writes the log entry rolling over the file when required.
@Override
public void append(final LogEvent event) {
//我的注释:检查并触发Rollover
getManager().checkRollover(event);
super.append(event);
}
可以看出,在append()执行第一步就是检查是否需要rollover。这个行为是通过RollingFileManager执行的。下面让我们看下它的代码。
RollingFileManager
/**
* Determines if a rollover should occur.
* @param event The LogEvent.
*/
public synchronized void checkRollover(final LogEvent event) {
//我的注释:符合条件则执行rollover
if (triggeringPolicy.isTriggeringEvent(event)) {
rollover();
}
}
符合条件,则执行rollover。至于如何判断的细节我们暂不关注,目前只需要了解这取决于我们配置的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()总结一下:
- 获取锁
- 获取RolloverDescription。descriptor中包含了一系列rollover需要执行的行为。它是通过strategy.rollover创建的。这里的strategy我们以DefaultRolloverStrategy的实现为例往下分析。
- 写页脚,writeFooter()。
- 关闭写入流,closeOutputStream()。
- 执行descriptor中的一系列操作。
- 返回并释放锁。
所以关键就在于strategy.rollover()定义了那些行为。似乎离真相越来越近了,让我们继续。
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 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;
//我的注释:获取归档压缩文件扩展名类的实例,并依据不同的压缩文件类型创建压缩行为。
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);
}
//我的注释:创建文件重命名行为
final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
manager.isRenameEmptyFiles());
//我的注释:合并压缩行为和文件重命名行为,创建RolloverDescription并返回
final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
}
总结一下:
- 计算归档文件的文件名。
- 根据归档文件扩展类型,创建压缩行为 fileExtension.createCompressAction()。FileExtension是一组枚举,包含支持的文件压缩类型及压缩行为。
- 判断是否需要 AttributeView,并添加posixAttributeViewAction。这里我们不关心,也就不问了。
- 创建文件重命名行为renameAction。FileRenameAction.excute()代码在这就不贴了,就是复制文件到指定位置。
- 合并所有归档需要的行为,并创建RolloverDescription实例,最后返回。
总结
至此,log4j2 的归档实现主要流程,我们大致了解了。文字描述一下就是:
- 停止写入原日志文件。
- 复制源文件到指定位置。
- 执行压缩。
- 创建新的日志文件,继续写入。
参考:
Log4j – Log4j 2 Appenders
log4j2 版本:log4j-core-2.9.1