logback之 FileAppender 的原理及避坑建议

简介: buffer 机制 通俗来说就是化零为整,把少量多次变成多量少次;具体来说就是进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数
我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励

欢迎关注微信公众号「架构染色」交流和学习

一、cache vs buffer

在系统设计中通常会有 cache 及 buffer 的设计:

  • cache :设备之间有速度差,高速设备访问低速设备会造成高速设备等待,导致使用率降低,为了减少低速设备对高速设备的影响,在两者之间加入 cache,通过加快访问速度,以提升高速设备的使用效率。
  • buffer :通俗来说就是化零为整,把少量多次变成多量少次;具体来说就是进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数

二、 FileAppender

2.1 FileAppender 属于 buffer 级的方案

  • FileAppender 内部有缓存 buffer,buffer 读写都加锁,从 buffer 写盘 与 log 写 buffer 会串行,产生 RT 变长的性能问题。

image.png

2.2 FileAppender 原理简析

FileAppender 内部使用 BufferedOutputStream , BufferedOutputStream 的 OutputStream 是 FileOutputStream;
通过 BufferedOutputStream 写文件的逻辑:

  1. 调用 write 方法,因为缓存大小有限,所以能写缓存就写缓存;如果缓存容不下,就直接写入其内部的 OutputStream 中,即写文件。

image.png

  1. 如果希望缓存不满的情况下也能够立即写入到 OutputStream 中,那么久调用 flush 方法。

三、同步 RollingFileAppender 源码分析

3.1 继承关系

image.png

RollingFileAppender 继承关系:RollingFileAppender -> FileAppender -> OutputStreamAppender -> UnsynchronizedAppenderBase,OutputStreamAppender 中的 append 方法调用了 subAppend 方法,subAppend 又调用了 writeOut 方法,writeOut 又调用了 LayoutWrappingEncoder 的 doEncode 方法,在 doEncode 方法中调用了 outputStream 的 write 方法,并且判断 immediateFlush 为 true 的话,则立即 flush

public class RollingFileAppender<E> extends FileAppender<E> { }
public class FileAppender<E> extends OutputStreamAppender<E> { }
public class OutputStreamAppender<E> extends UnsynchronizedAppenderBase<E> {
  @Override
  protected void append(E eventObject) {
    if (!isStarted()) {
      return;
    }

    subAppend(eventObject);
  }
  protected void subAppend(E event) {
    // 省略其他不重要的代码
      lock.lock();
      try {
        writeOut(event);
      } finally {
        lock.unlock();
      }
  }
  protected void writeOut(E event) throws IOException {
    // setLayout 方法中设置了 encoder = new LayoutWrappingEncoder<E>();
    this.encoder.doEncode(event);
  }
}
public class LayoutWrappingEncoder<E> extends EncoderBase<E> {
  public void doEncode(E event) throws IOException {
    String txt = layout.doLayout(event);
    outputStream.write(convertToBytes(txt));
    if (immediateFlush)
      outputStream.flush();
  }
}

再看代码追查一下 outputStream 的真实类型,FileAppender 是直接将日志输出到文件中,初始化了一个 ResilientFileOutputStream,其内部使用的是带缓冲的 BufferedOutputStream,然后调用超类的 setOutputStream 方法设置输出流,最终调用 encoder.init 方法将输出流对象赋值给了 outputStream。

public class FileAppender<E> extends OutputStreamAppender<E> {
    public void openFile(String file_name) throws IOException {
        LogbackLock var2 = this.lock;
        synchronized(this.lock) {
            File file = new File(file_name);
            // 如果日志文件所在的文件夹还不存在,就创建之
            if(FileUtil.isParentDirectoryCreationRequired(file)) {
                boolean resilientFos = FileUtil.createMissingParentDirectories(file);
                if(!resilientFos) {
                    this.addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
                }
            }

            ResilientFileOutputStream resilientFos1 = new ResilientFileOutputStream(file, this.append);
            resilientFos1.setContext(this.context);
            // 调用父类的 setOutputStream 方法
            this.setOutputStream(resilientFos1);
        }
    }
}

