[ASP.NET Core 3框架揭秘] 服务承载系统[2]: 承载长时间运行的服务[下篇]
三、配置选项
真正的应用开发总是会使用到配置选项,如演示程序中性能指标采集的时间间隔就应该采用配置选项的方式来指定。由于涉及对性能指标数据的发送,所以最好将发送的目标地址定义在配置选项中。如果有多种传输协议可供选择,就可以定义相应的配置选项。.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]: 承载服务启动流程[下篇]