Asp.Net Core2.2 源码阅读系列——控制台日志源码解析
为了让我们第一时间知道程序的运行状态,Asp.Net Core 添加了默认的日志输出服务。这看起来并没有什么问题,对于开发人员也相当友好,但如果不了解日志输出的细节,也有可能因为错误的日志级别配置导致性能问题,笔者的同事在一次进行性能测试的时候被输出日志误导,与其讨论分析了测试源码,排除业务代码因素,后来联想到应该是由于默认的日志输出导致(默认的日志级别 Microsoft 是 Inforamtion),随后将日志级别调高,性能立即飙升,问题解决。
虽然问题得到解决,但笔者脑中的对于到底为何日志输出会导致性能下降的疑问没有解决,一切查资料的方式,都不及先看源码来得直接,于是在github上拉取源码,经过详细的阅读分析,终于了解了技术细节,找到了高并发下,控制台日志输出导致性能低下的真正原因。
1.首先要弄清楚默认日志服务是如何添加的?
Asp.Net Core程序在启动时,IWebHostBuilder CreateDefaultBuilder(args) 方法中会为我们注册一些默认服务,这其中就包含默认的日志输出服务[GitHub源码地址]:
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
//部分源码
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new WebHostBuilder();
...
builder.UseKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"));
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
...
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole(); //手动高亮
logging.AddDebug(); //手动高亮
logging.AddEventSourceLogger(); //手动高亮
})
.ConfigureServices((hostingContext, services) =>
{
...
})
.UseIIS()
.UseIISIntegration()
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
});
return builder;
}
PS:如果还想了解默认添加的其他服务详细细节,可以参看Hosting源码地址
2. 日志源码
目前 Asp.Net Core 已经将扩展插件统统挪到 [aspnet/Extensions] 仓库下,包含了所有 Asp.Net Core 所使用的扩展组件,如日志,配置等,如需查找 Microsoft.Extensions.* 命名空间下的源码,可以参考这个仓库。
打开目录 Extensions/src/Logging/ ,可以看到日志相关的组件均在这个文件夹下,这里简单说下主要包含的Project:
- 日志抽象层,主要负责Logger以及LoggerFactory接口定义和默认实现,为IOC提供扩展方法
- Microsoft.Extensions.Logging.Abstractions
- Microsoft.Extensions.Logging
- 日志配置
- Microsoft.Extensions.Logging.Configuration
- 日志具体实现
- Microsoft.Extensions.Logging.Console
- Microsoft.Extensions.Logging.Debug
- Microsoft.Extensions.Logging.EventLog
先来看下代码图:
上图可以看到,核心类主要有以下几个:
- ConsoleLoggerProvider 实现了ILoggerProvider接口,主要负责创建ConsoleLogger
- ConsoleLoggerSettings ConsoleLogger日志配置类
- ConsoleLogger 实现ILogger接口,日志输出最终的执行类
重要!篇幅原因,以下源码均做了精简,如有需要可以点击文件名连接直接查看github源文件。
先来看 ConsoleLoggerProvider.cs 源码:
public class ConsoleLoggerProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, ConsoleLogger> _loggers = new ConcurrentDictionary<string, ConsoleLogger>();//手动高亮
private readonly Func<string, LogLevel, bool> _filter;
private IConsoleLoggerSettings _settings;
private readonly ConsoleLoggerProcessor _messageQueue = new ConsoleLoggerProcessor();//手动高亮
private static readonly Func<string, LogLevel, bool> falseFilter = (cat, level) => false;
//通过IOptionMonitor<> 实现动态修改日志参数功能,比如日志级别
public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> options)
{
// Filter would be applied on LoggerFactory level
_filter = trueFilter;
_optionsReloadToken = options.OnChange(ReloadLoggerOptions);
ReloadLoggerOptions(options.CurrentValue);
}
//3.0中将移除此构造函数
public ConsoleLoggerProvider(IConsoleLoggerSettings settings)
{
_settings = settings;
if (_settings.ChangeToken != null)
{
_settings.ChangeToken.RegisterChangeCallback(OnConfigurationReload, null);
}
}
//动态修改日志级别
private void ReloadLoggerOptions(ConsoleLoggerOptions options)
{
foreach (var logger in _loggers.Values)
{
logger.ScopeProvider = scopeProvider;
}
}
//通过此方法动态修改日志级别
private void OnConfigurationReload(object state)
{
_settings = _settings.Reload();
foreach (var logger in _loggers.Values)
{
logger.Filter = GetFilter(logger.Name, _settings);
}
}
//创建日志组件,注意,每个日志category name 创建一个日志实例,
//所以可以根据不同的name设置不通的日志级别,达到细粒度控制
public ILogger CreateLogger(string name)
{
return _loggers.GetOrAdd(name, CreateLoggerImplementation);
}
private ConsoleLogger CreateLoggerImplementation(string name)
{
return new ConsoleLogger(name, GetFilter(name, _settings), null, _messageQueue) { };
}
private Func<string, LogLevel, bool> GetFilter(string name, IConsoleLoggerSettings settings)
{
if (settings != null)
{
foreach (var prefix in GetKeyPrefixes(name))
{
LogLevel level;
if (settings.TryGetSwitch(prefix, out level))
{
return (n, l) => l >= level;
}
}
}
return falseFilter;
}
//日志级别匹配方式,比如name为 "A.B.C",则依次匹配 "A.B.C","A.B", "A"
private IEnumerable<string> GetKeyPrefixes(string name)
{
while (!string.IsNullOrEmpty(name))
{
yield return name;
var lastIndexOfDot = name.LastIndexOf('.');
if (lastIndexOfDot == -1)
{
yield return "Default";
break;
}
name = name.Substring(0, lastIndexOfDot);
}
}
}
可以看见,ConsoleLoggerProvider 持有一个线程安全的字典_loggers,用以保证每个category name(也就是业务代码中构造函数中的 ILogger<T> 中的 nameof(T))有且仅有一个ILogger 实例,之所以这么做,是为了可以更加细粒度控制每个logger的日志输出细节,比如log level。同时,可以通过 IOperationMonitor<> 实现动态日志细节配置控制。
另外还有一个名为 _messageQueue 的实例在 ConsoleLogger 构造时传进去,从名字看来似乎对日志输出做了排队处理,我们稍后再看。
再来看 ConsoleLogger.cs 源码:
public class ConsoleLogger : ILogger
{
private readonly ConsoleLoggerProcessor _queueProcessor;
private Func<string, LogLevel, bool> _filter;
[ThreadStatic]//手动高亮
private static StringBuilder _logBuilder;
static ConsoleLogger()
{
var logLevelString = GetLogLevelString(LogLevel.Information);
}
internal ConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor)
{
Name = name;
Filter = filter ?? ((category, logLevel) => true);
_queueProcessor = loggerProcessor;
}
public string Name { get; }
//日志写入接口实现
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel)) return;
var message = formatter(state, exception);
if (!string.IsNullOrEmpty(message) || exception != null)
{
WriteMessage(logLevel, Name, eventId.Id, message, exception);
}
}
// 日志通过stringbuilder进行装配
public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
{
var logBuilder = _logBuilder;
_logBuilder = null;
if (logBuilder == null)
{
logBuilder = new StringBuilder();
}
var logLevelString = GetLogLevelString(logLevel);
// category and event id
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append("[");
logBuilder.Append(eventId);
logBuilder.AppendLine("]");
if (!string.IsNullOrEmpty(message))
{
// message
logBuilder.Append(_messagePadding);
var len = logBuilder.Length;
logBuilder.AppendLine(message);
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
}
if (exception != null)
{
logBuilder.AppendLine(exception.ToString());
}
var hasLevel = !string.IsNullOrEmpty(logLevelString);
// Queue log message
_queueProcessor.EnqueueMessage(new LogMessageEntry() //装配完成日志入队
{
Message = logBuilder.ToString(),
MessageColor = DefaultConsoleColor,
LevelString = hasLevel ? logLevelString : null,
});
logBuilder.Clear();
if (logBuilder.Capacity > 1024)
{
logBuilder.Capacity = 1024;
}
_logBuilder = logBuilder;
}
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel == LogLevel.None)
{
return false;
}
return Filter(Name, logLevel);
}
//日志最终记录字段和LogLevel中的枚举名称通过此方法映射
private static string GetLogLevelString(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace:
return "trce";
case LogLevel.Debug:
return "dbug";
case LogLevel.Information:
return "info";
case LogLevel.Warning:
return "warn";
case LogLevel.Error:
return "fail";
case LogLevel.Critical:
return "crit";
default:
throw new ArgumentOutOfRangeException(nameof(logLevel));
}
}
}
此类是对ILogger接口的简单实现,可以看出,在调用Log() 接口时,内部调用了WriteMessage()方法,使用stringbuilder 对日志内容进行了拼接,然后果然丢进了_queueProcessor队列,并没有立即输出。
值得注意的是,笔者看到WriteMessage()方法中的 _logBuilder.Append() 日志内容时,没加任何锁,立即怀疑这不是会有线程安全问题么?然后抬头一看,_logBuilder的字段定义上加了 [ThreadStatic] 标签,相比于对这个方法加锁,对这个字段设置为线程静态字段才是完美的方案,不得不感叹微软程序员的严谨性!
最后看下 ConsoleLoggerProcessor.cs,寻找最终答案:
public class ConsoleLoggerProcessor : IDisposable
{
private const int _maxQueuedMessages = 1024;
private readonly BlockingCollection<LogMessageEntry> _messageQueue = new BlockingCollection<LogMessageEntry>(_maxQueuedMessages);
private readonly Thread _outputThread;
public IConsole Console;
public ConsoleLoggerProcessor()
{
// 开启消费阻塞队列线程
_outputThread = new Thread(ProcessLogQueue)
{
IsBackground = true,
Name = "Console logger queue processing thread"
};
_outputThread.Start();
}
public virtual void EnqueueMessage(LogMessageEntry message)
{
if (!_messageQueue.IsAddingCompleted)
{
try
{
//入队操作
_messageQueue.Add(message);
return;
}
catch (InvalidOperationException) { }
}
}
//消费队列
private void ProcessLogQueue()
{
try
{
foreach (var message in _messageQueue.GetConsumingEnumerable())
{
WriteMessage(message);
}
}
catch
{
try
{
_messageQueue.CompleteAdding();
}
catch { }
}
}
}
以上代码解释了为何在并发情况下,控制台日志输出会导致性能降低的原因:
该类中有一个BlockingCollection<> 阻塞队列,最大长度1024,用于实现日志输出的生产消费模型,再看 EnqueueMessage()方法,如果阻塞队列中已经达到1024条日志,则所有生产者将被阻塞。也就是说:一旦日志生产速度远远大于队列消费速度,生产者将会集中等待队列消费后才能竞争入队后返回,引发了性能瓶颈
到此,终于弄清楚之前的性能测试为何会受日志控制台输出的影响,对底层代码的分析,会加深对此类问题的理解,不但对之后排查类似问题有帮助,也让我们对生产消费模型场景有了更深的理解。
后记
笔者此次对日志相关源码还做了更多深入的阅读,同时依照 Microsoft.Extesions.Logging 中的接口实现了自定义日志组件,用于在生产中,从底层对很多信息进行获取和记录,比如traceid,在这个翻阅的过程中,感受到通过阅读源码,可以更加直接的理解 Asp.Net Core 相关的设计思想,以及代码实现,对于理解整体框架有极大的帮助,笔者后续也会继续阅读其他相关源码。对于目前在使用.Net Core 的同学,希望你同我一样,对了解事务的本质保持好奇心,持之以恒!
Asp.Net Core2.2 源码阅读系列——控制台日志源码解析的更多相关文章
- 源码阅读系列:EventBus
title: 源码阅读系列:EventBus date: 2016-12-22 16:16:47 tags: 源码阅读 --- EventBus 是人们在日常开发中经常会用到的开源库,即使是不直接用的 ...
- Spring源码阅读系列总结
最近一段时间,粗略的查看了一下Spring源码,对Spring的两大核心和Spring的组件有了更深入的了解.同时在学习Spring源码时,得了解一些设计模式,不然阅读源码还是有一定难度的,所以一些重 ...
- JDK1.8源码阅读系列之三:Vector
本篇随笔主要描述的是我阅读 Vector 源码期间的对于 Vector 的一些实现上的个人理解,用于个人备忘,有不对的地方,请指出- 先来看一下 Vector 的继承图: 可以看出,Vector 的直 ...
- SpringMVC源码阅读系列汇总
1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...
- 【合集】TiDB 源码阅读系列文章
[合集]TiDB 源码阅读系列文章 (一)序 (二)初识 TiDB 源码 (三)SQL 的一生 (四)INSERT 语句概览 (五)TiDB SQL Parser 的实现 (六)Select 语句概览 ...
- 【Dubbo源码阅读系列】之远程服务调用(上)
今天打算来讲一讲 Dubbo 服务远程调用.笔者在开始看 Dubbo 远程服务相关源码的时候,看的有点迷糊.后来慢慢明白 Dubbo 远程服务的调用的本质就是动态代理模式的一种实现.本地消费者无须知道 ...
- 【Dubbo源码阅读系列】服务暴露之远程暴露
引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A ...
- 【Dubbo源码阅读系列】服务暴露之本地暴露
在上一篇文章中我们介绍 Dubbo 自定义标签解析相关内容,其中我们自定义的 XML 标签 <dubbo:service /> 会被解析为 ServiceBean 对象(传送门:Dubbo ...
- DM 源码阅读系列文章(六)relay log 的实现
2019独角兽企业重金招聘Python工程师标准>>> 作者:张学程 本文为 DM 源码阅读系列文章的第六篇,在 上篇文章 中我们介绍了 binlog replication 处理单 ...
随机推荐
- 《Java 8 in Action》Chapter 6:用流收集数据
1. 收集器简介 collect() 接收一个类型为 Collector 的参数,这个参数决定了如何把流中的元素聚合到其它数据结构中.Collectors 类包含了大量常用收集器的工厂方法,toLis ...
- Keras实例教程(4)之迁移学习VGG
https://blog.csdn.net/baimafujinji/article/details/80743814
- Java-手动搭建SSM(Maven)
一.环境部署 操作系统:windows10专业版 jdk:1.8.0_144 IDE:eclipse-oxygen 服务器:tomcat 9.0 数据库:mysql 5.7.18 Maven:3.54 ...
- [python]变量和赋值
1. python的变量名以字母开头,包含字母.数字.下划线. 2. python是动态类型语言,即不需要预先声明变量的类型.变量的类型和值在赋值的时候被初始化. 变量赋值通过等号来执行. 代码: c ...
- 玲珑杯”ACM比赛 Round #18 A -- 计算几何你瞎暴力(瞎暴力)
题目链接:http://www.ifrog.cc/acm/problem/1143?contest=1020&no=0 题解:就是瞎暴力具体多暴力看一下代码就知道了. #include < ...
- 杭电多校第九场 hdu6425 Rikka with Badminton 组合数学 思维
Rikka with Badminton Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 524288/524288 K (Java/O ...
- Idea各种快捷生成Live Template的代码整合
Idea各种快捷生成整合 快速生成method方法注释 配置方法 打开Idea ---> Settings , 搜索 live 点击右边的 + 号,创建模板组 Template Group,之后 ...
- CenTOS7使用ACL控制目录权限,只给某个用户访问特定目录
前言 Linux 基本的权限控制仅可以对所属用户.所属组.其他用户进行的权限控制,而不能精确地控制每个用户的权限.ACL 规则就是用来解决这个问题的. 使用 ACL 规则,我们可以针对单一账户设置文件 ...
- MySQL中boolean类型设置
在用MySQL设置boolean的时候发现跟本就没有这种类型,后来查资料才知道: boolean类型用tinyint表示, MYSQL保存BOOLEAN值时用1代表TRUE,0代表FALSE,bool ...
- Go操作NSQ
NSQ是目前比较流行的一个分布式的消息队列,本文主要介绍了NSQ及Go语言如何操作NSQ. NSQ NSQ介绍 NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优异. NSQ的优势有 ...