【分享】我們用了不到200行代碼實現的文件日誌系統,極佳的IO性能和高併發支持,附壓力測試數據

来源:http://www.cnblogs.com/leotsai/archive/2017/06/24/the-best-logging-system.html
-Advertisement-
Play Games

很多項目都配置了日誌記錄的功能,但是,卻只有很少的項目組會經常去看日誌。原因就是日誌文件生成規則設置不合理,將嚴重的錯誤日誌跟普通的錯誤日誌混在一起,分析起來很麻煩。 其實,我們想要的一個日誌系統核心就這2個要求: 這樣的日誌系統最大的好處就是可以幫助我們一目瞭然的發現嚴重錯誤。結合管理員後臺直接訪 ...


 很多項目都配置了日誌記錄的功能,但是,卻只有很少的項目組會經常去看日誌。原因就是日誌文件生成規則設置不合理,將嚴重的錯誤日誌跟普通的錯誤日誌混在一起,分析起來很麻煩。

 

其實,我們想要的一個日誌系統核心就這2個要求:

  1. 日誌文件能夠按照 /_logs/{group}/yyyy-MM/yyyy-MM-dd-{sequnce}.log 這樣的規則生成;
  2. 調用寫日誌的方法能夠帶 group 這個字元串參數,差不多是這樣:LogHelper.TryLog(string group, string message);

 

 

這樣的日誌系統最大的好處就是可以幫助我們一目瞭然的發現嚴重錯誤。結合管理員後臺直接訪問的文件系統(或Windows資源管理器),可以隨時查看/刪除系統記錄的日誌。如下圖:

 

上面這張圖片就可以很方便的告訴我們,系統是否發生了急需解決的bug。這也是我們覺得一個日誌系統最大的好處。

 

但是,現成的日誌框架中,我們花了很多時間也沒有找到一個正好解決上面兩個需求的框架,於是,喜歡重覆發明輪子的我就花了1個小時寫了一個簡單、高效、調用方便的日誌系統。

 

一個好的日誌系統應該具備的核心功能:

1. 高併發:必須支持高併發的http請求;

2. 文件鎖:占用文件系統(文件鎖)的時間越少越好,因為管理員可能需要隨時把日誌文件導出來,以及刪除日誌文件(不要在刪除時提示文件被占用);

3. 無異常:記錄日誌的方法絕不能拋任何異常(其實就是最外層包了一個try-catch);

4. 高性能:加了記錄日誌的方法之後對系統性能幾乎沒有影響;

5. 靈活:支持任意字元串作為錯誤等級(特殊字元除外),用於生成目錄名稱。

 

代碼及實現原理分析

