ASP.NET Core 6框架揭秘实例演示[21]:如何承载你的后台服务

借助 .NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NET Core应用最终也体现为这样一个承载服务。

借助 .NET提供的服务承载(Hosting)系统,我们可以将一个或者多个长时间运行的后台服务寄宿或者承载我们创建的应用中。任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载,ASP.NET Core应用最终也体现为这样一个承载服务。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)

[S1401]利用承载服务收集性能指标(源代码
[S1402]依赖注入的应用(源代码
[S1403]配置选项的应用(源代码
[S1404]提供针对环境的配置(源代码
[S1405]日志的应用(源代码
[S1406]在配置中定义日志过滤规则(源代码

[S1401]利用承载服务收集性能指标

承载服务的项目一般会采用“Microsoft.NET.Sdk.Worker”这个SDK。服务承载模型涉及的接口和类型大都定义在“Microsoft.Extensions.Hosting.Abstractions”这个NuGet包,而具体实现在由NuGet包“Microsoft.Extensions.Hosting”来提供。我们演示的承载服务会定时采集当前进程的性能指标并将其分发出去。我们只关注处理器使用率、内存使用量和网络吞吐量这三种典型的指标,为此我们定义了如下这个PerformanceMetrics类型。我们并不会实现真正的性能指标收集,定义的静态方法Create会利用随机生成的指标来创建PerformanceMetrics对象。

public class PerformanceMetrics
{
    private static readonly Random _random = new();

    public int 	Processor { get; set; }
    public long 	Memory { get; set; }
    public long 	Network { get; set; }

    public override string ToString() => @$"CPU: {Processor * 100}%; Memory: {Memory / (1024* 1024)}M; Network: {Network / (1024 * 1024)}M/s";

    public static PerformanceMetrics Create() => new()
    {
        Processor 	= _random.Next(1, 8),
        Memory 	= _random.Next(10, 100) * 1024 * 1024,
        Network 	= _random.Next(10, 100) * 1024 * 1024
    };
}

承载服务通过IHostedService接口表示,该接口定义的StartAsync和StopAsync方法可以启动与关闭服务。我们将性能指标采集服务定义成如下这个PerformanceMetricsCollector类型。在实现的StartAsync方法中,我们一个定时器每隔5秒调用Create方法创建一个PerformanceMetrics对象,并将它承载的性能指标输出到控制台上。作为定期是的Timer对象会在StopAsync方法中被释放。

public sealed class PerformanceMetricsCollector : IHostedService
{
    private IDisposable? _scheduler;
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _scheduler = new Timer(Callback, null, TimeSpan.FromSeconds(5),TimeSpan.FromSeconds(5));
        return Task.CompletedTask;

        static void Callback(object? state)=> Console.WriteLine($"[{DateTimeOffset.Now}]{PerformanceMetrics.Create()}");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _scheduler?.Dispose();
        return Task.CompletedTask;
    }
}

服务承载系统通过IHost接口表示承载服务的宿主,该对象在应用启动过程中采用Builder模式由对应的IHostBuilder对象来构建。HostBuilder类型是对IHostBuilder接口的默认实现,所以我们采用如下方式创建一个HostBuilder对象,并调用其Build方法来提供作为宿主的IHost对象。在调用Build方法构建IHost对象之前,我们调用了ConfigureServices方法将PerformancceMetricsCollector注册成针对IHostedService接口的服务,并将生命周期模式设置成Singleton。

using App;
new HostBuilder()
.ConfigureServices(svcs => svcs
    .AddSingleton<IHostedService, PerformanceMetricsCollector>())
    .Build()
    .Run();

我们最后调用Run方法启动通过IHost对象表示的承载服务宿主,进而启动由它承载的PerformancceMetricsCollector服务,该服务将以图1所示的形式每隔5秒在控制台上输出“采集”的性能指标。

image
图1 承载指标采集服务

除了采用一般的服务注册方式,我们还可以按照如下的方式调用IServiceCollection接口的AddHostedService<THostedService>扩展方法来对承载服务PerformanceMetricsCollector进行注册。我们一般也不会通过调用构造函数的方式创建HostBuilder对象,而是使用定义在Host类型中的 工厂方法CreateDefaultBuilder创建来构建IHostBuilder对象。

using App;
Host.CreateDefaultBuilder(args)
    .ConfigureServices(svcs => svcs.AddHostedService<PerformanceMetricsCollector>())
    .Build()
    .Run();

[S1402]依赖注入的应用

服务承载系统整合依赖注入框架,针对承载服务的注册实际上就是将它注册到依赖注入框架中。既然承载服务实例最终是通过依赖注入容器提供的,那么它自身所依赖的服务当然也可以进行注册。我们接下来将PerformanceMetricsCollector提供的性能指标收集功能分解到由四个接口表示的服务中,IProcessorMetricsCollector、IMemoryMetricsCollector和INetworkMetricsCollector接口代表的服务分别用于收集三种对应的性能指标,而IMetricsDeliverer接口表示的服务则负责将收集的性能指标发送出去。

public interface IProcessorMetricsCollector
{
    int GetUsage();
}
public interface IMemoryMetricsCollector
{
    long GetUsage();
}
public interface INetworkMetricsCollector
{
    long GetThroughput();
}

public interface IMetricsDeliverer
{
    Task DeliverAsync(PerformanceMetrics counter);
}

我们定义的MetricsCollector类型实现了三个性能指标采集接口,采集的性能指标直接来源于通过静态方法Create创建的PerformanceMetrics对象。MetricsDeliverer类型实现了IMetricsDeliverer接口,实现的DeliverAsync方法直接将PerformanceMetrics对象承载的性能指标输出到控制台上。

public class MetricsCollector :
    IProcessorMetricsCollector,
    IMemoryMetricsCollector,
    INetworkMetricsCollector
{
    long INetworkMetricsCollector.GetThroughput() => PerformanceMetrics.Create().Network;

    int IProcessorMetricsCollector.GetUsage() => PerformanceMetrics.Create().Processor;

    long IMemoryMetricsCollector.GetUsage() => PerformanceMetrics.Create().Memory;
}

public class MetricsDeliverer : IMetricsDeliverer
{
    public Task DeliverAsync(PerformanceMetrics counter)
    {
        Console.WriteLine($"[{DateTimeOffset.UtcNow}]{counter}");
        return Task.CompletedTask;
    }
}

由于整个性能指标的采集工作被分解到四个接口表示的服务之中,所以我们可以采用如下所示的方式重新定义承载服务类型PerformanceMetricsCollector。如代码片段所示,我们在构造函数中注入四个依赖服务,StartAsync方法利用注入的IProcessorMetricsCollector、IMemoryMetricsCollector和INetworkMetricsCollector对象采集对应的性能指标,并利用IMetricsDeliverer对象将其发送出去。

public sealed class PerformanceMetricsCollector : IHostedService
{
    private readonly IProcessorMetricsCollector 	_processorMetricsCollector;
    private readonly IMemoryMetricsCollector 		_memoryMetricsCollector;
    private readonly INetworkMetricsCollector 		_networkMetricsCollector;
    private readonly IMetricsDeliverer 		_MetricsDeliverer;
    private IDisposable? 				_scheduler;

    public PerformanceMetricsCollector(
        IProcessorMetricsCollector processorMetricsCollector,
        IMemoryMetricsCollector memoryMetricsCollector,
        INetworkMetricsCollector networkMetricsCollector,
        IMetricsDeliverer MetricsDeliverer)
    {
        _processorMetricsCollector 	= processorMetricsCollector;
        _memoryMetricsCollector 	= memoryMetricsCollector;
        _networkMetricsCollector 	= networkMetricsCollector;
        _MetricsDeliverer 		= MetricsDeliverer;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
       _scheduler = new Timer(Callback, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
       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;
    }
}

在调用IHostBuilder接口的Build方法将IHost对象构建出来之前,包括承载服务在内的所有服务都可以通过它的ConfigureServices方法进行了注册。修改后的程序启动之后同样会在控制台上看到图14-1所示的输出结果(S1402)。

using App;
var collector = new MetricsCollector();
Host.CreateDefaultBuilder(args)
    .ConfigureServices(svcs => svcs
        .AddHostedService<PerformanceMetricsCollector>()
        .AddSingleton<IProcessorMetricsCollector>(collector)
        .AddSingleton<IMemoryMetricsCollector>(collector)
        .AddSingleton<INetworkMetricsCollector>(collector)
        .AddSingleton<IMetricsDeliverer, MetricsDeliverer>())
    .Build()
    .Run();

[S1403]配置选项的应用

真正的应用开发基本都会使用到配置选项,比如我们演示程序中性能指标采集的时间间隔就应该采用配置选项来指定。由于涉及对性能指标数据的发送,所以最好将发送的目标地址定义在配置选项中。如果有多种传输协议可供选择,就可以定义相应的配置选项。 .NET应用推荐采用Options模式来使用配置选项,所以可以定义如下这个MetricsCollectionOptions类型来承载三种配置选项。

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}";
}

传输协议和目标地址使用在MetricsDeliverer服务中,所以我们对它进行了如下的修改。如代码片段所示,我们在构造函数中利用注入的IOptions<MetricsCollectionOptions>服务来提供上面的两个配置选项。在实现的DeliverAsync方法中,我们将采用的传输协议和目标地址输出到控制台上。

public class MetricsDeliverer : IMetricsDeliverer
{
    private readonly TransportType _transport;
    private readonly Endpoint 	     _deliverTo;

    public MetricsDeliverer(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;
    }
}

承载服务类型PerformanceMetricsCollector同样应该采用这种方式来提取表示性能指标采集频率的配置选项。如下所示的代码片段是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;
    }
}

配置文件配置选项的常用来源,所以我们在根目录下添加了一个名为appsettings.json的配置文件,并在其中定义如下内容来提供上述三个配置选项。由Host类型的CreateDefaultBuilder工厂方法创建的IHostBuilder对象会自动加载这个配置文件。

{
  "MetricsCollection": {
    "CaptureInterval": "00:00:05",
    "Transport": "Udp",
    "DeliverTo": {
      "Host": "192.168.0.1",
      "Port": 3721
    }
  }
}

我们接下来对演示程序做相应的改动。之前针对依赖服务的注册是通过调用IHostBuilder对象的ConfigureServices方法利用作为参数的Action<IServiceCollection>对象完成的,该接口还有一个ConfigureServices方法重载,它的参数类型为Action<HostBuilderContext, IServiceCollection>,作为输入的HostBuilderContext上下文可以提供表示应用配置的IConfiguration对象。

using App;
var collector = new MetricsCollector();
Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, svcs) => svcs
        .AddHostedService<PerformanceMetricsCollector>()
        .AddSingleton<IProcessorMetricsCollector>(collector)
        .AddSingleton<IMemoryMetricsCollector>(collector)
        .AddSingleton<INetworkMetricsCollector>(collector)
        .AddSingleton<IMetricsDeliverer, MetricsDeliverer>()
        .Configure<MetricsCollectionOptions>(context.Configuration.GetSection("MetricsCollection")))
    .Build()
    .Run();

