問題背景
我的日志路徑是 /opt/logs/xxxx.log
,集群中共有兩台服務器:
- 一台服務器的日志文件最新更新時間 是今天,
- 另一台服務器的日志文件最新更新時間 卻是昨天甚至更久之前。
換言之,一台服務器從過去某個時間點開始,日志就不能正常打印了。另外,交代一下,這個項目不是我從零開始搭建的,是我接手維護的項目。我使用的日志框架是 slf4j + logback。
一開始,我在百度上找的解決方案:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
<deployment>
<!-- Exclusions allow you to prevent the server from automatically adding some dependencies -->
<exclusions>
<module name="org.slf4j" />
<module name="org.slf4j.impl" />
<!-- 如果項目中用到jcl-over-slf4j,可以將下面這個module也斃了 -->
<module name="org.slf4j.jcl-over-slf4j" />
</exclusions>
</deployment>
</jboss-deployment-structure>
我看了一下我的項目中:
jcl-over-slf4j jul-to-slf4j log4j-over-slf4j slf4j-api 都有,於是我試了一下
遺憾的是,問題沒能解決,只好繼續分析,我也猜不到原因,又看不到報錯,所以就想着能不能在源碼里找到一些對解決問題有幫助的異常。
源碼分析
例如,我們有一個簡單的 UserService 的例子,調用了 log.info 方法:
package service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger log = LoggerFactory.getLogger("service.UserService");
public String getPhoneByUsername(String username) {
String result = "12345678901";
log.info("{}'s phone number is {}", username, result);
return result;
}
}
logback 中實現 org.slf4j.Logger 接口的實例是類 ch.qos.logback.classic.Logger
。我們發現它的 trace/debug/info/warn/error 方法,都會調用私有方法 buildLoggingEventAndAppend :
private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
// 創建一個 LoggingEvent 對象
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
le.setMarker(marker);
// 調用 Appenders 來處理 LoggingEvent
callAppenders(le);
}
接下來,來看 callAppenders 的源碼:
/**
* Invoke all the appenders of this logger.
*
* @param event
* The event to log
*/
public void callAppenders(ILoggingEvent event) {
int writes = 0;
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// No appenders in hierarchy
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}
我們來看這個循環 for (Logger l = this; l != null; l = l.parent) { }
,這里從當前 Logger 向上查找父 Logger,拿 service.userService
舉例來說:
這個樹狀的 Logger 結構,是在 LoggerFactory.getLogger("service.UserService")
時創建的。在 logback 中,類 ch.qos.logback.classic.LoggerContext#getLogger
承擔了該任務。
ch.qos.logback.classic.Logger
的代碼繼續往下挖,到了 appendLoopOnAppenders
方法,這個方法能否繼續執行,關鍵就是 aai
是否為 null ?
transient private AppenderAttachableImpl<ILoggingEvent> aai;
private int appendLoopOnAppenders(ILoggingEvent event) {
if (aai != null) {
return aai.appendLoopOnAppenders(event);
} else {
return 0;
}
}
依然是 ch.qos.logback.classic.Logger
的源碼,aai
是在 addAppender 的時候創建的,如果Logger沒有一個appender,aai就是null:
public synchronized void addAppender(Appender<ILoggingEvent> newAppender) {
if (aai == null) {
aai = new AppenderAttachableImpl<ILoggingEvent>();
}
aai.addAppender(newAppender);
}
來看一下我的配置,位於 resources 文件夾下的 logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 這里我粘貼的是官方的配置,沒有粘貼我排查出問題的配置 -->
<file>logFile.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- keep 30 days' worth of history capped at 3GB total size -->
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
</root>
</configuration>
根據上面的 xml 配置,解析得到的 root logger 如下圖所示:
按照我個人的理解:<logger>
標簽支持 <append-ref>
子標簽,如果它包含至少一個 <append-ref>
,那么該 Logger 就有 aai 屬性。
另外,ch.qos.logback.classic.AsyncAppender
自帶 aai
,則是因為它的父類 ch.qos.logback.core.AsyncAppenderBase
有 aai
:
public class AsyncAppenderBase<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E> {
AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>();
}
再來看繼承體系:
我們常用的 ConsoleAppender
, FileAppender
, RollingFileAppender
這些是沒有 aai
屬性的!
現在,我們回到 ch.qos.logback.core.spi.AppenderAttachableImpl
源碼中的 appendLoopOnAppenders
方法,它調用了 doAppend
/**
* Call the <code>doAppend</code> method on all attached appenders.
*/
public int appendLoopOnAppenders(E e) {
int size = 0;
for (Appender<E> appender : appenderList) {
appender.doAppend(e);
size++;
}
return size;
}
調試關鍵點
AsyncAppender
如果想弄清楚 AsyncAppender 的異步工作過程,建議斷點打在兩處:
1、ch.qos.logback.core.AsyncAppenderBase
第 156 行,put 方法處:
此時調用堆棧如下圖所示:
2、ch.qos.logback.core.AsyncAppenderBase
第 265 行,內部類 Worker
的 run
方法內的循環處:
內部類
Worker
工作的線程名是以AsyncAppender-Worker-
為前綴的。
RollingFileAppender
現在,我們把斷點放在 ch.qos.logback.core.rolling.RollingFileAppender
的 222 行,看一下 subAppend
的執行:
/**
* This method differentiates RollingFileAppender from its super class.
*/
@Override
protected void subAppend(E event) {
// The roll-over check must precede actual writing. This is the
// only correct behavior for time driven triggers.
// We need to synchronize on triggeringPolicy so that only one rollover
// occurs at a time
synchronized (triggeringPolicy) {
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
rollover();
}
}
// 調用父類 OutputStreamAppender 的 subAppend 方法,這個父類方法是實際發生寫入的方法,如果不執行就沒法正常寫入日志!
super.subAppend(event);
}
下圖是我運行到 subAppend 時的調用棧信息:
我遇到的問題就在這個 rollover
我先貼一下,我有問題的 logback.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logFile.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<FileNamePattern>biz-%d{yyyy-MM-dd_HH}.%i.txt</FileNamePattern>
<MinIndex>1</MinIndex>
<!-- 最多保留20份歷史日志 -->
<MaxIndex>20</MaxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<!-- 每個文件最大1KB,這里是為了測試才改成 1KB 的 -->
<MaxFileSize>1KB</MaxFileSize>
</triggeringPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] %logger{36} %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
</root>
</configuration>
然后,我把打印日志的方法 UserService#getPhoneByUsername
循環執行個 200 次:
for (int i = 0; i < 100; i++) {
String result = getPhoneByUsername("Mi");
}
ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy
的 isTriggeringEvent 方法返回了 true,這就和我那台不打印日志的服務器的情況一樣了:
public boolean isTriggeringEvent(final File activeFile, final E event) {
long now = System.currentTimeMillis();
if (invocationGate.isTooSoon(now))
return false;
// 當日志文件大小超過了設置的大小(默認是 10MB,我這里改成了 1KB),則返回true
return (activeFile.length() >= maxFileSize.getSize());
}
斷點可以設置在 rollover
這個方法所在行。接下來,當文件大小超過指定大小,調用 rollback 時出現了異常,這個異常被 ch.qos.logback.core.UnsynchronizedAppenderBase
的 doAppend 方法捕獲了:
然后,真正寫入日志的方法沒有被執行,即 ch.qos.logback.core.OutputStreamAppender
的 subAppend
方法沒有被執行:
/**
* Actual writing occurs here.
* <p>
* Most subclasses of <code>WriterAppender</code> will need to override this
* method.
*
* @since 0.9.0
*/
protected void subAppend(E event) {
if (!isStarted()) {
return;
}
try {
// this step avoids LBCLASSIC-139
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
// the synchronization prevents the OutputStream from being closed while we
// are writing. It also prevents multiple threads from entering the same
// converter. Converters assume that they are in a synchronized block.
lock.lock();
try {
writeOut(event);
} finally {
lock.unlock();
}
} catch (IOException ioe) {
// as soon as an exception occurs, move to non-started state
// and add a single ErrorStatus to the SM.
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}
然后,我們可以看一下發生異常的棧信息:
我選擇從 ch.qos.logback.core.rolling.FixedWindowRollingPolicy
繼續細化分析問題:
這行還有 fileNamePattern 這個變量名,讓我聯想到了 xml 中 的 <FileNamePattern>
標簽。
模擬出錯的地方FileNamePattern
然后我就專門寫了一段代碼來模擬 FileNamePattern 的行為:
FileNamePattern pattern = new FileNamePattern("biz-%d{yyyy-MM-dd_HH}.%i.txt", new LoggerContext());
String s = pattern.convertInt(1);
調試參考點
斷點打在 ch.qos.logback.core.rolling.helper.FileNamePattern
第 65 行,看 parse 方法第 64 行的結果:
如圖所示,logback 中自制的分詞器將 biz-%d{yyyy-MM-dd_HH}.%i.txt
解析成了 tokenList。
接下來代碼執行到第 67 行,看一下 Node 的結果:
%d{yyyy-MM-dd_HH}
和 %i
被解析成了關鍵字,其他則保持字面值。
關鍵字 d 和 i 對應不同的轉換器:
static final Map<String, String> CONVERTER_MAP = new HashMap<String, String>();
static {
// i 對應 IntegerTokenConverter
CONVERTER_MAP.put(IntegerTokenConverter.CONVERTER_KEY, IntegerTokenConverter.class.getName());
// d 對應 DateTokenConverter
CONVERTER_MAP.put(DateTokenConverter.CONVERTER_KEY, DateTokenConverter.class.getName());
}
又因為 FixedWindowRollingPolicy
產生的參數值只有 Integer 型的,所以遇到 %d{yyyy-MM-dd_HH}
就會出錯。
至此“結案”了!
解決方案
這個可以求教官方文檔 閱讀,解決 FileNamePattern 和 rollingPolicy 不匹配的問題。
FixedWindowRollingPolicy
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logFile.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<!-- 修改的部分 Begin-->
<FileNamePattern>biz.%i.txt</FileNamePattern>
<!-- 修改的部分 END -->
<MinIndex>1</MinIndex>
<!-- 最多保留20份歷史日志 -->
<MaxIndex>20</MaxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<!-- 每個文件最大1KB,這里是為了測試才改成 1KB 的 -->
<MaxFileSize>1KB</MaxFileSize>
</triggeringPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] %logger{36} %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
</root>
</configuration>
SizeAndTimeBasedRollingPolicy
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logFile.log</file>
<!-- 修改的部分 Begin-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>biz-%d{yyyy-MM-dd_HH}.%i.txt</fileNamePattern>
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<!-- 修改的部分 END -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] %logger{36} %msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
</root>
</configuration>
總結
我的一台服務器的日志文件 logFile.log 小於設置的 MaxFileSize(50MB),因此可以正常工作;
另一台服務器的日志文件大小大於 MaxFileSize,在執行 rollback 方法時出錯,導致無法繼續正常寫入日志。
而導致 rollback 不能正常工作的原因是 FileNamePattern 和 rollingPolicy 不匹配。
參考文檔
logback Manual -- Chapter 4: Appenders 閱讀
jboss logback slf4j 日志文件不生成問題 閱讀
logback性能優化及源碼分析 閱讀