很多项目都配置了日志记录的功能,但是,却只有很少的项目组会经常去看日志。原因就是日志文件生成规则设置不合理,将严重的错误日志跟普通的错误日志混在一起,分析起来很麻烦。

其实,我们想要的一个日志系统核心就这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. 灵活:支持任意字符串作为错误等级(特殊字符除外),用于生成目录名称。

代码及实现原理分析

好了,是时候上代码了。

 using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Timers; namespace MvcSolution
{
public class FileLogger : DisposableBase, ILogger
{
private const int IntervalSeconds = ;
private const long MaxPerFileBytes = ;
private readonly Dictionary<string, LoggingGroup> _dict;
private readonly Timer _timer;
private bool _busy = false; public FileLogger()
{
this._dict = new Dictionary<string, LoggingGroup>();
this._timer = new Timer(IntervalSeconds * );
this._timer.Elapsed += TimerElapsed;
} public void Start()
{
_timer.Start();
} public void Stop()
{
_timer.Stop();
} private void TimerElapsed(object sender, ElapsedEventArgs e)
{
if (_busy)
{
return;
}
try
{
_busy = true;
this.DoWork();
}
catch (Exception)
{ }
finally
{
_busy = false;
}
} private void DoWork()
{
var items = new List<WritingItem>();
lock (_dict)
{
foreach (var key in _dict.Keys)
{
var group = this._dict[key];
if (group.Sb.Length == )
{
continue;
}
items.Add(new WritingItem(group));
group.Sb.Clear();
}
}
if (items.Count == )
{
return;
}
this.WriteToFile(items);
lock (_dict)
{
foreach (var item in items)
{
var group = this._dict[item.Group];
group.LastDate = item.LastDate;
group.LastFilePath = item.LastFilePath;
}
}
} public void Entry(string group, string message)
{
lock (this._dict)
{
if (!this._dict.ContainsKey(group))
{
this._dict[group] = new LoggingGroup(group);
}
this._dict[group].Sb.Append("\r\n" + message + "\r\n\r\n");
}
} private void WriteToFile(List<WritingItem> items)
{
lock (this)
{
foreach (var item in items)
{
try
{
var date = DateTime.Today.ToString("yyyy-MM-dd");
FileInfo file;
if (item.LastDate == date)
{
file = new FileInfo(item.LastFilePath);
var parent = file.Directory;
if (parent.Exists == false)
{
Directory.CreateDirectory(parent.FullName);
}
if (file.Exists && file.Length > MaxPerFileBytes)
{
var yearMonth = DateTime.Today.ToString("yyyy-MM");
var date2 = DateTime.Now.ToString("yyyy-MM-dd-HHmmss");
var relativePath = $"\\_logs\\{item.Group}\\{yearMonth}\\{date2}.log";
file = new FileInfo(AppContext.RootFolder + relativePath);
}
}
else
{
var yearMonth = DateTime.Today.ToString("yyyy-MM");
var relativePath = $"\\_logs\\{item.Group}\\{yearMonth}\\{date}.log";
file = new FileInfo(AppContext.RootFolder + relativePath);
var parent = file.Directory;
if (parent.Exists == false)
{
Directory.CreateDirectory(parent.FullName);
}
}
File.AppendAllText(file.FullName, item.Text); item.LastDate = date;
item.LastFilePath = file.FullName;
}
catch (Exception)
{ }
}
}
} private class WritingItem
{
public string Group { get; }
public string Text { get; }
public string LastDate { get; set; }
public string LastFilePath { get; set; } public WritingItem(LoggingGroup group)
{
this.Group = group.Key;
this.Text = group.Sb.ToString();
this.LastDate = group.LastDate;
this.LastFilePath = group.LastFilePath;
}
} private class LoggingGroup
{
public string Key { get; }
public StringBuilder Sb { get; }
public string LastDate { get; set; }
public string LastFilePath { get; set; } public LoggingGroup(string key)
{
this.Key = key;
this.Sb = new StringBuilder();
this.LastDate = "";
this.LastFilePath = "";
}
} protected override void DisposeInternal()
{
_timer.Dispose();
} ~FileLogger()
{
base.MarkDisposed();
}
} }

上面这个FileLogger类就是我们写的文件日志系统的核心类了。

首先要明白这个类有一个定时器Timer,这个Timer有什么用呢?Timer的用处就是定时将内存中记录的日志写入到磁盘,推荐设置为1秒写入一次。

正是因为有了这个Timer,才实现了高并发的处理。其原理大概是这样:

由于WEB服务器每秒钟可能会处理大量的http请求,如果某个请求抛了异常需要记录日志,这时候如果每个请求都直接往磁盘中写数据,那么磁盘开销是极其高的,并且文件锁会导致大量排队,这就极大的影响了WEB服务器的性能。所以,更好的做法是:每个http请求内抛的异常先写到内存(就是FileLogger类的StringBuilder啦),然后再定时将内存中的日志写入到磁盘,这样处于性能瓶颈的磁盘操作就变成单线程操作了。

如何使用这个FileLogger呢?

真的很简单啦,我们只是建了一个非常简单的helper类,如下:

 using System;
