前言

在现在许许多多的应用系统中,日志非常关键,它即是排查问题的强力工具,也是程序员居家旅行工作甩锅必备良品。

在团队中编码中,我们都要求对于那些会变更数据的接口、调用第三方的接口记录请求和响应参数,另外在关键的代码路径记录后续可供诊断的日志信息。

如果使用了微软官方的日志记录框架Microsoft.Extensions.Logging,我们通常会像下面代码一样记录日志。

这是我们经常会遇到的日志记录场景,其中会记录一些外部传入的参数。带参数的日志会有频繁的字符串拼接必然会使用更多的内存对GC造成更大的压力。当系统处理的请求越来越多的时候,日志记录就很可能会成为瓶颈。

  1. // 创建日志记录类,分别使用不同的方式来记录日志
  2. var logger = LoggerFactory.Create(l => l.AddConsole()).CreateLogger(typeof(OrderLogger));
  3. var orderLogger = new OrderLogger(logger);
  4. var member = new Member("8888","Justin Yu");
  5. orderLogger.LogByStringInterpolation(member, DateTime.Now);
  6. orderLogger.LogByStructure(member, DateTime.Now);
  7. OrderLogger.LogBySourceGenerator(logger, member, DateTime.Now);
  8. /// <summary>
  9. /// 会员
  10. /// </summary>
  11. /// <param name="MemberId">会员Id</param>
  12. /// <param name="Name">会员名</param>
  13. public record Member(string MemberId, string Name);
  14. /// <summary>
  15. /// 订单日志记录类
  16. /// 需要使用Source Generator 所以类型为partial
  17. /// </summary>
  18. public partial class OrderLogger
  19. {
  20. private readonly ILogger _logger;
  21. public OrderLogger(ILogger logger)
  22. {
  23. _logger = logger;
  24. }
  25. /// <summary>
  26. /// 使用字符串插值记录
  27. /// </summary>
  28. public void LogByStringInterpolation(Member member, DateTime now)=>
  29. _logger.LogInformation($"会员[{member}]在[{now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标");
  30. /// <summary>
  31. /// 使用参数化记录
  32. /// </summary>
  33. public void LogByStructure(Member member, DateTime now) =>
  34. _logger.LogInformation("会员[{Member}]在[{Now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标", member, now);
  35. /// <summary>
  36. /// 使用源代码生成
  37. /// </summary>
  38. [LoggerMessage(
  39. EventId = 0,
  40. Level = LogLevel.Information,
  41. Message = "会员[{member}]在[{Now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标")]
  42. public static partial void LogBySourceGenerator(ILogger logger, Member member, DateTime now);
  43. }

然后运行一下代码看看日志记录的结果,它们都能正常的输出我们想要的日志,那么究竟性能上有什么差别呢?让我们看看第二节的内容。

性能对比

我们这里实际上是测试这三种方式参数化日志记录的性能,为了避免日志输出到外部ConsoleFile可能影响我们的测试结果,我们自己编写一个什么都不做的ILogger实现。

  1. public class EmptyLogger : ILogger
  2. {
  3. public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
  4. { }
  5. public bool IsEnabled(LogLevel logLevel) => true;
  6. public IDisposable BeginScope<TState>(TState state) => default;
  7. }

然后使用下面代码进行Benchmark。

  1. [HtmlExporter]
  2. [MemoryDiagnoser]
  3. [Orderer(SummaryOrderPolicy.FastestToSlowest)]
  4. public class Benchmark
  5. {
  6. private static readonly ILogger Logger = new EmptyLogger();
  7. private readonly OrderLogger _orderLogger = new(Logger);
  8. private readonly Member _user = new("8888","Justin Yu");
  9. private readonly DateTime _now = DateTime.UtcNow;
  10. [Benchmark]
  11. public void LogByStringInterpolation() => _orderLogger.LogByStringInterpolation(_user, _now);
  12. [Benchmark]
  13. public void LogByStructure() => _orderLogger.LogByStructure(_user, _now);
  14. [Benchmark]
  15. public void LogBySourceGenerator() => OrderLogger.LogBySourceGenerator(_logger, _user, _now);
  16. }

跑分结果如下所示,可以看到使用了SourceGenerator的日志记录方式遥遥领先;另外也不要小看那么几十纳秒和Byte的差别,在高并发的系统中就是积少成多会带来不错的性能提升。



那么为什么使用SourceGenerator会有这样的效果?我们来看看SourceGenerator到底生成了什么。

关于SourceGenerator

SourceGenerator(下文使用SG替代)并不是什么新的东西,在.NET5时就已经支持,不过由于在那时还是比较新的技术,没有太多的应用场景。引用微软文档上的图片,SourceGenerator就是在编译时通过Roslyn来分析源代码然后生成新源代码参与编译,更详细的信息可以在文末找到链接。



它主要是为了在编译时生成代码,减少开发者写一些重复代码所花费的时间,另外也有着比反射和运行时生成代码(Emit)更高的性能和更低的使用门槛。

