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

其实,我们想要的一个日志系统核心就这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. Hadoop化繁为简-从安装Linux到搭建集群环境

    简介与环境准备 hadoop的核心是分布式文件系统HDFS以及批处理计算MapReduce.近年,随着大数据.云计算.物联网的兴起,也极大的吸引了我的兴趣,看了网上很多文章,感觉还是云里雾里,很多不必 ...

  2. Linux 零拷贝技术

    简介 零拷贝(zero-copy)技术可以减少数据拷贝和共享总线操作的次数,消除通信数据在存储器之间不必要的中间拷贝过程,有效地提高通信效率,是设计高速接口通道.实现高速服务器和路由器的关键技术之一. ...

  3. realmock 前后端分离方案

    realmock 前后端分离方案 express + randomjson 模拟后端服务,前端服务器(比如webpack, nigix等)将请求代理到该服务器地址即可 github地址:https:/ ...

  4. IOS打包相关问题

    使用了AFNetworking框架,模拟器和真机运行都不报错,但是提交商店报错Unsupported Architecture. Your executable contains unsupporte ...

  5. 像写C#一样编写java代码

    JDK8提供了非常多的便捷用法和语法糖,其编码效率几乎接近于C#开发,maven则是java目前为止最赞的jar包管理和build工具,这两部分内容都不算多,就合并到一起了. 愿编写java代码的过程 ...

  6. Excel表格导入Mysql数据库,一行存入多条数据的前后台完整实现思路(使用mybatis框架)

    现在有一张Excel表格: 存入数据库时需要这样存放: 现在需要将Excel表格做处理,将每一行拆分成多条数据存入数据库. 1.首先在前台jsp页面画一个按钮:,加入点击事件: <td styl ...

  7. PL/SQL Developer使用技巧以及快捷键设置

    1.类SQL PLUS窗口: File->New->Command Window,这个类似于oracle的客户端工具sql plus,但是比在cmd中的sqlplus好用多了. 2.设置关 ...

  8. GreenDao

    前言 我相信,在平时的开发过程中,大家一定会或多或少地接触到 SQLite.然而在使用它时,我们往往需要做许多额外的工作,像编写 SQL 语句与解析查询结果等.所以,适用于 Android 的ORM ...

  9. 写给Android App开发人员看的Android底层知识(1)

    这个系列的文章一共8篇,我酝酿了很多年,参考了很多资源,查看了很多源码,直到今天把它写出来,也是战战兢兢,生怕什么地方写错了,贻笑大方. (一)引言 早在我还是Android菜鸟的时候,有很多技术我都 ...

  10. 【JAVAWEB学习笔记】18_el&jstl&javaee的开发模式

    一.EL技术 1.EL 表达式概述 EL(Express Lanuage)表达式可以嵌入在jsp页面内部,减少jsp脚本的编写,EL 出现的目的是要替代jsp页面中脚本的编写. 2.EL从域中取出数据 ...