背景:
線上出現復現率很小的問題,log4j2帶SizeBasedTriggeringPolicy和CronTriggeringPolicy兩種策略的自定義log,出現覆蓋現象
經分析,不是觸發size 2MB的問題,是cron發生的問題
log文件 分析
最重要的是獲取
1.被壓縮文件的創建時間
2.zip文件的lastmodify時間
因爲是用創建時間+5分來命名zip文件的 ★
源碼分析
每次log.info 最後都會調入RollingFileAppender 就是log.info都會判斷是否要rollover,crontrigger是到時間了判斷,size是每打印一句就會判斷
public void append(LogEvent event) {
((RollingFileManager)this.getManager()).checkRollover(event);
super.append(event);
}
然後經過這一步根據上面的兩個策略判斷是否需要rollover
this.triggeringPolicy = {CompositeTriggeringPolicy@4082}
"CompositeTriggeringPolicy(policies=[SizeBasedTriggeringPolicy(size=1024), CronTriggeringPolicy(schedule=0 0/1 * * * ?)])"
triggeringPolicies = {TriggeringPolicy[2]@4312}
0 = {SizeBasedTriggeringPolicy@4316} "SizeBasedTriggeringPolicy(size=1024)"
1 = {CronTriggeringPolicy@4317} "CronTriggeringPolicy(schedule=0 0/1 * * * ?)"
state = {LifeCycle$State@4119} "STARTED"
name = "STARTED"
ordinal = 3
public synchronized void checkRollover(LogEvent event) { //event 將所有的log的信息都封裝進去
if (this.triggeringPolicy.isTriggeringEvent(event)) {
this.rollover();
}
}
在這裏的判斷中 cronPolicy直接返回false,因爲cron是直接在異步線程池裏操作的
public boolean isTriggeringEvent(LogEvent event) {
return false;
}
/*
sizebasedPolicy 會根據文件大小來判斷是否要更新manager的時間 然後在rollover()//這個策略沒有觸發,不用管
public boolean isTriggeringEvent(LogEvent event) {
boolean triggered = this.manager.getFileSize() > this.maxFileSize;
if (triggered) {
this.manager.getPatternProcessor().updateTime();
}
return triggered;
}
*/
然後執行manager的rollover()邏輯 ★這裏是主要邏輯
public synchronized void rollover() {
if (this.hasOutputStream()) {
if (this.rollover(this.rolloverStrategy)) { ★這裏move()
try {
this.size = 0L;
this.initialTime = System.currentTimeMillis();
this.createFileAfterRollover(); ★這裏create()
} catch (IOException var2) {
this.logError("Failed to create file after rollover", var2);
}
}
}
}
//這裏封裝了一個帶倆action的description rename和zipAction 都是cron衍生出來的
public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
rename:
writeFooter(); 不知道寫的啥,暫時先不管...
success = descriptor.getSynchronous().execute();//同步的rename 直接改 aaa(文件名) move到對應的位置
zip:
if (success && descriptor.getAsynchronous() != null) {//異步的zip coreSize爲0 maxeSize爲INTEGER_MAX的線程池,執行將上步remove過來的文件zip的操作
LOGGER.debug("RollingFileManager executing async {}", descriptor.getAsynchronous());
asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(), this));
releaseRequired = false;
}
aaalog是源文件
aaa是生成的zip文件和中間文件
synchronous = {FileRenameAction@16811} "FileRenameAction[D:\app1\files3\aaalog.txt to D:\app1\files3\20191102\aaa, renameEmptyFiles=false]"
asynchronous = {ZipCompressAction@16179} "ZipCompressAction[D:\app1\files3\20191102\aaa to D:\app1\files3\20191102\aaa.zip, level=9, deleteSource=true]"
source = {File@19877} "D:\app1\files3\aaalog.txt"
destination = {File@19878} "D:\app1\files3\20191102\aaa"
重命名其實就是move
public static boolean execute(final File source, final File destination, final boolean renameEmptyFiles) {
Files.move(Paths.get(source.getAbsolutePath()), Paths.get(destination.getAbsolutePath()),//★這裏是真實的move操作
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
LOGGER.trace("Renamed file {} to {} with Files.move", source.getAbsolutePath(),
destination.getAbsolutePath());
return true;
try {
source.delete(); //定時跑的時候,如果aaalog爲空,就直接刪除
} catch (final Exception exDelete) {
LOGGER.error("Unable to delete empty file {}: {} {}", source.getAbsolutePath(),
exDelete.getClass().getName(), exDelete.getMessage());
}
進入cron
RollingFileManager.initialize();
裏面有初始化cron線程池的操作
public void initialize(final RollingFileManager aManager) {
this.manager = aManager;
final Date now = new Date();
final Date lastRollForFile = cronExpression.getPrevFireTime(new Date(this.manager.getFileTime()));
final Date lastRegularRoll = cronExpression.getPrevFireTime(new Date());
aManager.getPatternProcessor().setCurrentFileTime(lastRegularRoll.getTime());
LOGGER.debug("LastRollForFile {}, LastRegularRole {}", lastRollForFile, lastRegularRoll);
aManager.getPatternProcessor().setPrevFileTime(lastRegularRoll.getTime());
aManager.getPatternProcessor().setTimeBased(true);
if (checkOnStartup && lastRollForFile != null && lastRegularRoll != null &&
lastRollForFile.before(lastRegularRoll)) {
lastRollDate = lastRollForFile;
rollover();
}
final ConfigurationScheduler scheduler = configuration.getScheduler();
if (!scheduler.isExecutorServiceSet()) {
// make sure we have a thread pool
scheduler.incrementScheduledItems();
}
if (!scheduler.isStarted()) {
scheduler.start();
}
lastRollDate = lastRegularRoll;
future = scheduler.scheduleWithCron(cronExpression, now, new CronTrigger());
LOGGER.debug(scheduler.toString());
}
crontriggerstage 會在初始化的時候 執行這一句
future = scheduler.scheduleWithCron(cronExpression, now, new CronTrigger());
每次都會跑 跑的就是
private void rollover() {
manager.rollover(cronExpression.getPrevFireTime(new Date()), lastRollDate);
if (future != null) {
lastRollDate = future.getFireTime();
}
}
核心代碼 ★ 爲啥跟上面一樣呢?因爲有個size策略 所以每次log.info都會執行,又有個cron策略,所以線程池也會執行
if (rollover(rolloverStrategy)) { 這裏有個同步的action和異步的action
try {
size = 0;
initialTime = System.currentTimeMillis();
createFileAfterRollover(); //★這裏直接同步創建新的源文件
} catch (final IOException e) {
logError("Failed to create file after rollover", e);
}
}
//★ 同上,這裏創建文件
@Override
protected OutputStream createOutputStream() throws IOException {
final String filename = getFileName();
LOGGER.debug("Now writing to {} at {}", filename, new Date());
final File file = new File(filename);
final FileOutputStream fos = new FileOutputStream(file, isAppend);
if (file.exists() && file.length() == 0) {
try {
// 而且重命名的時間和當前時間一致 把源文件的創建時間設置成當前時間
FileTime now = FileTime.fromMillis(System.currentTimeMillis());
Files.setAttribute(file.toPath(), "creationTime", now);
} catch (Exception ex) {
LOGGER.warn("Unable to set current file tiem for {}", filename);
}
}
defineAttributeView(Paths.get(filename));
return fos;
}
cronpolicy
manager.rollover(cronExpression.getPrevFireTime(new Date()), lastRollDate);
if (future != null) {
lastRollDate = future.getFireTime();
}
分析文件創建,刪除,zip時間代碼如下
獲取descriptor 裏面倆操作
一個同步的rename 一個異步的zip
rename是直接執行的 下面的代碼 這步驟是同步執行的,將 source move 到指定位置
Files.move(Paths.get(source.getAbsolutePath()), Paths.get(destination.getAbsolutePath()),
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
LOGGER.trace("Renamed file {} to {} with Files.move", source.getAbsolutePath(),
destination.getAbsolutePath());
return true;
source是怎麼重新出現的 因爲是定時任務 所以就是定時的時間 createFileAfterRollover() 創建的就是當時的時間
protected OutputStream createOutputStream() throws IOException {
final String filename = getFileName();
LOGGER.debug("Now writing to {} at {}", filename, new Date());
final File file = new File(filename);
final FileOutputStream fos = new FileOutputStream(file, isAppend);
if (file.exists() && file.length() == 0) {
try {
FileTime now = FileTime.fromMillis(System.currentTimeMillis());
Files.setAttribute(file.toPath(), "creationTime", now);
} catch (Exception ex) {
LOGGER.warn("Unable to set current file tiem for {}", filename);
}
}
defineAttributeView(Paths.get(filename));
return fos;
}
又怎麼被刪除 只要爲空 每次定時任務掃到就會刪掉
又出現 同上
/* This executor pool will create a new Thread for every work async action to be performed. Using it allows
us to make sure all the Threads are completed when the Manager is stopped. */
private final ExecutorService asyncExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.MILLISECONDS,
new EmptyQueue(), threadFactory);
一種合理的解釋:線程池裏新建了15分鐘 的zip任務,由於某種原因卡住了,卡到20才執行完,
此時20的已經創建好,且zip完了,就會覆蓋,至於是哪個覆蓋哪個就不知道了
zip的創建時間
public static boolean execute(final File source, final File destination, final boolean deleteSource,
final int level) throws IOException {
if (source.exists()) {
try (final FileInputStream fis = new FileInputStream(source);
final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(destination))) {
zos.setLevel(level);
final ZipEntry zipEntry = new ZipEntry(source.getName());
zos.putNextEntry(zipEntry);
final byte[] inbuf = new byte[BUF_SIZE];
int n;
while ((n = fis.read(inbuf)) != -1) {//創建時間
zos.write(inbuf, 0, n);
}
}
if (deleteSource && !source.delete()) {
LOGGER.warn("Unable to delete " + source.toString() + '.');
}
return true;
}
return false;
}
總結爲 定時move,同步create 異步 zip(modifytime)
我們的邏輯爲,如果 zip modifytime-createtime>5分 重命名文件+5分 文件本來名就是move createtime 的名
如果<5分,就不重命名
正常情況下
move和createtime 是一致的 都是0/5這樣的分鐘數
modifytime 是異步的zip創建時間
異常情況見圖片吧,概率很低