Mudo C++網路庫第五章學習筆記
self發表於2018-10-14
高效的多執行緒日誌
- 日誌(logging)有兩個意思:
- 診斷日誌(diagnostic log), 常用日誌庫提供日誌功能;
- 交易日誌(transaction log), 用於記錄狀態變更, 通過回放日誌可以逐步恢復每一次修改後的狀態;
- 日誌通常用於故障診斷和追蹤(trace), 也可用於效能分析;
- 日誌通常是分散式系統中事故調查時的唯一線索, 用來追尋蛛絲馬跡, 查出原凶;
- Log Everything All The Time;
- 關於程式, 日誌通常要記錄:
- 收到每條內部訊息的ID(還可以包括關鍵欄位、長度、hash等);
- 收到的每條外部訊息的全文;
- 發出每條訊息的全文, 每條訊息都有全域性唯一的id;
- 關鍵內部狀態的變更, 等等;
- 每條日誌都有時間戳, 這樣就能完整追蹤分散式系統中一個事件的來龍去脈;
- 一個日誌檔案可分為前端(frontend)和後端(backend)兩部分;
- 前端提供應用程式使用的介面(API), 並生成日誌訊息(log message);
- 後端則負責把日誌訊息寫到目的地(destination);
- 典型的多生產者-單消費者問題, 對生產者(前端)而言, 要儘量做到低延遲、低CPU開銷、無阻塞;
- 對消費者(後端)而言, 要做到足夠大的吞吐量, 並佔用較少資源;
- 對C++程式而言, 最好整個程式(包括主程式和程式庫)都使用相同的日誌庫, 日誌有一個整體的日誌輸出, 而且不要各個元件有各自的日誌輸出;
- 從這個意義上講, 日誌庫是個singleton模式(單例模式);
- muduo沒有用標準庫中的iostream, 而是自己寫的LogStream class, 主要是出於效能原因;
功能需求
- 日誌訊息有多種級別(level): TRACE、DEBUG、INFO、ERROR、FATAL等;
- 日誌訊息可能有多個目的地(appender), 如檔案、socket、SMTP等;
- 日誌訊息的格式可配置(layout), 例如org.apache.log4j.PatternLayout;
- 可以設定執行時過濾器(filter), 控制不同元件的日誌訊息的級別和目的地;
- 質量保證(QA)測試環境的時候輸出DEBUG級別的日誌, 在生產環境輸出INFO級別的日誌;
- 只要呼叫muduo::Logger::setLogLevel()就能立即生效;
- 對於分散式系統中的服務程式而言, 日誌的目的第(destination)只有一個:本地檔案;
- 日誌檔案的滾動(rolling)是必需的, 這樣可以簡化日誌歸檔(archive)的實現;
- rolling的條件通常有兩個: 檔案大小(例如每寫滿1GB就換下一個檔案)和時間(例如每天零點新建一個日誌檔案, 不論前一個檔案有沒有寫滿);
- 一般的日誌庫都會自動根據檔案大小和時間來主動滾動日誌檔案;
- 能主動rolling, 自然也就不必支援SIGUSR1了, 畢竟多執行緒程式處理signal很麻煩;
- 日誌庫的壓縮與歸檔(archive)不是日誌庫應該有的功能, 而應該交給專門的指令碼去做;
- 磁碟空間監控也不是日誌庫的必備功能;
- muduo日誌庫: 其一是定期(預設3秒), 將緩衝區內的日誌訊息flush到磁碟;
- 其二是每條記憶體中的日誌訊息都帶有cookie(或者叫哨兵值/sentry), 其值為某個函式的地址, 這樣通過core dump檔案中查詢cookie就能找到尚未來得及寫入磁碟的訊息;
- 日誌訊息格式有幾個要點:
- 儘量每條日誌佔一行;
- 時間戳精確到微妙;
- 始終使用GMT時區(Z);
- 答應執行緒id;
- 列印日誌級別;
- 列印原始檔名和行號;
效能需求
- 日誌庫的高效性體現在幾個方面:
- 每秒寫上千萬條日誌的時候沒有明顯的效能損失;
- 能應對一個程式生產大量日誌資料的場景, 例如1GB/min;
- 不阻塞正常的執行流程;
- 在多程式程式中, 不造成爭用(contention);
- 磁碟頻寬約是110MB/S, 日誌庫應該能瞬時寫滿這個頻寬(不必持續太久);
- 假如每條日誌訊息的平均長度是110位元組, 這就意味著1秒要寫100萬條日誌;
- muduo日誌庫實現了幾點優化措施:
- 時間戳字串中的日期和時間部分是快取的, 一秒內的多條日誌只需要重新格式化微妙部分;
- 日誌訊息的前4個欄位是定長的, 因此可以避免在執行期求字串長度(不會反覆呼叫strlen);
- 因為編譯器認識memcpy()函式, 對於定長的記憶體複製, 會在編譯期把它的inline展開為高效的目的碼;
- 執行緒id是預先格式化為字串, 在輸出日誌訊息時只需要簡單拷貝幾個位元組;
- 每行日誌訊息的原始檔名部分採用了編譯期計算來獲得basename, 避免執行期strrchr()開銷;
多執行緒非同步日誌
- 多執行緒程式對日誌庫提出了新的需求:執行緒安全, 即多個程式可以併發寫日誌, 兩個執行緒的日誌訊息不會出現交織;
- 多執行緒的每個程式最好只寫一個日誌檔案;
- 用一個背景執行緒收集日誌訊息, 並寫入日誌檔案, 其他業務執行緒只管往這個日誌執行緒傳送日誌訊息, 這稱為非同步日誌;
- 非阻塞日誌;
- 我們需要一個佇列將日誌前端的資料傳送到後端(日誌執行緒);
- muduo日誌庫採用的是雙緩衝(double buffering)技術;
- 基本思路是準備兩塊buffer:A和B, 前端負責往buffer A填資料(日誌訊息), 後端負責將buffer B的資料寫入檔案;
- 當buffer A寫滿之後, 交換A和B, 讓後端將buffer A的資料寫入檔案, 而前端則往buffer B填入新的日誌訊息, 如此往復;
- 為了及時將日誌訊息寫入檔案, 即便buffer A未滿, 日誌庫也會每3秒執行一次上述交換寫入操作;
- 對於非同步日誌來說, 這是典型的生產速度高於消費速度問題, 會造成資料在記憶體中的堆積, 嚴重時引發效能問題(可用記憶體不足)或程式崩潰(分配記憶體失敗);
- 直接丟棄掉多餘的日誌buffer, 以騰出記憶體, 這樣可以防止程式庫本身引起程式故障, 是一種自我保護措施;
- 也可以加入網路報警功能, 通知人工介入, 以儘快修復故障;
- 效能不能憑感覺說了算, 一定要有典型場景的測試資料做為支撐;
- muduo庫的非同步日誌實現用了一個全域性鎖;
- java的ConcurrentHashMap那樣用多個桶子(bucket), 前端寫日誌的時候再按執行緒id雜湊到不同的bucket中, 以減少contention(後端實現比較複雜);
- Linux預設會把core dump寫到當前目錄, 而且檔名是固定的core;