using System.Text;
using System.Web; namespace MvcSolution
{
public class LogHelper
{
private static ILogger _logger;
public static ILogger Logger
{
get
{
if (_logger == null)
{
_logger = Ioc.Get<ILogger>();
}
return _logger;
}
} public static void TryLog(string group, Exception exception)
{
try
{
var sb = new StringBuilder($"【{DateTime.Now.ToFullTimeString()}】{exception.GetAllMessages()}\r\n[stacktrace]: \r\n{exception.StackTrace}\r\n");
AppendHttpRequest(sb);
Logger.Entry(group, sb.ToString());
}
catch (Exception)
{ }
} public static void TryLog(string group, string message)
{
try
{
var sb = new StringBuilder($"【{DateTime.Now.ToFullTimeString()}】{message}\r\n");
AppendHttpRequest(sb);
Logger.Entry(group, sb.ToString());
}
catch (Exception)
{ }
} private static void AppendHttpRequest(StringBuilder sb)
{
if (HttpContext.Current == null)
{
return;
}
var request = HttpContext.Current.Request;
sb.Append($"[{request.UserHostAddress}]-{request.HttpMethod}-{request.Url.PathAndQuery}\r\n");
foreach (var header in request.Headers.AllKeys)
{
sb.Append($"{header}: {request.Headers.Get(header)}\r\n");
}
}
}
}

然后在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,感兴趣的朋友欢迎关注哦。

【分享】我们用了不到200行代码实现的文件日志系统,极佳的IO性能和高并发支持,附压力测试数据的更多相关文章

  1. 200行代码,7个对象——让你了解ASP.NET Core框架的本质

    原文:200行代码,7个对象--让你了解ASP.NET Core框架的本质 2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘&g ...

  2. 200行代码实现简版react🔥

    200行代码实现简版react

  3. 不到 200 行代码,教你如何用 Keras 搭建生成对抗网络(GAN)【转】

    本文转载自:https://www.leiphone.com/news/201703/Y5vnDSV9uIJIQzQm.html 生成对抗网络(Generative Adversarial Netwo ...

  4. 200行代码实现Mini ASP.NET Core

    前言 在学习ASP.NET Core源码过程中,偶然看见蒋金楠老师的ASP.NET Core框架揭秘,不到200行代码实现了ASP.NET Core Mini框架,针对框架本质进行了讲解,受益匪浅,本 ...

  5. SpringBoot,用200行代码完成一个一二级分布式缓存

    缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂.早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快. 后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存 ...

  6. 200 行代码实现基于 Paxos 的 KV 存储

    前言 写完[paxos 的直观解释]之后,网友都说疗效甚好,但是也会对这篇教程中一些环节提出疑问(有疑问说明真的看懂了 ),例如怎么把只能确定一个值的 paxos 应用到实际场景中. 既然 Talk ...

  7. 200行代码,7个对象——让你了解ASP.NET Core框架的本质

    2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...

  8. 200行代码,7个对象——让你了解ASP.NET Core框架的本质[3.x版]

    2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...

  9. JavaScript开发区块链只需200行代码

    用JavaScript开发实现一个简单区块链.通过这一开发过程,你将理解区块链技术是什么:区块链就是一个分布式数据库,存储结构是一个不断增长的链表,链表中包含着许多有序的记录. 然而,在通常情况下,当 ...

随机推荐

  1. java自带的http get/post请求servlet

    http请求方式太多,有java自带的,也有httpClient,用的地方还挺多,所以在此做一个小小的总结: public class HttpRequest { /** * 向指定URL发送GET方 ...

  2. vs项目和msql不兼容解决方案

    当vs的工程项目加载了libmysql.lib 即:附加包含目录,附加库目录,附加依赖项都设置好之后,如过编译出现如下: error LNK2019: 无法解析的外部符号 _mysql_real_co ...

  3. Exchanger类详解

    Exchanger并发辅助类,允许在并发任务之间交换数据.具体来说Exchanger类在两个线程之间定义同步点.当两个线程到达同步点时,它们交换数据结构.需要注意的是Exchanger类只能同步两个线 ...

  4. 在已经部署svn 服务器上,搭建svn项目 成功版

    1.进入svn目录,建立版本库 svnadmin create svntest svntest为svn项目名称 2. hooks/ 目录下新建 post-commit 文件 [钩子脚本] #!/bin ...

  5. JTable常见用法细则+设置某列可编辑+滚动表格

    JTable常见用法细则 JTable是Swing编程中很常用的控件,这里总结了一些常用方法以备查阅.欢迎补充,转载请注明作者与出处. 一.创建表格控件的各种方式: 1)  调用无参构造函数. JTa ...

  6. (HTTPS)web 项目如何实现https

    HTTPS实际是SSL over HTTP, 该协议通过SSL在发送方把原始数据进行加密,在接收方解密,因此,所传送的数据不容易被网络黑客截获和破解.本文介绍HTTPS的三种实现方法.方法一 静态超链 ...

  7. Swift 入门之简单语法(四)

    函数 目标 掌握函数的定义 掌握外部参数的用处 掌握无返回类型的三种函数定义方式 代码实现 函数的定义 格式 func 函数名(行参列表) -> 返回值 {代码实现} 调用 let result ...

  8. koa-router中路由/后面不填参数就会报404的解决办法

    koa-router 中使用路由参数时会遇到一个问题,就是像下面的代码在没有传入 id 是会报 404 错误 router.get('/:id', (err, ctx, next) => { / ...

  9. Not supported by Zabbix Agent & zabbix agent重装

    zabbix服务器显示一些监控项不起效,提示错误[Not supported by Zabbix Agent], 最后定位为zabbix客户端版本过低. Not supported by Zabbix ...

  10. offsetHeight/Width clientHeight/Width scrollHeight/Width等高宽算法

    图解: jquery里的对应取法: clientHeight/Width:innerHeight/Width(), offsetHeight/Width: outerHeight/Width(). w ...