• 最新面试必问:怎么写一个又好又快的日志库


    引子

    上一篇使用责任链模式搭了一个高可扩展的日志框架,并引入高性能的I/O库以提升写日志性能。

    在这个框架下,“日志写文件”作为一个拦截器出现:

    1. // 日志拦截器class OkioLogInterceptor(private var dir: String) : LogInterceptor { private val handlerThread = HandlerThread("log_to_file_thread") private val handler: Handler private var bufferedSink: BufferedSink? = null private val dispatcher: CoroutineDispatcher init { // 开启线程串行地处理日志请求 handlerThread.start() handler = Handler(handlerThread.looper, callback) dispatcher = handler.asCoroutineDispatcher("log_to_file_dispatcher") } override fun log(priority: Int, tag: String, log: String, chain: Chain) { GlobalScope.launch(dispatcher) { // 使用 Okio 将日志输出到文件 checkSink().use { it.writeUtf8("[$tag] $log") it.writeUtf8("\n") } } chain.proceed(priority, tag, log) } // 构建缓冲输出流 private fun checkSink(): BufferedSink { if (bufferedSink == null) { bufferedSink = logFile.appendingSink().buffer() } return bufferedSink!! }}
    2. 复制代码

    这是写文件日志拦截器的示意伪代码,完整代码可以点击这里面试题 | 怎么写一个又好又快的日志库?(一)

    除了高扩展性和高性能I/O,还有别的地方可以优化?

    压缩日志

    将日志日内容压缩不仅能进一步提升I/O性能,减少客户端日志上传的流量,还能为公司省钱(云存储都挺贵的)。

    Gzip 是一种常用的压缩格式。Linux 就用这种格式压缩文件。除了用在文件压缩,Gzip 还用于网络压缩,它是在RFC 2016中规定的三种标准HTTP压缩格式之一。

    关于 Gzip 压缩格式的详细介绍可以点击这里。

    借助于装饰者模式、适配器模式、以及 Kotlin 的扩展方法语法,为原先的输出流新增 Gzip 功能很简单。

    原先构建输出流的代码如下:

    1. val bufferedSink = logFile.appendingSink().buffer()
    2. 复制代码

    其中appendingSink()和buffer()都是扩展方法:

    1. fun File.appendingSink(): Sink = FileOutputStream(this, true).sink()fun OutputStream.sink(): Sink = OutputStreamSink(this, Timeout())fun Sink.buffer(): BufferedSink = RealBufferedSink(this)
    2. 复制代码

    即先构建了 FileOutputStream,然后将其适配成 OutputStreamSink,再装饰成 RealBufferedSink,最终形成 RealBufferedSink( OutputStreamSink( FileOutputStream( File ) ) )这样套娃的结构。

    Okio 中实现 Gzip 压缩输出的类叫GzipSink,它也有类似的装饰构造方法:

    1. inline fun Sink.gzip() = GzipSink(this)
    2. 复制代码

    只需在原有调用链上插入 gzip() 即可实现压缩:

    1. val bufferedSink = logFile.appendingSink().gzip().buffer()
    2. 复制代码

    现在的套娃结构变成RealBufferedSink( GzipSink( OutputStreamSink( FileOutputStream( File ) ) ) )

    跑了一下测试程序,测试方法为连续输出1万条长log,不使用 Gzip 时,日志文件大小为 251 MB,加了 Gzip 之后,只有 2.1 MB。整整缩小了是 100+ 倍

    把日志文件的后缀改成gz,这样从云端下载之后就能直接只用压缩软件解压看到原始日志。

    为了对比加入压缩后 Okio 和 java.io 的速度性能差异,重写了 java.io 版压缩输出流的代码:

    1. val outputStream = logFile.outputStream().gzip().writer().buffered()
    2. 复制代码

    其中outputStream()和buffered()是系统预定的装饰流扩展方法:

    1. // 构造 FileOutputStream 并持有 File 实例public inline fun File.outputStream(): FileOutputStream { return FileOutputStream(this)}// 构造 BufferedWriter 并持有 Writer 实例public inline fun Writer.buffered(bufferSize: Int = DEFAULT_BUFFER_SIZE): BufferedWriter = if (this is BufferedWriter) this else BufferedWriter(this, bufferSize)
    2. 复制代码

    而gzip()和writer()是自定义的装饰流扩展方法:

    1. // 构建 GZIPOutputStream 并持有 OutputStreamfun OutputStream.gzip() = GZIPOutputStream(this)// 构建 OutputStreamWriter 并持有 OutputStreamfun OutputStream.writer(charset: Charset = Charsets.UTF_8) = OutputStreamWriter(this, charset)
    2. 复制代码

    GZIPOutputStream是针对 OutputStream 的,所以不得不使用 OutputStreamWrite 将 Writer 接口适配成 OuptStream 接口。

    完整的 java.io 版压缩日志拦截器代码如下:

    1. class FileWriterLogInterceptor private constructor(private var dir: String) : LogInterceptor { private val handlerThread = HandlerThread("log_to_file_thread") private val handler: Handler // 统计耗时起点 private var startTime = System.currentTimeMillis() // 用于记录平均内存的列表 private val memorys = mutableListOf<Long>() private var fileWriter: Writer? = null private var logFile = File(getFileName()) val callback = Handler.Callback { message -> val sink = checkFileWriter() // 每来一条日志,记录此时的内存占用 memorys.add(Runtime.getRuntime().totalMemory()/(1024*1024)) when (message.what) { // 输出日志结束的标记 TYPE_FLUSH -> { sink.use { it.flush() fileWriter = null } // 统计耗时即内存终点 Log.v( "test", "fileWriter work is ok done=${System.currentTimeMillis() - startTime, memory=${memorys.average()}" ) } // 正常写日志 TYPE_LOG -> { val log = message.obj as String sink.write(log) sink.write("\n") } } false } companion object { private const val TYPE_FLUSH = -1 private const val TYPE_LOG = 1 //300 ms 无日志请求,则进行冲刷 private const val FLUSH_LOG_DELAY_MILLIS = 300L // 设计单例,防止启动多个写日志线程 @Volatile private var INSTANCE: FileWriterLogInterceptor? = null fun getInstance(dir: String): FileWriterLogInterceptor = INSTANCE ?: synchronized(this) { INSTANCE ?: FileWriterLogInterceptor(dir).apply { INSTANCE = this } } } // 启动写日志线程 init { handlerThread.start() handler = Handler(handlerThread.looper, callback) } override fun log(priority: Int, tag: String, log: String, chain: Chain) { if (!handlerThread.isAlive) handlerThread.start() handler.run { removeMessages(TYPE_FLUSH) obtainMessage(TYPE_LOG, "[$tag] $log").sendToTarget() val flushMessage = handler.obtainMessage(TYPE_FLUSH) // 倒计时,用于判断“已经无新日志” sendMessageDelayed(flushMessage, FLUSH_LOG_DELAY_MILLIS) } chain.proceed(priority, tag, log) } override fun enable(): Boolean { return true } // 以今天日期为文件名 private fun getToday(): String = SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().time) private fun getFileName() = "$dir${File.separator}${getToday()}.log" // 构建 java.io 压缩文件输出流 private fun checkFileWriter(): Writer { if (fileWriter == null) { fileWriter = logFile.outputStream().gzip().writer().buffered() } return fileWriter!! } // 自定义装饰流构造方法,以简化流构建代码 private fun OutputStream.gzip() = GZIPOutputStream(this) private fun OutputStream.writer(charset: Charset = Charsets.UTF_8) = OutputStreamWriter(this, charset)}
    2. 复制代码

    关于其中每一个细节的讲解可以点击面试题 | 怎么写一个又好又快的日志库?(一)

    下面是测试代码:

    1. // 分别给 EasyLog 配置 Okio 日志拦截器和 FileWriter 日志拦截器//EasyLog.addInterceptor(FileWriterLogInterceptor.getInstance(this.filesDir.absolutePath))EasyLog.addInterceptor(OkioLogInterceptor.getInstance(this.filesDir.absolutePath))MainScope().launch(Dispatchers.Default) { // 连续输出1万条日志并压缩 repeat(10_000) { EasyLog.v(str4, "test") }}
    2. 复制代码

    输出日志如下:

    1. fileWriter work is ok done=5130, memory=160.70305938812237fileWriter work is ok done=5172, memory=157.5844831033793fileWriter work is ok done=5155, memory=168.01649670065987Okio work is ok done=4765, memory=130.96940611877625Okio work is ok done=4752, memory=130.21985602879425Okio work is ok done=4779, memory=135.28374325134973
    2. 复制代码

    Okio 有 8% 左右的速度优势,及 20% 左右的内存优势。

    附面试思维导图(仅供参考)

     

    总结

    • 压缩日志是提升日志库性能的手段之一,常用的 Gzip 是压缩手段之一,Okio 和 java.io 都提供了对 Gzip 的支持,不过 Okio 在速度和内存上都稍好于 java.io。

  • 相关阅读:
    Spring源码-面试题-Bean的生命周期
    electronjs入门-聊天应用程序,与Electron.js通信
    K-近邻算法的 sklearn 实现
    电脑如何在网页上下载视频 浏览器如何下载网页视频
    detectron2环境搭建及自定义coco数据集(voc转coco)训练
    Flink 命令行提交、展示和取消作业
    C#中密封类和密封方法
    以高字节地址为字地址是什么
    水仙花数_pyhon实现
    美妆行业全网声量统计与传播趋势分析,完美日记位居品牌声量榜一
  • 原文地址:https://blog.csdn.net/m0_72134256/article/details/126408905