我们利用提供的Action<HostBuilderContext, IServiceCollection>委托通过调用IServiceCollection接口的Configure<TOptions>扩展方法从提供的HostBuilderContext对象中提取出配置,并对MetricsCollectionOptions配置选项做了绑定。我们修改后的程序运行之后在控制台上会输出如图2所示结果。

image
图2 引入配置选项

[S1404]提供针对环境的配置

应用程序总是针对某个具体环境进行部署的,开发(Development)、预发(Staging)和产品(Production)是三种典型的部署环境,这里的部署环境在服务承载系统中统称为承载环境(Hosting Environment)。一般来说,不同的承载环境往往具有不同的配置选项,下面我们将演示如何为不同的承载环境提供相应的配置选项。具体的做法很简单:将共享或者默认的配置定义在基础配置文件(如appsettings.json)中,将差异化的部分定义在针对具体环境的配置文件(如appsettings.staging.json和appsettings.production.json)中。对于我们演示的实例来说,我们可以采用图3所示的方式添加额外的两个配置文件来提供针对预发环境和产品环境的差异化配置。

image
图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.3",
      "Port": 3721
    }
  }
}

由于我们在调用Host的CreateDefaultBuilder方法时传入了命令行参数(args),所以默认创建的IHostBuilder会将其作为配置源。也正因为如此,我们可以采用命令行参数的形式设置当前的承载环境(对应配置名称为“environment”)。如图4所示,我们分别指定不同的承载环境先后四次运行我们的程序,从输出的IP地址可以看出,应用程序确实是根据当前承载环境加载对应的配置文件的。输出结果还体现了另一个细节,那就是默认采用的是产品(Production)环境。

