當我們的程序運行到線上,或者說它處於一個我們無法調試,或者不方便調試的狀態下,日誌有助於我們查看當前程序的運行狀態,幫我們排除故障。也可以幫我們記錄服務器運行的記錄,從而可以還原一些處理過的關鍵信息,可以幫助我們避免一些無法查證的事情。
但是同步日誌有可能在性能方面,無法滿足服務器的要求,故而考慮設計異步日誌組件。
這個日誌組件並非是我獨立思考設計,其中參考了很多前輩的思想和經驗。在這寫下一些總結,來幫助自己更好的思考整個過程。
首先一個日誌組件應該具備以下特性:
- 日誌應該是分級別的,如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操作系統下編譯運行。
查看完整代碼可以點擊這裏。
這份代碼仍然有許多可以優化的地方,如將緩衝區變更爲無鎖隊列,放入共享內存運行,增加容災性等等。希望有興趣的朋友,可以繼續探索~