异步日志实现

  上周发来个运营需求:服务器得接收各类运营消息,并记录下来(二进制文件、入库)。

  我们的消息处理是单线程轮询取队列的方式,如在响应函数中直接调IO等耗时操作,整个处理线程都会被阻塞。所以设计了这个异步日志模块。核心代码如下:

//如果写得非常快,瞬间把两片buf都写满了,会阻塞在awakeChan处,等writeLoop写完log即恢复
//两片buf的好处:在当前线程即可交换,不用等到后台writeLoop唤醒
func (self *AsyncLog) Append(pdata []byte) {
    isAwakenWriteLoop := false
    self.Lock()
    {
        self.curBuf = append(self.curBuf, pdata)
        if len(self.curBuf) == cap(self.curBuf) {
            _swapBuf(&self.curBuf, &self.spareBuf)
            isAwakenWriteLoop = true
        }
    }
    self.Unlock()

    if isAwakenWriteLoop {
        self.awakeChan <- true //Notice:不能放在临界区
    }
}
func (self *AsyncLog) _writeLoop(bufSize int) {
    bufToWrite1 := make([][]byte, 0, bufSize)
    bufToWrite2 := make([][]byte, 0, bufSize)
    for {
        <-self.awakeChan //没人写数据即阻塞:超时/buf写满,唤起【这句不能放在临界区,否则死锁】

        self.Lock()
        {
            //此时bufToWrite为空,交换
            _swapBuf(&bufToWrite1, &self.spareBuf)
            _swapBuf(&bufToWrite2, &self.curBuf)
        }
        self.Unlock()

        //将bufToWrite中的数据全写进log,并清空
        self.writeLogFunc(bufToWrite1, bufToWrite2)
        _clearBuf(&bufToWrite1)
        _clearBuf(&bufToWrite2)
    }
}
func (self *AsyncLog) _timeOutWrite() {
    for {
        time.Sleep(Flush_Interval * time.Second)
        self.awakeChan <- true
    }
}

  Append()给逻辑线程调用的,负责填充buffer,buffer被写满即唤醒后台写线程_writeLoop进行IO。共有四片buffer,在前后台之间交换,提高处理效率。

  需要额外注意的地方——锁的使用。保证线程安全的前提下,尽量减少临界区代码。

  线程安全:注意锁重叠。感觉这是踩坑经验练粗来的~,多看别人的竞态分析,很有帮助。

  附上本段代码的race condition:

    1、"go chan"内部也是锁实现的,chan操作不要放在临界区,否则就锁中套锁了,极其危险。

    2、比如连续两次触发buf被写满,第二次的chan会阻塞,挂起Append()的线程

      若chan位于临界区内则还占用着Mutex

      后台writeLoop被唤醒时,同样要访问临界区,就被挂起了

      然后两线程此时就都挂着咯~

  减少临界区:多利用栈变量,转移共享变量的资源,这样真正的操作可以放在临界区之外。

  还有个timeout线程,间隔5秒唤醒一次_writeLoop写日志,免得Append()调用不多,数据一直不落地。

  【其它问题】

  1、强关进程,内存中的缓冲数据很可能丢失。

  2、逻辑如果写的非常快,Append()可能变成阻塞调用。

    a)这个可以搞定:前后台各增加一个bufferList;前台buffer被写满就仍进bufferList,两片buff都满了new个新的,保证前台一直有buffer可用。

    b)后台临界增加“交换前台的bufferList”,IO完毕后清空后台bufferList。

  【c++】

  后续打算用c++重写,用c++的话就不必timeout线程了,用条件变量替代“go chan”,自带超时,仅需一条后台线程即可。

  得额外处理new出buffer的回收:初始的四片buffer(curBuf、spareBuf、bufToWrite1、bufToWrite2)连同new出的,其内存块均可能被移至bufferList。

  为保证初始的四片buffer始终在bufferList的前四个位置,buffer的swap应换为move:

    1)前台:curBuf移进List;spareBuf非空则curBuf = move(spareBuf);反之curBuf = new()

    2)后台:交换bufferList,curBuf移至后台bufferList,curBuf = move(bufToWrite1),spareBuf = move(bufToWrite2)

    3)IO完毕后,若bufToWrite1、bufToWrite2为nullptr,将后台bufferList头部移进去,其它的回收。

    4)用shared_ptr包装buffer,方便自动回收。

