使用Arthas抽丝剥茧排查线上应用日志打满问题

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: ## 现象 在应用的 `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

现象

在应用的 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的配置

链接

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
Java Shell
「sh脚步模版自取」测试线排查的三个脚本:启动、停止、重启、日志保存
「sh脚步模版自取」测试线排查的三个脚本:启动、停止、重启、日志保存
37 1
|
2月前
|
机器学习/深度学习 存储 监控
Elasticsearch 在日志分析中的应用
【9月更文第2天】随着数字化转型的推进,日志数据的重要性日益凸显。日志不仅记录了系统的运行状态,还提供了宝贵的洞察,帮助企业改进产品质量、优化用户体验以及加强安全防护。Elasticsearch 作为一个分布式搜索和分析引擎,因其出色的性能和灵活性,成为了日志分析领域的首选工具之一。本文将探讨如何使用 Elasticsearch 作为日志分析平台的核心组件,并详细介绍 ELK(Elasticsearch, Logstash, Kibana)栈的搭建和配置流程。
269 4
|
14天前
|
存储 SQL 监控
|
14天前
|
自然语言处理 监控 数据可视化
|
1月前
|
Java 程序员 应用服务中间件
「测试线排查的一些经验-中篇」&& 调试日志实战
「测试线排查的一些经验-中篇」&& 调试日志实战
22 1
「测试线排查的一些经验-中篇」&& 调试日志实战
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
43 3
|
2月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
46 2
|
2月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
3月前
|
数据库 Java 监控
Struts 2 日志管理化身神秘魔法师,洞察应用运行乾坤,演绎奇幻篇章!
【8月更文挑战第31天】在软件开发中,了解应用运行状况至关重要。日志管理作为 Struts 2 应用的关键组件,记录着每个动作和决策,如同监控摄像头,帮助我们迅速定位问题、分析性能和使用情况,为优化提供依据。Struts 2 支持多种日志框架(如 Log4j、Logback),便于配置日志级别、格式和输出位置。通过在 Action 类中添加日志记录,我们能在开发过程中获取详细信息,及时发现并解决问题。合理配置日志不仅有助于调试,还能分析用户行为,提升应用性能和稳定性。
57 0
|
14天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
125 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板