前言
目前本人從事 JAVA開發
之前講過《你的日志組件記錄夠清晰嘛?--自己開發日志組件 Logger》 日志文件,當你是羡慕java下面的log4j,打印日志夠清晰,可以很清晰定位打印日志所在文件,行號等;
於是嘗試了重寫了日志組件來模擬清晰打印;
序言
最近和群里大佬們研究游戲服務器架構的時候,討論像魔獸,完美國際等游戲世界場景無縫地圖實現方案;討論兩周后開始動手BB自己的服務器架構已經線程模型規划;
以上是最新服務器架構圖;具體現在不BB,也不介紹具體關系,今天的重點是日志
然后出現一個問題,就是當服務器承載3000左右,log4j在高並發下 導致我的所有線程BLOCK了;咳咳;
也算是遇到了;當時想的是log4j比較是比較老的版本,很多東西肯定不是很適用了,想着換log4j2,再次進行測試,當服務器承載到5000的時候依然所有線程BLOCK;
當時在網上尋求各種解決辦法依然未能解決我的線程BLOCK,於是我只能再一次走上重復造輪子的道路;
想起了以前的寫的日志組件,翻頁成java版本;
PS:本人有一個習慣,就是類似架構或者代碼,習慣用java和C#各寫一遍;
重點是代碼,
C#.net重構
本次工作重點是對之前版本進行迭代和重構;
重點優化是代碼的結構,讓代碼更清晰;思路更清晰;
重要的是加入磁盤io的雙緩沖區來解決寫入速度,提高io效率;
本次重點加入可讀取配置文件模塊