另外一个方面就是为了Native AOT,比如Dapper、EF等为代表严重依赖运行时代码生成的ORM框架,在Native AOT中其实是不可用的,引入SourceGenerator以后就可以在运行时生成代码,为Native AOT加上这些框架的支持。

目前在.NET框架中已经有Logging、System.Text.Json、Regex等组件已经支持SourceGenerator,都有着较大的性能提成。

SG为Logger生成了什么?

SG生成的代码是可以输出到文件中的,只需要在项目文件中的<PropertyGroup>内加入这样的一个配置,设置值为True

  1. <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>

然后我们就可以在下面的目录中找到SG为我们生成的代码。路径在项目文件同级目录,根据Debug模式和Release模式如下所示:



打开文件夹以后,可以看到有一个*.g.cs结尾的文件,这就是由SG生成的文件。



使用IDE打开这个文件,就可以看到为我们生成的代码是什么样的(如下所示)。可以看到SG为我们填充了LogBySourceGenerator的主体部分,实现高性能的秘密之一就是使用了LoggerMessage.Define,它的所需要的内存分配要少的许多。

  1. // <auto-generated/>
  2. #nullable enable
  3. partial class OrderLogger
  4. {
  5. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.5.2210")]
  6. private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::Member, global::System.DateTime, global::System.Exception?> __LogBySourceGeneratorCallback =
  7. global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::Member, global::System.DateTime>(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, nameof(LogBySourceGenerator)), "会员[{member}]在[{Now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
  8. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.5.2210")]
  9. public static partial void LogBySourceGenerator(global::Microsoft.Extensions.Logging.ILogger logger, global::Member member, global::System.DateTime now)
  10. {
  11. if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
  12. {
  13. __LogBySourceGeneratorCallback(logger, member, now, null);
  14. }
  15. }
  16. }

而填充的规则是由上面代码中的特性决定的,如下摘抄出来的代码。

  1. [LoggerMessage(
  2. EventId = 0,
  3. Level = LogLevel.Information,
  4. Message = "会员[{member}]在[{Now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标")]
  5. public static partial void LogBySourceGenerator(ILogger logger, Member member, DateTime now);

至于SG是如何生成代码的不在本文的讨论范围,大家有兴趣的可以查看下方的源码链接。

https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen

编译时警告

这是SG另外一个很不错的功能,我们在软件开发过程中经常会犯一些错误,导致不能正常的使用框架的功能或者产生一些BUG。比如:

按照Logger的约定来说,为了方便SG生成代码,我们需要将类和方法名变为partial,假设我忘记了这个,写了如下的代码:

  1. /// <summary>
  2. /// 源代码生成 - 忘记写partial
  3. /// </summary>
  4. [LoggerMessage(
  5. EventId = 0,
  6. Level = LogLevel.Information,
  7. Message = "会员[{member}]在[{Now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标")]
  8. public static void LogBySourceGenerator(ILogger logger, Member member, DateTime now);

就会有下面这样的错误,提醒你必须加partial关键字。



再如,在参数化的模版里面有merbernow两个参数,我们编写代码的时候像下方一样少传了now参数。

  1. /// <summary>
  2. /// 源代码生成
  3. /// </summary>
  4. [LoggerMessage(
  5. EventId = 0,
  6. Level = LogLevel.Information,
  7. Message = "会员[{member}]在[{Now:yyyy-MM-dd HH:mm:ss}]充值了一个小目标")]
  8. public static partial void LogBySourceGenerator(ILogger logger, Member member);

那么SG构建时便可以提醒你,模版中的Now没有听对应的参数。



这功能会非常有用,能节省很多问题排查的时间。

总结

随着 C# Source Generator的出现,编写高性能的日志记录API变得更加容易。使用Source Generator方法有几个主要好处:

  • 允许保留日志记录结构,并启用消息模板所需的确切格式语法。
  • 允许为模板占位符提供替代名称,允许使用格式说明符。
  • 允许按原样传递所有原始数据,在对其进行处理之前,不需要进行任何复杂的存储(除了创建 string)。
  • 提供特定于日志记录的诊断,针对重复的事件 ID 发出警告。

与手动使用 LoggerMessage.Define 相比,还有一些好处:

  • 语法更短、更简单:使用声明性属性,而不是对样本进行编码。
  • 引导式开发人员体验:生成器会发出警告,帮助开发人员做正确的事。
  • 支持任意数量的日志记录参数。 LoggerMessage.Define 最多支持六个。
  • 支持动态日志级别。 单独使用 LoggerMessage.Define 不可能做到这一点。

参考文献及附录

本项目源码地址:https://github.com/InCerryGit/BlogCodes

Logger文档:https://docs.microsoft.com/zh-cn/dotnet/core/extensions/logger-message-generator

SourceGenerator MSDOC文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/roslyn-sdk/source-generators-overview

SourceGenerator 入门手册: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md

