前幾天看到一個網友的評論:「這種通常本身實現個用用就好了 不必整第三方庫」。
的確,不少我的或公司都本身實現了簡單寫日誌函數在產品中使用便可,通常不喜歡第三方庫,一來認爲第三方庫代碼多,勢必影響性能,二來帶來的不可預見的代碼黑盒子,影響軟件總體可靠。
這是一個普遍的論點,我並不否定它的存在合理性,但仍是想對傳統的簡單寫日誌函數和iLOG3函數庫作個比較,分兩部分:性能和代碼複雜度,看完後你會發現你會對日誌函數庫iLOG3感興趣的。
1、性能
通常來說,簡單寫日誌函數確定要比日誌函數庫要快,可是我分別作了壓力測試,結果發現iLOG3居然比簡單寫日誌函數要快近一倍!下面是測試案例
隨手寫的最簡單的寫日誌函數代碼(源碼包test目錄下的bench_tiny.c)
static int _WriteLogBase( char *c_filename , long c_fileline , int log_level , char *format , va_list valist )
{
int fd ;
char buf[ 1024 + 1 ] ;
long len ;
time_t tt ;
struct tm stime ;
pid_t pid ;
unsigned long tid ;
int n ;
/* 打開日誌文件 */
fd = open( "bench_tiny.log" , O_CREAT|O_APPEND|O_WRONLY , S_IRWXU|S_IRWXG|S_IRWXO ) ;
if( fd == -1 )
return -1;
/* 組織日誌內容 */
time( & tt );
localtime_r( & tt , & stime );
pid = getpid() ;
tid = (unsigned long)pthread_self() ;
memset( buf , 0x00 , sizeof(buf) );
len = snprintf( buf , sizeof(buf) , "%04d-%02d-%02d %02d:%02d:%02d | INFO | %d:%lu:%s:%ld | %s\n" , stime.tm_year+1900 , stime.tm_mon+1 , stime.tm_mday , stime.tm_hour , stime.tm_min , stime.tm_sec , pid , tid , c_filename , c_fileline , "log" ) ;
/* 輸出日誌 */
n = write( fd , buf , len ) ;
if( n == -1 )
return -1;
/* 關閉日誌文件 */ /* 關鍵場合必須及時關閉文件,不然日誌有丟失可能 */
close( fd );
return 0;
}
#define LOG_LEVEL_DEBUG 0
#define LOG_LEVEL_INFO 1
#define LOG_LEVEL_WARN 2
#define LOG_LEVEL_ERROR 3
#define LOG_LEVEL_FATAL 4
/* 以不一樣日誌等級寫行日誌 */
int DebugLog( char *c_filename , long c_fileline , char *format , ... )
{
...
}
int InfoLog( char *c_filename , long c_fileline , char *format , ... )
{
va_list valist ;
/* 暫不寫日誌等級過濾邏輯 */
/* ... */
/* 調用通用底層函數輸出日誌 */
va_start( valist , format );
_WriteLogBase( c_filename , c_fileline , LOG_LEVEL_INFO , format , valist );
va_end( valist );
return 0;
}
int ErrorLog( char *c_filename , long c_fileline , char *format , ... )
{
...
}
...
在個人環境裏跑出這樣的成績
$ rm -f *.log* ; sleep 1 ; time ./bench_tiny 10 10 10000 ; head -1 bench_tiny.log ; wc *.log* ; rm -f *.log*
real 0m21.863s
user 0m1.688s
sys 0m19.924s
2014-02-22 12:58:25 | INFO | 2457:3086568336:bench_tiny.c:144 | log
1000000 8000000 69000000 bench_tiny.log
嗯,10個進程,每一個進程開10個線程,每一個線程循環調用函數InfoLog()一萬次,花費21秒左右,共寫出100萬行日誌,約6900萬字節
如今輪到iLOG3出場,用源代碼包中test目錄下的壓測示例test_press_mpt.c。
公平起見,把按文件大小轉檔關掉。
// SetLogRotateMode( press , LOG_ROTATEMODE_SIZE );
開跑
$ rm -f *.log* ; sleep 1 ; time ./test_press_mpt 10 10 10000 ; head -1 test_press_mpt.log ; wc *.log* ; rm -f *.log
real 0m13.745s
user 0m1.136s
sys 0m12.444s
2014-02-22 13:02:33 | INFO | 2893:3086392208:test_press_mpt.c:120 | log
1000000 8000000 73000000 test_press_mpt.log
一樣開10個進程,每一個進程開10個線程,每一個線程循環調用函數iLOG3裏的InfoLog()一萬次,花費13秒左右,共寫出100萬行日誌,約7300萬字節,你沒看錯,iLOG3日誌函數庫的日誌輸出性能比隨手寫的簡單寫日誌函數快了近一倍。
爲何會這樣呢?等我再對比完代碼複雜度後一塊兒總結 ^_^
2、代碼複雜度
咱們從iLOG3裏的InfoLog做爲入口一層層剝離下去,看一下寫一第二天志須要跑哪些路徑,和隨手寫的簡單寫日誌函數代碼有哪些區別。
/* 寫普通訊息日誌 */
int InfoLog( LOG *g , char *c_filename , long c_fileline , char *format , ... )
{
WRITELOGBASE( g , LOG_LEVEL_INFO )
return 0;
}
/* 代碼宏 */
#define WRITELOGBASE(_g_,_log_level_) \
va_list valist; \
int nret ; \
if( (_g_) == NULL ) \
return LOG_RETURN_ERROR_PARAMETER; \
if( (_g_)->output == LOG_OUTPUT_FILE && (_g_)->log_pathfilename[0] == '\0' ) \
return 0; \
if( TEST_LOGLEVEL_NOTENOUGH( _log_level_ , (_g_) ) ) \
return 0; \
va_start( valist , format ); \
nret = WriteLogBase( (_g_) , c_filename , c_fileline , _log_level_ , format , valist ) ; \
va_end( valist ); \
if( nret < 0 ) \
return nret;
/* 寫日誌基函數 */
int WriteLogBase( LOG *g , char *c_filename , long c_fileline , int log_level , char *format , va_list valist )
{
long writelen ;
int nret ;
if( format == NULL )
return 0;
/* 初始化行日誌緩衝區 */
g->logbuf.buf_remain_len = g->logbuf.buf_size - 1 - 1 ;
g->logbuf.bufptr = g->logbuf.bufbase ;
/* 填充行日誌緩衝區 */
if( g->pfuncLogStyle )
{
nret = g->pfuncLogStyle( g , & (g->logbuf) , c_filename , c_fileline , log_level , format , valist ) ;
if( nret )
return nret;
}
/* 自定義過濾日誌 */
if( g->pfuncFilterLog )
{
nret = g->pfuncFilterLog( g , & (g->open_handle) , log_level , g->logbuf.bufbase , g->logbuf.buf_size-1-1 - g->logbuf.buf_remain_len ) ;
if( nret )
return nret;
}
/* 打開文件 */
if( g->open_flag == 0 )
{
if( TEST_ATTRIBUTE( g->log_options , LOG_OPTION_CHANGE_TEST ) || TEST_ATTRIBUTE( g->log_options , LOG_OPTION_OPEN_ONCE ) )
{
if( g->pfuncOpenLogFirst )
{
nret = g->pfuncOpenLogFirst( g , g->log_pathfilename , & (g->open_handle) ) ;
if( nret )
return nret;
g->open_flag = 1 ;
}
}
else if( TEST_ATTRIBUTE( g->log_options , LOG_OPTION_OPEN_AND_CLOSE ) )
{
/* 打開日誌文件 */
if( g->pfuncOpenLog )
{
nret = g->pfuncOpenLog( g , g->log_pathfilename , & (g->open_handle) ) ;
if( nret )
return nret;
g->open_flag = 1 ;
}
}
}
/* 導出日誌緩衝區 */
if( g->pfuncWriteLog )
{
nret = g->pfuncWriteLog( g , & (g->open_handle) , log_level , g->logbuf.bufbase , g->logbuf.buf_size-1-1 - g->logbuf.buf_remain_len , & writelen ) ;
if( nret )
return nret;
}
/* 關閉日誌 */
if( g->open_flag == 1 )
{
if( g->output == LOG_OUTPUT_FILE || g->output == LOG_OUTPUT_CALLBACK )
{
if( TEST_ATTRIBUTE( g->log_options , LOG_OPTION_CHANGE_TEST ) )
{
if( g->pfuncChangeTest )
{
nret = g->pfuncChangeTest( g , & (g->test_handle) ) ;
if( nret )
return nret;
}
}
else if( TEST_ATTRIBUTE( g->log_options , LOG_OPTION_OPEN_AND_CLOSE ) )
{
/* 關閉日誌文件 */
if( g->pfuncCloseLog )
{
nret = g->pfuncCloseLog( g , & (g->open_handle) ) ;
if( nret )
return nret;
g->open_flag = 0 ;
}
}
}
}
/* 若是輸出到文件 */
if( g->output == LOG_OUTPUT_FILE )
{
/* 日誌轉檔偵測 */
if( g->rotate_mode == LOG_ROTATEMODE_NONE )
{
}
else if( g->rotate_mode == LOG_ROTATEMODE_SIZE && g->log_rotate_size > 0 )
{
g->skip_count--;
if( g->skip_count < 1 )
{
RotateLogFileSize( g , writelen );
}
}
else if( g->rotate_mode == LOG_ROTATEMODE_PER_DAY )
{
RotateLogFilePerDate( g );
}
else if( g->rotate_mode == LOG_ROTATEMODE_PER_HOUR )
{
RotateLogFilePerHour( g );
}
}
/* 清空一級緩存 */
g->cache1_tv.tv_sec = 0 ;
g->cache1_stime.tm_mday = 0 ;
return 0;
}
以上能夠看出二者路徑基本一致,iLOG3的WriteLogBase也是格式化行日誌緩衝區、打開日誌文件、輸出日誌、關閉日誌文件、日誌轉檔處理,無非都是抽象成日誌控制框架,即經過回調函數來掛接實現而沒有直接調用具體功能函數,這樣設計的好處是用戶能夠編寫本身的打開輸出關閉日誌文件等函數來替代iLOG3默認掛接的來實現更靈活的日誌控制。
好,如今我來解釋一下爲何第三方的日誌函數庫iLOG3比本身隨手寫的簡單寫日誌函數速度快近一倍的緣由。
首先,日誌函數庫iLOG3的原型就是簡單寫日誌函數擴充而來,因此它們的處理路徑和基本邏輯差很少,可是iLOG3把做爲函數庫的不少耗時的配置解析等工做盡可能都放在寫日誌前就預先處理好,真正寫日誌時跑的函數調用關係、運行邏輯路徑等和簡單寫日誌函數跑過的基本一致,能夠理解成iLOG3寫日誌時調用的其實就是簡單寫日誌函數,到此它們的性能就差很少了,而後,iLOG3擁有日誌句柄這個數據結構,爲緩存層等設計提供了方便,iLOG3內部作了大量嚴謹、精巧的性能優化,其中就包括時間緩存(保證緩存不會串日誌)、字符串轉換緩存、文件大小轉檔步進算法等,恰當受控的優化的效果是顯而易見的,最終性能就比隨手寫的簡單寫日誌函數要快了。
最後,再次展示一下我開發iLOG3的設計準則:
·做爲軟件的重要基礎模塊,日誌函數庫應儘可能實現的輕便、簡易和穩定,在保證高可靠性的前提下,高性能低影響是首要設計目標。提供多層API以及可選的外部配置文件方式使用。
·基本功能必不可少,如日誌等級、行日誌和十六進制塊日誌等,對性能和易用性影響較大的高級功能可不加儘可能不加。(要不要實現日誌轉檔是一個很糾結的問題,我一直堅持認爲用運維shell來實現會更好,但迫於同類日誌庫的壓力,我仍是實現了)
·日誌模塊至少應分爲兩部分:核心和外部配置,中間用API膠合起來,便於擴展和靈活替換
·線程安全
是否是越看越心動了?那就趕忙下載來玩玩吧
首頁傳送門 :
http://git.oschina.net/calvinwilliams/iLOG3 源代碼包doc目錄中包含了用戶指南和參考手冊,裏面有更詳盡的說明 聲明:文本的寫做目的並非想說服你們改用第三方日誌函數庫來替代本身寫的簡單寫日誌函數,而是想帶領你們深刻探討諸如寫日誌這類在某些場景中軟件必備模塊的一些思考,至於用不用iLOG3則須要你更多更全面甚至是非技術層面的考量。