log.info问题总结

一、CPU飙升

1.1 异常日志

从开发人员给的描述里面看, 基本没啥可以具体怀疑的地方. 进一步观察该应用的各个指标, 只有 CPU 使用率比较异常, 其它没有啥异常的地方.

一般 CPU 高, 要从线程看起, 做了几个 thread dump, 大概推断出问题的所在, 进一步分析, 验证了这个推断.

从 thread dump 看到好几个线程都是在 blocked 状态, 它们都是在等 java.io.PrintStream 某个 instance 的一把锁, 这个锁正是 PrintStream 的 println 方法里面的.

为什么会调用这个 println 方法呢? 原来这些 thread 都遇到了一个 CommonException, 程序捕获这个 CommonException 之后, 就在 catch 块里面调用了 CommonException 的 printStacktrace() 方法; 然而这个方法对于没有指定要 print 到哪里去的情况, 默认是输出到 System.err, 这个 System.err 正是一个 PrintStream.

其实看到这里感觉也没啥问题, 很多人都是把 log 输出到 System.out, System.err 的. 这个应用里面, System.out, System.err 都是重定向到 log 文件的, 也就是和 log4j 一样, 都输出到了文件系统. 他们的文件都叫 stdout.log. 仔细查看输出的问题, 就会发现, 输出文件的效率真是太高了. 每秒2个文件, 每个6M:

随便打开一个文件来看, 里面基本全是出错的 stacktrace, 每个 stacktrace 大概只有10行, 6M 的 log 文件里面大概有3500个线程的 stacktrace. 这意味着每秒有大概7000次写入. 由于一个线程不可能连续写入这种 stacktrace, 所以每秒有7000次的锁获取/释放发生在 System.err, 也就是上面的 PrintStream 的同步锁上面. 由于这个锁是一个高频率使用, 并且必然会发生竞争的锁, 所以不可能有偏向锁, 自旋锁存在, 必然膨胀到最重量的锁.

那么这样问题就可以解释了:

30个线程不停的在出错, 不停的要写日志到 System.err 中, 这里有个锁, 它们要频繁的获取/释放, 它们从获取到释放的时间又超短, 仅仅写一个10行日志. 本来获取释放锁操作就是一个比较重量的操作, 这个重量就是指的 CPU 消耗比较高. 频繁短期的获取释放锁必然会导致 CPU 飙升.

1.2 正常日志

首先使用top命令查看进程运行情况

# top
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 7020 root      20   0 2538892 164144  11856 S  90.3  8.7  61:23.54 java
11022 root      20   0 2560528 241340  11920 S  0.3 12.8 311:23.23 java
26805 root      20   0   32612   4036   2472 S  0.3  0.2  24:50.95 AliYunDunUpdate
26838 root      10 -10  134120  14524   5924 S  0.3  0.8 343:05.22 AliYunDun
    1 root      20   0   43280   3300   2108 S  0.0  0.2   2:16.82 systemd
    2 root      20   0       0      0      0 S  0.0  0.0   0:01.78 kthreadd
    3 root      20   0       0      0      0 S  0.0  0.0   1:30.68 ksoftirqd/0
    5 root       0 -20       0      0      0 S  0.0  0.0   0:00.00 kworker/0:0H
    7 root      rt   0       0      0      0 S  0.0  0.0   0:00.00 migration/0
    8 root      20   0       0      0      0 S  0.0  0.0   0:00.00 rcu_bh                 
    9 root      20   0       0      0      0 S  0.0  0.0  65:15.85 rcu_sched             
   10 root      rt   0       0      0      0 S  0.0  0.0   2:14.65 watchdog/0             
   12 root      20   0       0      0      0 S  0.0  0.0   0:00.00 kdevtmpfs             
   13 root       0 -20       0      0      0 S  0.0  0.0   0:00.00 netns                 
   14 root      20   0       0      0      0 S  0.0  0.0   0:00.00 khungtaskd             
   15 root       0 -20       0      0      0 S  0.0  0.0   0:00.00 writeback             
   16 root       0 -20       0      0      0 S  0.0  0.0   0:00.00 kintegrityd

查询到7020这个进程有异常,在继续查看具体异常线程。

# top -Hp 7020
 PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 23328 root      20   0 2538892 164144  11856 S  90.0  8.7   0:00.00 java

