【分享】我们用了不到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开发实现一个简单区块链.通过这一开发过程,你将理解区块链技术是什么:区块链就是一个分布式数据库,存储结构是一个不断增长的链表,链表中包含着许多有序的记录. 然而,在通常情况下,当 ...
随机推荐
- selenium+python
最近在学习selenium自动化测试,但是一直遇到一个问题,总是打不开指定的网址,今天突然成功了, 主要原因是因为selenium版本太低的缘故,所以只需要在终端输入:pip install -U s ...
- Java核心技术 卷I chapter05 继承
2017年4月10日19:41:44 仅仅用于打好基础 1. 在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承! 2.关键字super的使用方法: (1) 子类中想调用父类中的 ...
- 微软 Build 2017 开发者大会:Azure 与 AI 的快速发展
欢迎大家持续关注葡萄城控件技术团队博客,更多更好的原创文章尽在这里~~ 一年一度的微软 Build 大会准时起航,本年度大会从旧金山移师西雅图,一个近年来凭借女神汤唯而在中国家喻户晓的美国西部海滨城市 ...
- Linux上bash的部分基础特性:
命令补全: tab shell程序在接收到用户执行命令的请求,分析完成后,最左侧的字符串会被当做命令 命令查找机制: 查找内部命令: 根据PATH环境变量中设定的目录,自左而右逐个搜索目录下的文件名 ...
- C#中对于变量的声明和初始化
C#变量初始化是C#强调安全性的另一个例子.简单地说,C#编译器需要用某个初始值对变量进行初始化,之后才能在操作中引用该变量.大多数现代编译器把没有初始化标记为警告,但C#编译器把它当作错误来看待. ...
- 广义后缀树(GST)算法的简介
导言 最近软件安全课上,讲病毒特征码的提取时,老师讲了一下GST算法.这里就做个小总结. 简介 基本信息 广义后缀树的英文为Generalized Suffix Tree,简称GST. 算法目的 ...
- [C#] BarcodeLib -- 一个精简而不失优雅的条形码生成库
BarcodeLib -- 一个精简而不失优雅的条形码生成库 引言 在百度进行“C# 条形码”等类似关键字搜索的时候,基本上是使用 ZXing 类库进行条形码的生成.今天我所介绍的是另一款类库 Bar ...
- Ubuntu设置终端相对短路径
这个设置相对实际上是比较简单的.在自己的家目录打开.bashrc 找到PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$' 只需要将w修改为大写W保存, ...
- How To Configure VMware fencing using fence_vmware_soap in RHEL High Availability Add On(RHEL Pacemaker中配置STONITH)
本文主要简单介绍一下如何在RHEL 7 Pacemaker中配置一个fence_vmware_soap类型的STONITH设备(仅供测试学习). STONITH是Shoot-The-Other-Nod ...
- LINQ之LINQ to Objects(上)
LINQ概述 LINQ,语言集成查询(Language Integrated Query),它允许使用C#或VB代码以查询数据库相同的方式来操作不同的数据源. 1.LINQ体系结构 从上图可以看出,L ...