文章摘要(AI生成)
最近接手的服务经常出现线上内存溢出问题,导致服务需要频繁重启。经过查看内存使用记录,发现内存占用在上线后1天就达到4G以上,其中大部分内存被char数组占用了61%。这些char数组中存储了大量的日志记录,而由于老服务的日志打印过于频繁,每条日志的大小在6、7K之间。此外,阻塞队列设置得过小,并且没有设置neverBlock导致业务线程阻塞,导致堆文件里充斥着大量日志。通过查看logback的源码可以发现,当存储日志时,logback会先将日志放入阻塞队列中,根据neverBlock的值决定是采用阻塞方式还是非阻塞方式存储。neverBlock为false时,采用非阻塞方式存储,否则采用阻塞方式存储。
最近接手了一个服务,经常会有线上内存溢出出现,最近更是频繁触发报警。导致隔一周不上线,服务就需要重启。
周五腾出手来,看了下线上内存使用记录,发现内存占用在上线后1天就达到4G以上:
觉得奇怪,于是在周五再次报警之后,dump了下线上的内存使用记录,使用visual VM查看,发现char数组占用了61%的内存:
这些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在做日志存储的时候,会先把需要存储的日志放到阻塞队列中:
- 如果
neverBlock
为false
,则会通过循环等待的方式,尝试向阻塞队列中放入日志记录,直到队列中的日志被消费,可以保存到阻塞队列中为止。 - 如果
neverBlock
为true
,则会尝试向阻塞队列中添加要保存的日志记录。若阻塞队列已满,则直接将要保存的日志抛弃。
而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
指定阈值后,如果阻塞队列的空闲大小<指定阈值,则丢弃掉INFO
、DEBUG
和TRACE
级别的日志,只将ERROR
和WARN
级别的日志放到阻塞队列,等待后续打印。
评论区