[ASP.NET Core 3框架揭秘] 服务承载系统[1]: 承载长时间运行的服务[上篇]

借助.NET Core提供的承载(Hosting)系统,我们可以将任意一个或者多个长时间运行(Long-Running)的服务寄宿或者承载于托管进程中。ASP.NET Core应用仅仅是该承载系统的一种典型的服务类型而已,任何需要在后台长时间运行的操作都可以定义成标准化的服务并利用该系统来承载。

一、承载长时间运行服务

一个ASP.NET Core应用本质上是一个需要长时间运行的服务,开启这个服务是为了启动一个网络监听器。当监听到抵达的HTTP请求之后,该监听器会将请求传递给应用提供的管道进行处理。管道完成了对请求处理之后会生成HTTP响应,并通过监听器返回客户端。除了这种最典型的承载服务,我们还有很多其他的服务承载需求,下面通过一个简单的实例来演示如何承载一个服务来收集当前执行环境的性能指标

我们演示的承载服务会定时采集并分发当前进程的性能指标。简单起见,我们只关注处理器使用率、内存使用量和网络吞吐量这3种典型的性能指标,为此定义了下面的PerformanceMetrics类型。我们并不会实现真正的性能指标收集,所以定义静态方法Create利用随机生成的指标数据创建一个PerformanceMetrics对象。

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

  1. public sealed class PerformanceMetricsCollector : IHostedService
  2. {
  3. private IDisposable _scheduler;
  4. public Task StartAsync(CancellationToken cancellationToken)
  5. {
  6. _scheduler = new Timer(Callback, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
  7. return Task.CompletedTask;
  8.  
  9. static void Callback(object state)
  10. {
  11. Console.WriteLine($"[{DateTimeOffset.Now}]{PerformanceMetrics.Create()}");
  12. }
  13. }
  14.  
  15. public Task StopAsync(CancellationToken cancellationToken)
  16. {
  17. _scheduler?.Dispose();
  18. return Task.CompletedTask;
  19. }
  20. }

在调用Build方法之前,可以调用IHostBuilder接口的ConfigureServices方法将PerformancceMetricsCollector注册成针对IHostedService接口的服务,并将生命周期模式设置成Singleton。除了采用普通的依赖服务注册方式,针对IHostedService服务的注册还可以调用IServiceCollection接口的AddHostedService<THostedService>扩展方法来完成,如下所示的编程方式与上面是完全等效的。

  1. class Program
  2. static void Main()
  3. {
  4. new HostBuilder()
  5. .ConfigureServices(svcs => svcs.AddHostedService<PerformanceMetricsCollector>())
  6. .Build()
  7. .Run();
  8. }
  9. }

最后调用Run方法启动通过IHost对象表示的承载服务宿主,进而启动由它承载的PerformancceMetricsCollector服务,该服务将以下图所示的形式每隔5秒显示由它“采集”的性能指标。(源代码从下载)

二、依赖注入

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

  1. public class FakeMetricsCollector :
  2. IProcessorMetricsCollector,
  3. IMemoryMetricsCollector,
  4. INetworkMetricsCollector
  5. {
  6. long INetworkMetricsCollector.GetThroughput()
  7. => PerformanceMetrics.Create().Network;
  8.  
  9. int IProcessorMetricsCollector.GetUsage()
  10. => PerformanceMetrics.Create().Processor;
  11.  
  12. long IMemoryMetricsCollector.GetUsage()
  13. => PerformanceMetrics.Create().Memory;
  14. }
  15.  
  16. {
  17. public Task DeliverAsync(PerformanceMetrics counter)
  18. {
  19. Console.WriteLine($"[{DateTimeOffset.UtcNow}]{counter}");
  20. return Task.CompletedTask;
  21. }
  22. }

由于整个性能指标的采集工作被分解到4个接口表示的服务之中,所以可以采用如下所示的方式重新定义承载服务类型PerformanceMetricsCollector。如下面的代码片段所示,可以直接在构造函数中注入4个依赖服务。对于在StartAsync方法创建的调用器来说,它会利用3个对应的服务采集3种类型的性能指标,并利用IMetricsDeliverer服务将其发送出去。

在调用IHostBuilder接口的Build方法创建作为宿主的IHost对象之前,包括承载服务在内的所有服务都可以通过它的ConfigureServices方法进行注册,我们采用如下方式注册了作为承载服务的PerformanceMetricsCollector和它依赖的4个服务。修改后的程序启动之后同样会在控制台上看到上面图片所示的输出结果。(源代码从这里下载)

  1. class Program
  2. {
  3. static void Main()
  4. {
  5. var collector = new FakeMetricsCollector();
  6. new HostBuilder()
  7. .ConfigureServices(svcs => svcs
  8. .AddSingleton<IProcessorMetricsCollector>(collector)
  9. .AddSingleton<IMemoryMetricsCollector>(collector)
  10. .AddSingleton<INetworkMetricsCollector>(collector)
  11. .AddSingleton<IMetricsDeliverer, FakeMetricsDeliverer>()
  12. .AddSingleton<IHostedService, PerformanceMetricsCollector>())
  13. .Build()
  14. .Run();
  15. }
  16. }