时间: 2024-10-27 06:38:24

异步日志实现的相关文章

log4j 异步日志问题分析

1. 常用的DailyRollingFileAppender与RollingFileAppender是否同步? 1.1 代码分析 2. log4j 1.2.x提供了异步appender是什么?AsyncAppender 2.1 AsyncAppender配置 2.2 AsyncAppender分析 3. log4j 2.x 异步日志问题的解决方案及分析 3.1 log4j 2.x 异步日志问题的解决方案 3.2 log4j 2.x 异步日志性能高的关键 1. 常用的DailyRollingFil

Log4j2中的同步日志与异步日志

1.背景 Log4j 2中记录日志的方式有同步日志和异步日志两种方式,其中异步日志又可分为使用AsyncAppender和使用AsyncLogger两种方式. 2.Log4j2中的同步日志 所谓同步日志,即当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句. 下面通过一个例子来了解Log4j2中的同步日志,并借此来探究整个日志输出过程. log4j2.xml配置如下: <?xml version="1.0" encoding="UTF-8"

异步日志

[z]https://www.jianshu.com/p/9f0c67facbe2 简介 Apache Log4j 2 is an upgrade to Log4j that provides significant improvements over its predecessor, Log4j 1.x, and provides many of the improvements available in Logback while fixing some inherent problems

log4j2用asyncRoot配置异步日志是如何使用disruptor

用asyncRoot配置对应的对接disruptor类是AsyncLoggerConfigDisruptor,用Log4jContextSelector启动参数配置全局异步的对应的对接disruptor类是AsyncLoggerDisruptor.下面分析的是AsyncLoggerConfigDisruptor disruptor的创建与启动需要的部件实现 AsyncLoggerConfigDisruptor.start方法用来创建并启动disruptor实例 创建disruptor需要Even

log4j2用Log4jContextSelector启动参数配置全局异步日志是如何使用disruptor

与 log4j2用asyncRoot配置异步日志是如何使用disruptor差异有几个: 给disruptor实例的EventFactory不同 此处EventFactory采用的是RingBufferLogEvent.FACTORY,newInstance逻辑大致是: public RingBufferLogEvent newInstance() { final RingBufferLogEvent result = new RingBufferLogEvent(); if (Constant

logback异步日志

一.为什么使用异步日志Why 为提高程序性能,尽量默认都使用异步日志,如果不使用,可能日志在打包的时候,会占用大量磁盘IO和CPU,导致程序性能下降 二.依赖 <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.20</version> </dependency> <depend

Log4j2使用总结(异步日志)

TimeBased Triggering Policy 基于时间的触发策略.该策略主要是完成周期性的log文件封存工作.有两个参数: interval,integer型,指定两次封存动作之间的时间间隔.单位:以日志的命名精度来确定单位,比如yyyy-MM-dd-HH 单位为小时,yyyy-MM-dd-HH-mm 单位为分钟 modulate,boolean型,说明是否对封存时间进行调制.若modulate=true,则封存时间将以0点为边界进行偏移计算.比如,modulate=true,inte

Log4j2:异步日志中打印方法名和行号信息

1. 解决方案 异步logger,还需要在pom.xml中添加disruptor的依赖: includeLocation结合异步logger使用,当其设置为true时,才会显示具体的行号,以及日志所在的类名: 如果设置为false,哪怕<Pattern>设置了输出行号也不会显示出来: 2. pom配置 <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifa

你所不知道的日志异步落库

在互联网设计架构过程中,日志异步落库,俨然已经是高并发环节中不可缺少的一环.为什么说是高并发环节中不可缺少的呢? 原因在于,如果直接用mq进行日志落库的时候,低并发下,生产端生产数据,然后由消费端异步落库,是没有什么问题的,而且性能也都是异常的好,估计tp99应该都在1ms以内.但是一旦并发增长起来,慢慢的你就发现生产端的tp99一直在增长,从1ms,变为2ms,4ms,直至send timeout.尤其在大促的时候,我司的系统就经历过这个情况,当时mq的发送耗时超过200ms,甚至一度有不少t