3秒鍾完成50萬條並發日志 文件寫入


前言

目前本人從事 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 }
View Code

 

.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 }
View Code

 

 根據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);
            }

        }
View Code

 

重點優化地方已經講解完畢;

測試

在雙緩沖輸出日志的情況下性能測試;

 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     }
View Code

 

輸出結果:

文件大小:

以上是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);
            }
        }
    }
View Code

 

結果:稍微比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;增加系統穩定性;

再一次的重復造輪子;

本日志組件在於,異步寫入文件和控制台輸出;采用文件流加雙緩沖的方式即時吸入文件方式;

沒有更多緩存的目的是防止程序崩潰日志沒有寫入情況;

這種情況下,就算程序突然異常退出情況下,能保證極少數的日志沒有寫入文件中;

 

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓完整代碼版本請注意下面的svn地址,↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM