C++ 高性能無鎖日志系統


服務器編程中,日志系統需要滿足幾個條件

.高效,日志系統不應占用太多資源

.簡潔,為了一個簡單的日志功能引入大量第三方代碼未必值得

.線程安全,服務器中各個線程都能同時寫出日志

.輪替,服務器不出故障是不重啟的,半年一年的日志放到一個文件會導致文件過大

.及時保存,程序故障導致異常退出,此時需要通過日志診斷問題,不緩沖的日志系統更易用


著名的日志庫有log4xxx系列,提供了非常靈活的功能,當然隨之而來的代價就是龐大的庫。在大多數服務器應用中,所需的功能不多,我偏向於選擇一個支持按時間輪替的簡潔的日志庫。

為了同時做到線程安全和支持輪替,大多數日志系統都使用鎖。寫出日志時,首先獲取鎖,如果需要輪替,則進行輪替操作,否則寫到現有文件,最后釋放鎖。

google開源的leveldb的日志系統中,同時做到了“線程安全”和輪替,但是沒有用鎖,這引發了我的興趣。

仔細閱讀發現它的運作原理是

.每個log操作,都會生成相關的字符串,最終調用write,寫出到日志系統的文件描述符fd。

.進行rotate操作時,重新命名舊文件,保持舊文件的打開狀態,然后打開新文件,將fd設置為新文件。

.接下來sleep 200ms,然后把close舊文件

那么輪替過程中,fd的值為fd_old或者fd_new,只要fd的讀寫是原子的,不會讀取到非fd_old和fd_new的其他值即可(fd是int,這點可以做到)。write操作就沒有問題

如果由於系統繁忙,fd讀取為fd_old之后,走到操作系統的write之前,線程被切換,並且經過了200ms,那么fd_old就有可能會在sleep 200ms之后被關閉,那么write就可能失敗。

因此這種做法是簡潔的,能夠應對絕大多數情況,但並非安全,而且切換時需要sleep 200ms也是個讓人頭疼的問題。


借鑒leveldb的做法,加上posix上的dup2調用則可以完美的解決這個問題。

.輪替時,首先重命名舊文件,保持舊文件的打開狀態,然后打開新文件。

rename(oldname, newname);
fd = open(oldname,...);

.使用dup2系統函數把fd_new復制到fd_old上

dup2(fd, fd_);

.關閉fd_new

close(fd);

其中dup2是原子操作,它會關閉fd_old並且把fd_old也指向fd_new打開的文件。因此fd_old這個文件描述符總是保持打開狀態,並且值不變,但是前后指向了不同的文件。另一邊write也是個原子操作,它與dup2不會交叉進行,因此保證了日志系統的正確性。


詳情參見開源庫handy中的logging.h和logging.cc,里面一部分代碼采用了C++11的語法

https://github.com/yedf/handy/tree/master/handy

handy的日志系統中,日志要做的內容就是使用snprintf格式化要輸出的內容,然后調用write,沒有多余的工作,因此做到了簡潔高效

通過前面介紹的原理同時實現了無鎖的線程安全,和日志輪替

每次日志的輸出都write,即使程序崩潰,日志也已經到了操作系統層,不會丟失,易於調試問題

當然高效與及時保存有一定的沖突,如果緩存多條數據然后合並write能夠提升一定的性能,但這里我選擇簡潔與易用

 

handy的日志系統性能測試可以參見項目examples下的log-bench.cc,在我筆記本電腦上的虛擬機的壓力測試中,輸出文件為/dev/null時,能夠達到75w/s的qps

PS:handy的日志輪替中,對lastRotate_的讀取和修改並非原子類型,可能會導致多輪替一次,解決方法為使用C++11中的原子類型,或者就容忍了(多輪替一次會在后續的操作中失敗,僅僅多輸出了一條信息到標准錯誤)。

 本文用 菊子曰發布


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM