muduo网络库学习笔记之日志系统

9 篇文章 0 订阅

基本框架

muduo日志系统采用的是多生产者单消费者模型,基本思路是,前端线程(产生日志消息的线程)和后端线程(负责将缓冲区中的日志信息写入磁盘文件的线程)各自维护一块buffer,前端线程维护bufferA,后端线程维护bufferB。多个前端线程往bufferA中写日志,当bufferA被写满 或者 当设定的磁盘刷新时间间隔到达,唤醒后端线程,交换bufferA和bufferB,后端线程把bufferA中的日志消息写入磁盘文件,前端线程则可以往bufferB中写入日志消息,然后等待再次唤醒后端线程将bufferB中的日志消息写入磁盘文件,如此往复。如下图所示:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

使用两块buffer的好处是,前端线程的buffer写满后,交换bufferA和bufferB后,后端线程将bufferA写入磁盘的过程不会阻塞前端线程往bufferB中写日志,因为bufferA和bufferB交换过程所处的临界区很小,后面会结合代码说明这一点。在具体的实现上,muduo采用了四块buffer,前端线程和后端线程各两块,此外,前后端还各自维护了一块buffer数组,后面结合代码进行说明。基本逻辑如下图所示。

在这里插入图片描述
(有多种可能的情况,这里只画了一种可能的情况,其它的可能情况,陈硕老师的《Linux多线程服务器编程》中写得非常详细)

结合代码分析

代码结构及其主要功能

muduo网络库中日志系统相关的代码文件主要如下:


├── AsyncLogging.cc
├── AsyncLogging.h
├── Condition.cc
├── Condition.h
├── CountDownLatch.cc
├── CountDownLatch.h
├── FileUtil.cc
├── FileUtil.h
├── LogFile.cc
├── LogFile.h
├── LogStream.cc
├── LogStream.h
├── Logging.cc
├── Logging.h
├── Mutex.h
├── Thread.cc
├── Thread.h
├── noncopyable.h

核心代码文件功能的简要说明:

  • AsyncLogging.{h, cc} 维护前端线程的buffer,提供了后端线程将日志消息写入磁盘的接口;
  • FileUtil.{h, cc} 提供了具体的文件操作,即真正的将日子消息写入文件的操作;
  • LogFile.{h, cc} 封装了FileUtil中的文件操作,提供了间隔刷新和日志文件回滚操作;
  • LogStream.{h, cc} 实现了输出流,类似于 cout << ;
  • Logging.{h, cc} 封装了 LogStream,提供了写日志的接口;
  • 其他的一些文件用来辅助实现日志系统;

