欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

记一次logback引发线上的内存报警
(last modified Mar 26, 2023, 7:11 PM )
by
侧边栏壁纸
  • 累计撰写 176 篇文章
  • 累计创建 61 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

记一次logback引发线上的内存报警

橙序员
2023-03-26 / 0 评论 / 0 点赞 / 1,023 阅读 / 836 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

最近接手的服务经常出现线上内存溢出问题,导致服务需要频繁重启。经过查看内存使用记录,发现内存占用在上线后1天就达到4G以上,其中大部分内存被char数组占用了61%。这些char数组中存储了大量的日志记录,而由于老服务的日志打印过于频繁,每条日志的大小在6、7K之间。此外,阻塞队列设置得过小,并且没有设置neverBlock导致业务线程阻塞,导致堆文件里充斥着大量日志。通过查看logback的源码可以发现,当存储日志时,logback会先将日志放入阻塞队列中,根据neverBlock的值决定是采用阻塞方式还是非阻塞方式存储。neverBlock为false时,采用非阻塞方式存储,否则采用阻塞方式存储。

最近接手了一个服务,经常会有线上内存溢出出现,最近更是频繁触发报警。导致隔一周不上线,服务就需要重启。

周五腾出手来,看了下线上内存使用记录,发现内存占用在上线后1天就达到4G以上:

image-20230326183044888

觉得奇怪,于是在周五再次报警之后,dump了下线上的内存使用记录,使用visual VM查看,发现char数组占用了61%的内存:

image-20230326183408458

这些char数组中,存储了大量日志记录。由于老服务凌乱的开发代码日志打印的就大,每条日志6、7K,阻塞队列设置的太小,又没有设置neverBlock导致业务线程阻塞了,就会导致堆文件里全是日志了。

neverBlock是什么作用呢?查看logback源码:

    private void put(E eventObject) {
        if (neverBlock) {
            blockingQueue.offer(eventObject);
        } else {
            putUninterruptibly(eventObject);
        }
    }

    private void putUninterruptibly(E eventObject) {
        boolean interrupted = false;
        try {
            while (true) {
                try {
                    blockingQueue.put(eventObject);
                    break;
                } catch (InterruptedException e) {
                    interrupted = true;
                }
            }
        } finally {
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

通过阅读这段代码可以发现,logback在做日志存储的时候,会先把需要存储的日志放到阻塞队列中:

  1. 如果neverBlockfalse,则会通过循环等待的方式,尝试向阻塞队列中放入日志记录,直到队列中的日志被消费,可以保存到阻塞队列中为止。
  2. 如果neverBlocktrue,则会尝试向阻塞队列中添加要保存的日志记录。若阻塞队列已满,则直接将要保存的日志抛弃。

neverBlock默认的设置为false, 这就意味着我们要一直等待阻塞队列中的日志记录被消费,但是随着程序的执行,会有源源不断的日志等待被放到阻塞队列。这些等待消费的队列一直在java堆,也就是我们的内存中。如果jvm堆设置过小则会FULL GC,如果Jvm堆和机器内存大小相同,则会触发机器运维报警。

所以,一般在服务丢弃日志和服务因日志积压不可用之间,我们更倾向于后者,所以需要将log-back.xml的配置改为:

  <appender name="ERROR-FILE-ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <neverBlock>true</neverBlock>
    <discardingThreshold >0</discardingThreshold>
    <queueSize>1024</queueSize>
    <appender-ref ref="ERROR-FILE" />
  </appender>

discardingThreshold参数为阻塞队列的空闲阈值,低于这个阈值则直接丢弃日志,我们可以看源码中的append()方法实现:

    public static final int ERROR_INT = 40000;
    public static final int WARN_INT = 30000;
    public static final int INFO_INT = 20000;
    public static final int DEBUG_INT = 10000;
    public static final int TRACE_INT = 5000;   

		protected boolean isDiscardable(ILoggingEvent event) {
      Level level = event.getLevel();
      return level.toInt() <= 20000;
    }

		private boolean isQueueBelowDiscardingThreshold() {
        return (blockingQueue.remainingCapacity() < discardingThreshold);
    }

    @Override
    protected void append(E eventObject) {
        if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
            return;
        }
        preprocess(eventObject);
        put(eventObject);
    }

可以发现,discardingThreshold指定阈值后,如果阻塞队列的空闲大小<指定阈值,则丢弃掉INFODEBUGTRACE级别的日志,只将ERRORWARN级别的日志放到阻塞队列,等待后续打印。

0

评论区