【进阶】logback之 RollingFileAppender 的原理及避坑建议Applehope

一、cache vs buffer

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

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

二、 FileAppender

2.1 FileAppender 属于 buffer 级的方案

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

2.2 FileAppender 原理简析

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

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

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

三、同步 RollingFileAppender 源码分析

3.1 继承关系

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 未落盘将导致无有效信息来引导排错
    • 运行阶段则可按照满刷盘+定时刷盘的方式来执行

五、最后说一句

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


原文链接:https://juejin.cn/post/7183152831677857847
来源:稀土掘金
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值