找到了当前异常进程下的异常线程后使用jstack查看详细情况

# jstack -l 6377 > error.log
​```

将线程ID打印成16进制形式
# printf "%x\n" 23328
5b20
然后从jstack里查询该线程信息


grep ‘18e9’ error.log --color

“http-bio-6379-exec-200” #8869954 daemon prio=5 os_prio=0 tid=0x00007f74a81f6800 nid=0x5b20 waiting for monitor entry [0x00007f742457f000]

“http-bio-7020-exec-200” #8869954 daemon prio=5 os_prio=0 tid=0x00007f74a81f6800 nid=0x5b20 waiting for monitor entry [0x00007f742457f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.log4j.Category.callAppenders(Category.java:204)

  • waiting to lock <0x00000000800371d0> (a org.apache.log4j.spi.RootLogger)
    at org.apache.log4j.Category.forcedLog(Category.java:391)
    at org.apache.log4j.Category.log(Category.java:856)
    at org.slf4j.impl.Log4jLoggerAdapter.info(Log4jLoggerAdapter.java:368)
    ​```
    在log4j 中,logger.info等日志记录方法是同步的(使用了synchronized),大量的日志导致线程阻塞在callAppenders()这个方法,也就是这个地方导致压测阻塞,响应耗时比较久的问题。
public void callAppenders(LoggingEvent event) {
        int writes = 0;
​
        for(Category c = this; c != null; c = c.parent) {
            synchronized(c) {
                if (c.aai != null) {
                    writes += c.aai.appendLoopOnAppenders(event);
                }
​
                if (!c.additive) {
                    break;
                }
            }
        }
​
        if (writes == 0) {
            this.repository.emitNoAppenderWarning(this);
        }
​
    }
1.3 日志死锁

https://www.iteye.com/blog/zl378837964-2373591

二、频繁的GC

线上服务突然告警,jvm疯狂触发老年代gc,登录后台查看gc并不能释放老年代空间,之前这个服务一直正常运行了几个月,第一时间下载jvm的dump文件,并对服务重启,防止内存不足影响线上业务。

使用MAT分析dump文件,先按照retained Heap排序,

发现ThreadLocal 和StringBuilder这两个类占用了大量内存,ThreadLocal也是引用的StringBuilder,点开StringBuilder发现内容是一些日志文件,初步确定是log文件导致的内存泄漏,先将线上的日志输出级别提高到error(我们系统支持动态调整日志输出级别)。

搜索Log4j2在什么地方使用了StringBuilder,排查发现org.apache.logging.log4j.message.ParameterizedMessage这个类中有一个复用StringBuilder的代码,线上使用log4j-api的版本是2.8

public String getFormattedMessage() {
​
        if (formattedMessage == null) {
​
            final StringBuilder buffer = getThreadLocalStringBuilder();
​
            formatTo(buffer);
​
            formattedMessage = buffer.toString();
​
        }
​
        return formattedMessage;
​
    }
​
    private static StringBuilder getThreadLocalStringBuilder() {
​
        StringBuilder buffer = threadLocalStringBuilder.get();
​
        if (buffer == null) {
​
            buffer = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
​
            threadLocalStringBuilder.set(buffer);
​
        }
​
        buffer.setLength(0);
​
        return buffer;
​
    }

这段代码中Log4j2引用了一个ThreadLocal中的StringBuilder,这样复用StringBuilder可以大幅提高日志输出效率,但注意这段代码buffer.setLength(0),这个操作只会将StringBuilder的写入重置为从0开始写入,但不会回收StringBuilder已经占用的内存,类似于如果StringBuilder中原有值 “哈哈哈哈哈哈”,执行setLength(0)之后再写入"123",那StringBuilder虽然存储的值是 “123”,但在Builder的数组中存储的其实是 “123哈哈哈”,只是覆盖了前三个位置,后面的三个位置仍然占用内存,释放不了。由于当前的ThreadLocal是tomcat的线程池里的线程,ThreadLocal就基本没啥可能会被释放,导致StringBuilder也不会被回收。这个StringBuilder的内存只会增加不会减少,由此导致内存泄漏。

后来查看log4j2-api的最新版本2.13.0