好了,是時候上代碼了。

  1 using System;
  2 using System.Collections.Generic;
  3 using System.IO;
  4 using System.Text;
  5 using System.Timers;
  6 
  7 namespace MvcSolution
  8 {
  9     public class FileLogger : DisposableBase, ILogger
 10     {
 11         private const int IntervalSeconds = 1;
 12         private const long MaxPerFileBytes = 1024000;
 13         private readonly Dictionary<string, LoggingGroup> _dict;
 14         private readonly Timer _timer;
 15         private bool _busy = false;
 16 
 17         public FileLogger()
 18         {
 19             this._dict = new Dictionary<string, LoggingGroup>();
 20             this._timer = new Timer(IntervalSeconds * 1000);
 21             this._timer.Elapsed += TimerElapsed;
 22         }
 23 
 24         public void Start()
 25         {
 26             _timer.Start();
 27         }
 28 
 29         public void Stop()
 30         {
 31             _timer.Stop();
 32         }
 33 
 34         private void TimerElapsed(object sender, ElapsedEventArgs e)
 35         {
 36             if (_busy)
 37             {
 38                 return;
 39             }
 40             try
 41             {
 42                 _busy = true;
 43                 this.DoWork();
 44             }
 45             catch (Exception)
 46             {
 47 
 48             }
 49             finally
 50             {
 51                 _busy = false;
 52             }
 53         }
 54 
 55         private void DoWork()
 56         {
 57             var items = new List<WritingItem>();
 58             lock (_dict)
 59             {
 60                 foreach (var key in _dict.Keys)
 61                 {
 62                     var group = this._dict[key];
 63                     if (group.Sb.Length == 0)
 64                     {
 65                         continue;
 66                     }
 67                     items.Add(new WritingItem(group));
 68                     group.Sb.Clear();
 69                 }
 70             }
 71             if (items.Count == 0)
 72             {
 73                 return;
 74             }
 75             this.WriteToFile(items);
 76             lock (_dict)
 77             {
 78                 foreach (var item in items)
 79                 {
 80                     var group = this._dict[item.Group];
 81                     group.LastDate = item.LastDate;
 82                     group.LastFilePath = item.LastFilePath;
 83                 }
 84             }
 85         }
 86 
 87         public void Entry(string group, string message)
 88         {
 89             lock (this._dict)
 90             {
 91                 if (!this._dict.ContainsKey(group))
 92                 {
 93                     this._dict[group] = new LoggingGroup(group);
 94                 }
 95                 this._dict[group].Sb.Append("\r\n" + message + "\r\n\r\n");
 96             }
 97         }
 98 
 99         private void WriteToFile(List<WritingItem> items)
100         {
101             lock (this)
102             {
103                 foreach (var item in items)
104                 {
105                     try
106                     {
107                         var date = DateTime.Today.ToString("yyyy-MM-dd");
108                         FileInfo file;
109                         if (item.LastDate == date)
110                         {
111                             file = new FileInfo(item.LastFilePath);
112                             var parent = file.Directory;
113                             if (parent.Exists == false)
114                             {
115                                 Directory.CreateDirectory(parent.FullName);
116                             }
117                             if (file.Exists && file.Length > MaxPerFileBytes)
118                             {
119                                 var yearMonth = DateTime.Today.ToString("yyyy-MM");
120                                 var date2 = DateTime.Now.ToString("yyyy-MM-dd-HHmmss");
121                                 var relativePath = $"\\_logs\\{item.Group}\\{yearMonth}\\{date2}.log";
122                                 file = new FileInfo(AppContext.RootFolder + relativePath);
123                             }
124                         }
125                         else
126                         {
127                             var yearMonth = DateTime.Today.ToString("yyyy-MM");
128                             var relativePath = $"\\_logs\\{item.Group}\\{yearMonth}\\{date}.log";
129                             file = new FileInfo(AppContext.RootFolder + relativePath);
130                             var parent = file.Directory;
131                             if (parent.Exists == false)
132                             {
133                                 Directory.CreateDirectory(parent.FullName);
134                             }
135                         }
136                         File.AppendAllText(file.FullName, item.Text);
137 
138                         item.LastDate = date;
139                         item.LastFilePath = file.FullName;
140                     }
141                     catch (Exception)
142                     {
143 
144                     }
145                 }
146             }
147         }
148         
149         private class WritingItem
150         {
151             public string Group { get; }
152             public string Text { get; }
153             public string LastDate { get; set; }
154             public string LastFilePath { get; set; }
155 
156             public WritingItem(LoggingGroup group)
157             {
158                 this.Group = group.Key;
159                 this.Text = group.Sb.ToString();
160                 this.LastDate = group.LastDate;
161                 this.LastFilePath = group.LastFilePath;
162             }
163         }
164 
165 
166         private class LoggingGroup
167         {
168             public string Key { get; }
169             public StringBuilder Sb { get; }
170             public string LastDate { get; set; }
171             public string LastFilePath { get; set; }
172 
173             public LoggingGroup(string key)
174             {
175                 this.Key = key;
176                 this.Sb = new StringBuilder();
177                 this.LastDate = "";
178                 this.LastFilePath = "";
179             }
180         }
181 
182 
183         protected override void DisposeInternal()
184         {
185             _timer.Dispose();
186         }
187 
188         ~FileLogger()
189         {
190             base.MarkDisposed();
191         }
192     }
193     
194 }
View Code

 

上面這個FileLogger類就是我們寫的文件日誌系統的核心類了。

 

首先要明白這個類有一個定時器Timer,這個Timer有什麼用呢?Timer的用處就是定時將記憶體中記錄的日誌寫入到磁碟,推薦設置為1秒寫入一次。

 

正是因為有了這個Timer,才實現了高併發的處理。其原理大概是這樣:

 

