异步日志组件的实现

本文介绍了异步日志组件的实现,以解决同步日志可能带来的性能问题。日志组件应具备分级别记录、日志滚动、线程ID记录及截断等特性。通过使用缓冲队列和多线程,实现写入和磁盘IO操作的分离,提高性能。代码适用于Linux系统,并留有优化空间,如无锁队列和共享内存等。
摘要由CSDN通过智能技术生成

当我们的程序运行到线上,或者说它处于一个我们无法调试,或者不方便调试的状态下,日志有助于我们查看当前程序的运行状态,帮我们排除故障。也可以帮我们记录服务器运行的记录,从而可以还原一些处理过的关键信息,可以帮助我们避免一些无法查证的事情。

但是同步日志有可能在性能方面,无法满足服务器的要求,故而考虑设计异步日志组件。

这个日志组件并非是我独立思考设计,其中参考了很多前辈的思想和经验。在这写下一些总结,来帮助自己更好的思考整个过程。

首先一个日志组件应该具备以下特性:

  • 日志应该是分级别的,如TRACE,DEBUG,INFO,WARN,ERROR这几个级别较为常见,不同的场景下,我们应该开启的日志级别应该是不同的,比如在开发过程中,我们更多的想要去记录程序的运行状态,那么,对日志级别的要求就会高一些,记录的信息也更多一些,但是在程序上线后,很多日志对于逻辑的处理并没有很大的帮助,反而对性能成为了不小的负荷,故而应该只记录关键信息
  • 日志应该是支持rollSize的,这个字段代表着一个日志的大小应该是有上限的,因为过大的日志,我们的文本阅读其实加载起来也非常吃力,并且不利于排查,这个可以根据日志大小或时间段来分割,比如日志大于10M时,新建一个文件,又或者每隔30分钟,或者1小时新建一个文件
  • 支持线程ID的记录,这样方便我们排查问题
  • 支持截断,这个为可选项,用来控制日志的长度

要完成以上特性,则必须为相应的特性增加相应的处理,用以完成必要的操作。

除了以上的日志组件本身所要求的特性,我们还希望提升它的性能,考虑到日志在日常使用下的状态,那么应该是一个多个地方在使用日志,统一在一处来进行处理。故而,可以抽象为一个多写,单独的抽象模型。

线程模型
我们用一个list来做缓冲队列,所以list就是我们需要征用的资源,当有线程在对list进行操作的时候,需要进行加锁(lock)操作。因此,我们的日志对象还需要mutex这样的对象。

故而我们的日志类,应该拥有如下成员变量:

	static bool                                                     m_toFile;                   //是否写入控制台
    static FILE*                                                    m_logFile;
    static bool                                                     m_truncate;              //是否截断日志
    static LOG_LEVEL                                                m_currentLevel;            //当前日志级别
    static std::string                                              m_fileName;
    static std::string                                              m_PID;
    static int64_t                                                  m_rollSize;
    static int64_t                                                  m_currentWrittenSize;       //已经写入的字数
    static std::list<std::string>                                   m_buffer;
    static std::unique_ptr<std::thread>                             m_thread;
    static std::mutex                                               m_mutex;
    static std::condition_variable                                  m_cond;
    static bool                                                     m_Exit;
    static bool                                                     m_running;

当写入线程给日志对象扔进数据时,实际上,我们是将它写入缓存队列,而另外启动一个线程,专门来进行对磁盘的IO操作,因为IO操作本身是一个耗时操作,故而,这样可以降低处理线程的等待时间。

下面是我们的写入代码:

void AsyncLog::output(int32_t logLevel, const char* fileName, int32_t lineNo, const char* args, ...)
{
    if(logLevel < m_currentLevel || logLevel > LOG_FATAL)
        return;

    std::string logInfo;

    makeLinePrefix(logLevel, logInfo);  //构建日志头部信息

    char buf[512] = { 0 };
    snprintf(buf, sizeof buf, "[%s:%d]", fileName, lineNo);
    logInfo.append(buf);

    va_list ap;
    va_start(ap, args);
    int32_t nLogMsgLength = vsnprintf(nullptr, 0, args, ap);
    va_end(ap);

    std::string strMsg;
    if((int32_t)strMsg.capacity() < nLogMsgLength)
    {
        strMsg.resize(nLogMsgLength + 1);
    }

    va_list aq;
    va_start(aq, args);
    vsnprintf((char*)strMsg.data(), strMsg.capacity(), args, aq);
    va_end(aq);

    if(m_truncate && nLogMsgLength > MAX_LENGHT_LINE)
    {
        logInfo.append(strMsg.c_str(), MAX_LENGHT_LINE);
    }else{
        logInfo.append(strMsg.c_str(), nLogMsgLength);
    }

    if(!m_fileName.empty())
    {
        logInfo += "\n";
    }

    //写入缓冲区,习惯用{}来对RAII技术的加锁区域包裹,实际上这里可以不写{}
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        m_buffer.push_back(logInfo);
        m_cond.notify_one();
    }

}

再来看一看我们单独启动的线程中,是如何处理缓冲区中的内容的:

void AsyncLog::writeThreadProc()
{
    m_running = true;
    while(m_running)
    {
        if(!m_fileName.empty())
        {
            if(m_currentWrittenSize >= m_rollSize)
            {
                char buf[64] = { 0 };
                time_t now = time(0);
                tm tim;
                localtime_r(&now, &tim);
                strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", &tim);
                std::string newFileName = m_fileName + "." + buf + ".log";
                if(!createNewFile(newFileName.c_str()))
                    return;
                m_currentWrittenSize = 0;
            }
        }

        std::string logInfo;
        {
            std::unique_lock<std::mutex> guard(m_mutex);
            while(m_buffer.empty())
            {
                if(m_Exit)
                    return;
                m_cond.wait(guard);
            }
            logInfo = m_buffer.front();
            m_buffer.pop_front();
        }

        std::cout << logInfo;

        if(m_logFile)
        {
            if(!writeToFile(logInfo))
                return;

            m_currentWrittenSize += logInfo.length();
        }
    }

    m_running = false;
}

这份代码中,虽然C++相关的代码是可以跨系统的,但是因为采用了不少的Linux系统调用,故而应在Linux操作系统下编译运行。

查看完整代码可以点击这里

这份代码仍然有许多可以优化的地方,如将缓冲区变更为无锁队列,放入共享内存运行,增加容灾性等等。希望有兴趣的朋友,可以继续探索~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值