ASP.NET Core 6框架揭秘實例演示[21]:如何承載你的後台服務
- 2022 年 3 月 15 日
- 筆記
- .NET 6, .NET Core, [02] 編程技巧, Asp.Net, Asp.Net Core, ASP.NET Core 6框架揭秘
藉助 .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秒在控制台上輸出「採集」的性能指標。
除了採用一般的服務註冊方式,我們還可以按照如下的方式調用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所示結果。
[S1404]提供針對環境的配置
應用程式總是針對某個具體環境進行部署的,開發(Development)、預發(Staging)和產品(Production)是三種典型的部署環境,這裡的部署環境在服務承載系統中統稱為承載環境(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.3", "Port": 3721 } } }
由於我們在調用Host的CreateDefaultBuilder方法時傳入了命令行參數(args),所以默認創建的IHostBuilder會將其作為配置源。也正因為如此,我們可以採用命令行參數的形式設置當前的承載環境(對應配置名稱為「environment」)。如圖4所示,我們分別指定不同的承載環境先後四次運行我們的程式,從輸出的IP地址可以看出,應用程式確實是根據當前承載環境載入對應的配置文件的。輸出結果還體現了另一個細節,那就是默認採用的是產品(Production)環境。
[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對象輸出到控制台上的。由於承載系統自身在進行服務承載過程中也會輸出一些日誌,所以它們也會輸出到控制台上。
[S1406]在配置中定義日誌過濾規則
如果需要對輸出的日誌進行過濾,可以將過濾規則定義在配置文件中。為了避免在「產品」環境因輸出過多的日誌影響性能,我們在appsettings.production.json配置文件中以如下的形式將類別以「Microsoft.」為前綴的日誌(最低)等級設置為 Warning。
{ "MetricsCollection": { "DeliverTo": { "Host": "192.168.0.3", "Port": 3721 } }, "Logging": { "LogLevel": { "Microsoft": "Warning" } } }
如果此時分別針對開發(Development)環境和產品(Production)環境以命令行的形式啟動修改後的應用程式,就會發現針對開發環境控制台會輸出類型前綴為「Microsoft.」的日誌,但是在針對產品環境的控制台上卻找不到它們的蹤影。