一個高效可拓展的異步C++日志庫:RING LOG,本文分享了了其設(shè)計方案與技術(shù)原理等內(nèi)容
導(dǎo)論
同步日志與缺點
傳統(tǒng)的日志也叫同步日志,每次調(diào)用一次打印日志API就對應(yīng)一次系統(tǒng)調(diào)用write寫日志文件,在日志產(chǎn)生不頻繁的場景下沒什么問題
可是,如果日志打印很頻繁,同步日志有什么問題?
- 一方面,大量的日志打印陷入等量的write系統(tǒng)調(diào)用,有一定系統(tǒng)開銷
- 另一方面,使得打印日志的進(jìn)程附帶了大量同步的磁盤IO,影響性能
那么,如何解決如上的問題?就是
異步日志與隊列實現(xiàn)的缺點
異步日志,按我的理解就是主線程的日志打印接口僅負(fù)責(zé)生產(chǎn)日志數(shù)據(jù)(作為日志的生產(chǎn)者),而日志的落地操作留給另一個后臺線程去完成(作為日志的消費者),這是一個典型的生產(chǎn)-消費問題,如此一來會使得:
主線程調(diào)用日志打印接口成為非阻塞操作,同步的磁盤IO從主線程中剝離出來,有助于提高性能
對于異步日志,我們很容易借助隊列來一個實現(xiàn)方式:主線程寫日志到隊列,隊列本身使用條件變量、或者管道、eventfd等通知機制,當(dāng)有數(shù)據(jù)入隊列就通知消費者線程去消費日志
但是,這樣的異步隊列也有一定的問題:
- 生產(chǎn)者線程產(chǎn)生N個日志,對應(yīng)后臺線程就會被通知N次,頻繁日志寫入會造成一定性能開銷
- 不同隊列實現(xiàn)方式也各有缺點:
- 用數(shù)組實現(xiàn):空間不足時,隊列內(nèi)存不易拓展
- 用鏈表實現(xiàn):每條消息的生產(chǎn)消費都對應(yīng)內(nèi)存的創(chuàng)建銷毀,有一定開銷
好了,可以開始正文了
簡介
RING LOG是一個適用于C++的異步日志, 其特點是效率高(實測每秒支持125+萬日志寫入)、易拓展,尤其適用于頻繁寫日志的場景
一句話介紹原理:
使用多個大數(shù)組緩沖區(qū)作為日志緩沖區(qū),多個大數(shù)組緩沖區(qū)以雙循環(huán)鏈表方式連接,并使用兩個指針p1和p2指向鏈表兩個節(jié)點,分別用以生成數(shù)據(jù)、與消費數(shù)據(jù)
生產(chǎn)者可以是多線程,共同持有p1來生產(chǎn)數(shù)據(jù),消費者是一個后臺線程,持有p2去消費數(shù)據(jù)
大數(shù)組緩沖區(qū) + 雙循環(huán)鏈表的設(shè)計,使得日志緩沖區(qū)相比于隊列有更強大的拓展能力、且避免了大量內(nèi)存申請釋放,提高了異步日志在海量日志生成下的性能表現(xiàn)
此外,RING LOG還優(yōu)化了每條日志的UTC格式時間的生成,明顯提高日志性能
具體工作原理
數(shù)據(jù)結(jié)構(gòu)
Ring Log的緩沖區(qū)是若干個cell_buffer以雙向、循環(huán)的鏈表組成
cell_buffer是簡單的一段緩沖區(qū),日志追加于此,帶狀態(tài):
- FREE:表示還有空間可追加日志
- FULL:表示暫時無法追加日志,正在、或即將被持久化到磁盤;
Ring Log有兩個指針:
- Producer Ptr:生產(chǎn)者產(chǎn)生的日志向這個指針指向的cell_buffer里追加,寫滿后指針向前移動,指向下一個cell_buffer;Producer Ptr永遠(yuǎn)表示當(dāng)前日志寫入哪個cell_buffer,被多個生產(chǎn)者線程共同持有
- Consumer Ptr:消費者把這個指針指向的cell_buffer里的日志持久化到磁盤,完成后執(zhí)行向前移動,指向下一個cell_buffer;Consumer Ptr永遠(yuǎn)表示哪個cell_buffer正要被持久化,僅被一個后臺消費者線程持有
起始時刻,每個cell_buffer狀態(tài)均為FREE Producer Ptr與Consumer Ptr指向同一個cell_buffer
整個Ring Log被一個互斥鎖mutex保護(hù)
大致原理
消費者
后臺線程(消費者)forever loop:
1.上鎖,檢查當(dāng)前Consumer Ptr:
- 如果對應(yīng)cell_buffer狀態(tài)為FULL,釋放鎖,去STEP 4;
- 否則,以1秒超時時間等待條件變量cond;
2.再次檢查當(dāng)前Consumer Ptr:
- 若cell_buffer狀態(tài)為FULL,釋放鎖,去STEP 4;
- 否則,如果cell_buffer無內(nèi)容,則釋放鎖,回到STEP 1;
- 如果cell_buffer有內(nèi)容,將其標(biāo)記為FULL,同時Producer Ptr前進(jìn)一位;
3.釋放鎖
4.持久化cell_buffer
5.重新上鎖,將cell_buffer狀態(tài)標(biāo)記為FREE,并清空其內(nèi)容;Consumer Ptr前進(jìn)一位;
6.釋放鎖
生產(chǎn)者
1.上鎖,檢查當(dāng)前Producer Ptr對應(yīng)cell_buffer狀態(tài):
如果cell_buffer狀態(tài)為FREE,且生剩余空間足以寫入本次日志,則追加日志到cell_buffer,去STEP X;
2.如果cell_buffer狀態(tài)為FREE但是剩余空間不足了,標(biāo)記其狀態(tài)為FULL,然后進(jìn)一步探測下一位的next_cell_buffer:
- 如果next_cell_buffer狀態(tài)為FREE,Producer Ptr前進(jìn)一位,去STEP X;
- 如果next_cell_buffer狀態(tài)為FULL,說明Consumer Ptr = next_cell_buffer,Ring Log緩沖區(qū)使用完了;則我們繼續(xù)申請一個new_cell_buffer,將其插入到cell_buffer與next_cell_buffer之間,并使得Producer Ptr指向此new_cell_buffer,去STEP X;
3.如果cell_buffer狀態(tài)為FULL,說明此時Consumer Ptr = cell_buffer,丟棄日志;
4.釋放鎖,如果本線程將cell_buffer狀態(tài)改為FULL則通知條件變量cond
在大量日志產(chǎn)生的場景下,Ring Log有一定的內(nèi)存拓展能力;實際使用中,為防止Ring Log緩沖區(qū)無限拓展,會限制內(nèi)存總大小,當(dāng)超過此內(nèi)存限制時不再申請新cell_buffer而是丟棄日志
圖解各場景
初始時候,Consumer Ptr與Producer Ptr均指向同一個空閑cell_buffer1
然后生產(chǎn)者在1s內(nèi)寫滿了cell_buffer1,Producer Ptr前進(jìn),通知后臺消費者線程持久化
消費者持久化完成,重置cell_buffer1,Consumer Ptr前進(jìn)一位,發(fā)現(xiàn)指向的cell_buffer2未滿,等待
超過一秒后cell_buffer2雖有日志,但依然未滿:消費者將此cell_buffer2標(biāo)記為FULL強行持久化,并將Producer Ptr前進(jìn)一位到cell_buffer3
消費者在cell_buffer2的持久化上延遲過大,結(jié)果生產(chǎn)者都寫滿cell_buffer3456,已經(jīng)正在寫cell_buffer1了
生產(chǎn)者寫滿寫cell_buffer1,發(fā)現(xiàn)下一位cell_buffer2是FULL,則拓展換沖區(qū),新增new_cell_buffer
UTC時間優(yōu)化
每條日志往往都需要UTC時間:yyyy-mm-dd hh:mm:ss(PS:Ring Log提供了毫秒級別的精度)
Linux系統(tǒng)下本地UTC時間的獲取需要調(diào)用localtime函數(shù)獲取年月日時分秒
在localtime調(diào)用次數(shù)較少時不會出現(xiàn)什么性能問題,但是寫日志是一個大批量的工作,如果每條日志都調(diào)用localtime獲取UTC時間,性能無法接受
在實際測試中,對于1億條100字節(jié)日志的寫入,未優(yōu)化locatime函數(shù)時 RingLog寫內(nèi)存耗時245.41s,僅比傳統(tǒng)日志寫磁盤耗時292.58s快將近一分鐘;而在優(yōu)化locatime函數(shù)后,RingLog寫內(nèi)存耗時79.39s,速度好幾倍提升
策略
為了減少對localtime的調(diào)用,使用以下策略
RingLog使用變量_sys_acc_sec記錄寫上一條日志時,系統(tǒng)經(jīng)過的秒數(shù)(從1970年起算)、使用變量_sys_acc_min記錄寫上一條日志時,系統(tǒng)經(jīng)過的分鐘數(shù),并緩存寫上一條日志時的年月日時分秒year、mon、day、hour、min、sec,并緩存UTC日志格式字符串
每當(dāng)準(zhǔn)備寫一條日志:
- 調(diào)用gettimeofday獲取系統(tǒng)經(jīng)過的秒tv.tv_sec,與_sys_acc_sec比較;
- 如果tv.tv_sec 與 _sys_acc_sec相等,說明此日志與上一條日志在同一秒內(nèi)產(chǎn)生,故年月日時分秒是一樣的,直接使用緩存即可;
- 否則,說明此日志與上一條日志不在同一秒內(nèi)產(chǎn)生,繼續(xù)檢查:tv.tv_sec/60即系統(tǒng)經(jīng)過的分鐘數(shù)與_sys_acc_min比較;
- 如果tv.tv_sec/60與_sys_acc_min相等,說明此日志與上一條日志在同一分鐘內(nèi)產(chǎn)生,故年月日時分是一樣的,年月日時分 使用緩存即可,而秒sec = tv.tv_sec%60,更新緩存的秒sec,重組UTC日志格式字符串的秒部分;
- 否則,說明此日志與上一條日志不在同一分鐘內(nèi)產(chǎn)生,調(diào)用localtime重新獲取UTC時間,并更新緩存的年月日時分秒,重組UTC日志格式字符串
小結(jié):如此一來,localtime一分鐘才會調(diào)用一次,頻繁寫日志幾乎不會有性能損耗
性能測試
對比傳統(tǒng)同步日志、與RingLog日志的效率(為了方便,傳統(tǒng)同步日志以sync log表示)
1. 單線程連續(xù)寫1億條日志的效率
分別使用Sync log與Ring log寫1億條日志(每條日志長度為100字節(jié))測試調(diào)用總耗時,測5次,結(jié)果如下:
單線程運行下,Ring Log寫日志效率是傳統(tǒng)同步日志的近3.7倍,可以達(dá)到每秒127萬條長為100字節(jié)的日志的寫入
2、多線程各寫1千萬條日志的效率
分別使用Sync log與Ring log開5個線程各寫1千萬條日志(每條日志長度為100字節(jié))測試調(diào)用總耗時,測5次,結(jié)果如下:
多線程(5線程)運行下,Ring Log寫日志效率是傳統(tǒng)同步日志的近3.8倍,可以達(dá)到每秒135.5萬條長為100字節(jié)的日志的寫入
2. 對server QPS的影響
現(xiàn)有一個Reactor模式實現(xiàn)的echo Server,其純凈的QPS大致為19.32萬/s
現(xiàn)在分別使用Sync Log、Ring Log來測試:echo Server在每收到一個數(shù)據(jù)就調(diào)用一次日志打印下的QPS表現(xiàn)
對于兩種方式,分別采集12次實時QPS,統(tǒng)計后大致結(jié)果如下:
傳統(tǒng)同步日志sync log使得echo Server QPS從19.32w萬/s降低至11.42萬/s,損失了40.89%RingLog使得echo Server QPS從19.32w萬/s降低至16.72萬/s,損失了13.46%
TODO
- 日志本身緩存大小的配置
- 程序正常退出、異常退出,此時在buffer中緩存的日志會丟失
- 第N天23:59:59秒產(chǎn)生的日志有時會被刷寫到第N+1天的日志文件中
-
API
+關(guān)注
關(guān)注
2文章
1518瀏覽量
62449 -
文件
+關(guān)注
關(guān)注
1文章
571瀏覽量
24834 -
C++
+關(guān)注
關(guān)注
22文章
2114瀏覽量
73890 -
日志
+關(guān)注
關(guān)注
0文章
139瀏覽量
10684
發(fā)布評論請先 登錄
相關(guān)推薦
寫好C++代碼需要遵循的10個最佳實踐
現(xiàn)代C++項目的最佳實踐
Visual C++小波變換技術(shù)與工程實踐
在NDK開發(fā)中C++的代碼中怎么實現(xiàn)日志輸出
《Visual C++編程基礎(chǔ)與實踐》中文電子教材詳細(xì)資料免費下載
C++程序設(shè)計教程之C++的初步知識的詳細(xì)資料說明
![<b class='flag-5'>C++</b>程序設(shè)計教程之<b class='flag-5'>C++</b>的初步知識的詳細(xì)資料說明](https://file.elecfans.com/web1/M00/89/2B/o4YBAFyJ-q-AYUB7AAVUPZRjpCQ608.png)
評論