Arthas實踐--抽絲剝繭排查線上應用日誌打滿問題
現象
在應用的 service_stdout.log
裡一直輸出下面的日誌,直接把磁碟打滿了:
23:07:34.441 [TAIRCLIENT-1-thread-1] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 14 times in a row. 23:07:34.460 [TAIRCLIENT-1-thread-3] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 3 times in a row. 23:07:34.461 [TAIRCLIENT-1-thread-4] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 3 times in a row. 23:07:34.462 [TAIRCLIENT-1-thread-5] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 3 times in a row.
service_stdout.log
是程序標準輸出的重定向,可以初步判定是tair外掛把日誌輸出到了stdout裡。
儘管有了初步的判斷,但是具體logger為什麼會打到stdout裡,還需要進一步排查,常見的方法可能是本地debug。
下面介紹利用arthas直接在線上定位問題的過程,主要使用sc
和getstatic
命令。
定位logger的具體實現
日誌是io.netty.channel.nio.NioEventLoop
然後用arthas的sc
命令來檢視具體的io.netty.channel.nio.NioEventLoop
是從哪裡載入的。
class-info io.netty.channel.nio.NioEventLoop
code-source file:/opt/app/plugins/ tair-plugin/lib/netty-all-4.0.35.Final.jar!/
name io.netty.channel.nio.NioEventLoop
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name NioEventLoop
modifier final,public
annotation
interfaces
super-class +-io.netty.channel.SingleThreadEventLoop
+-io.netty.util.concurrent.SingleThreadEventExecutor
+-io.netty.util.concurrent.AbstractScheduledEventExecutor
+-io.netty.util.concurrent.AbstractEventExecutor
+-java.util.concurrent.AbstractExecutorService
+-java.lang.Object
class-loader +-tair-plugin's ModuleClassLoader
classLoaderHash 73ad2d6
可見,的確是從tair外掛里加載的。
檢視NioEventLoop的程式碼,可以發現它有一個logger
的field:
public final class NioEventLoop extends SingleThreadEventLoop {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(NioEventLoop.class);
使用arthas的getstatic
命令來檢視這個logger
具體實現類是什麼(使用-c
引數指定classloader):
$ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'getClass().getName()'
field: logger
@String[io.netty.util.internal.logging.Slf4JLogger]
可以發現是Slf4JLogger
。
再檢視io.netty.util.internal.logging.Slf4JLogger的實現,發現它內部有一個logger的field:
package io.netty.util.internal.logging;
import org.slf4j.Logger;
/**
* <a href="http://www.slf4j.org/">SLF4J</a> logger.
*/
class Slf4JLogger extends AbstractInternalLogger {
private static final long serialVersionUID = 108038972685130825L;
private final transient Logger logger;
那麼使用arthas的getstatic
命令來檢視這個logger
屬性的值:
$ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger'
field: logger
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[io.netty.channel.nio.NioEventLoop],
level=null,
effectiveLevelInt=@Integer[10000],
parent=@Logger[Logger[io.netty.channel.nio]],
childrenList=null,
aai=null,
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]
可見,logger的最本質實現類是:ch.qos.logback.classic.Logger
。
再次用getstatic
命令來確定jar包的location:
$ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger.getClass().getProtectionDomain().getCodeSource().getLocation()'
field: logger
@URL[
BUILTIN_HANDLERS_PREFIX=@String[sun.net.www.protocol],
serialVersionUID=@Long[-7627629688361524110],
protocolPathProp=@String[java.protocol.handler.pkgs],
protocol=@String[jar],
host=@String[],
port=@Integer[-1],
file=@String[file:/opt/app/plugins/tair-plugin/lib/logback-classic-1.2.3.jar!/],
query=null,
authority=@String[],
path=@String[file:/opt/app/plugins/tair-plugin/lib/logback-classic-1.2.3.jar!/],
userInfo=null,
ref=null,
hostAddress=null,
handler=@Handler[com.taobao.pandora.loader.jar.Handler@1a0c361e],
hashCode=@Integer[126346621],
tempState=null,
factory=@TomcatURLStreamHandlerFactory[org.apache.catalina.webresources.TomcatURLStreamHandlerFactory@3edd7b7],
handlers=@Hashtable[isEmpty=false;size=4],
streamHandlerLock=@Object[java.lang.Object@488ccac9],
serialPersistentFields=@ObjectStreamField[][isEmpty=false;size=7],
]
可見這個ch.qos.logback.classic.Logger
的確是tair外掛里加載的。
定位logger的level
上面已經定位logger的實現類是ch.qos.logback.classic.Logger
,但是為什麼它會輸出DEBUG
level的日誌?
其實在上面的getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger'
輸出裡,已經打印出它的level是null了。如果對logger有所瞭解的話,可以知道當child logger的level為null時,它的level取決於parent logger的level。
我們再來看下ch.qos.logback.classic.Logger
的程式碼,它有一個parent logger的屬性:
public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {
/**
* The parent of this category. All categories have at least one ancestor
* which is the root category.
*/
transient private Logger parent;
所以,我們可以通過getstatic
來獲取到這個parent屬性的內容。然後通過多個parent操作,可以發現level都是null,最終發現ROOT level是DEBUG 。
$ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger.parent.parent.parent.parent.parent'
field: logger
@Logger[
serialVersionUID=@Long[5454405123156820674],
FQCN=@String[ch.qos.logback.classic.Logger],
name=@String[ROOT],
level=@Level[DEBUG],
effectiveLevelInt=@Integer[10000],
parent=null,
childrenList=@CopyOnWriteArrayList[isEmpty=false;size=2],
aai=@AppenderAttachableImpl[ch.qos.logback.core.spi.AppenderAttachableImpl@1ecf9bae],
additive=@Boolean[true],
loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],
]
所以 io.netty.channel.nio.NioEventLoop
的logger的level取的是ROOT logger的配置,即預設值DEBUG
。
具體實現可以檢視ch.qos.logback.classic.LoggerContext
public LoggerContext() {
super();
this.loggerCache = new ConcurrentHashMap<String, Logger>();
this.loggerContextRemoteView = new LoggerContextVO(this);
this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
this.root.setLevel(Level.DEBUG);
loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
initEvaluatorMap();
size = 1;
this.frameworkPackages = new ArrayList<String>();
}
為什麼logback輸出到了stdout裡
上面我們得到結論
- tair外掛裡的logback預設的level是DEBUG,導致netty裡的日誌可以被打印出來
那麼我們可以猜測:
- tair裡的logback沒有特殊配置,或者只配置了tair自己的package,導致ROOT logger的日誌直接輸出到stdout裡
那麼實現上tair裡是使用了logger-api
,它通過LoggerFactory.getLogger
函式獲取到了自己package的logger,然後設定level為INFO
,並設定了appender。
換而言之,tair外掛裡的logback沒有設定ROOT logger,所以它的預設level是DEBUG,並且預設的appender會輸出到stdout裡。
所以tair外掛可以增加設定ROOT logger level為INFO
來修復這個問題。
private static com.taobao.middleware.logger.Logger logger
= com.taobao.middleware.logger.LoggerFactory.getLogger("com.taobao.tair");
public static com.taobao.middleware.logger.Logger infolog
= com.taobao.middleware.logger.LoggerFactory.getLogger("com.taobao.tair.custom-infolog");
public static int JM_LOG_RETAIN_COUNT = 3;
public static String JM_LOG_FILE_SIZE = "200MB";
static {
try {
String tmp = System.getProperty("JM.LOG.RETAIN.COUNT", "3");
JM_LOG_RETAIN_COUNT = Integer.parseInt(tmp);
} catch (NumberFormatException e) {
}
JM_LOG_FILE_SIZE = System.getProperty("JM.LOG.FILE.SIZE", "200MB");
logger.setLevel(Level.INFO);
logger.activateAppenderWithSizeRolling("tair-client", "tair-client.log", "UTF-8",
TairUtil.splitSize(JM_LOG_FILE_SIZE, 0.8 / (JM_LOG_RETAIN_COUNT + 1)), JM_LOG_RETAIN_COUNT);
logger.setAdditivity(false);
logger.activateAsync(500, 100);
logger.info("JM_LOG_RETAIN_COUNT " + JM_LOG_RETAIN_COUNT + " JM_LOG_FILE_SIZE " + JM_LOG_FILE_SIZE);
infolog.setLevel(Level.INFO);
infolog.activateAppenderWithSizeRolling("tair-client", "tair-client-info.log", "UTF-8", "10MB", 1);
infolog.setAdditivity(false);
infolog.activateAsync(500, 100);
總結
- tair外掛裡直接以api的方式設定了自己package下的logger
- tair外掛裡netty的logger的packge和tair並不一樣,所以它最終取的是ROOT logger的配置
- logback預設的ROOT logger level是
DEBUG
,輸出是stdout - 利用arthas的
sc
命令定位具體的類 - 利用arthas的
getstatic
獲取static filed的值 - 利用logger parent層聯的特性,可以向上一層層獲取到ROOT logger的配置
連結
- Arthas開源:https://github.com/alibaba/arthas