美文网首页
完全掌控logging的生与死(二)自定义异步刷盘Appende

完全掌控logging的生与死(二)自定义异步刷盘Appende

作者: 牧羊人刘俏 | 来源:发表于2020-12-29 14:55 被阅读0次

一般来说在我们logback.xml配置文件中,root的logger一般会引用
两个类型的Appender,一个是ConsoleAppender,一个是RollingFileAppender,
ConsoleAppender负责将logger的日志信息写到Console控制台。其定义如下

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

而RollingFileAppender则是按照策略滚动的生成归档日志文件,定义如下

 <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/info_%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 100MB -->
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

定义好这两个Appender之后,就可以在不同的logger去ref的引用就可以了,如下的root logger引用定义如下

 <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>

由于我们在root的logger上ref了这两个appender,那么所有的logger都会继承这两个Appender。
但是在RollingFileAppender有个小瑕疵,我们知道Appender的doAppend方法会将事件写入到outputStream,一路封装之后,在RollingFileAppender会调用
subAppend方法,如下,根据事件驱动,先检查是否要归档,如果不要直接的调用super.subAppend(event)

    @Override
    protected void subAppend(E event) {
        // The roll-over check must precede actual writing. This is the
        // only correct behavior for time driven triggers.

        // We need to synchronize on triggeringPolicy so that only one rollover
        // occurs at a time
        synchronized (triggeringPolicy) {
            if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
                rollover();
            }
        }

        super.subAppend(event);
    }

在OutputStreamAppender类中

 protected void subAppend(E event) {
        if (!isStarted()) {
            return;
        }
        try {
            // this step avoids LBCLASSIC-139
            if (event instanceof DeferredProcessingAware) {
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            // the synchronization prevents the OutputStream from being closed while we
            // are writing. It also prevents multiple threads from entering the same
            // converter. Converters assume that they are in a synchronized block.
            // lock.lock();

            byte[] byteArray = this.encoder.encode(event);
            writeBytes(byteArray);

        } catch (IOException ioe) {
            // as soon as an exception occurs, move to non-started state
            // and add a single ErrorStatus to the SM.
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }

核心的语句就两条,

//格式化日志事件
 byte[] byteArray = this.encoder.encode(event);
//写到outputStream里面去
            writeBytes(byteArray);

而writeBytes代码如下

 private void writeBytes(byte[] byteArray) throws IOException {
        if(byteArray == null || byteArray.length == 0)
            return;
        
        lock.lock();
        try {
            this.outputStream.write(byteArray);
            if (immediateFlush) {
                this.outputStream.flush();
            }
        } finally {
            lock.unlock();
        }
    }

如下的核心语句,我们可以把immediateFlush设置为false,这样就不会立即刷盘了,默认是true,性能不好。

 this.outputStream.write(byteArray);
            if (immediateFlush) {
                this.outputStream.flush();
            }

但是设置成false之后,我们是否也能灵活的控制刷盘策略呢,是可以的,如下
我们定义一个AsyncFlushRollingFileAppender继承于RollingFileAppender,重写start方法,如下

 @Override
    public void start() {
        super.start();
        setImmediateFlush(true);
        if (schedule == null) {
            synchronized (this) {
                if (schedule == null) {
                    this.schedule = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder()
                            .setNameFormat("asyncFlush-pool-%d").setDaemon(true).build());
                    schedule.scheduleWithFixedDelay(() -> {
                        lock.lock();
                        try {
                            if (getOutputStream() != null ) {
                                getOutputStream().flush();
                            }
                        } catch (Exception e) {
                            logger.error(e.getMessage(), e);
                        } finally {
                            lock.unlock();
                        }
                    }, 1000, 1000L, TimeUnit.MILLISECONDS);
                }
            }
        }
    }

这样我们使用线程池,做到了一秒刷新一次。
只需要在logback-spring.xml文件里面添加如下的配置就可以了

 <encoder>
            <ImmediateFlush>false</ImmediateFlush>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] ${LOG_LEVEL_PATTERN:-%p} ${PID:- } [%traceId] [%t] [%logger] : %m%n</pattern>
            <charset>UTF-8</charset>
        </encoder>

如上我们通过自定义的Appender灵活的实现刷盘的策略。当然上述自定义Appender还是比较的弱,比如在日志量很大的时候,我们如何实现自定义的丢弃策略且有灵活的缓冲策略的Appender,很显然上面的这个自定义Appender是做不到的。后面我们会自定义一个更加灵活的Appender来实现前面的场景。

相关文章

网友评论

      本文标题:完全掌控logging的生与死(二)自定义异步刷盘Appende

      本文链接:https://www.haomeiwen.com/subject/ccmtoktx.html