異步日誌組件的實現

當我們的程序運行到線上,或者說它處於一個我們無法調試,或者不方便調試的狀態下,日誌有助於我們查看當前程序的運行狀態,幫我們排除故障。也可以幫我們記錄服務器運行的記錄,從而可以還原一些處理過的關鍵信息,可以幫助我們避免一些無法查證的事情。

但是同步日誌有可能在性能方面,無法滿足服務器的要求,故而考慮設計異步日誌組件。

這個日誌組件並非是我獨立思考設計,其中參考了很多前輩的思想和經驗。在這寫下一些總結,來幫助自己更好的思考整個過程。

首先一個日誌組件應該具備以下特性:

  • 日誌應該是分級別的,如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操作系統下編譯運行。

查看完整代碼可以點擊這裏

這份代碼仍然有許多可以優化的地方,如將緩衝區變更爲無鎖隊列,放入共享內存運行,增加容災性等等。希望有興趣的朋友,可以繼續探索~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章