由於WEB伺服器每秒鐘可能會處理大量的http請求,如果某個請求拋了異常需要記錄日誌,這時候如果每個請求都直接往磁碟中寫數據,那麼磁碟開銷是極其高的,並且文件鎖會導致大量排隊,這就極大的影響了WEB伺服器的性能。所以,更好的做法是:每個http請求內拋的異常先寫到記憶體(就是FileLogger類的StringBuilder啦),然後再定時將記憶體中的日誌寫入到磁碟,這樣處於性能瓶頸的磁碟操作就變成單線程操作了。

 

如何使用這個FileLogger呢?

真的很簡單啦,我們只是建了一個非常簡單的helper類,如下:

 1 using System;
 2 using System.Text;
 3 using System.Web;
 4 
 5 namespace MvcSolution
 6 {
 7     public class LogHelper
 8     {
 9         private static ILogger _logger;
10         public static ILogger Logger
11         {
12             get
13             {
14                 if (_logger == null)
15                 {
16                     _logger = Ioc.Get<ILogger>();
17                 }
18                 return _logger;
19             }
20         }
21 
22         public static void TryLog(string group, Exception exception)
23         {
24             try
25             {
26                 var sb = new StringBuilder($"【{DateTime.Now.ToFullTimeString()}】{exception.GetAllMessages()}\r\n[stacktrace]: \r\n{exception.StackTrace}\r\n");
27                 AppendHttpRequest(sb);
28                 Logger.Entry(group, sb.ToString());
29             }
30             catch (Exception)
31             {
32 
33             }
34         }
35 
36         public static void TryLog(string group, string message)
37         {
38             try
39             {
40                 var sb = new StringBuilder($"【{DateTime.Now.ToFullTimeString()}】{message}\r\n");
41                 AppendHttpRequest(sb);
42                 Logger.Entry(group, sb.ToString());
43             }
44             catch (Exception)
45             {
46                 
47             }
48         }
49 
50         private static void AppendHttpRequest(StringBuilder sb)
51         {
52             if (HttpContext.Current == null)
53             {
54                 return;
55             }
56             var request = HttpContext.Current.Request;
57             sb.Append($"[{request.UserHostAddress}]-{request.HttpMethod}-{request.Url.PathAndQuery}\r\n");
58             foreach (var header in request.Headers.AllKeys)
59             {
60                 sb.Append($"{header}: {request.Headers.Get(header)}\r\n");
61             }
62         }
63     }
64 }

 

然後在WEB應用程式啟動的時候,註入ILogger的實現類為FileLogger並啟動FileLogger的Timer定時器:

調用的地方如下方代碼所示:

public ActionResult Log()
{
    LogHelper.TryLog("home-log", "阿克大廈卡薩丁卡薩丁,暗殺神大,啊實打實大拉聖誕快樂啊,阿薩斯柯達速度快八十多,啊實打實大咖快睡吧");
    return new ContentResult(){Content = "ok"};
}

public ActionResult Loge()
{
    try
    {
        var i = int.Parse("abc");
    }
    catch (Exception ex)
    {
        LogHelper.TryLog("home-log-ex", ex);
    }
    return new ContentResult() { Content = "ok" };
}

 

性能測試

測試環境用的VS2017自帶的IIS Express。之前寫過一篇博文講IIS多線程工作機制的,有興趣的朋友可以轉過去看看,對於理解高併發壓力測試有幫助哦:

http://www.cnblogs.com/leotsai/p/understanding-iis-multithreading-system.html

 

測試工具:ab(全稱ApacheBench)

測試代碼:MvcSolution.Web.Public.Controllers.HomeController下麵的Log和Loge兩個方法

總請求數:10萬

併發:1000

最關心的指標:Requests per second,每秒處理請求數,也叫吞吐率。

 

測試1:使用LogHelpper.TryLog(string group, string message)方法記錄日誌,下麵是測試結果截圖:

 

可以看到全部執行成功,每秒處理請求數:420次;

 

測試2:使用LogHelpper.TryLog(string group, Exception exception)方法記錄日誌,下麵是測試結果截圖:

每秒處理請求數:397次;

 

測試3:我們想看看把記錄日誌的代碼註釋掉後,該方法本來的吞吐率,請看下方測試結果截圖:

每秒處理請求數:436.

 

