现象

在应用的 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命令。

  • https://alibaba.github.io/arthas/sc.html

  • https://alibaba.github.io/arthas/getstatic.html

定位logger的具体实现

日志是 io.netty.channel.nio.NioEventLoop输出的,到netty的代码里查看,发现是DEBUG级别的输出:

  • https://github.com/netty/netty/blob/netty-4.0.35.Final/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java#L673

然后用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-c73ad2d6io.netty.channel.nio.NioEventLooplogger'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

往期精选

本号专注于后端技术、JVM问题排查和优化、Java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。

让我知道你“在看”

Arthas实践--抽丝剥茧排查线上应用日志打满问题的更多相关文章

  1. Linux命令排查线上问题常用的几个

    排查线上问题常用的几个Linux命令 https://www.cnblogs.com/cjsblog/p/9562380.html top 相当于Windows任务管理器 可以看到,输出结果分两部分, ...

  2. 记一次linux通过jstack定位CPU使用过高问题或排查线上死锁问题

    一.java定位进程 在服务器中终端输入命令:top 可以看到进程ID,为5421的cpu这列100多了. 记下这个数字:5421 二.定位问题进程对应的线程 然后在服务器中终端输入命令:top -H ...

  3. Arthas协助排查线上skywalking不可用问题

    前言 首先描述下问题的背景,博主有个习惯,每天上下班的时候看下skywalking的trace页面的error情况.但是某天突然发现生产环境skywalking页面没有任何数据了,页面也没有显示任何的 ...

  4. 你要偷偷学会排查线上CPU飙高的问题,然后惊艳所有人!

    GitHub 20k Star 的Java工程师成神之路,不来了解一下吗! GitHub 20k Star 的Java工程师成神之路,真的不来了解一下吗! GitHub 20k Star 的Java工 ...

  5. 轻松排查线上Node内存泄漏问题

    I. 三种比较典型的内存泄漏 一. 闭包引用导致的泄漏 这段代码已经在很多讲解内存泄漏的地方引用了,非常经典,所以拿出来作为第一个例子,以下是泄漏代码: 'use strict'; const exp ...

  6. 推荐几个我近期排查线上http接口偶发415时用到的工具

    导读:近期有一个业务部门的同学反馈说他负责的C工程在小概率情况下SpringMvc会返回415,通过输出的日志可以确定是SpringMvc找不到content-type这个头了,具体为什么找不到了呢? ...

  7. 利用JVM在线调试工具排查线上问题

    在生产上我们经常会碰到一些不好排查的问题,例如线程安全问题,用最简单的threaddump或者heapdump不好查到问题原因.为了排查这些问题,有时我们会临时加一些日志,比如在一些关键的函数里打印出 ...

  8. 记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理

    昨晚我正在床上睡得着着的,突然来了一条短信. 啥,线上MySQL死锁了,我赶紧登录线上系统,查看业务日志. 能清楚看到是这条insert语句发生了死锁. MySQL如果检测到两个事务发生了死锁,会回滚 ...

  9. JVM jmap dump 分析dump文件 / 如何使用Eclipse MemoryAnalyzer MAT 排查线上问题

    jhat简介 jhat用来分析java堆的命令,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言 这个工具并不是想用于应用系统中而是用于"离线" ...

随机推荐

  1. win10输入法问题,已禁止IME 问题解决

    第一种较为简单的解决方法: windows+R打开「运行」,然后打ctfmon,确定. 另外一种解法: windows的老bug了解决办法: 1. I. WIN + X 打开控制面板 -> 管理 ...

  2. [转]5 种使用 Python 代码轻松实现数据可视化的方法

    数据可视化是数据科学家工作中的重要组成部分.在项目的早期阶段,你通常会进行探索性数据分析(Exploratory Data Analysis,EDA)以获取对数据的一些理解.创建可视化方法确实有助于使 ...

  3. 【cf570】D. Tree Requests(dsu on tree)

    传送门 题意: 给出一个以\(1\)为根的有根树.之后有\(m\)个询问,每个询问为\(v_i,h_i\),需要回答以\(v_i\)为根的子树中,深度为\(h_i\)的那些结点所代表的字符能否构成回文 ...

  4. vue调用高德地图:vue-amap

    前言:之前没有接触过页面调用地图的项目,某次面试,老板要求我用vue-amap调用高德地图,回家以后,我去网上查了一些案例和教程,看似很简单的引入调用,我却整整弄了一宿,还没弄出来!!!百般无奈之下, ...

  5. c# 第31节 构造函数与析构函数、new关键字作用

    本节内容: 1:构造和析构的简介 2:构造函数的定义和使用 3:new关键字的作用 4:析构函数的定义和使用 1:构造和析构的简介 2:构造函数的定义和使用 构造函数: 当实例化的一个对象,就默认执行 ...

  6. 《LSM算法原理》

    记内存中的树为T0, 硬盘上的树按时间顺序,记做T1, ..., Tk 读: T0 Tk -> Tk-1 -> ... -> T0 写 T0 T0超过一定大小后,插入硬盘变为Tk+1 ...

  7. 二分法查找(C语言)

    二分法是一种高效的查找方法,其适用于已经排好序的数组 基本思路 从数组最中间的数开始查找判断,若不是需要查找的数字,则比较大小,之后则在从中间分开的两边中的一边从最中间开始查找判断,以此类推 算法描述 ...

  8. 9.29 csp-s模拟测试55 联+赛+题

    T1 联 $n$最大到$1e18$,根本没法做,但$m$只有$1e5$,发现有很多区间是一起动的,或者根本没动,所以可以把区间离散化掉,然后线段树区间修改,对于第三种修改,只需要把它分解成一段一段相同 ...

  9. [学习笔记] 舞蹈链(DLX)入门

    "在一个全集\(X\)中若干子集的集合为\(S\),精确覆盖(\(\boldsymbol{Exact~Cover}\))是指,\(S\)的子集\(S*\),满足\(X\)中的每一个元素在\( ...

  10. CSP-S 2019 题解

    D1T1-格雷码 题中给出了构造格雷码的方法. $solve(n,k)$表示求出$2^n$意义下排名为$k$的格雷码, 只要比较一下考虑最高位的0/1取值就好了. 部分分提示了要开$unsigned\ ...