我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励欢迎关注微信公众号「架构染色」交流和学习
一、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 写文件的逻辑:
- 调用 write 方法,因为缓存大小有限,所以能写缓存就写缓存;如果缓存容不下,就直接写入其内部的 OutputStream 中,即写文件。
- 如果希望缓存不满的情况下也能够立即写入到 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);
}
}
四、使用需注意
自动刷盘
- 缓存满了(空间不足)直接写入 OutputStream
- 缓存不满不刷盘,则会出现丢日志的现象
手动刷盘
- 手动定时调用 flush 方法,强制将缓存数据写入 OutputStream 中
- 若定时手动写,要留意进程退出前 是否有日志尚在 buffer 中未落盘。即丢日志的现象
分阶段控制刷盘策略
- 启动阶段要保障每一个日志都要刷盘,否则启动报错日志,若在 buffer 未落盘将导致无有效信息来引导排错
- 运行阶段则可按照满刷盘+定时刷盘的方式来执行
五、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。