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直接在線上定位問題的過程,主要使用scgetstatic命令。

定位logger的具體實現

日誌是io.netty.channel.nio.NioEventLoop輸出的,到netty的代碼裏查看,發現是DEBUG級別的輸出:

然後用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的配置

鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章