public class ResilientFileOutputStream extends ResilientOutputStreamBase {
    private File file;
    private FileOutputStream fos;

    public ResilientFileOutputStream(File file, boolean append) throws FileNotFoundException {
        this.file = file;
        this.fos = new FileOutputStream(file, append);
        // OutputStream os 在超类 ResilientOutputStreamBase 里
        this.os = new BufferedOutputStream(this.fos);
        this.presumedClean = true;
    }
}

public class OutputStreamAppender<E> extends UnsynchronizedAppenderBase<E> {
  private OutputStream outputStream;
  protected Encoder<E> encoder;
  public void setOutputStream(OutputStream outputStream) {
    lock.lock();
    try {
      // close any previously opened output stream
      closeOutputStream();
      encoderInit();
    } finally {
      lock.unlock();
    }
  }
  // 将 outputStream 送入 encoder
  void encoderInit() {
    encoder.init(outputStream);
  }
}

四、使用需注意

  1. 自动刷盘

    • 缓存满了(空间不足)直接写入 OutputStream
    • 缓存不满不刷盘,则会出现丢日志的现象
  2. 手动刷盘

    • 手动定时调用 flush 方法,强制将缓存数据写入 OutputStream 中
    • 若定时手动写,要留意进程退出前 是否有日志尚在 buffer 中未落盘。即丢日志的现象
  3. 分阶段控制刷盘策略

    • 启动阶段要保障每一个日志都要刷盘,否则启动报错日志,若在 buffer 未落盘将导致无有效信息来引导排错
    • 运行阶段则可按照满刷盘+定时刷盘的方式来执行

五、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

欢迎点击链接扫马儿关注、交流。

参考并感谢

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5月前
|
Java API 开发者
Java日志框架整理
Java日志框架整理
62 0
|
6月前
|
Java
SpringBoot 日志终极解决方案
SpringBoot 日志终极解决方案
49 0
|
1月前
|
Java Spring
日志之旅:深入Spring整合Logback的高效日志管理
日志之旅:深入Spring整合Logback的高效日志管理
23 2
|
8月前
|
开发框架 Java 数据库
【面试题精讲】SpringBoot的传播机制详解
【面试题精讲】SpringBoot的传播机制详解
|
4月前
|
XML Java API
忽视日志吃大亏,手把手教你玩转 SpringBoot 日志
一、日志重要吗 程序中的日志重要吗? 在回答这个问题前,笔者先说个事例: ❝ 笔者印象尤深的就是去年某个同事,收到了客户反馈的紧急bug。尽管申请到了日志文件,但因为很多关键步骤没有打印日志,导致排查进度很慢,数个小时都没能排查到问题,也无法给出解决对策。导致了客户程序一直阻断,最终产生了不少损失。 事后,经过仔细推敲,成功复现了这个bug,其实是一个很不起眼的数据转换导致的。可因为日志内容的匮乏,排查起来难度很大。其实只要在数据转换前后进行日志输出,这个问题就是一样的事。但可惜没如果,故事的最后,开发部门还是遭到了客户的投诉,影响到了部门绩效 ❞
|
网络协议 Java API
【日志技术专题】「logback入门到精通」彻彻底底带你学会logback框架的使用和原理(入门介绍篇)
【日志技术专题】「logback入门到精通」彻彻底底带你学会logback框架的使用和原理(入门介绍篇)
147 0
|
设计模式 缓存 Java
logback之 AsyncAppender 的原理、源码及避坑建议
AsyncAppender 接收日志,放入其内部的一个阻塞队列,专开一个线程从阻塞队列中取数据(每次一个)丢给链路下游的Appender 如 FileAppender,如此可把日志写盘 变成 日志写内存,减少写日志的 RT。
550 0
|
Java Maven
从原理和源码梳理Springboot FatJar 的机制
从原理和源码梳理Springboot FatJar 的机制
195 0
从原理和源码梳理Springboot FatJar 的机制
|
安全 Java BI
一文读懂logback的配置
前言 文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820… 种一棵树最好的时间是十年前,其次是现在
226 0