結論:即使使用TryLog(string group, Exception exception)重載,對系統的影響為:(436-397)/436 =  8.9%。先不要被這個8.9%嚇到了,這數字是基於每個請求都記錄日誌的情況下產生的,而在實際項目運行過程中,如果算1000次請求記錄一次錯誤日誌的話,那就變成0.0089%了,不到萬一之影響啊。

 如果按照TryLog(string group, string message)重載,對系統的影響為:(436-421)/436 =  3.4%,換算成每千次請求記錄一次日誌,則只有0.0034%的影響。而這個重載還是我們系統中用的最多的一個記錄日誌的方法。

 

所以,現在可以放心的使用這個日誌系統了。

 

所以,自己寫一個高性能日誌系統也沒有那麼難嘛。 

 

獲取源碼並加入討論QQ群:539301714

本文中所有的代碼已提交到我們的ASP.NET MVC開源框架 MVCSolution項目中了,GitHub地址:

https://github.com/leotsai/mvcsolution

 

MVCSolution 是我們團隊基於ASP.NET MVC搭建的一整套WEB應用程式框架,包括大量的最佳實踐,代碼包含:單元測試、EF CodeFirst 資料庫定義、資料庫訪問、資料庫事務最佳實踐、日誌系統、加解密、JSON/XML序列化和反序列化、session管理、記憶體隊列管理、多層級異常處理、標準ajax框架、以及基於grunt的JavaScript前端框架。

 

由於有不少朋友在學習MvcSolution的過程中遇到一些問題或者想問問為什麼這麼設計,於是我們建了一個QQ群方便大家交流:539301714,歡迎加群哦~

 

後面我們還會將admin後臺通過web方式查看和管理日誌文件系統的源碼公開出來,到時也會提交到MvcSolution,感興趣的朋友歡迎關註哦。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. JVM簡介 初識JVM JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的電腦,是通過在實際的電腦上模擬模擬各種電腦功能來實現的。 Java語言的一個非常重要的特點就是與平臺的無關性。而使用Java虛擬機是實現這一 ...
  • ./Factory.class.php ./match_list_c.php ./MatchModel.class.php ./Model.class.php ./MySQLDB.class.php ./template/.htaccess Deny from All ./template/matc ...
  • 題目描述 現給定n個閉區間[ai, bi],1<=i<=n。這些區間的並可以表示為一些不相交的閉區間的並。你的任務就是在這些表示方式中找出包含最少區間的方案。你的輸出應該按照區間的升序排列。這裡如果說兩個區間[a, b]和[c, d]是按照升序排列的,那麼我們有a<=b<c<=d。 請寫一個程式: ...
  • 今天練習ArrayList與LinkedList,在網上看到有關它倆應用效率的題型。覺得很有價值,保留一下。 附加: 遇到java類型後面跟三個點是代表的情況了,就補充一下: 相關參考鏈接: http://pengcqu.iteye.com/blog/502676 ...
  • 全球最大的軟體製造商微軟2月12日警告公眾稱其一部分珍貴的Windows NT和Windows 2000操作系統源代碼被泄漏到了一些線上文件共用網路中。 微軟稱被泄漏的代碼只是整個程式的一小部分,但這沒有阻止出於好奇心和懷有惡意的人設法將其納入囊中。考慮到2月13日在互聯網文件共用網路中交換的文件大 ...
  • 一個開發者,如何才能更值錢? 答案非常簡單:掌握稀缺資源。 那麼,怎樣才能持續不斷地掌握稀缺資源,讓自己更值錢呢? 請看接下來介紹的 2 種識別稀缺的方法和 2 種培養稀缺的策略。 稀缺資源的秘密 資源有很多,比如知識、技能、關係、社會資源、信息、天賦等等,哪種資源才是稀缺的呢? 答案可能不在資源本 ...
  • 頁面報錯: 後臺錯誤: Field error in object 'user' on field 'birthday': rejected value [2013-06-24]; codes [typeMismatch.user.birthday,typeMismatch.birthday,typ ...
  • 一、定義 ArrayList和LinkedList是兩個集合類,用於儲存一系列的對象引用(references)。 引用的格式分別為: 1 ArrayList<String> list = new ArrayList<String>(); 1 LinkedList<Integer> list = n ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...