zoukankan      html  css  js  c++  java
  • 性能秒杀log4net的NLogger日志组件(附测试代码与NLogger源码)

    NLogger特点(200行代码的日志组件):

    一:不依赖于第三方插件和支持.net2.0

    二:支持多线程高并发

    三:支持读写双缓冲对列

    四:自定义日志缓冲区大小

    五:支持即时触发刷盘机制

    六:先按日期再按文件大小RollingFile日志

    七:支持日志存储位置,日志文件前缀的个性化定义

    一:为什么要特别强调不依赖于第三方插件和支持.net2.0

    NLogger包括名称空间也未超过200行代码,可见日志是相当轻量级的,如果是依赖于第三方软件的支持,有失轻量级的定义。

    NLogger的第一个版本是基本于.net4.0开发,但是发现在实际应用的时候很难降级到.net2.0的项目,因为第一个版本用到了很多.net4.0的特性,主要表现在:

    1,多线程处理是用的Task

    2,内存数据存储是用的Tuple<>

    3,集合并发处理是用的 ConcurrentQueue

    4,用了Linq语法

    如此多的.net4特性降级到.net2.0,确实花费了很多时间来重构这个代码,举个例子:

    .net2.0与.net4.0在数据集合上的运用,表现的最为不同的就是:

    .net2.0 不支持集合的并发处理,如果是多线程操纵集合,必须要借助于lock来锁定对象,然而lock后的集合就从多线程变为单线程的处理了,如此的性能很让人沮丧。

    .net4.0引入的Concurrent系列的并发集合,让很多不会多线程编程的伙伴都把多线程玩的溜溜转,性能还如此的高。

    二:支持多线程高并发

    良好的日志应用组件,支持多线程高并发是必不可少的特性。先标记一下测试机的硬件环境

    ,不同的硬件环境对测试结果是有较大影响的。

    测试代码:

    开启10个线程,每个线程写入10W数据,确认一下代码中数值0的个数是否正确:

    for (int count = 0; count < 10; count++)
    {
        Thread writeThread = new Thread(new ParameterizedThreadStart((para) =>
        {
            Console.WriteLine(string.Format("开启线程{0}", para));
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                NLogger.WriteLog("test_", string.Format("日志测试数据,序号:{0}", i.ToString()));
            }
            sw.Stop();
            Console.WriteLine(string.Format("线程{0}写入日志结束,共用时{1}毫秒", para, sw.ElapsedMilliseconds));
        }));
        writeThread.IsBackground = true;
        writeThread.Start(count);
    }

    测试结果:

    11秒的时间就把100W条数据刷到了缓存里。在4G内存的笔记本里,处理速度能达到10W条/秒。

    三:为什么要用读写双缓冲队列

    在操作第二步的时候,业务程序写入的100W条数据,绝大多数还在缓存对列里,还没有持久化到硬盘上,可以通过如下代码监视读写缓存和已持久化到硬盘上的数据。  

    Thread watchThread = new Thread(new ParameterizedThreadStart((para) =>
    {
        DateTime startDT = DateTime.Now;
    
        while (NLogger.totalCount < 1000000)
        {
            DateTime sectionDt = DateTime.Now;
            TimeSpan ts = sectionDt - startDT;
            Console.WriteLine(string.Format("已用时{0}秒  已写入{1}条  写缓存{2}条  读缓存{3}条", (int)ts.TotalSeconds, NLogger.totalCount, NLogger.writeQueue.Count, NLogger.readQueue.Count));
            Thread.Sleep(1000);
        }
    }));
    watchThread.IsBackground = true;
    watchThread.Start("");

    可以看到在第10秒的时候,100W条数据已经被业务程序全部处理完成。其中有近83W条数据在缓存里,有13W条数据已经刷到了硬盘里,因是为按秒监控,有4W条数据的误差。持续了50秒,才把100W条数据全部刷到硬盘里。

    四:自定义日志缓冲区大小

    大量的日志存储在缓冲队列里,在刷新硬盘的时候,不可能一条一条的刷新数据,虽然现在的固态硬盘已经在市场上流行了很多年,还是没有完全普及,在很多计算机上IO还是瓶颈。如果能做到一次能刷新多条数据,就会提高刷盘的速度。每次刷多少数据到硬盘才能达到最优值?本机是设置的64KB,不同的计算机可能这个值有所不同。

    具体的实现代码如下:

    while (true)
    {
        if (readQueue.Count > 0)
        {
            string[] qItem = readQueue.Dequeue() as string[];
            totalCount = totalCount + 1;
            string[] tempItem = tempQueue.Find(d => d[0] == qItem[0] && d[1] == qItem[1]);
            if (tempItem == null)
            {
                tempQueue.Add(qItem);
            }
            else
            {
                tempItem[2] = string.Concat(tempItem[2], Environment.NewLine, qItem[2]);
                if (tempItem[2].Length > 64 * 1024)  //(1 * 1024 * 1024 = 1M);
                {
                    break;
                }
            }
        }
        else
        {
            break;
        }
    }

    五:支持即时触发刷盘机制

    在多线程的世界里, AutoResetEvent, ManualResetEvent这两个类是十分重要的。它们的区别,网上是到处都有介绍的,本篇博客就只做一个入门级的介绍。

    这两个类称为信号量类,主要包括如下三个方法:

    WaitOne()

    Set()

    Reset()

    如果把线程比作水管,WaitOne() 就是水阀,Set()就是通知打开水阀,Reset()就是通知关闭水阀。介绍信号量的博客都喜欢举这个例子,照葫芦画瓢引用了这个例子。

    在日志类里开启一个刷盘的线程,在闲时是不运行,也不会占用很多的系统资源,因为WaitOne()水阀状态是关闭着的。一旦别的线程往缓冲队列里写日志数据,就用Set()信号量通知打开水阀,日志刷盘完毕后,用Reset()信号量通知把水阀关起来。

    这样就用信号量的原理实现了即时刷盘的机制。

    六:RollingFile日志

    在特别的场景下,比如即时消息系统,WEB应用系统,日志量都很大,一天可以达到G级别量的日志,一般情况下,记录本文件超过10M,打开速度就比较慢,而且不便于定位和查看。这时就需要对日志文件进行分割,分割日志文件常用的手法一般从两个维度进行,一个是按时间维度,一个是按日志的体积。NLogger采用了时间维度和日志维度叠加的方法来进行日志的分割。

    分割日志是一件很容易的事情,难的是对新的日志文件的命名,比如现在的日志文件名是test_20170212(7).log,怎么能计算出下一个日志文件应该是test_20170212(8).log呢?用正则表达式的方式,先匹配出文件名的数字,再计算出下一个文件的名字。这儿的正则式就有含量了,抗干扰性要强,容错性要高,才能匹配出更精准的数字。

    七:支持日志存储位置,日志文件前缀的个性化定义

    并不是每个程序的日志信息都存储在自身程序目录里,有可能定义到其它盘符,或者其它服务器上的共享目录,支持日志存储位置自定义是有必要的。

    不同的应用场景,为了区分日志的分类,有必要在日志文件名前加一个前缀,很幸运NLoger支持了这一功能。

    八:与标杆日志组件log4net一试高低

    Log4net的普及度是很高的,这儿不做详细介绍如何配置使用了。把测试的代码贴出来,熟手一看就明白了。

    这儿做一个Log4net的标配文件:

    <configuration>
      <configSections>
        <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
      </configSections>
      <log4net>
        <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" >
          <Encoding value="UTF-8" />
          <file value="Logs/" />
          <!--记录日志写入文件时,不锁定文本文件,防止多线程时不能写Log,官方说线程非安全-->
          <lockingModel type="log4net.Appender.FileAppender+MinimalLock"/>
          <!--按照何种方式产生多个日志文件(日期[Date],文件大小[Size],混合[Composite])-->
          <rollingStyle value="Composite" />
          <!--按照日期格式输出文件路径-->
          <datePattern value="yyyyMMdd'.txt'"/>
          <!--是否只写到一个文件中-->
          <staticLogFileName value="false"/>
          <!--每个文件的大小-->
          <maximumFileSize value="1MB"/>
          <!--最多产生的日志文件数,超过则只保留最新的n个。设定值value="-1"为不限文件数-->
          <maxSizeRollBackups value="100"/>
          <!--程序启动后,是否追加到文件-->
          <appendToFile value="true" />
          <!--日志缓存大小-->
          <bufferSize value="128" />
          <!--日志格式-->
          <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date [%thread] %message %newline" />
          </layout>
        </appender>
        <root>
          <appender-ref ref="RollingFileAppender" />
          <level value="DEBUG" />
        </root>
      </log4net>
    </configuration>

    为了测试的公平性,测试代码的逻辑与测试数据和NLogger都是相同的:

    for (int count = 0; count < 10; count++)
    {
        Thread writeThread = new Thread(new ParameterizedThreadStart((para) =>
        {
            log4net.ILog log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
            Console.WriteLine(string.Format("开启线程{0}", para));
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                log.Info(string.Format("日志测试数据,序号:{0}", i.ToString()));
            }
            sw.Stop();
            Console.WriteLine(string.Format("线程{0}写入日志结束,共用时{1}毫秒", para, sw.ElapsedMilliseconds));
        }));
        writeThread.IsBackground = true;
        writeThread.Start(count);
    }

    log4net写日志,测试开启10个线程,共写100W条数据到硬盘,吃了个午饭回来还没有执行结束。NLogger 10秒刷100W条数据到缓存,50秒刷100W条数据到硬盘,这样的对比,意义已经不大了。

    测试源码下载(包括NLogger .net2.0版源码)

    想了解更多,请翻阅以前的博客,在网页右下角点击推荐。

    下篇将NLogger  .net4.0版贴出来,性能是优于.net2.0版的。

  • 相关阅读:
    Java对象转型
    .Net之路(十二)Cookie对象
    java学习笔记-包
    MongoDB的安装和简单使用
    考试系统维护中对项目管理的一点体会
    .Net之路(十一)StringBuilder和string
    考试系统调试优化总结
    我的2013——走过就有收获
    针对:Arraylist集合无法修改,下一次枚举无法操作的解决方案
    函数第二部分:为什么说动态参数是没有计划好的参数-Python基础前传(11)
  • 原文地址:https://www.cnblogs.com/xcj26/p/6391853.html
Copyright © 2011-2022 走看看