多緩衝提高日誌系統性能
前言:無論什麼專案肯定都少不了日誌系統,所以一個高效能的日誌系統是不可避免的。
本文介紹的是自己用c++11實現的一個簡單的多緩衝區日誌系統,比較水,僅供參考^_^
主題:
- 日誌系統及重要性
- 單緩衝日誌系統模型及缺陷
- 多緩衝buffer介紹及優勢
- 多緩衝區缺陷
- Buffer類設計及分析
- Logger類設計及分析
日誌系統及重要性:
日誌資訊對於一個優秀專案來說是非常重要的,因為無論再優秀的軟體都有可能產生崩潰或異常,此時,日誌系統就能發揮它的作用。
快速定位到錯誤地點以及錯誤內容,或者檢視最近資訊等。
一般來說一個日誌系統會分級寫日誌,比如INFO資訊日誌(使用者的一些操作等),ERROR錯誤日誌(系統崩潰或異常),FAIL失敗日誌(某項操作失敗)等等。
由於日誌系統非常重要,它會出現在我們程式的每個角落,所以一個好的日誌系統就非常重要了,現存的有許多好的實現比如c++的log4,下面介紹是按自己的思路實現的一個非常簡單的日誌系統。
單緩衝日誌系統模型及缺陷
最簡單的日誌系統就是單緩衝或者無緩衝的。
無緩衝:
無緩衝最簡單,在個需要輸出日誌資訊的地點都輸出資訊到檔案並寫入磁碟即可,但是注意現在程式一般都是併發執行,多程序或多執行緒寫檔案我們要加鎖。
這樣效率就比較低了,比如你有20個執行緒在執行,每次輸出日誌都要先搶到鎖然後在輸出,並且輸出到磁碟本身就很慢,這樣不僅輸出日誌效率低,更可能會影響到程式的執行(會阻塞程式,因為日誌輸出是無處不在的)。
單緩衝:
單緩衝就是我們開闢一塊固定大小的空間,每次日誌輸出都先輸出到緩衝中,等到緩衝區滿在一次重新整理到磁碟上,這樣相比較無緩衝效率提高了一些,不用每次都輸出到磁碟檔案上,待到一定數量再重新整理到磁碟上。但是每次輸出到日誌檔案上的執行緒或程序都要加鎖,還是存在一個搶鎖的過程,效率也不高。
模型如下
從上圖能看出來,每次寫緩衝還是存在搶鎖和阻塞的過程,這樣效率還是比較低的,相對無緩衝來說,僅僅減少了磁碟IO的次數。但在磁碟IO時,程式依舊會阻塞
磁碟IO依舊是瓶頸
多緩衝 buffer介紹
既然單塊緩衝滿足不了我們的要求,效率依然比較低,那麼我們可以嘗試選擇多塊緩衝來實現。程式只關注當前緩衝,其餘多塊緩衝交給後臺執行緒來處理。
模型如下
當前緩衝為我們程式寫緩衝Buffer,備用緩衝Buffer為我們提前開闢緩衝,噹噹前curBuf緩衝滿時交換Buffer。如下圖
在實際中我們用指標來操控(程式碼中我使用的std::shared_ptr,只用交換指標即可),交換完畢後如下圖
此時,我們可以喚醒後臺執行緒處理已滿緩衝,當前緩衝交換後為空,程式可以繼續寫當前緩衝而不會因為磁碟IO而阻塞。
如果程式寫日誌速度非常快,我們可以開大緩衝,或者將當前緩衝設定為兩塊,備用緩衝可設定為多塊,在實際編寫程式時,因為我用的是
list<std::shared_ptr>
這種結構來儲存,當備用緩衝不夠時,會建立一塊,然後list會自動push_back,這樣慢慢程式會達到最適應自己的緩衝大小。
優勢很明顯了,我們程式只管寫,一切由後臺執行緒來完成,不會阻塞在磁碟IO上。
多緩衝區缺陷
多緩衝區設計是有缺陷的,相比較單緩衝是避免了磁碟IO這一耗時的操作,但如果程式寫日誌量非常大時,每次寫curBuf當前緩衝都要先搶鎖,可見效率之低,等待鎖的時間耗費非常大。多個執行緒或程序操作一塊或兩塊緩衝,鎖的顆粒度非常大。我們可以嘗試減小鎖的顆粒度
解決方案可以參考Java的ConcurrentHashMap原理,ConcurrentHashMap是內部建立多個桶,每次hash到不同的桶中,鎖只鎖相應的桶,那麼等於減少了鎖的顆粒度,阻塞在鎖上的頻率也就大大降低。
如下圖
當前程式Buffer開闢多個,類似多個桶,鎖只鎖相應的桶即可,減小了鎖的顆粒度
這麼做算已時間換空間了,然後如果沒有這麼大需求上面方案即可解決。
Buffer類設計及分析
Buffer類的設計參考了netty中的buffer,一塊緩衝,三個標記分別為可讀位置readable,可寫位置writable,容量capacity。
其實readable和writable位置都為0,capacity為容量大小。
寫緩衝時writable移動,讀緩衝時readable移動,writable <= capacity。
緩衝區我使用了vector<char>
,參考了陳碩前輩的muduo,使用vector<char>
一方面它內部和陣列是一樣的,其次我們還可以藉助vector特性來管理它。Buffer類比較簡單
class Buffer
{
/* 初始化預設大小 */
static size_t initializeSize;
public:
/* 建構函式,初始化buffer,並且設定可讀可寫的位置 */
explicit Buffer(size_t BufferSize = initializeSize):
readable(0), writable(0)
{
/* 提前開闢好大小 */
buffer.resize(BufferSize);
}
/* 返回緩衝區的容量 */
size_t Capacity()
{
return buffer.capacity();
}
/* 返回緩衝區的大小 */
size_t Size()
{
return writable;
}
/* set Size */
void setSize(void)
{
readable = 0;
writable = 0;
}
/* 向buffer中新增資料 */
void append(const char* mesg, int len)
{
strncpy(WritePoint(), mesg, len);
writable += len;
}
/* 返回buffer可用大小 */
size_t avail()
{
return Capacity()-writable;
}
private:
/* 返回可讀位置的指標 */
char* ReadPoint()
{
return &buffer[readable];
}
/* 返回可寫位置的指標 */
char* WritePoint()
{
return &buffer[writable];
}
/* 返回可讀位置 */
size_t ReadAddr()
{
return readable;
}
/* 返回可寫位置 */
size_t WriteAddr()
{
return writable;
}
private:
std::vector<char> buffer;
size_t readable;
size_t writable;
};
Logger類設計及分析
Logger類我的實現遵從與剛才說的多緩衝模型。
curBuf為一塊,備用Buffer為兩塊,並且可自適應改變。
class Logger
{
public:
/* 建立日誌類例項 */
static std::shared_ptr<Logger> setLogger();
static std::shared_ptr<Logger> setLogger(size_t bufSize);
/* 得到日誌類例項 */
static std::shared_ptr<Logger> getLogger();
/* 按格式輸出日誌資訊到指定檔案 */
static void logStream(const char* mesg, int len);
private:
/* shared_ptr智慧指標管理log類 */
static std::shared_ptr<Logger> myLogger;
/* 當前緩衝 */
static std::shared_ptr<Buffer> curBuf;
/* list管理備用緩衝 */
static std::list<std::shared_ptr<Buffer>> bufList;
/* 備用緩衝中返回一塊可用的緩衝 */
static std::shared_ptr<Buffer> useFul();
/* 條件變數 */
static std::condition_variable readableBuf;
/* 後臺執行緒需要處理的Buffer數目 */
static int readableNum;
/* 互斥鎖 */
static std::mutex mutex;
/* 後臺執行緒 */
static std::thread readThread;
/* 執行緒執行函式 */
static void Threadfunc();
static void func();
/* 條件變數條件 */
static bool isHave();
};
從上面程式碼可以看出當前Buffer和備用Buffer都用智慧指標來管理,我們不用操心資源釋放等問題,因為為指標,當前Buffer和備用Buffer交換起來速度非常快。
初始化函式
std::shared_ptr<Logger>
Logger::
setLogger()
{
if(myLogger == nullptr)
{
/* 建立日誌類 */
myLogger = std::move(std::make_shared<Logger>());
/* 建立當前Buffer */
curBuf = std::make_shared<Buffer>();
/* 建立兩塊備用Buffer */
bufList.resize(2);
(*bufList.begin()) = std::make_shared<Buffer>();
(*(++bufList.begin())) = std::make_shared<Buffer>();
}
return myLogger;
}
都是由智慧指標來管理
useful類,返回一個可用的備用Buffer
std::shared_ptr<Buffer>
Logger::
useFul()
{
auto iter = bufList.begin();
/* 查詢是否存在可用的Buffer */
for(; iter != bufList.end(); ++iter)
{
if((*iter)->Size() == 0)
{
break;
}
}
/* 不存在則建立一塊新Buffer並返回 */
if(iter == bufList.end())
{
std::shared_ptr<Buffer> p = std::make_shared<Buffer>();
/* 統一使用右值來提高效率 */
bufList.push_back(std::move(p));
return p;
}
return *iter;
}
這算是自適應過程了,隨著程式的執行會返回適應的大小。
logStream寫日誌類
void
Logger::
logStream(const char* mesg, int len)
{
/* 上鎖,使用unique_lock為和condition_variable條件變數結合 */
std::unique_lock<std::mutex> locker(mutex);
/* 判斷當前緩衝是已滿,滿了則與備用緩衝交換新的 */
if(curBuf->avail() > len)
{
curBuf->append(mesg, len);
}
else
{
/* 得到一塊備用緩衝 */
auto useBuf = useFul();
/* 交換指標即可 */
curBuf.swap(useBuf);
/* 可讀緩衝數量增加 */
++readableNum;
/* 喚醒阻塞後臺執行緒 */
readableBuf.notify_one();
}
}
執行緒主要執行函式
void
Logger::func()
{
std::unique_lock<std::mutex> locker(mutex);
auto iter = bufList.begin();
/* 如果備用緩衝並無資料可讀,阻塞等待喚醒 */
if(readableNum == 0)
{
readableBuf.wait(locker, Logger::isHave);
}
/* 找資料不為空的Buffer */
for(; iter != bufList.end(); ++iter)
{
if((*iter)->Size() != 0)
break;
}
/* 如果到末尾沒找到,沒有資料可讀 */
if(iter == bufList.end())
{
return;
}
else
{
/* 將滿的緩衝寫到檔案中 */
int fd = open("1.txt", O_RDWR | O_APPEND, 00700);
if(fd < 0)
{
perror("open error\n");
exit(1);
}
write(fd, iter->get(), (*iter)->Capacity());
/* 清空緩衝 */
bzero(iter->get(), (*iter)->Capacity());
/* 歸位readable和writable */
(*iter)->setSize();
/* 可讀緩衝數量減1 */
--readableNum;
}
}
僅僅是一個簡單的實現,如有更優方案或錯誤還望指出,謝謝~
完