• 开源日志库log4cpp & muduo


    IO接口

    read/write/fsync

    • linux底层操作
    • 内核调用,涉及到进程上下文的切换,即用户态到核心态的转换,这是个比较消耗性能的操作

    fread/fwrite/fflush

    • c语言标准规定的io流操作,建立在read/write/fsync之上
    • 在用户层,又增加了一层缓冲机制,用于减少内核调用次数,但是增加了一次内存拷贝

    两者之间的关系:
    在这里插入图片描述

    对于输入设备,调用fsync/fflush将清空相应的缓冲区,缓冲区内数据将被丢弃

    对于输出设备或磁盘文件,fflush只能保证数据到达内核缓冲区,并不能保证数据到达物理设备,应该在调用fflush后,调用fsync,确保数据存入磁盘

    fflush/fsync功能区别

    fflush(FILE *)

    是libc.a中提供的方法,用来将流中未写的数据传送到内核。如果参数为null,将导致所有流冲洗

    把C库中的缓冲调用write函数写到磁盘(实际是写到内核的缓冲区)

    fsync(int fd)

    参数是一个int型的文件描述符,fsync是系统提供的系统调用,将数据写到磁盘上

    把内核缓冲刷到磁盘上

    总体流程:

    c库缓冲/用户缓冲 —fflush—> 内核缓冲 —fsync—> 磁盘

    sync()/fflush()/fsync()三个函数的区别

    1. 三者的用途不同

      sync, 是同步整个系统的磁盘数据

      fsync, 将打开的一个文件到缓冲区的数据同步到磁盘上

      fflush, 刷新打开的流

    2. 都是同步,但三者的同步等级不同

      sync, 将缓冲区数据写回磁盘,保持同步(无参数)

      fsync, 将缓冲区的数据写到文件中(有一个参数 int fd)

      fflush, 将文件流里未写出的数据立刻写出

    log4cpp开源日志库

    同步日志方式性能比较差只能用在客户端,异步日志方式可以用在服务端

    每次写日志都要去检查一次日志文件的大小,性能会很差

    1s钟写入1w条左右日志的日志库 是性能比较差的,会影响服务的性能

    log-src/src-log4cpp/4-RollingFileAppender.cpp 测试同步日志每秒写文件次数

    log-src/src-log4cpp/5-StringQueueAppender.cpp 测试异步日志每秒写文件次数

    在build目录中,进行编译运行

    同步日志

    运行测试文件,ops为每秒写入日志行数(电脑是ssd硬盘)

    以下两种都是同步日志方式,RollingFileAppender差于FileAppender

    在这里插入图片描述

    滚动日志 RollingFileAppender

    • 简介

      可配置日志文件个数,每个日志文件的大小

      _append函数在每次写入日志的时候,都要去获取文件大小,如果超过设定的大小就新创建一个日志文件

      性能较差

    在这里插入图片描述

    • 滚动原理:

      1)每次写入先获取日志文件大小,如果没有超过设定大小,直接写入

      2)如果超过设定大小,进行滚动

      3)比如最后一个日志文件log.3,删除log.3

      4)log.2->log.3, log.1->log.2, log->log.1

      5)新创建 .log,将日志写入 .log中

    类似一个固定大小的队列,先入先出原理,满了就把最先进来的踢出去

    • 日志性能分析

      实时写入磁盘,单笔write

      回滚日志每次都读取日志文件大小,不能每次读取文件大小

    普通日志 FileAppender

    异步日志

    StringQueueAppender

    功能是将日志记录到一个字符串队列中,该字符串队列使用了STL中的两个容器,

    即字符串容器std::string和队列容器std::queue,具体如下:

    std::queue _queue;

    _queue变量是StringQueueAppender类中用于具体存储日志的内存队列。

    先将日志存到队列中,再异步获取存日志的队列,写入文件,可以实现异步日志

    muduo开源日志库

    在这里插入图片描述

    日志写入队列的时候用mutex+notify进行通知

    mutex:多线程互斥

    日志写入磁盘是批量写

    该日志库如何做到高性能?

    双缓存机制

    1. 日志notify的问题
    • 写满一个buffer才notify一次,进行插入日志
    • 另外一个线程通过wait_timeout去读取日志,然后写入磁盘(当收到notify/timeout超时,就去执行)

    在这里插入图片描述

    1. 使用两个buffer,能够避免buffer不断分配,一个用来读,一个写

    2. buffer 默认4M一个

      写满4M ,notify一次

    双队列

    代码重点:双缓冲、双队列、锁的粒度、move语义、批量日志插入队列、日志读取写入磁盘

    • append为前台线程

      维护双缓冲区currentBuffer_,nextBuffer_;一个队列buffers_(是vector)

      当需要写入日志,如果currentBuffer_剩余空间够,直接写入;

      否则,将currentBuffer_加入到buffers_中,nextBuffer_空间给currentBuffer_,如果日志信息写入过快,把currentBuffer_和nextBuffer_都用光了,只好分配一块新的currentBuffer_;并通知后台线程

    • threadFunc为后台线程

      维护双缓冲区newBuffer1,newBuffer2;一个队列buffersToWrite(用来和前端buffers_交换)

      等待超时,将currentBuffer_写入buffers_,置空,nextBuffer_也置空(加锁)

      感觉后台线程2个缓冲区的作用是用于清空前台缓冲区

      接下来交换bufferToWrite和buffers_,这就完成了将记录了日志消息的buffer从前端到后端的传输,后端日志线程慢慢进行IO即可

    个人总结

    前台线程全程加锁,因为用到的缓冲buffer与后台线程是共享数据

    前台线程向1个缓冲buffer中写入日志,使用2个缓冲buffer做交替,避免不断分配buffer

    缓冲buffer写满之后,通知后台线程开始工作

    后台线程同时有个定时逻辑,到时间自动开始;将有数据的前台buffer放到队列中,清空前台2个缓冲buffer

    swap两个队列,用新的队列去写日志文件

    代码实现主要在AsyncLogging.h/cc

    class AsyncLogging : noncopyable
    {
     public:
    
      AsyncLogging(const string& basename,
                   off_t rollSize,
                   int flushInterval = 3);
    
      ~AsyncLogging()
      {
        if (running_)
        {
          stop();
        }
      }
    
      void append(const char* logline, int len); // 负责收集业务线程发来的日志
    
      void start() // 启动线程
      {
        running_ = true;
        thread_.start();
        latch_.wait();
      }
    
      void stop() NO_THREAD_SAFETY_ANALYSIS // 关闭线程
      {
        running_ = false;
        cond_.notify();
        thread_.join();
      }
    
     private:
    
      void threadFunc(); // 异步线程处理逻辑
    
      typedef detail::FixedBuffer Buffer;
      typedef std::vector> BufferVector;
      typedef BufferVector::value_type BufferPtr;
    
      const int flushInterval_;
      std::atomic running_;
      const string basename_;
      const off_t rollSize_;
      Thread thread_;
      CountDownLatch latch_;
      MutexLock mutex_;
      Condition cond_ GUARDED_BY(mutex_);
      BufferPtr currentBuffer_ GUARDED_BY(mutex_); // 当前写入的buffer
      BufferPtr nextBuffer_ GUARDED_BY(mutex_); // 下一个备用buffer
      BufferVector buffers_ GUARDED_BY(mutex_); // 写入完毕,待打印的buffer集合
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
  • 相关阅读:
    Python机器视觉--OpenCV进阶(核心)--图像金字塔(高斯金字塔与拉普拉斯金字塔)
    23设计模式之 --------- 什么是设计模式?
    CentOS 6.5安装配置LNMP服务器(Nginx+PHP+MySQL)
    Redis 7.0 源码调试环境搭建与源码导读技巧
    【论文合集】2022年12月医学影像期刊论文合集
    IDEA 高版本 PlantUML 插件默认主题修改
    【进阶】Spring中的注解与反射
    C/C++-内存
    基于OpenCV设计的流媒体播放器(RTSP、RTMP)
    【c语言中的指针常量和常量指针介绍】
  • 原文地址:https://blog.csdn.net/Dorothy_yy/article/details/126810838