双缓冲区技术-异步日志系统

1. 问题场景

    在设计模式中,生产者-消费者模式肯定是排在前面位置的,在实际开发过程中,也常常需要使用这个模式。

    在讲解设计模式的书籍中,只会从抽象的角度对生产者-消费者模式进行讲解。面对实际的编程问题,还需要具体问题具体分析。

    这里给出一个使用场景,你可以停下来思考一下该如何解决问题:

    你需要实现一个日志库,每一个线程调用日志库API函数来写入日志信息,所有的日志信息需要持久化到本地的文件系统。

    咱们来分析一下,有哪些问题需要解决:

  • 多线程调用日志库的API,所以API函数必须是线程安全的。
  • 日志信息无法预计频率,要考虑波峰和波谷。
  • 日志信息需要持久化到本地文件系统中,涉及到文件操作,而文件的IO操作速度相对于CPU来说是很慢的。
  • 每一条日志信息不应该是立刻写入到文件,而是应该缓存在内存中,当数据量积累到一定的大小再写入到文件(当然,也要考虑定时写入文件)。

2. 解决思路

    咱们再回到生产者-消费者模式上。书籍上在介绍这种模式时,一般都是同步模式,即:

  • 生产者产生一个数据后通知消费者,然后等待数据被“消费”;
  • 消费者收到生产者的通知后,“消费”数据,然后再通知生产者继续生产。

    生产和消费交替执行,所以我称之为同步模式。

    但是,在上面所说的日志系统中,显然不能用同步模式。因为日志的产生速度与写文件速度是无法预估的,例如:在某个场景下日志的产生速度非常快,而在另一个场景下日志的产生速度特别慢。

    对于这样的需求,生产者(日志的产生)和消费者(把日志写入文件)速度不匹配,显然应该使用不同的线程来执行。此时,你是不是立刻想到使用消息队列来进行数据缓冲,不就解决了这个速度不匹配的问题?也就是这样:

  • 生产者:往队列的头部插入日志信息(入队)
  • 消费者:从队列的尾部读取日志信息(出队)

    当然,你还考虑到因为他们是不同的线程,在操作同一个队列时,需要用一个锁(Mutex)来保护消息队列。大概就是这个样子:

 

    看起来很完美,但是有一个问题:消费者从消息队列每读取一条日志信息就,写入文件系统,但是写文件操作是很耗时的。频繁的从消息队列中获取数据,而且每次都要上锁,一定会对生产者的写日志效率产生影响,因为生产者也要对消息队列上锁才能把日志信息插入队列的头部,如果此时消息队列正好被消费者锁住了,那么生产者就必须伤心的等待了~~这样就会很大影响到日志系统整体的吞吐率。

3. 使用双缓冲

    既然消费者的写文件速度比较慢,一定不能影响了生产者的写入效率,所以我们可以用两个消息队列来分别存储:正在写入的日志信息,正在读取的日志信息,也就是所谓的“双缓冲”技术。用图画出来就是这个样子:

 

    注意上图中,内存中用来缓存日志的空间不一定要用消息队列,因为日志信息往往都是字符串类型,直接用一块连续的堆或栈的空间来存储就可以了,所以图中就用“缓冲区1”、“缓冲区2”来表示。

    在这个模型中,生产者向缓冲区1中写日志信息;而消费者从缓冲区2中读取日志信息,这样的话,消费者的写文件操作无论怎么慢都不会影响到生产者产生日志了。

    此时,你肯定要说:缓冲区1和缓冲区2是两个独立的内存空间,当缓冲区1被写满后如何把其中的内容“复制”到缓冲区2中呢?

    好问题,这也是日志系统实现高吞吐率的关键地方!

4. 缓冲区交换

    最直觉的想法就是在某个时刻(比如:缓冲区1写满了,缓冲区2空了,定时),把缓冲器1中的内容用memcpy或者其他的系统函数,复制到缓冲区2中。 当然在复制数据的过程中需要对这两个缓冲区都上锁,在临界区完成复制或者移动操作,而且这个移动操作要尽可能的快,这样才能对生产者和消费者产生最小的影响。但是如果数据量比较大,移动操作还是比较耗时。

    再仔细想想,其实我们需要的不是真正的移动操作,而是有一个地方让生产者存放产生的数据,同样的有一个地方让消费者读取数据,只要达成这个目的就可以了。

    还有一个更好的方法,就是直接交换两个缓冲区的地址。我们只需要把生产者在写入数据时的指向缓冲区1的指针重新指向缓冲区2, 把消费者读取数据时指向的缓冲区2的指针重新指向缓冲区1,这样就达到了交换缓冲区的目的了。

  • 交换缓冲区之前:生产者向缓冲区1中写日志,消费者从缓冲区2中读日志。
  • 交换缓冲区之后:生产者向缓冲区2中写日志,消费者从缓冲区1中读日志。

    在执行交换操作的时候,也需要对这两个缓冲区上锁。但是在这个临界区操作的是:交换两个指针所指向的缓冲区空间,所以执行速度会非常快。

    具体到语言层面,对于C来说就是交换两个4字节的地址,对于C++来说可以利用容器类型的swap函数。

    这样画图更好理解:

    图中:左侧是执行交换操作之前的样子,右侧是执行交换操作之后的样子。可以看到生产者和消费者在任意时刻操作的都是不同的缓冲区,所以不存在相互影响,而且也达到了快速交换内容的目的。

    通过这样的双缓冲技术实现的日志系统,实际测试下来发现,吞吐率比很多开源的日志库要高很多。大家如果有兴趣,可以简单测试一下。

【总结】

    写到这里,我想表达的内容基本结束了。

    你可能还有其他的一些疑点,比如:什么时候交换缓冲区?写入缓冲区满了之后怎么处理?这些就属于另一个话题了。

    在这个实际的使用场景中,通过双缓冲技术,很好地解决了生产者和消费者之间的异步操作和速度不匹配问题,提高了日志系统的整体吞吐率。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下是一个使用 log4j.xml 配置异步日志的示例: ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false"> <appender name="ASYNC" class="org.apache.log4j.AsyncAppender"> <param name="BufferSize" value="1024" /> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> </appender> <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c{1}:%L - %m%n" /> </layout> </appender> <appender name="FILE" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="logs/app.log" /> <param name="MaxFileSize" value="10MB" /> <param name="MaxBackupIndex" value="10" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c{1}:%L - %m%n" /> </layout> </appender> <root> <level value="INFO" /> <appender-ref ref="ASYNC" /> </root> </log4j:configuration> ``` 在这个配置文件中,我们定义了三个 Appender:一个 ConsoleAppender 和一个 RollingFileAppender,以及一个 AsyncAppender,将其它两个 Appender 作为子 Appender 异步输出日志。AsyncAppender 的 BufferSize 属性定义了缓冲区大小,一旦达到这个大小,就会异步输出缓冲区中的日志。 在 root 节点中,我们将日志级别设为 INFO,同时将 AsyncAppender 指定为使用的 Appender。这样,在应用程序中记录的所有日志都会被异步输出到 Console 和 RollingFile 中。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值