为了让我们第一时间知道程序的运行状态,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:

  1. 日志抽象层,主要负责Logger以及LoggerFactory接口定义和默认实现,为IOC提供扩展方法
  • Microsoft.Extensions.Logging.Abstractions
  • Microsoft.Extensions.Logging
  1. 日志配置
  • Microsoft.Extensions.Logging.Configuration
  1. 日志具体实现
  • Microsoft.Extensions.Logging.Console
  • Microsoft.Extensions.Logging.Debug
  • Microsoft.Extensions.Logging.EventLog

  先来看下代码图:

   上图可以看到,核心类主要有以下几个:

  1. ConsoleLoggerProvider 实现了ILoggerProvider接口,主要负责创建ConsoleLogger
  2. ConsoleLoggerSettings ConsoleLogger日志配置类
  3. 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 源码阅读系列——控制台日志源码解析的更多相关文章

  1. 源码阅读系列:EventBus

    title: 源码阅读系列:EventBus date: 2016-12-22 16:16:47 tags: 源码阅读 --- EventBus 是人们在日常开发中经常会用到的开源库,即使是不直接用的 ...

  2. Spring源码阅读系列总结

    最近一段时间,粗略的查看了一下Spring源码,对Spring的两大核心和Spring的组件有了更深入的了解.同时在学习Spring源码时,得了解一些设计模式,不然阅读源码还是有一定难度的,所以一些重 ...

  3. JDK1.8源码阅读系列之三:Vector

    本篇随笔主要描述的是我阅读 Vector 源码期间的对于 Vector 的一些实现上的个人理解,用于个人备忘,有不对的地方,请指出- 先来看一下 Vector 的继承图: 可以看出,Vector 的直 ...

  4. SpringMVC源码阅读系列汇总

    1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...

  5. 【合集】TiDB 源码阅读系列文章

    [合集]TiDB 源码阅读系列文章 (一)序 (二)初识 TiDB 源码 (三)SQL 的一生 (四)INSERT 语句概览 (五)TiDB SQL Parser 的实现 (六)Select 语句概览 ...

  6. 【Dubbo源码阅读系列】之远程服务调用(上)

    今天打算来讲一讲 Dubbo 服务远程调用.笔者在开始看 Dubbo 远程服务相关源码的时候,看的有点迷糊.后来慢慢明白 Dubbo 远程服务的调用的本质就是动态代理模式的一种实现.本地消费者无须知道 ...

  7. 【Dubbo源码阅读系列】服务暴露之远程暴露

    引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A ...

  8. 【Dubbo源码阅读系列】服务暴露之本地暴露

    在上一篇文章中我们介绍 Dubbo 自定义标签解析相关内容,其中我们自定义的 XML 标签 <dubbo:service /> 会被解析为 ServiceBean 对象(传送门:Dubbo ...

  9. DM 源码阅读系列文章(六)relay log 的实现

    2019独角兽企业重金招聘Python工程师标准>>> 作者:张学程 本文为 DM 源码阅读系列文章的第六篇,在 上篇文章 中我们介绍了 binlog replication 处理单 ...

随机推荐

  1. 自然语言处理(NLP)的一般处理流程!

    1. 什么是NLP 自然语言处理 (Natural Language Processing) 是人工智能(AI)的一个子领域.自然语言处理是研究在人与人交互中以及在人与计算机交互中的语言问题的一门学科 ...

  2. count(*) count(1) count(column)的区别

    count(1)中的1并不是指第一个column: count(*)和count(1)一样,包括对值为NULL的统计: count(column)不包括对值为NULL的统计,这里的column指的不是 ...

  3. k好数(动态规划)

    问题描述 如果一个自然数N的K进制表示中任意的相邻的两位都不是相邻的数字,那么我们就说这个数是K好数.求L位K进制数中K好数的数目.例如K = 4,L = 2的时候,所有K好数为11.13.20.22 ...

  4. TokuDB · 引擎特性 · HybridDB for MySQL高压缩引擎TokuDB 揭秘

    原文出处:阿里云RDS-数据库内核组 HybridDB for MySQL(原名petadata)是面向在线事务(OLTP)和在线分析(OLAP)混合场景的关系型数据库.HybridDB采用一份数据存 ...

  5. ES6之模块化导入导出

    1.概述 在js的历史上一直没有模块(module)体系,无法将一个大程序拆分成相互依赖的小文件,再用简单的方法拼装起来,这对开发大型的.复杂的项目形成了巨大障碍. 在 ES6 之前,社区制定了一些模 ...

  6. JavaScript 数据结构与算法之美 - 归并排序、快速排序、希尔排序、堆排序

    1. 前言 算法为王. 想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远. 笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算 ...

  7. VS Code 配置 Python 开发环境

    1.终端运行 Python2.安装 Python 插件3.查看.安装外部库4.代码补全工具5.代码检查工具5.1.pylint5.2.flake8 和 yapf 本文基于 VS Code 1.36.1 ...

  8. 同步vmware虚拟机和主机的时间

    1. 打开虚拟机->设置 2. 选择选项标签页,选中VMware Tools,勾选“将客户机时间与主机同步”

  9. 洛谷-P1414 又是毕业季II -枚举因子

    P1414 又是毕业季II:https://www.luogu.org/problemnew/show/P1414 题意: 给定一个长度为n的数列.要求输出n个数字,每个数字代表从给定数列中最合理地取 ...

  10. CF915C Permute Digits 字符串 贪心

    You are given two positive integer numbers a and b. Permute (change order) of the digits of a to con ...