三、配置選項
真正的應用開發總是會使用到配置選項,如演示程序中性能指標采集的時間間隔就應該采用配置選項的方式來指定。由於涉及對性能指標數據的發送,所以最好將發送的目標地址定義在配置選項中。如果有多種傳輸協議可供選擇,就可以定義相應的配置選項。.NET Core應用推薦采用Options模式來使用配置選項,所以可以定義如下這個MetricsCollectionOptions類型來承載3種配置選項。
public class MetricsCollectionOptions { public TimeSpan CaptureInterval { get; set; } public TransportType Transport { get; set; } public Endpoint DeliverTo { get; set; } } public enum TransportType { Tcp, Http, Udp } public class Endpoint { public string Host { get; set; } public int Port { get; set; } public override string ToString() => $"{Host}:{Port}"; }
傳輸協議和目標地址使用在FakeMetricsDeliverer服務中,所以我們對它進行了相應的改寫。如下面的代碼片段所示,我們在構造函數中通過注入的IOptions<MetricsCollectionOptions>服務來提供上面的兩個配置選項。在實現的DeliverAsync方法中,可以將采用的傳輸協議和目標地址輸出到控制台上。
public class FakeMetricsDeliverer : IMetricsDeliverer { private readonly TransportType _transport; private readonly Endpoint _deliverTo; public FakeMetricsDeliverer(IOptions<MetricsCollectionOptions> optionsAccessor) { var options = optionsAccessor.Value; _transport = options.Transport; _deliverTo = options.DeliverTo; } public Task DeliverAsync(PerformanceMetrics counter) { Console.WriteLine($"[{DateTimeOffset.Now}]Deliver performance counter {counter} to {_deliverTo} via {_transport}"); return Task.CompletedTask; } }
與FakeMetricsDeliverer提取配置選項類似,在承載服務類型PerformanceMetricsCollector中同樣可以采用Options模式來提供表示性能指標采集頻率的配置選項。如下所示的代碼片段是PerformanceMetricsCollector采用配置選項后的完整定義。
public sealed class PerformanceMetricsCollector : IHostedService { private readonly IProcessorMetricsCollector _processorMetricsCollector; private readonly IMemoryMetricsCollector _memoryMetricsCollector; private readonly INetworkMetricsCollector _networkMetricsCollector; private readonly IMetricsDeliverer _metricsDeliverer; private readonly TimeSpan _captureInterval; private IDisposable _scheduler; public PerformanceMetricsCollector( IProcessorMetricsCollector processorMetricsCollector, IMemoryMetricsCollector memoryMetricsCollector, INetworkMetricsCollector networkMetricsCollector, IMetricsDeliverer metricsDeliverer, IOptions<MetricsCollectionOptions> optionsAccessor) { _processorMetricsCollector = processorMetricsCollector; _memoryMetricsCollector = memoryMetricsCollector; _networkMetricsCollector = networkMetricsCollector; _metricsDeliverer = metricsDeliverer; _captureInterval = optionsAccessor.Value.CaptureInterval; } public Task StartAsync(CancellationToken cancellationToken) { _scheduler = new Timer(Callback, null, TimeSpan.FromSeconds(5), _captureInterval); return Task.CompletedTask; async void Callback(object state) { var counter = new PerformanceMetrics { Processor = _processorMetricsCollector.GetUsage(), Memory = _memoryMetricsCollector.GetUsage(), Network = _networkMetricsCollector.GetThroughput() }; await _metricsDeliverer.DeliverAsync(counter); } } public Task StopAsync(CancellationToken cancellationToken) { _scheduler?.Dispose(); return Task.CompletedTask; } }
使用配置文件可以提供上述3個配置選項,所以我們在根目錄下添加了一個名為appSettings.json的配置文件。由於演示的應用程序采用的SDK類型為“Microsoft.NET.Sdk”,程序運行過程中會將編譯程序集的目標目錄作為當前目錄,所以需要將配置文件的“Copy to output directory”屬性設置為“Copy always”,這樣可以確保它在編譯時總是被復制到目標目錄。我們通過在配置文件中定義如下內容來提供上述3個配置選項。
{ "MetricsCollection": { "CaptureInterval": "00:00:05", "Transport": "Udp", "DeliverTo": { "Host": "192.168.0.1", "Port": 3721 } } }
下面針對配置選項的使用對演示程序做相應的改動。如下面的代碼片段所示,我們調用了IHostBuilder對象的ConfigureAppConfiguration方法,並利用提供的Action<IConfigurationBuilder>對象注冊了指向配置文件appsettings.json的JsonConfigurationSource對象。從名稱可以看出,ConfigureAppConfiguration方法的目的在於初始化應用程序所需的配置。
class Program { static void Main() { var collector = new FakeMetricsCollector(); new HostBuilder() .ConfigureAppConfiguration(builder=>builder.AddJsonFile("appsettings.json")) .ConfigureServices((context,svcs) => svcs .AddSingleton<IProcessorMetricsCollector>(collector) .AddSingleton<IMemoryMetricsCollector>(collector) .AddSingleton<INetworkMetricsCollector>(collector) .AddSingleton<IMetricsDeliverer, FakeMetricsDeliverer>() .AddSingleton<IHostedService, PerformanceMetricsCollector>() .AddOptions() .Configure<MetricsCollectionOptions>(context.Configuration.GetSection("MetricsCollection"))) .Build() .Run(); } }
之前針對依賴服務的注冊是通過調用IHostBuilder對象的ConfigureServices方法利用作為參數的Action<IServiceCollection>對象完成的,IHostBuilder接口還有一個ConfigureServices方法重載,它的參數類型為Action<HostBuilderContext, IServiceCollection>,作為上下文的HostBuilderContext對象可以提供應用的配置,我們在上面調用的就是ConfigureServices方法重載。
如上面的代碼片段所示,我們利用提供的Action<HostBuilderContext, IServiceCollection>對象通過調用IServiceCollection接口的AddOptions擴展方法注冊了Options模式所需的核心服務,然后調用Configure<TOptions>擴展方法從提供的HostBuilderContext對象中提取出當前應用的配置,並將它和對應的配置選項類型MetricsCollectionOptions做了綁定。我們修改后的程序運行之后在控制台上輸出的結果如下圖所示,可以看出,輸出的結果與配置文件的內容是匹配的。(源代碼從這里下載)
四、承載環境
應用程序總是針對某個具體的環境進行部署,開發(Development)、預發(Staging)和產品(Production)是3種典型的部署環境。這里的部署環境在承載系統中統稱為承載環境(Hosting Environment)。一般來說,不同的承載環境往往具有不同的配置選項,下面演示如何為不同的承載環境提供相應的配置選項。
《讀取配置數據[下篇]》已經演示了如何提供針對具體環境的配置文件,具體的做法很簡單:將共享或者默認的配置定義在基礎配置文件(如appsettings.json)中,將差異化的部分定義在針對具體承載環境的配置文件(如appsettings.staging.json和appsettings.production.json)中。對於我們演示的實例來說,可以采用如下圖所示的方式添加額外的兩個配置文件來提供針對預發和產品環境的差異化配置。
對於演示實例提供的3個配置選項來說,假設針對承載環境的差異化配合僅限於發送的目標終結點(IP地址和端口),就可以采用如下方式將它們定義在針對預發環境的appsettings.staging.json和針對產品環境的appsettings.production.json中。
appsettings.staging.json
{ "MetricsCollection": { "DeliverTo": { "Host": "192.168.0.2", "Port": 3721 } } }
appsettings.production.json
{ "MetricsCollection": { "DeliverTo": { "Host": "192.168.0.2", "Port": 3721 } } }
在提供了針對具體承載環境的配置文件之后,還需要解決兩個問題:第一,如何將它們注冊到應用采用的配置框架中;第二,如何確定當前的承載環境。前者可以調用IHostBuilder接口的ConfigureAppConfiguration方法來完成,從命名可以看出,這個方法注冊的是針對“應用”層面的配置。我們可以將這里所謂的“應用”理解為承載的服務,也就是說,采用這種方式注冊的配置是為承載的服務使用的。實際上,IHostBuilder接口還有一個ConfigureHostConfiguration方法,它注冊的服務是供服務宿主(Host)自身使用的,而當前的承載環境就可以利用此配置來指定。
我們將上述這兩個問題的解決方案實現在改寫的程序中。如下面的代碼片段所示,為了使演示的應用程序可以采用命令行的形式來指定承載環境,可以調用HostBuilder接口的ConfigureHostConfiguration方法,並利用提供的Action<IConfigurationBuilder>對象注冊了針對命令行的配置源。為了注冊針對承載環境的配置,可以調用類型為Action<HostBuilderContext, IConfigurationBuilder>的ConfigureAppConfiguration方法,因為我們需要HostBuilderContext上下文對象得到當前的承載環境。
class Program { static void Main(string[] args) { var collector = new FakeMetricsCollector(); new HostBuilder() .ConfigureHostConfiguration(builder => builder.AddCommandLine(args)) .ConfigureAppConfiguration((context, builder) => builder .AddJsonFile(path: "appsettings.json", optional: false) .AddJsonFile( path: $"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true)) .ConfigureServices((context, svcs) => svcs .AddSingleton<IProcessorMetricsCollector>(collector) .AddSingleton<IMemoryMetricsCollector>(collector) .AddSingleton<INetworkMetricsCollector>(collector) .AddSingleton<IMetricsDeliverer, FakeMetricsDeliverer>() .AddSingleton<IHostedService, PerformanceMetricsCollector>() .AddOptions() .Configure<MetricsCollectionOptions>(context.Configuration.GetSection("MetricsCollection"))) .Build() .Run(); } }
我們調用ConfigureAppConfiguration方法注冊了兩個配置文件:一個是承載基礎或者默認配置的appsettings.json文件,另一個是針對當前承載環境的appsettings.{environment}.json文件。前者是必需的,后者是可選的,這樣做的目的在於確保即使當前承載環境不存在對應配置文件的情況也不會拋出異常(此時應用只會使用appsettings.json文件中定義的配置)。
下面以命令行的形式運行修改后的應用程序,承載環境通過命令行參數environment來指定。下圖是先后4次運行演示實例得到的輸出結果,從輸出的IP地址可以看出,應用程序確實是根據當前承載環境加載對應的配置文件的。輸出結果還體現了另一個細節:應用程序默認使用的是產品(Production)環境。(源代碼從這里下載)
五、日志
在具體的應用開發時不可避免地會涉及很多針對“診斷日志”的編程,下面演示在通過承載系統承載的應用中如何記錄日志。對於演示實例來說,它用於發送性能指標的FakeMetricsDeliverer對象會將收集的指標數據輸出到控制台上,下面將這段文字以日志的形式進行輸出,為此我們將這個類型進行了如下改寫。
public class FakeMetricsDeliverer : IMetricsDeliverer { private readonly TransportType _transport; private readonly Endpoint _deliverTo; private readonly ILogger _logger; private readonly Action<ILogger, DateTimeOffset, PerformanceMetrics, Endpoint, TransportType, Exception> _logForDelivery; public FakeMetricsDeliverer(IOptions<MetricsCollectionOptions> optionsAccessor, ILogger<FakeMetricsDeliverer> logger) { var options = optionsAccessor.Value; _transport = options.Transport; _deliverTo = options.DeliverTo; _logger = logger; _logForDelivery = LoggerMessage.Define<DateTimeOffset, PerformanceMetrics, Endpoint, TransportType>(LogLevel.Information, 0, "[{0}]Deliver performance counter {1} to {2} via {3}"); } public Task DeliverAsync(PerformanceMetrics counter) { _logForDelivery(_logger, DateTimeOffset.Now, counter, _deliverTo, _transport, null); return Task.CompletedTask; } }
如上面的代碼片段所示,我們直接在構造函數中注入了ILogger<FakeMetricsDeliverer>對象並利用它來記錄日志。為了避免對同一個消息模板的重復解析,可以使用靜態類型LoggerMessage提供的委托對象來輸出日志,這也是FakeMetricsDeliverer中采用的編程模式。為了將日志框架引入應用程序,我們需要在初始化應用時注冊相應的服務,為此需要將應用程序做相應的改寫。如下面的代碼片段所示,我們調用IHostBuilder接口的ConfigureLogging擴展方法注冊了日志框架的核心服務,並利用提供的Action<ILoggingBuilder>對象注冊了針對控制台作為輸出渠道的ConsoleLoggerProvider。
class Program { static void Main(string[] args) { var collector = new FakeMetricsCollector(); new HostBuilder() .ConfigureHostConfiguration(builder => builder.AddCommandLine(args)) .ConfigureAppConfiguration((context, builder) => builder .AddJsonFile(path: "appsettings.json", optional: false) .AddJsonFile( path: $"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true)) .ConfigureServices((context, svcs) => svcs .AddSingleton<IProcessorMetricsCollector>(collector) .AddSingleton<IMemoryMetricsCollector>(collector) .AddSingleton<INetworkMetricsCollector>(collector) .AddSingleton<IMetricsDeliverer, FakeMetricsDeliverer>() .AddSingleton<IHostedService, PerformanceMetricsCollector>() .AddOptions() .Configure<MetricsCollectionOptions>(context.Configuration.GetSection("MetricsCollection"))) .ConfigureLogging(builder => builder.AddConsole()) .Build() .Run(); } }
再次運行修改后的程序,控制台上的輸出結果如下圖所示。由輸出結果可以看出,這些文字是由我們注冊的ConsoleLoggerProvider提供的ConsoleLogger對象輸出到控制台上的。由於承載系統自身在進行服務承載過程中也會輸出一些日志,所以它們也會輸出到控制台上。
如果對輸出的日志進行過濾,可以將過濾規則定義在配置文件中。假設對於類別以Microsoft.為前綴的日志,我們只希望等級不低於Warning的才會被輸出,這樣會避免太多的消息被輸出到控制台上造成對性能的影響,所以可以將產品環境對應的appsettings.production.json文件的內容做如下修改。
{ "MetricsCollection": { "DeliverTo": { "Host": "192.168.0.3", "Port": 3721 } }, "Logging": { "LogLevel": { "Microsoft": "Warning" } } }
為了應用日志配置,我們還需要對應用程序做相應的修改。如下面的代碼片段所示,在對ConfigureLogging擴展方法的調用中,可以利用HostBuilderContext上下文對象得到當前配置,進而得到名為Logging的配置節。我們將這個配置節作為參數調用ILoggingBuilder對象的AddConfiguration擴展方法將承載的過濾規則應用到日志框架上。
class Program { static void Main(string[] args) { var collector = new FakeMetricsCollector(); new HostBuilder() .ConfigureHostConfiguration(builder => builder.AddCommandLine(args)) .ConfigureAppConfiguration((context, builder) => builder .AddJsonFile(path: "appsettings.json", optional: false) .AddJsonFile( path: $"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true)) .ConfigureServices((context, svcs) => svcs .AddSingleton<IProcessorMetricsCollector>(collector) .AddSingleton<IMemoryMetricsCollector>(collector) .AddSingleton<INetworkMetricsCollector>(collector) .AddSingleton<IMetricsDeliverer, FakeMetricsDeliverer>() .AddSingleton<IHostedService, PerformanceMetricsCollector>() .AddOptions() .Configure<MetricsCollectionOptions>(context.Configuration.GetSection("MetricsCollection"))) .ConfigureLogging((context,builder) => builder .AddConfiguration(context.Configuration.GetSection("Logging")) .AddConsole()) .Build() .Run(); } }
服務承載系統[1]: 承載長時間運行的服務[上篇]
服務承載系統[2]: 承載長時間運行的服務[下篇]
服務承載系統[3]: 服務承載模型[上篇]
服務承載系統[4]: 服務承載模型[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程[下篇]