Asp.Net Core2.2 源碼閱讀系列——控制台日誌源碼解析

来源:https://www.cnblogs.com/xboo/archive/2019/09/23/11574091.html
-Advertisement-
Play Games

  為了讓我們第一時間知道程式的運行狀態,Asp.Net Core 添加了預設的日誌輸出服務。這看起來並沒有什麼問題,對於開發人員也相當友好,但如果不瞭解日誌輸出的細節,也有可能因為錯誤的日誌級別配置導致性能問題,筆者的同事在一次進行性能測試的時候被輸出日誌誤導,與其討論分析了測 ...


  為了讓我們第一時間知道程式的運行狀態,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 的同學,希望你同我一樣,對瞭解事務的本質保持好奇心,持之以恆!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • “容器”這兩個字很少被 Python 技術文章提起。一看到“容器”,大家想到的多是那頭藍色小鯨魚:Docker,但這篇文章和它沒有任何關係。本文里的容器,是 Python 中的一個抽象概念,是對專門用來裝其他對象的數據類型的統稱。 在 Python 中,有四類最常見的內建容器類型: 列表(list) ...
  • 溫馨提示 請收藏再看。此文篇幅太長,你短時間看不完;此文乾貨太多,錯過太可惜。 示例代碼可以關註 (公眾號)回覆 獲取。 收穫 1. 講解詳細:能讓你掌握使用 及類似校驗工具的各種使用姿勢 2. 內容全面:可以當做知識字典來查詢 what 註意:hibernate validator 與 持久層框架 ...
  • 閑及無聊 又打開了CSDN開始看一看有什麼先進的可以學習的相關帖子,這時看到了一位大神寫的簡歷裝X必備,手寫Spring MVC。 我想這個東西還是有一點意思的 就拜讀了一下大佬的博客 通讀了一遍相關代碼 感覺和我想象中spring的運作流程基本相同 但是我腦海中基本上只有一個非常簡單的基本概念 而 ...
  • 多好,多簡單,多好 ...
  • 一、題目 設平面上分佈著n個白點和n個黑點,每個點用一對坐標(x, y)表示。一個黑點b=(xb,yb)支配一個白點w=(xw, yw)當且僅當xb>=xw和yb>=yw。 若黑點b支配白點w,則黑點b和白點w可匹配(可形成一個匹配對)。 在一個黑點最多只能與一個白點匹配,一個白點最多只能與一個黑點 ...
  • 在webform中,驗證的流程大致如下圖: 在AOP中: 在Filter中: AuthorizeAttribute許可權驗證 登錄後有許可權控制,有的頁面是需要用戶登錄才能訪問的,需要在訪問頁面增加一個驗證,也不能每個action都一遍。 1、寫一個CustomAuthorAttribute,繼承自Au ...
  • Ajax請求數據響應格式,一個醒目組必須是同意的,前端才知道怎麼應付,還有很多其他情況,比如異常了,有ExceptionFilter,按照固定格式返回,比如沒有許可權,Authorization,按照固定格式返回。 Http請求的本質: 請求--應答式,響應可以那麼豐富?不同的類型其實方式一樣的,只不 ...
  • MVCApplication Application_Statr--RegisterRoutes--給RouteCollection添加規則,請求進到網站 X 請求地址被路由按照順序匹配,遇到一個溫和的就結束,就到對應的控制器和action。 在程式中使用log4net,首先nuget引入程式集 L ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...