日志消息从前端写入到后端写入磁盘文件的过程

  1. 日志系统启动(暂时先不考虑日志系统启动的过程),前端调用Logging中 Logger 类提供的写日志接口,如下所示;

    说明:只会截取重要的代码片段,省略无关代码片段,非必要代码通过注释说明, 下同

    #define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
      muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
    #define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
      muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
    #define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
      muduo::Logger(__FILE__, __LINE__).stream()
    // ...
    

    前端程序只需要通过类似于 LOG_INFO << “this is a log message”; 的调用即可写一条日志,即 Logger(…).stream() << “…”;

  2. 每写一条日志都会创建一个Logger实例,如第1步所示,调用stream()方法,获取封装的 LogStream 成员变量;

    LogStream 类封装了一个 FixedBuffer 成员,重载了 operator<< 函数,将各种类型的消息写入 FixedBuffer 中。FixedBuffer 类维护了一块固定大小的 buffer,类似 iostream。

  3. 前端调用 LOG_XXX 的接口后,将日志写入 FixedBuffer 中,然后需要将 FixedBuffer 中的日志拷贝到由 AsyncLogging 类维护的前端的buffer中,然后执行上文图中所示的前端buffer和后端buffer的流转。

    将FixedBuffer类中维护的buffer拷贝到 AsyncLogging 类维护的buffer中,实现的非常巧妙。具体而言,Logger类在析构函数中实现了拷贝操作,如下面的代码片段所示:

    Logger::~Logger()
    {
    		// ...
    		// using Buffer = FixedBuffer;
        const LogStream::Buffer& buf(stream().buffer());    // 获取FixedBuffer中的buffer,注意,这里返回的引用,不会产生拷贝开销
        output(buf.data(), buf.length());    // output 是一个回调函数,是Logger提供给前端程序的接口
    }
    

    上面output函数如下所示,

    void output(const char* msg, int len)
    {
        // ...
        AsyncLogger_->append(msg, len);       // AsyncLogger 为AsyncLogging的实例对象,append函数实现了日志消息写入前端buffer的操作
    }
    

    总结一下第1步到第3步的流程:

    LOG_INFO << "hello world";
    // 1. 调用 LOG_XXX 接口,创建 Logger 临时对象,并调用 stream() 成员方法,获取 Logger类中封装的 LogStream类对象;
    // LogStream类重载了 operator<< 方法,此外LogStream类中封装了 FixedBuffer 对象,
    // 重载的 operator<< 方法将日志消息写入 FixedBuffer 封装的固定大小的buffer中,
    
    // 2. LOG_INFO << "hello world"; 语句执行完毕,创建的临时对象被销毁,Logger对象销毁前,调用 Logger 的析构函数;
    // 在Logger的析构函数中,通过回调函数 void output(const char* message, int len);  将日志写入由 AsyncLogging 类维护的前端的buffer中;
    Logger::~Logger()
    {
        const LogStream::Buffer& buf(stream().buffer());
        output(buf.data(), buf.length());
    }
    // output 函数实际上是由用户程序注册,调用 Logger 中的static方法
    typedef void (*OutputFunc)(const char* msg, int len);
    static void setOutput(OutputFunc);
    
    // 3. output 函数一般为:
    void output(const char* msg, int len)
    {
        AsyncLogger_->append(msg, len);
    }
    // AsyncLogger_ 为 AsyncLogging 实例对象
    
    // 4. AsyncLogging::append(const char* msg, int len) 函数
    void AsyncLogging::append(const char* logline, int len) {
      MutexLockGuard lock(mutex_);
      if (currentBuffer_->avail() > len)
        currentBuffer_->append(logline, len);
      else {
    		// ...
        buffers_.push_back(currentBuffer_);
        // ...
      }
    }
    

    **思考题1:**LogStream中的FixedBuffer是必须的吗?可以直接写入由 AsyncLogging 类维护的前端的buffer中吗?

    **思考题2:**每写一条日志消息都要创建一个Logger对象,写完后又立即销毁,对日志系统的性能影响大吗?

  4. 日志消息写入由 AsyncLogging 类维护的前端的buffer中后,当前端buffer被写满 或者 当设定的磁盘刷新时间间隔到达,会唤醒后端线程,将前端buffer写入磁盘。在具体的实现上,muduo采用了四块buffer,前端线程和后端线程各两块,此外,前后端还各自维护了一块buffer数组,下面结合代码说明。

    // 摘自 muduo 的 AsyncLogging.cc 中的源码
    void AsyncLogging::append(const char* logline, int len) {
      MutexLockGuard lock(mutex_);          // AsyncLogging通过一个mutex全局锁来实现前端buffer的访问
    	// 前端两块buffer,currentBuffer_ 和 nextBuffer_
      if (currentBuffer_->avail() > len)     // currentBuffer_ 未写满,继续写
        currentBuffer_->append(logline, len);
      else {                                 // currentBuffer_ 写满,需要唤醒后端线程进行写磁盘操作
        buffers_.push_back(currentBuffer_);  // 将currentBuffer_ 添加到前端维护的 buffers_ 数组中
        currentBuffer_.reset();              // 这里的 currentBuffer_ 由智能指针实现
        if (nextBuffer_)                     
          currentBuffer_ = std::move(nextBuffer_);   // nextBuffer_ 不为空,将nextBuffer_移动赋值给currentBuffer_ 
        else
    			// 这里的else分支很少会被执行,因为大多数情况下,nextBuffer_ 不会被写满;
    			// 这里需要结合后端的执行流来理解
          currentBuffer_.reset(new Buffer);        
        currentBuffer_->append(logline, len);
        cond_.notify();     // 唤醒后端线程
      }
    }
    

    这里先给出后端执行流的关键代码和必要注释,需要结合上面的前端执行流的代码对注释进行理解;

    // 真正执行日志写磁盘的后端线程执行的函数
    void AsyncLogging::threadFunc() {
      // ...
      LogFile output(basename_);       // LogFile封装了实际的写文件操作
    
    	// 后端的两块buffer和一块buffer数组
      // 一个细节:前端的buffer(在AsyncLogging的构造函数中初始化)和后端buffer在程序启动的时候全部填充为0,
    	// 避免程序热身时 page fault 引发的性能不稳定
      BufferPtr newBuffer1(new Buffer);
      BufferPtr newBuffer2(new Buffer);
      newBuffer1->bzero();
      newBuffer2->bzero();
      BufferVector buffersToWrite;
      buffersToWrite.reserve(16);    
    	
      while (running_) {
        // ... 
        {
          MutexLockGuard lock(mutex_);
          if (buffers_.empty())  // unusual usage!
          {
    				// 理解这一步对后端线程异步写日志很关键
    				// 若buffer_为空,说明前端线程的 currentBuffer_ 还未写满,
    				// 结合 AsyncLogging::append 函数中的 if (currentBuffer_->avail() > len) 条件判断进行思考
    				// 通过条件变量 cond_ 阻塞该部分,最多等待 flushInterval_ 秒,该部分就会解除阻塞;
            cond_.waitForSeconds(flushInterval_);
    				// 结合AsyncLogging::append函数中的 cond_.notify(); 来思考后端线程被唤醒的两个条件:
    				// 1.前端buffer被写满 或者 2.当设定的磁盘刷新时间间隔到达
    				// 前端的 currentBuffer_ 为写满,buffer_ 就为空,因此进入if语句,cond_.waitForSeconds(flushInterval_); 进行阻塞;
    				// 要么 currentBuffer_ 写满,前端进入else语句,cond_.notify(); 唤醒被阻塞的后端线程;
    				// 要么 flushInterval_ 时间已过,解除阻塞,即使 currentBuffer_ 没被写满,也会被后端线程刷新到磁盘中;
          }
    
    			// 前后端buffer的交换操作,此时已经离开了临界区,因此后端的写磁盘不会阻塞前端的写日志
          buffers_.push_back(currentBuffer_);
          currentBuffer_.reset();
          currentBuffer_ = std::move(newBuffer1);
          buffersToWrite.swap(buffers_);
          if (!nextBuffer_) {
            nextBuffer_ = std::move(newBuffer2);
          }
        }
    
        assert(!buffersToWrite.empty());
    
        if (buffersToWrite.size() > 25) {
          // 一种保护措施,若buffersToWrite.size() > 25,说明在一个时间小片段中,产生了大量的日志消息,很可能是系统存在攻击或故障
    			// 25,应该是经验值?
          buffersToWrite.erase(buffersToWrite.begin() + 2, buffersToWrite.end());
        }
    
    		// 写入文件
        for (size_t i = 0; i < buffersToWrite.size(); ++i) {
          output.append(buffersToWrite[i]->data(), buffersToWrite[i]->length());
        }
    
        if (buffersToWrite.size() > 2) {
          // drop non-bzero-ed buffers, avoid trashing
          buffersToWrite.resize(2);
        }
    
    		// 恢复后端线程的buffer
        if (!newBuffer1) {
          assert(!buffersToWrite.empty());
          newBuffer1 = buffersToWrite.back();
          buffersToWrite.pop_back();
          newBuffer1->reset();
        }
    
        if (!newBuffer2) {
          // 同上面的 newBuffer1 操作
        }
    
        buffersToWrite.clear();
        output.flush();   // 刷新至磁盘
      }
      output.flush();
    }
    

其他的一些细节

陈硕老师在书中对日志系统这部分已经阐述的非常详尽,一些细节就不再展开。不得不佩服陈硕老师的写作水平,通俗易懂且没有一丝拖沓。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值