1. 程式人生 > >多緩衝提高日誌系統性能

多緩衝提高日誌系統性能

前言:無論什麼專案肯定都少不了日誌系統,所以一個高效能的日誌系統是不可避免的。
本文介紹的是自己用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());
        /* 歸位readablewritable */
        (*iter)->setSize();
        /* 可讀緩衝數量減1 */
        --readableNum;
    }
}

僅僅是一個簡單的實現,如有更優方案或錯誤還望指出,謝謝~