public static final int MAX_REUSABLE_MESSAGE_SIZE = size("log4j.maxReusableMsgSize", (128 * 2 + 2) * 2 + 2);
​
public String getFormattedMessage() {
​
        if (formattedMessage == null) {
​
            final StringBuilder buffer = getThreadLocalStringBuilder();
​
            formatTo(buffer);
​
            formattedMessage = buffer.toString();
​
            StringBuilders.trimToMaxSize(buffer, Constants.MAX_REUSABLE_MESSAGE_SIZE);
​
        }
​
        return formattedMessage;
​
    }

可以看到新版本中增加一个操作trimToMaxSize,代码如下:

/**
​
     * Ensures that the char[] array of the specified StringBuilder does not exceed the specified number of characters.
​
     * This method is useful to ensure that excessively long char[] arrays are not kept in memory forever.
​
     *
​
     * @param stringBuilder the StringBuilder to check
​
     * @param maxSize the maximum number of characters the StringBuilder is allowed to have
​
     * @since 2.9
​
     */
​
    public static void trimToMaxSize(final StringBuilder stringBuilder, final int maxSize) {
​
        if (stringBuilder != null && stringBuilder.capacity() > maxSize) {
​
            stringBuilder.setLength(maxSize);
​
            stringBuilder.trimToSize();
​
        }
​
    }

参考:

https://blog.csdn.net/jslcylcy/article/details/104371137

https://juejin.cn/post/6981424418413084703

https://zhuanlan.zhihu.com/p/71740544

https://blog.csdn.net/jiang18238032891/article/details/115832280

https://www.jianshu.com/p/fb124b45096a

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 可能是因为在不同的环境下,log.info的输出级别不同,或者在某些情况下,log.info被禁用了。你可以检查一下你的代码中是否有设置log输出级别的语句,或者查看一下你的运行环境是否有相关的配置。如果问题仍然存在,你可以尝试使用其他的log输出函数来进行调试。 ### 回答2: log.info在程序中使用的目的是为了记录程序的运行状态和调试信息。在某些情况下,log.info可能会显示,而在其他情况下可能不会显示。这个情况可能有以下几种可能性: 1.日志级别设置:log.info是日志的一种级别,通常用于输出一些重要的信息。在某些情况下,日志级别可能被设置为比info更高的级别,如warn、error或fatal,这样log.info的信息就不会被显示出来了。 2.日志输出配置:日志系统通常有一个配置文件,通过配置文件可以设置日志的输出格式、输出位置等。在某些情况下,log.info的信息可能被配置为不输出到控制台或文件。 3.条件判断:在程序中,可能会根据一些条件来判断是否需要输出log.info的信息。比如,在某些情况下,程序可能会判断一些条件不满足,就不会执行log.info语句。 4.调试模式:在开发阶段,可能会设置程序的调试模式,以方便开发人员调试程序。在调试模式下,log.info的信息通常会被显示出来;而在运行模式下,log.info的信息可能不会显示,以避免对用户造成干扰。 总结来说,log.info有显示和不显示的情况是由于日志的级别设置、日志输出配置、条件判断和运行模式等因素所导致的。为了确保log.info的信息能够被正确显示,需要检查相关的配置和条件判断,并确保日志级别和日志输出的设置是合适的。 ### 回答3: log.info是日志输出的一种方法,通常在代码中用来输出一些调试信息或者程序的运行状态。它的输出是否显示取决于日志的级别和配置。 在Log4j或者其他日志框架中,日志被分为不同的级别,包括debug、info、warn、error等。其中,debug是最低级别的,只在开发和调试阶段使用,输出的信息非常详细;info级别稍高,用来输出一些程序运行的关键信息;warn则用来输出警告信息;error是最高级别的,用来输出错误信息。 在配置文件中,我们可以指定日志的级别,当级别设置为debug时,log.info的输出信息是会显示的;当级别设置为info或者更高级别时,log.info的输出信息也会显示;但如果级别设置为warn或者error时,log.info的输出信息将不会显示。 另外,有些情况下,即使日志的级别设置为debug或者infolog.info的输出信息也可能不显示。例如,当日志输出到文件时,文件的大小或者写入的速度限制了日志的输出,导致部分信息无法显示出来。 总之,log.info的显示与否取决于日志的级别和配置,以及日志输出的限制条件。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值