1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 /** 9 * 10 * @author 失足程序員 11 * @Blog http://www.cnblogs.com/ty408/ 12 * @mail 492794628@qq.com 13 * @phone 13882122019 14 * 15 */ 16 namespace Net.Sz.Framework.Szlog 17 { 18 19 /// <summary> 20 /// 初始化輔助函數 21 /// 22 /// <para>默認是不打印棧楨的,因為比較耗時:如果需要請設置 LOGSTACKTRACE = true 或者 ↓↓↓</para> 23 /// <para>AppSettings 設置 log_print_stackrace 日志是否輸出調用棧楨 true or false</para> 24 /// <para>AppSettings 設置 log_print_console 日志是否輸出到控制台 true or false</para> 25 /// <para>AppSettings 設置 log_print_level 日志的等級,忽律大小寫 DEBUG INFO WARN ERROR</para> 26 /// <para>AppSettings 設置 log_print_path 日志的文件名帶目錄,log/sz.log</para> 27 /// <para>AppSettings 設置 log_print_file 日志是否輸出到文件 true or false</para> 28 /// <para>AppSettings 設置 log_print_file_buffer 日志雙緩沖輸出到文件 true or false</para> 29 /// </summary> 30 public class CommUtil 31 { 32 /// <summary> 33 /// 日志路徑存儲 34 /// </summary> 35 internal static string LOGPATH = "log/sz.log"; 36 37 /// <summary> 38 /// 日志等級 39 /// <para>默認 LogLevel.DEBUG 打印</para> 40 /// </summary> 41 public static LogLevel LOG_PRINT_LEVEL = LogLevel.DEBUG; 42 43 /// <summary> 44 /// 是否顯示控制台消息 45 /// <para>默認 true 打印</para> 46 /// </summary> 47 public static bool LOG_PRINT_CONSOLE = true; 48 49 /// <summary> 50 /// 是否輸出文件消息 51 /// <para>默認 true 打印</para> 52 /// </summary> 53 public static bool LOG_PRINT_FILE = true; 54 /// <summary> 55 /// 輸出日志到文件的時候使用buff雙緩沖減少磁盤IO,可能導致日志打印不及時 56 /// <para>雙緩沖對輸出到控制台不受印象</para> 57 /// <para>默認 true</para> 58 /// </summary> 59 public static bool LOG_PRINT_FILE_BUFFER = true; 60 61 /// <summary> 62 /// 是否打印棧楨 63 /// <para>默認 false 不打印</para> 64 /// </summary> 65 public static bool LOG_PRINT_STACKTRACE = false; 66 67 68 /// <summary> 69 /// 設置日志輸出目錄 70 /// </summary> 71 /// <param name="path"></param> 72 static public void SetLogRootPath(string logPath) 73 { 74 ResetLogDirectory(logPath); 75 LOGPATH = logPath; 76 } 77 78 /// <summary> 79 /// 構建輸出目錄 80 /// </summary> 81 /// <param name="logPath"></param> 82 static public void ResetLogDirectory(string logPath) 83 { 84 string bpath = System.IO.Path.GetDirectoryName(logPath); 85 if (!Directory.Exists(bpath)) { Directory.CreateDirectory(bpath); } 86 } 87 88 89 /// <summary> 90 /// 友好方法,不對外,初始化 91 /// </summary> 92 internal static void InitConfig() 93 { 94 if (System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("log_print_path")) 95 { 96 string log_print_path = System.Configuration.ConfigurationManager.AppSettings["log_print_path"].ToString(); 97 SetLogRootPath(log_print_path); 98 } 99 else SetLogRootPath(LOGPATH); 100 101 Console.WriteLine("當前日志存儲路徑:" + LOGPATH); 102 103 if (System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("log_print_level")) 104 { 105 string log_print_level = System.Configuration.ConfigurationManager.AppSettings["log_print_level"].ToString(); 106 if (!Enum.TryParse(log_print_level, false, out LOG_PRINT_LEVEL)) 107 LOG_PRINT_LEVEL = LogLevel.DEBUG; 108 } 109 110 Console.WriteLine("當前日志級別:" + LOG_PRINT_LEVEL); 111 112 if (System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("log_print_file")) 113 { 114 string log_print_file = System.Configuration.ConfigurationManager.AppSettings["log_print_file"].ToString(); 115 if (!Boolean.TryParse(log_print_file, out LOG_PRINT_FILE)) 116 LOG_PRINT_FILE = true; 117 } 118 119 Console.WriteLine("當前日志是否輸出文件:" + LOG_PRINT_FILE); 120 121 if (System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("log_print_file_buffer")) 122 { 123 string log_print_file_buffer = System.Configuration.ConfigurationManager.AppSettings["log_print_file_buffer"].ToString(); 124 if (!Boolean.TryParse(log_print_file_buffer, out LOG_PRINT_FILE_BUFFER)) 125 LOG_PRINT_FILE_BUFFER = true; 126 } 127 128 Console.WriteLine("當前日志buff雙緩沖輸出文件:" + LOG_PRINT_FILE_BUFFER); 129 130 if (System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("log_print_console")) 131 { 132 string log_print_console = System.Configuration.ConfigurationManager.AppSettings["log_print_console"].ToString(); 133 if (!Boolean.TryParse(log_print_console, out LOG_PRINT_CONSOLE)) 134 LOG_PRINT_CONSOLE = true; 135 } 136 137 Console.WriteLine("當前日志是否輸出控制台:" + LOG_PRINT_CONSOLE); 138 139 if (System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("logs_print_tackrace")) 140 { 141 string logs_print_tackrace = System.Configuration.ConfigurationManager.AppSettings["logs_print_tackrace"].ToString(); 142 if (!Boolean.TryParse(logs_print_tackrace, out LOG_PRINT_STACKTRACE)) 143 LOG_PRINT_STACKTRACE = false; 144 } 145 146 Console.WriteLine("當前日志是否輸出棧楨:" + LOG_PRINT_STACKTRACE); 147 } 148 149 } 150 }
.config文件AppSettings模塊加入配置節點,可以設置日志輸出參數
1 /// <para>默認是不打印棧楨的,因為比較耗時:如果需要請設置 LOGSTACKTRACE = true 或者 ↓↓↓</para> 2 /// <para>AppSettings 設置 log_print_stackrace 日志是否輸出調用棧楨 true or false</para> 3 /// <para>AppSettings 設置 log_print_console 日志是否輸出到控制台 true or false</para> 4 /// <para>AppSettings 設置 log_print_level 日志的等級,忽律大小寫 DEBUG INFO WARN ERROR</para> 5 /// <para>AppSettings 設置 log_print_path 日志的文件名帶目錄,log/sz.log</para> 6 /// <para>AppSettings 設置 log_print_file 日志是否輸出到文件 true or false</para> 7 /// <para>AppSettings 設置 log_print_file_buffer 日志雙緩沖輸出到文件 true or false</para>
日志級別枚舉獨立出來

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 8 /** 9 * 10 * @author 失足程序員 11 * @Blog http://www.cnblogs.com/ty408/ 12 * @mail 492794628@qq.com 13 * @phone 13882122019 14 * 15 */ 16 namespace Net.Sz.Framework.Szlog 17 { 18 19 /// <summary> 20 /// 日志級別 21 /// </summary> 22 public enum LogLevel 23 { 24 /// <summary> 25 /// 完全不輸出任何日志 26 /// </summary> 27 Null = 0, 28 /// <summary> 29 /// 輸出 DEBUG 以上級別 30 /// </summary> 31 DEBUG = 1, 32 /// <summary> 33 /// 輸出 INFO 以上級別 34 /// </summary> 35 INFO = 2, 36 /// <summary> 37 /// 輸出 WARN 以上級別 38 /// </summary> 39 WARN = 3, 40 /// <summary> 41 /// 輸出 ERROR 以上級別 42 /// </summary> 43 ERROR = 4 44 } 45 46 }
根據log4j使用習慣加入,日志級別判斷減少調用和創建,
public bool IsDebugEnabled() { return CommUtil.LOG_PRINT_LEVEL <= LogLevel.DEBUG; } public bool IsInfoEnabled() { return CommUtil.LOG_PRINT_LEVEL <= LogLevel.INFO; } public bool IsWarnEnabled() { return CommUtil.LOG_PRINT_LEVEL <= LogLevel.WARN; } public bool IsErrorEnabled() { return CommUtil.LOG_PRINT_LEVEL <= LogLevel.ERROR; }
這個大家都懂的,也就是為了減少無用執行的;
本次優化還有一個重點之處在於日志的構建調用線程執行,而不是寫入線程執行;
並且加入在構建日志信息的時候是否打印堆棧信息,也就是調用棧楨情況;因測試這個比較耗時;所以默認不開放的;
/// <summary> /// /// </summary> /// <param name="level"></param> /// <param name="msg"></param> /// <param name="exception"></param> /// <param name="f">棧楨深度</param> /// <returns></returns> string GetLogString(string level, Object msg, Exception exception, int f) { string tmp1 = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff: "); StringBuilder sb = new StringBuilder(); sb.Append("[") .Append(tmp1) .Append(level) .Append(":"); if (CommUtil.LOG_PRINT_STACKTRACE) { /*獲取堆棧信息非常耗性能*/ StackFrame frame = new StackTrace(f, true).GetFrame(0); sb.Append(frame.GetMethod().DeclaringType.FullName); sb.Append("."); sb.Append(frame.GetMethod().Name); sb.Append("."); sb.Append(frame.GetFileLineNumber()); } sb.Append("] "); sb.AppendLine(msg.ToString()); if (exception != null) { sb .Append(exception.GetType().FullName) .Append(": ") .AppendLine(exception.Message) .AppendLine(exception.StackTrace) .AppendLine("----------------------Exception--------------------------"); } return sb.ToString(); }
StreamWriter wStream = null; FileStream fStream = null;
C#下面寫入文件用流寫入;streamwriter類;提供了write方法,這個函數值輸入到基礎流;需要調用Flush();才是把流寫入文件中;
那么優化的雙緩沖方案就來了;循環寫入數據的時候,設置50次一個io,
注意:你的日志量並不大還是一條日志一個io,重點在於你日志量很大的時候,間隔50條日志一次文件io
while (msgs.Count > 0) { CreateFile(); if (CommUtil.LOG_PRINT_FILE_BUFFER) { /*雙緩沖,減少磁盤IO*/ for (int i = 0; i < 50; i++) { String msg; if (msgs.TryDequeue(out msg)) { wStream.Write(msg); } else break; } /*輸入流到文件*/ wStream.Flush(); fStream.Flush(); } else { String msg; if (msgs.TryDequeue(out msg)) { /*輸入流到文件*/ wStream.Write(msg); wStream.Flush(); fStream.Flush(); } else break; } }
本次優化文件寫入條件加入文件寫入指定文件測試代碼,但是不保證並發沖突,意圖在於調試的時候,測試一些流程日志
/// <summary> /// 增加日志 /// </summary> /// <param name="level"></param> /// <param name="msg"></param> /// <param name="exception"></param> void AddLog(string level, Object msg, Exception exception) { string logmsg = GetLogString(level, msg, exception, 3); if (exception != null) { if (CommUtil.LOG_PRINT_FILE) { /*處理如果有異常,需要把異常信息打印到單獨的文本文件*/ if (wfileerror == null) lock (typeof(WriterFile)) if (wfileerror == null) /*雙重判定單例模式,防止並發*/ wfileerror = new WriterFile(CommUtil.LOGPATH, "log-error-file", true); wfileerror.Add(logmsg); } } if (CommUtil.LOG_PRINT_FILE) { /*處理到日志文件*/ if (wfile == null) lock (typeof(WriterFile)) if (wfile == null) /*雙重判定單例模式,防止並發*/ wfile = new WriterFile(CommUtil.LOGPATH, "log-file", false); wfile.Add(logmsg); } if (CommUtil.LOG_PRINT_CONSOLE) { /*處理到控制台*/ if (wconsole == null) lock (typeof(WriterFile)) if (wconsole == null) /*雙重判定單例模式,防止並發*/ wconsole = new WriterConsole("log-console"); wconsole.Add(logmsg); } }
日志線程,需要是才會創建;如果沒有調用不會創建線程;
本次優化日志線程分為日志文件線程,錯誤日志文件線程和日志控制台線程;
加入如果有exception,把當前日志寫入error文件進行備份,方便查詢exception;(並未有關閉操作,一定會寫)
本次在日志操作加入每天一個文件備份;

/// <summary> /// 創建文件以及備份文件操作 /// </summary> public void CreateFile() { String logPath = FileName; if (this.Error) { logPath += "_error.log"; } if (File.Exists(logPath)) { /*檢查文件備份,每日一個備份*/ DateTime dtime = File.GetLastWriteTime(logPath); string day1 = dtime.ToString("yyyy-MM-dd"); string day2 = DateTime.Now.ToString("yyyy-MM-dd"); /*獲取文件的上一次寫入時間是否不是今天日期*/ if (!day1.Equals(day2)) { Close(); wStream = null; fStream = null; /*備份*/ File.Move(logPath, logPath + "_" + day1 + ".log"); } } if (fStream == null) { /*追加文本*/ fStream = new FileStream(logPath, System.IO.FileMode.Append); /*重建流*/ wStream = new System.IO.StreamWriter(fStream); } }
重點優化地方已經講解完畢;
測試
在雙緩沖輸出日志的情況下性能測試;

1 class Program 2 { 3 4 static SzLogger log = null; 5 6 static void Main(string[] args) 7 { 8 CommUtil.LOG_PRINT_CONSOLE = false; 9 CommUtil.LOG_PRINT_FILE = true; 10 //CommUtil.LOG_PRINT_FILE_BUFFER = false; 11 log = SzLogger.getLogger(); 12 /*配置可以防在config里面*/ 13 CommUtil.LOG_PRINT_CONSOLE = true; 14 log.Debug("Debug"); 15 /*修改打印級別,不會輸出info*/ 16 CommUtil.LOG_PRINT_LEVEL = LogLevel.WARN; 17 log.Info("Info"); 18 log.Warn("Warn"); 19 log.Error("Error"); 20 21 /*取消控制台打印*/ 22 CommUtil.LOG_PRINT_CONSOLE = false; 23 24 Console.WriteLine("准備好測試了請敲回車"); 25 Console.ReadLine(); 26 27 long time = TimeUtil.CurrentTimeMillis(); 28 for (int k = 0; k < 5; k++) 29 { 30 /*5個線程*/ 31 new System.Threading.Thread(() => 32 { 33 /*每個線程 10萬 條日志*/ 34 for (int i = 0; i < 100000; i++) 35 { 36 Program.log.Error(i + " ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss我測ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"); 37 } 38 }).Start(); 39 } 40 System.Threading.Thread.Sleep(20); 41 while (Program.log.Count > 0) 42 { 43 44 } 45 Console.WriteLine("50萬條日志並發寫入結束" + (TimeUtil.CurrentTimeMillis() - time)); 46 Console.ReadLine(); 47 } 48 }
輸出結果:
文件大小:
以上是C#版本介紹結束;
Java
java版本和C#版本其實都是翻譯問題,思路都是一個思路;
采用NetBeans 8.2+ 工具,maven 項目管理;
java版本重點也還是在於日志寫入思路;
采用流文件寫入通道;之前也是對比了網上其他園友的寫入文件方式來測試輸出,可能不是最優方案如果園友有更高效的方式請告知;
BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), "utf-8"));
java中統樣采用50條日志的磁盤io操作
1 while (!logs.isEmpty()) { 2 if (System.currentTimeMillis() - bigen > 1000) { 3 /*一秒鍾檢查一次文件備份*/ 4 bigen = System.currentTimeMillis(); 5 createFileWriter(); 6 } 7 if (CommUtil.LOG_PRINT_FILE_BUUFER) { 8 for (int i = 0; i < 50; i++) { 9 String poll = logs.poll(); 10 if (poll == null) { 11 break; 12 } 13 write(poll); 14 } 15 flush(); 16 } else { 17 /*非緩存單次壓入文件*/ 18 String poll = logs.poll(); 19 if (poll != null) { 20 write(poll); 21 flush(); 22 } 23 } 24 }
依然還是那個問題,如果日志並發不足,磁盤io可能會是一條日志一個io;
測試
測試代碼

private static SzLogger log = null; public static void main(String[] args) throws Exception { CommUtil.LOG_PRINT_CONSOLE = false; log = SzLogger.getLogger(); System.out.print("准備就緒請敲回車"); System.in.read(); long bigen = System.currentTimeMillis(); ArrayList<Thread> threads = new ArrayList<>(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { log.error(i + " cssssssssssssssssdgdfgdfgdyrsbsfgsrtyhshstjhsrthsbsdhae063.00365ssssssssssssssssssssssssss"); } } }); thread.start(); threads.add(thread); } for (Thread thread : threads) { thread.join(); } while (true) { if (log.logSize() == 0) { System.out.println((System.currentTimeMillis() - bigen)); System.exit(0); } } }
結果:稍微比C#慢了一點;不排除是系統優化或者我使用不當的問題;
--- exec-maven-plugin:1.2.1:exec (default-cli) @ net.sz.game.engine.szlog --- [03-23 17:25:25:562:INFO :CommUtil.initConfig():75] 設置系統字符集sun.stdout.encoding:utf-8 [03-23 17:25:25:565:INFO :CommUtil.initConfig():76] 設置系統字符集sun.stderr.encoding:utf-8 [03-23 17:25:25:565:INFO :CommUtil.initConfig():128] 日志級別:DEBUG [03-23 17:25:25:566:INFO :CommUtil.initConfig():129] 輸出文件日志目錄:../log/sz.log [03-23 17:25:25:566:INFO :CommUtil.initConfig():130] 是否輸出控制台日志:false [03-23 17:25:25:566:INFO :CommUtil.initConfig():131] 是否輸出文件日志:true [03-23 17:25:25:566:INFO :CommUtil.initConfig():132] 是否使用雙緩沖輸出文件日志:true 准備就緒請敲回車 3779 ------------------------------------------------------------------------ BUILD SUCCESS ------------------------------------------------------------------------ Total time: 8.327s Finished at: Thu Mar 23 17:25:32 CST 2017 Final Memory: 8M/300M ------------------------------------------------------------------------
總結
本次優化在於解決高並發下日志沖突問題,導致線程BLOCK;增加系統穩定性;
再一次的重復造輪子;
本日志組件在於,異步寫入文件和控制台輸出;采用文件流加雙緩沖的方式即時吸入文件方式;
沒有更多緩存的目的是防止程序崩潰日志沒有寫入情況;
這種情況下,就算程序突然異常退出情況下,能保證極少數的日志沒有寫入文件中;