.NET性能优化-使用SourceGenerator-Logger记录日志的更多相关文章

  1. SQL性能优化常见措施(Lock wait timeout exceeded)

    SQL性能优化常见措施 目 录 1.mysql中explain命令使用 2.mysql中mysqldumpslow的使用 3.mysql中修改my.ini配置文件记录日志 4.mysql中如何加索引 ...

  2. HBase设计与开发性能优化(转)

    本文主要是从HBase应用程序设计与开发的角度,总结几种常用的性能优化方法.有关HBase系统配置级别的优化,这里涉及的不多,这部分可以参考:淘宝Ken Wu同学的博客. 1. 表的设计 1.1 Pr ...

  3. HBase性能优化方法总结(转)

    本文主要是从HBase应用程序设计与开发的角度,总结几种常用的性能优化方法.有关HBase系统配置级别的优化,这里涉及的不多,这部分可以参考:淘宝Ken Wu同学的博客. 1. 表的设计 1.1 Pr ...

  4. .NET程序性能优化基本要领

    想了解更多关于新的编译器的信息,可以访问     .NET Compiler Platform ("Roslyn") 基本要领 在对.NET 进行性能调优以及开发具有良好响应性的应 ...

  5. HBase性能优化方法总结(一):表的设计

    本文主要是从HBase应用程序设计与开发的角度,总结几种常用的性能优化方法.有关HBase系统配置级别的优化,可参考:淘宝Ken Wu同学的博客. 下面是本文总结的第一部分内容:表的设计相关的优化方法 ...

  6. Spring:利用PerformanceMonitorInterceptor来协助应用性能优化

    前段时间对公司产品做性能优化,如果单依赖于测试,进度就会很慢.所以就通过对代码的方式来完成,并以此来加快项目进度.具体的执行方案自然就是要知道各个业务执行时间,针对业务来进行优化. 因为项目中使用了S ...

  7. hbase性能优化总结

    hbase性能优化总结 1. 表的设计 1.1 Pre-Creating Regions 默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都 ...

  8. IdentityServer4源码颁发token分析及性能优化

    IdentityServer4源码地址 IdentityModel源码地址 以下的流程用ResourceOwnerPassword类型获取token作为介绍 分两种获取形式说明 token请求地址为默 ...

  9. redmine在linux上的mysql性能优化方法与问题排查方案

    iredmine的linux服务器mysql性能优化方法与问题排查方案     问题定位:   客户端工具: 1. 浏览器inspect-tool的network timing工具分析   2. 浏览 ...

  10. Storm 性能优化

    目录 场景假设 调优步骤和方法 Storm 的部分特性 Storm 并行度 Storm 消息机制 Storm UI 解析 性能优化 场景假设 在介绍 Storm 的性能调优方法之前,假设一个场景:项目 ...

随机推荐

  1. Springboot目录结构分析

    1 src/main/java 存储源码 2 src/main/resource 资源文件夹    (1)src/main/resource/static 用于存放静态资源,如css.js.图片.文件 ...

  2. linux篇-Parse error: syntax error, unexpected ‘new’ (T_NEW) in /usr/local/nginx/html/cacti/lib/adodb

    1首先这是基于lnmp模式进行的 2yum安装 yum -y install httpd mysql mysql-server php php-mysql php-json php-pdo 3lib库 ...

  3. C# settings 文件基础用法

    原文 自定义设置项类型 Serializable 修饰的枚举,可作为设置项类型 [Serializable] public enum DeviceBrand { None = 0, [Descript ...

  4. [USACO2021DEC] HILO 踩标做法

    [USACO2021DEC] HILO Solution 参考自 官方题解 里提到的一篇 Obliteration.pdf,但是里面作者写出了极多错误...然后式子还错错得对了. 令 \(y=n-x\ ...

  5. Mysql命令行插入字段超长不报错,而jdbc报错问题分析

    异常信息 exception.ServiceException: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long ...

  6. SpringCloud 简介

    目录 什么是微服务? 初识 SpringCloud SpringCloud VS Dubbo 什么是微服务? <互联网系统架构演变> "微服务"一词源于 Martin ...

  7. 爬取豆瓣TOP250电影

    自己跟着视频学习的第一个爬虫小程序,里面有许多不太清楚的地方,不如怎么找到具体的电影名字的,那么多级关系,怎么以下就找到的是那个div呢? 诸如此类的,有许多,不过先做起来再说吧,后续再取去弄懂. i ...

  8. 1. Docker的中央仓库安装设置及镜像的操作

    具体也可参考:https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.3e221b11G7cfhr https://d ...

  9. FlinkSQL 之乱序问题

    乱序问题 在业务编写 FlinkSQL 时, 非常常见的就是乱序相关问题, 在出现问题时,非常难以排查,且无法稳定复现,这样无论是业务方,还是平台方,都处于一种非常尴尬的地步. 在实时 join 中, ...

  10. SAP BDC 用户输入日期转系统日期格式: CONVERT_DATE_TO_EXTERNAL

    BDC中,日期输入格式不正确:可调用FM  CONVERT_DATE_TO_EXTERNAL DATA:l_bdcfield LIKE bdcdata-fval."BDC field val ...