1. 程式人生 > >[ASP.NET Core 3框架揭祕] 服務承載系統[2]: 承載長時間執行的服務[下篇]

[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();
    }
}
如果此時分別針對開發(Development)環境和產品(Production)環境以命令列的形式啟動修改後的應用程式,就會發現針對開發環境控制檯上會輸出型別字首為“Microsoft.”的日誌,但是針對產品環境的控制檯上卻找不到它們的蹤影。(原始碼從這裡下載)

服務承載系統[1]: 承載長時間執行的服務[上篇]
服務承載系統[2]: 承載長時間執行的服務[下篇]
服務承載系統[3]: 服務承載模型[上篇]
服務承載系統[4]: 服務承載模型[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程