muduo多线程异步日志分析

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/puliao4167/article/details/89884107

    最近在看muduo的源码,对于其日志系统的实现颇为感兴趣,找了两三天好好研究了一下,本文记录一些所学到的知识。

基础知识

    日志是每个高性能服务器必备的组件,分为两种:诊断日志和交易日志。诊断日志,主要的作用是供开发人员和运维人员进行故障诊断和追踪,如果系统在运行过程中出现异常,也可以通过脚本语言对日志进行排查,muduo的日志系统就是属于这一类。交易日志主要是用来记录系统状态的改变,当发生宕机等事故可以通过交易日志进行rollback,如mysql的redo日志。

    日志消息格式如下,这里的级别指的是日志的类型:TRACE、DEBUG、INFO、WARN、ERROR、FATAL。日志系统就按照如下消息格式进行记录。

日期 时间 线程 级别 正文 源文件名称 行号

    日志的滚动。目的是简化日志的查询检索,方便管理。滚动条件一般有两个:文件大小(如到了一定的限度就新建一个文件继续记录),时间(如每一天新建一个日志文件)

    一个日志系统除了要实现把系统打印的log或者发生的error记录成日志文件,在高性能服务器上还需要提高性能,如果在业务逻辑处理的线程中让其直接调用相应的系统调用生成日志文件,就易发生阻塞,降低业务逻辑事件的处理效率。因此,muduo使用多线程异步日志。整个系统库分成前端和后端。前端通过调用日志接口使用API,将消息传递给后端线程,后端线程负责将日志消息生成日志文件。因此是一个多生产者——单消费者问题。

    通常对于此类的并发问题,通常可以使用无阻塞队列实现,同时可以保证线程安全,但是如果用队列就会出现如下问题:对于生产者线程,每次生成一条日志消息,都会及时发送给消息队列;对于消费者,由于是单线程,如果对于每一个消息都要立刻放到日志文件的话,效率就会降低,因此muduo采用四个缓冲区技术实现,前端后端各有两个buffer,一个作为当前缓冲,一个作为备用缓冲。

代码和细节

    先整理一下相关文件,主要包含LogStream{h,cc} Logging{h,cc} LogFile{h,cc} AsyncLogging{h,cc}

    来看一下测试程序,然后在一步步深入源码。

/*
 * @Description: 利用muduo日志api打印异步日志 
 * @Author: haha_giraffe
 */
#include "muduo/base/AsyncLogging.h"
#include "muduo/base/Logging.h"
using namespace muduo;

AsyncLogging *logptr=NULL;

void asyncoutput(const char *msg,int len){
    logptr->append(msg,len);//前台进程传递日志
}

int main(int argc,char *argv[]){
    LOG_INFO<<"hello";
    char name[256];
    strcpy(name,argv[0]);
    AsyncLogging asynclog("asynclog",500*1000*1000);    
    asynclog.start();//这里先让后台线程运行
    logptr=&asynclog;
    Logger::setOutput(asyncoutput);
    for(int i=0;i<10;i++){
        LOG_INFO<<"ASYNC LOG";
        struct timespec ts = { 0, 500*1000*1000 };
        nanosleep(&ts, NULL);
    }
    
}

       当我们调用LOG_INFO的时候,其实是调用如下的宏定义,__FILE__ 是内置宏,代表源文件的文件名,__LINE__ 是内置宏,代表该行代码的所在行号。此时生成一个Logger类的临时对象,并调用stream()方法

#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
  muduo::Logger(__FILE__, __LINE__).stream()

    再来仔细看看Logger类。首先定义了枚举类LogLevel,表示日志类型,由低到高。然后两个内部类,一个SourceFile,另一个Impl。Impl类中包含了LogStream对象,通过这里可以将调用流对象将日志生成文件。可以看到Logger:stream()返回一个LogStream对象的引用,如果在LogStream类中对<<重载,可以将其输出到标准输出或者生成文件,这样逻辑就通了。

class Logger
{
 public:
  enum LogLevel
  {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL,
    NUM_LOG_LEVELS,
  };

  // compile time calculation of basename of source file
  class SourceFile
  {
   public:
    template<int N>
    inline SourceFile(const char (&arr)[N])
      : data_(arr),
        size_(N-1)
    {
      const char* slash = strrchr(data_, '/'); // builtin function
      if (slash)
      {
        data_ = slash + 1;
        size_ -= static_cast<int>(data_ - arr);
      }
    }

    explicit SourceFile(const char* filename)
      : data_(filename)
    {
      const char* slash = strrchr(filename, '/');
      if (slash)
      {
        data_ = slash + 1;
      }
      size_ = static_cast<int>(strlen(data_));
    }

    const char* data_;
    int size_;
  };

  Logger(SourceFile file, int line);
  Logger(SourceFile file, int line, LogLevel level);
  Logger(SourceFile file, int line, LogLevel level, const char* func);
  Logger(SourceFile file, int line, bool toAbort);
  ~Logger();

