文前导读
muduo 网络库源码剖析系列的第二篇文章,主要着眼于 muduo 网络库中的日志系统后端的设计与实现
为了保证自己对 muduo 的代码有较为深入的理解,我自己写了一个 tmuduo 网络库,用来验证自己对 muduo 源码上的一些想法。
仓库地址为:git@github.com:Phoenix500526/Tmuduo.git,欢迎 fork、start 以及 follow,一起学习。
AsyncLogging.{h,cc} 的实现
AsyncLogging 是整个日志系统的后端线程,其中 append()
函数作为前端 Logger 类的输出回调函数。当用户使用如LOG_DEBUG << "Hello world"
语句时,会将日志信息封装为 Buffer
1 | // AsyncLogging.h 文件(节选) |
1 | // AsyncLogging.cc |
整个 AsyncLogging 类的设计采用了双缓冲的方式实现,主要的思路是
采用两个缓冲:A 和 B。前端负责往缓冲 A 中写数据,而后端负责将缓冲 B 的数据写入文件。当 A 写完后,则交换 A 和 B,然后让后端将缓冲 A 写入文件,而前端则继续往 B 中填充数据,并不断重复这个过程。
这样的做法,好处是在新建日志的时候不必等待磁盘文件操作,另外批处理的方式也能避免后端线程被频繁唤醒。实际实现中,为了进一步减少日志前端的等待,使用了两个工作缓冲区 + 两个备用缓冲区,共四个缓冲区。
另外,日志系统的后端实现中仅使用了缓冲区,而没有使用消息队列,主要还是出于功能和性能的考虑。从功能上讲,后端线程需要负责间隔刷新,当时间间隔 (默认是 3 秒) 抵达时,不论是否有数据抵达,后端线程都需要刷新缓冲区以避免程序崩溃而导致日志丢失。其次,从性能上讲,只有单个消费者,因此不使用阻塞队列,就可以避免每次有日志信息抵达就要去 notify 后端线程一次。
对于 AsyncLogging 的实现而言并不复杂,真正有难度的点在于对日志刷新情况的把握,主要可以分为:
正常情况:
- 前端未写满缓冲区就发生了超时,此时程序应当有何种行为?
- 前端在发生超时以前就写满了缓冲区,此时程序应当有何种行为?
- 前端产生了过多的日志导致写超出的情况,此时程序应当有何种行为?
- 后端因写入速度太慢,导致写超出的情况,,此时程序应当有何种行为?
异常情况:出现了死锁或者死循环,导致后端日志消息堆积,此时程序应当有何种行为?
对于上述的情况,陈硕在他的书 《Linux 多线程服务端编程 —— 使用 muduo C++ 网络库》中有详细的描述,并配有时序图,在此不赘述。
LogFile.{h, cc} 的源码及实现
1 | //LogFile.h |
1 | // LogFile.cc |
对于 LogFile 而言,其主要的作用是实现文件滚动。文件滚动的触发条件有两个:
- 当文件的大小超过一定值后
- 每隔一天滚动一次
当满足滚动条件时,新建的日志文件名应满足:程序名.时间.主机名.进程id.log(例如:logtest.20200713-171322.hostname.1234.log)。
其中主要的函数有两个 rollFile
和 append_unlocked
。主要说说 append_unlocked()
函数。
对于滚动的第一个条件而言,每次写入日志信息时都会用 记录写入信息的大小。通过 writtenBytes()
就可以知道当前往文件中写入了多少字节的数据。
对于第二个条件,append_unlock()
会记录所插入的日志条数,每当插入日志的记录数超过 checkEveryN 时就检测当前时间,并从中取出天数和 startOfPeriod_ 进行比较,如果不在同一天,则执行 rollFile()
一些其他的问题
为什么 AsyncLogging 中的部分函数需要使用到锁,而 LogFile 当中要提供无锁版本?
这主要还是因为 AsyncLogging 和许多日志前端打交道,是一对多的问题,因此必须考虑线程安全性。而 LogFile 只和 AsyncLogging 打交道,是一对一的情况,主要以无锁版本为主。实际上在 muduo 网络库中的 example 以及测试代码中,AsyncLogging 使用的都是 LogFile 的 append_unlocked()
函数
- 本文作者: Phoenix
- 本文链接: http://hacker-cube.com/2020/11/03/muduo-网络库日志后端/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!