為何要列印日志?讓程式裸奔不是一件很快樂的事麼?
有些BUG就像薛定谔的貓,具有波粒二象性,當你試圖去觀察它時它就消失了,當你不去觀察它時,它又會出現。當你在測試人員面前賭咒發誓,親自路演把程式跑一遍的時候,這些bug就會神奇的消失;一旦離開你的騷操作重新回到測試人員手中,這些bug又會突然的出現。這就是開發人員和測試人員互相對天發誓自證清白的尬聊場面。
因為這些幽靈Bug破壞了團隊的氛圍,傷害了開發和測試人員的感情,進而導緻了産品品質下降。 是以非常有必要找到這些bug發生的原因。
試圖找到這些具備量子特性的bug發生的原因,通常的手段就是列印日志,因為你很難通過幾次思想實驗或幾次尬聊就能自證清白。
列印日志是查找、分析、定位問題的一個非常有效的手段。尤其對背景程式而言更加重要,背景程式需要長期運作,運作期間會面臨各種各樣的情況,這些情況包括正常業務邏輯,非正常業務邏輯,甚至是異常情況。我們往往需要在不同情況下将這些發生的事情按時間軸記錄下來,進而在系統出現問題或瓶頸時進行回溯分析。
Java中有現成的日志列印類log4j,并将日志定義了4個級别:ERROR、WARN、INFO、DEBUG。通常級别越高輸出的日志越少,如果你的背景程式一天就能輸出一個G的ERROR日志,那也是非常不容易的事,基本可以告别程式員生涯了,還打啥日志啊?還能讓測試人員愉快的工作麼?
通常在出現ERROR日志時,我們就需要定位問題,這就需要列印大量的DEBUG或INFO日志來進行問題回溯。這篇文章不是介紹Java如何列印日志,網絡上的文章已經很多了。
這裡說一下C++在高并發下如何列印日志,這裡實作最基本功能(僅做抛磚引玉之用)。通常我們會寫一個日志輸出函數,如下:
void Logout(const char* pszLogFile, const char* pszLog)
{
// 參數檢查
assert(pszLogFile);
assert(pszLog);
if(NULL == pszLogFile || NULL == pszLog)
return;
// 打開日志檔案
FILE* pFile = fopen(pszLogFile, "ab+");
if(pFile)
{
// 寫日志
fwrite((void*)pszLog, 1, strlen(pszLog), pFile);
fclose(pFile);
}
}
如果是不同級别的日志,可以定義不同日志檔案名,你也可以豐富上面的日志函數,增加發生的時間、所線上程ID、所在檔案名等輔助資訊。
通常情況下這種方式沒有問題,通過一次檔案IO操作将資訊記錄下來。但如果是一個背景服務,當高并發發生的時候,問題就會出現了。此時将會有多個線程同時寫日志的情況發生,尤其是那些INFO類型的日志,比如記錄HTTP請求的request資訊,這時你的日志将會成為系統的瓶頸。畢竟大量的檔案IO操作也是一種負擔,這些IO操作可能要超過你的正常業務邏輯,比如寫資料庫、網絡IO等。
此時我們需要将日志先寫入記憶體塊中,當記憶體寫滿後在一次性Flush到磁盤中,這樣就避免了大量的磁盤IO操作。下面是CFileMem封裝類,很簡單,裡面注釋的很詳細就不解釋了。
頭檔案.h
#include <windows.h> // for CRITICAL_SECTION
// 預設記憶體塊大小,大于該值時進行将緩存資料寫入磁盤檔案
#define SIZE_DEFAULT_MEM (1024*64) // 64K
class CFileMem
{
public:
CFileMem();
virtual ~CFileMem();
public:
// 打開記憶體檔案,dwMemBlockSize為緩存塊大小,大于該值時進行一次IO操作
BOOL Open(const char* pszPathFile, DWORD dwMemBlockSize = SIZE_DEFAULT_MEM);
/*
寫資料到記憶體檔案,dwFileSize将傳回目前磁盤檔案的大小,
該值可用于外部判斷日志檔案是否過大,比如當dwFileSize大于多少M時,可重命名檔案
進而避免單個日志檔案過大
*/
BOOL Write(const char* pszLog, DWORD& dwFileSize);
BOOL Write(const unsigned char* pData, DWORD dwDataSize, DWORD& dwFileSize);
// 将記憶體資料寫入磁盤
BOOL Flush(DWORD& dwFileSize);
// 關閉記憶體檔案
void Close();
// 重命名檔案
void Rename(const char* pszOldPathFile, const char* pszNewPathFile);
protected:
CRITICAL_SECTION m_csMem; // 記憶體池鎖
char m_szPathFile[256]; // 日志檔案路徑名
char* m_pMemBlock; // 記憶體塊,用于存儲檔案資料
DWORD m_dwMemBlockSize; // 記憶體塊大小
DWORD m_dwPos; // 記憶體塊的目前位置
};
實作檔案.cpp
#include "FileMem.h"
#include <stdio.h>
CFileMem::CFileMem()
{
m_pMemBlock = NULL;
m_dwMemBlockSize = 0;
m_dwPos = 0;
memset(m_szPathFile, 0, 256);
::InitializeCriticalSection(&m_csMem);
}
CFileMem::~CFileMem()
{
Close();
::DeleteCriticalSection(&m_csMem);
}
// 打開記憶體檔案,dwMemBlockSize為緩存塊大小,大于該值時進行一次IO操作
BOOL CFileMem::Open(const char* pszPathFile, DWORD dwMemBlockSize)
{
if(!pszPathFile)
return FALSE;
// 關閉之前已打開記憶體塊
Close();
// 儲存日志檔案全路徑名
strcpy(m_szPathFile, pszPathFile);
if(dwMemBlockSize <= 0)
return FALSE;
m_pMemBlock = (char*)malloc(dwMemBlockSize);
if(NULL == m_pMemBlock)
return FALSE;
memset(m_pMemBlock, 0, dwMemBlockSize);
m_dwMemBlockSize = dwMemBlockSize;
m_dwPos = 0;
return TRUE;
}
/*
寫資料到記憶體檔案,dwFileSize将傳回目前磁盤檔案的大小,
該值可用于外部判斷日志檔案是否過大,比如當dwFileSize大于多少M時,可重命名檔案
進而避免單個日志檔案過大
*/
BOOL CFileMem::Write(const char* pszLog, DWORD& dwFileSize)
{
return Write((const unsigned char*)pszLog, strlen(pszLog), dwFileSize);
}
BOOL CFileMem::Write(const unsigned char* pData, DWORD dwDataSize, DWORD& dwFileSize)
{
dwFileSize = 0;
if(NULL == pData || 0 == dwDataSize)
return FALSE;
// 如果内部沒有開辟緩沖區,則直接寫檔案
if(NULL == m_pMemBlock || 0 == m_dwMemBlockSize)
{
FILE* pFile = ::fopen(m_szPathFile, "ab+");
if(NULL == pFile)
return FALSE;
::fwrite(pData, 1, dwDataSize, pFile);
// 擷取磁盤檔案大小
::fseek(pFile, 0L, SEEK_END);
dwFileSize = ::ftell(pFile);
::fclose(pFile);
return TRUE;
}
::EnterCriticalSection(&m_csMem);
// 如果記憶體塊已滿,則寫入磁盤檔案
DWORD dwTotalSize = m_dwPos + dwDataSize;
if(dwTotalSize >= m_dwMemBlockSize)
{
FILE* pFile = ::fopen(m_szPathFile, "ab+");
if(NULL == pFile)
{
::LeaveCriticalSection(&m_csMem);
return FALSE;
}
// 将目前記憶體中資料寫入檔案
::fwrite(m_pMemBlock, 1, m_dwPos, pFile);
::fwrite(pData, 1, dwDataSize, pFile);
// 擷取磁盤檔案大小
::fseek(pFile, 0L, SEEK_END);
dwFileSize = ::ftell(pFile);
::fclose(pFile);
// 清空記憶體塊
memset(m_pMemBlock, 0, m_dwMemBlockSize);
m_dwPos = 0;
}
else
{
// 如果記憶體未滿,将目前資料寫入記憶體
memcpy(m_pMemBlock+m_dwPos, pData, dwDataSize);
m_dwPos += dwDataSize;
}
::LeaveCriticalSection(&m_csMem);
return TRUE;
}
// 将緩沖區的内容寫入磁盤
BOOL CFileMem::Flush(DWORD& dwFileSize)
{
// 參數
dwFileSize = 0;
if(NULL == m_pMemBlock || 0 == m_dwMemBlockSize || 0 == m_dwPos)
return TRUE;
::EnterCriticalSection(&m_csMem);
FILE* pFile = ::fopen(m_szPathFile, "ab+");
if(NULL == pFile)
{
::LeaveCriticalSection(&m_csMem);
return FALSE;
}
// 記憶體資料寫入磁盤檔案
::fwrite(m_pMemBlock, 1, m_dwPos, pFile);
// 擷取磁盤檔案大小
::fseek(pFile, 0L, SEEK_END);
dwFileSize = ::ftell(pFile);
::fclose(pFile);
// 清空記憶體塊
memset(m_pMemBlock, 0, m_dwMemBlockSize);
m_dwPos = 0;
::LeaveCriticalSection(&m_csMem);
return TRUE;
}
void CFileMem::Close()
{
// 将記憶體中檔案寫入磁盤
DWORD dwFileSize = 0;
Flush(dwFileSize);
// 釋放記憶體塊
free(m_pMemBlock);
m_pMemBlock = NULL;
m_dwMemBlockSize = 0;
m_dwPos = 0;
memset(m_szPathFile, 0, 256);
}
// 重命名檔案
void CFileMem::Rename(const char* pszOldPathFile, const char* pszNewPathFile)
{
::EnterCriticalSection(&m_csMem);
::rename(pszOldPathFile, pszNewPathFile);
::LeaveCriticalSection(&m_csMem);
}
測試函數:
#include <assert.h>
#include <stdio.h>
#include <time.h> // for Clock
// 傳統的日志輸出函數
void Logout(const char* pszLogFile, const char* pszLog)
{
// 參數檢查
assert(pszLogFile);
assert(pszLog);
if(NULL == pszLogFile || NULL == pszLog)
return;
// 打開日志檔案
FILE* pFile = fopen(pszLogFile, "ab+");
if(pFile)
{
// 寫日志
fwrite((void*)pszLog, 1, strlen(pszLog), pFile);
fclose(pFile);
}
}
static void TestFileMem()
{
const char* pszLogFile1 = "c:/test1.log";
const char* pszLogInfo = "Hello world\r\n";
int iCount = 10000;
// 循環寫日志1萬次
// 第一種方式:傳統寫日志方式
clock_t cStart = clock();
for(int i = 0; i < iCount; i++)
Logout(pszLogFile1, pszLogInfo);
clock_t cEnd = clock();
long lElapsed = cEnd - cStart;
printf("Logout Elpased time %d ms\r\n", lElapsed);
// 第二種方式:使用CFileMem寫日志
const char* pszLogFile2 = "c:/test2.log";
CFileMem fm;
DWORD dwFileSize = 0;
fm.Open(pszLogFile2);
cStart = clock();
for(i = 0; i < iCount; i++)
fm.Write(pszLogInfo, dwFileSize);
fm.Close();
cEnd = clock();
lElapsed = cEnd - cStart;
printf("CFileMem Elpased time %d ms\r\n", lElapsed);
}
輸出結果:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2YfNWawNyZuBnLzUGN1QGNkhTOhdTZ1IGMilTZlRjZlVjY3AzYzkTOhhzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
通過測試比較,第二種方式速度更加快速,而且是線程安全的。
我寫的“HttpServer:一款Windows平台下基于IOCP模型的高并發輕量級web伺服器”,就使用該類進行日志列印,效果不錯。
感謝您的閱讀!