image
图4 针对承载环境加载配置文件

[S1405]日志的应用

应用开发中不可避免地会涉及很多针对“诊断日志”的应用,我们接下来就来演示承载服务如何记录日志。对于我们的演示实例来说,用于发送性能指标的MetricsDeliverer对象会将收集的指标数据输出到控制台上,下面将这段文字以日志的形式进行输出,为此我们将这个类型进行了如下的修改。

public class MetricsDeliverer : IMetricsDeliverer
{
    private readonly TransportType _transport;
    private readonly Endpoint _deliverTo;
    private readonly ILogger _logger;
    private readonly Action<ILogger, DateTimeOffset, PerformanceMetrics, Endpoint, TransportType, Exception?> _logForDelivery;

    public MetricsDeliverer(IOptions<MetricsCollectionOptions> optionsAccessor, ILogger<MetricsDeliverer> 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<MetricsDeliverer>对象并来记录日志。为了避免对同一个消息模板的重复解析,我们可以使用LoggerMessage类型定义的委托对象来输出日志,这也是MetricsDeliverer中采用的编程模式。运行修改后的程序会控制台上的输出如图5所示的结果。由输出结果可以看出,这些文字是由我们注册的ConsoleLoggerProvider提供的ConsoleLogger对象输出到控制台上的。由于承载系统自身在进行服务承载过程中也会输出一些日志,所以它们也会输出到控制台上。

image
图5 将日志输出到控制台上

[S1406]在配置中定义日志过滤规则

如果需要对输出的日志进行过滤,可以将过滤规则定义在配置文件中。为了避免在“产品”环境因输出过多的日志影响性能,我们在appsettings.production.json配置文件中以如下的形式将类别以“Microsoft.”为前缀的日志(最低)等级设置为 Warning。

{
  "MetricsCollection": {
    "DeliverTo": {
      "Host": "192.168.0.3",
      "Port": 3721
    }
  },
  "Logging": {
    "LogLevel": {
     "Microsoft": "Warning"
    }
  }
}

如果此时分别针对开发(Development)环境和产品(Production)环境以命令行的形式启动修改后的应用程序,就会发现针对开发环境控制台会输出类型前缀为“Microsoft.”的日志,但是在针对产品环境的控制台上却找不到它们的踪影。

image
图6 根据承载环境过滤日志

页面下部广告