  LogStream& stream() { return impl_.stream_; }

  static LogLevel logLevel();//这是一个函数,函数名logLevel,返回是一个枚举类型LogLevel
  static void setLogLevel(LogLevel level);

  typedef void (*OutputFunc)(const char* msg, int len);
  typedef void (*FlushFunc)();
  static void setOutput(OutputFunc);
  static void setFlush(FlushFunc);
  static void setTimeZone(const TimeZone& tz);

 private:

class Impl
{
 public:
  typedef Logger::LogLevel LogLevel;
  Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
  void formatTime();
  void finish();

  Timestamp time_;
  LogStream stream_;
  LogLevel level_;
  int line_;
  SourceFile basename_;
};

  Impl impl_;

};

    所以顺序是Logger构造函数—>impl_构造函数—>stream()—>operator<<—>FixedBuffer::append()—>Logger析构—> g_output(注意,g_output是一个函数指针,也是一个接口,用户自己可以通过Logger::setOutput指定函数,函数中通过异步AsyncLogging::append()将其异步输出到日志文件,如果不指定就默认输出到stdout中)

扫描二维码关注公众号,回复: 7659623 查看本文章
Logger::Logger(SourceFile file, int line)
  : impl_(INFO, 0, file, line)
{
}


Logger::Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
  : time_(Timestamp::now()),
    stream_(),
    level_(level),
    line_(line),
    basename_(file)
{
  formatTime();
  CurrentThread::tid();
  stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());
  stream_ << T(LogLevelName[level], 6);
  if (savedErrno != 0)
  {
    stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
  }
}


LogStream& Logger::stream() { return impl_.stream_; }

void FixedBuffer::append(const char* /*restrict*/ buf, size_t len)
  {
    // FIXME: append partially
    if (implicit_cast<size_t>(avail()) > len)
    {
      memcpy(cur_, buf, len);
      cur_ += len;
    }
  }

Logger::~Logger()
{
  impl_.finish();//在日志结尾添加文件和行号
  const LogStream::Buffer& buf(stream().buffer());
  g_output(buf.data(), buf.length());//将日志传输到后端,由后端的线程写入日志文件
  if (impl_.level_ == FATAL)
  {
    g_flush();
    abort();
  }
}

异步日志实现

    最后来看一下异步日志AsyncLogging类,主要用于生成异步日志文件,前端线程调用append(),后端线程通过类对象调用start()函数在后台运行。

    前端线程中有两个缓冲区,currentBuffer_和nextBuffer_,还有一个vector存放待写入文件中已经填满的缓冲区buffers。每生成一个日志就需要调用一次append(),在append()中,首先判断当前缓冲区是否有足够剩余空间,如果有则填充,如果没有则将其放入buffers,然后将日志填充到备用的nextBuffer,如果备用的也用完了,则分配一个新的buffer。

    后端线程也有两个缓冲区和一个vector,将空闲的newbuffer1移动到currentBuffer_,然后将buffers与buffersToWrite交换,后面就可以将buffersToWrite中的日志数据写入文件,最后将newbuffer2替换给nextBuffer,这样前端始终有一个备用缓冲区。具体参见书籍。

//前端线程调用
void AsyncLogging::append(const char* logline, int len)
{
  muduo::MutexLockGuard lock(mutex_);
  if (currentBuffer_->avail() > len)
  {
    currentBuffer_->append(logline, len);
  }
  else
  {
    buffers_.push_back(std::unique_ptr<Buffer>(currentBuffer_.release()));

    if (nextBuffer_)
    {
      currentBuffer_ = std::move(nextBuffer_);
    }
    else
    {
      currentBuffer_.reset(new Buffer); // Rarely happens
    }
    currentBuffer_->append(logline, len);
    cond_.notify();
  }
}
//后台线程调用,
void AsyncLogging::threadFunc()
{
  //...省略部分代码
  while (running_)
  {
    {
      muduo::MutexLockGuard lock(mutex_);
      if (buffers_.empty())  // unusual usage!
      {
        cond_.waitForSeconds(flushInterval_);
      }
      buffers_.push_back(std::unique_ptr<Buffer>(currentBuffer_.release()));
      currentBuffer_ = std::move(newBuffer1);
      buffersToWrite.swap(buffers_);
      if (!nextBuffer_)
      {
        nextBuffer_ = std::move(newBuffer2);
      }
    }
    //...
    //...
  }
  output.flush();
}

结语

    muduo的异步日志性能固然很好,由于日志对于整个系统是非常重要的一环,在理解了muduo日志系统的原理和设计后,接下来打算自己摸索着做一个。

参考 《Linux多线程服务器编程使用moduo C++网络库》

博客 https://blog.csdn.net/u011228842/article/details/84000269

猜你喜欢

转载自blog.csdn.net/puliao4167/article/details/89884107