【分享】我们用了不到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开发实现一个简单区块链.通过这一开发过程,你将理解区块链技术是什么:区块链就是一个分布式数据库,存储结构是一个不断增长的链表,链表中包含着许多有序的记录. 然而,在通常情况下,当 ...
随机推荐
- Docker - 定制镜像
Dockerfile Docker Hub拥有大量高质的官方镜像:可直接使用的服务类镜像.语言应用镜像.基础操作系统镜像等,满足绝大部分需求. 此外,可以通过定制镜像的方式来满足实际使用中的特定需求. ...
- haproxy安装配置for mysql负载均衡(ubantu)
1.安装pcre apt-get update (apt-get install openssl libssl-dev ==>可能需要安装的依赖包) apt-get install libpc ...
- POJ1850-Code 递推数学
题目链接:http://poj.org/problem?id=1850 题目大意: 按照字典序对升序排列组成的字母进行编号,给出一个长度不超过10的串,求出它的编号是多少?如果无法进行编号则输出0. ...
- git学习笔记之一
Git是比较优秀的分布式版本管理工具,这次学习了git的基本命令,现在作一些归纳总结,已备复习之用. Git 认识 Git 直接用hash值记录提交的修改文件的快照,本地操作无需联网 Git 有三种状 ...
- foreach底层机制
简单例子 直接了解foreach底层有些困难,我们需要从更简单的例子着手.下面上一个简单例子: public class Simple { public static void main(String ...
- UML学习笔记之类之间的关系
1. 导航关系(Directed Association) A类有一个成员变量保存B的引用. 2.包含关系(Aggregation.Composition) (1)弱包含 含义:每个部门包含多个 ...
- Servlet,过滤器,监听器,拦截器的区别
1.过滤器 Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途是过滤字符编码.做一些业务逻辑判断等.其工作原理是,只要你在web.xml ...
- 安卓TextView限定行数最大值,点击按钮显示所有内容
问题展示 如上图所示,在普通的TextView中,要求: 最多显示3行 超过三行显示展开按钮 且点击展开按钮显示完整内容 这个需求看似简单,但解决起来会遇到两个较为棘手的问题:1,如何判断是否填满了前 ...
- Fast Fourier Transform ——快速傅里叶变换
问题: 已知$A=a_{0..n-1}$, $B=b_{0..n-1}$, 求$C=c_{0..2n-2}$,使: $$c_i = \sum_{j=0}^ia_jb_{i-j}$$ 定义$C$是$A$ ...
- Java Class Loader
Reference: [1] http://www.cnblogs.com/kevin2chen/p/6714214.html 当调用 java命令运行一个java程序时,会启动一个java虚拟机进程 ...