为了让我们第一时间知道程序的运行状态,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. windows下搭建syslog服务器及基本配置

    一.环境 windows7 64位+ kiwi_syslog_server_9.5.0 kiwi_syslog百度云下载地址: 链接: https://pan.baidu.com/s/1EpPBNsL ...

  2. 用代码说话:synchronized关键字和多线程访问同步方法的7种情况

    synchronized关键字在多线程并发编程中一直是元老级角色的存在,是学习并发编程中必须面对的坎,也是走向Java高级开发的必经之路. 一.synchronized性质 synchronized是 ...

  3. HBase 系列(五)——HBase 常用 Shell 命令

    一.基本命令 打开 Hbase Shell: # hbase shell 1.1 获取帮助 # 获取帮助 help # 获取命令的详细信息 help 'status' 1.2 查看服务器状态 stat ...

  4. [Flowable] - 工作流是什么?BPM是什么?

    工作流管理系统基本概念 近两年随着电子商务环境不断演进(例如阿里巴巴的B2B电子商务平台),从原来支持企业内部单系统的业务流程.到企业内部应用.服务的集成,再进一步向企业与合作伙伴之间业务交互,工作流 ...

  5. CSS3 表单

    <form action="http://baidu.com"> <input type="text" placeholder="请 ...

  6. unity编辑器扩展_02(分别在Hierarchy,Project中创建一个选项)

    在Hierarchy面板创建选项的代码: [MenuItem("GameObject/Test",false,1)]    static void Test1()    {     ...

  7. Codeforces 975D

    题意略. 思路:我们来写一下公式: P1:(x1 + t * Vx1,y1 + t * Vy1)                P2:(x2 + t * Vx2,y2 + t * Vy2) x1 + ...

  8. Mac os 下 python爬虫相关的库和软件的安装

      由于最近正在放暑假,所以就自己开始学习python中有关爬虫的技术,因为发现其中需要安装许多库与软件所以就在这里记录一下以避免大家在安装时遇到一些不必要的坑. 一. 相关软件的安装:   1. h ...

  9. abp(net core)+easyui+efcore实现仓储管理系统——菜单 (十六)

    系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+easyui+efcore实现仓储管理系统——解决方案介绍(二) ...

  10. KVC&KVO&运行时

    运行时:要先了解程序运行的三个阶段 1.编译阶段:clang将OC代码转换成C++,查看运行机制调用的方法 2.链接阶段:与我们使用到得库文件进行链接 3.运行阶段:我们要谈的运行时主要针对这个阶段, ...