【分享】我们用了不到200行代码实现的文件日志系统,极佳的IO性能和高并发支持,附压力测试数据
很多项目都配置了日志记录的功能,但是,却只有很少的项目组会经常去看日志。原因就是日志文件生成规则设置不合理,将严重的错误日志跟普通的错误日志混在一起,分析起来很麻烦。
其实,我们想要的一个日志系统核心就这2个要求:
- 日志文件能够按照 /_logs/{group}/yyyy-MM/yyyy-MM-dd-{sequnce}.log 这样的规则生成;
- 调用写日志的方法能够带 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性能和高并发支持,附压力测试数据的更多相关文章
- 200行代码,7个对象——让你了解ASP.NET Core框架的本质
原文:200行代码,7个对象--让你了解ASP.NET Core框架的本质 2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘&g ...
- 200行代码实现简版react🔥
200行代码实现简版react
- 不到 200 行代码,教你如何用 Keras 搭建生成对抗网络(GAN)【转】
本文转载自:https://www.leiphone.com/news/201703/Y5vnDSV9uIJIQzQm.html 生成对抗网络(Generative Adversarial Netwo ...
- 200行代码实现Mini ASP.NET Core
前言 在学习ASP.NET Core源码过程中,偶然看见蒋金楠老师的ASP.NET Core框架揭秘,不到200行代码实现了ASP.NET Core Mini框架,针对框架本质进行了讲解,受益匪浅,本 ...
- SpringBoot,用200行代码完成一个一二级分布式缓存
缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂.早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快. 后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存 ...
- 200 行代码实现基于 Paxos 的 KV 存储
前言 写完[paxos 的直观解释]之后,网友都说疗效甚好,但是也会对这篇教程中一些环节提出疑问(有疑问说明真的看懂了 ),例如怎么把只能确定一个值的 paxos 应用到实际场景中. 既然 Talk ...
- 200行代码,7个对象——让你了解ASP.NET Core框架的本质
2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...
- 200行代码,7个对象——让你了解ASP.NET Core框架的本质[3.x版]
2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...
- JavaScript开发区块链只需200行代码
用JavaScript开发实现一个简单区块链.通过这一开发过程,你将理解区块链技术是什么:区块链就是一个分布式数据库,存储结构是一个不断增长的链表,链表中包含着许多有序的记录. 然而,在通常情况下,当 ...
随机推荐
- Lua快捷键
快捷键 含义 ctrl + shift + L 多行编辑 ctrl + D 继续向下选中下一个相同的文本 Alt + F3 选中所有相同所选中德文本
- Android系统--输入系统(十二)Dispatch线程_总体框架
Android系统--输入系统(十二)Dispatch线程_总体框架 1. Dispatch线程框架 我们知道Dispatch线程是分发之意,那么便可以引入两个问题:1. 发什么;2. 发给谁.这两个 ...
- 通过BitSet完成对单词使用字母的统计
什么是BitSet BitSet类实现了一组位或标记(flag),这些位可被分别设置或清除.当需要跟踪一组布尔值时,这种类很有用. 您只需让每一位对应一个值,并根据需要设置或清除即可. 标记(flag ...
- Hive load from hdfs 出错
hive 加载HDFS的数据时出现错误, FATAL:SemanticException [Error 10028] search了一下,跟他一样Hive load from hdfs 出错. 我按照 ...
- CentOS 6.x 本地yum源配置与使用
系统默认已经安装了可使用yum的软件包,所以可以直接配置: # mount /dev/cdrom /mnt 挂载镜像,可以写到配置文件 ...
- Spark实战之读写HBase
1 配置 1.1 开发环境: HBase:hbase-1.0.0-cdh5.4.5.tar.gz Hadoop:hadoop-2.6.0-cdh5.4.5.tar.gz ZooKeeper:zooke ...
- Longest Palindromic Substring2015年6月20日
Given a , and there exists one unique longest palindromic substring. 自己的解决方案; public class Solution ...
- 小tips:用java模拟小球做抛物线运动
这几天刚刚学习了java线程,然后跟着书做了几个关于线程的练习,其中有一个练习题是小球动起来.这个相信很简单,只要运用线程就轻松能够实现.然后看到了它的一个课后思考题,怎样让小球做个抛物线运动,这点我 ...
- Qzone 高性能 HTTPS 实践
WeTest导读 自从去年QQ空间移动端页面开始切换到HTTPS之后,页面性能遇到了比较大的挑战,HTTPS对页面访问速度带来了比较大的影响,所以我们通过实践总结了一些能够提升HTTPS页面访问速度的 ...
- TreeSet集合排序方式一:自然排序Comparable
TreeSet集合默认会进行排序.因此必须有排序,如果没有就会报类型转换异常. 自然排序 Person class->实现Comparable,实现compareTo()方法 package H ...