【问题标题】:Graceful shutdown with Generic Host in .NET Core 2.1在 .NET Core 2.1 中使用通用主机正常关闭
【发布时间】:2018-12-05 06:53:11
【问题描述】:

.NET Core 2.1 引入了新的通用主机,它允许托管具有 Web 主机所有优势的非 HTTP 工作负载。目前,没有太多的信息和食谱,但我以以下文章为起点:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice

我的 .NET Core 应用程序启动,通过 RabbitMQ 消息代理侦听新请求并根据用户请求关闭(通常通过控制台中的 Ctrl+C)。然而,关机不是优雅的——应用程序在将控制权返回给操作系统时仍有未完成的后台线程。我通过控制台消息看到它 - 当我在控制台中按 Ctrl+C 时,我看到我的应用程序有几行控制台输出,然后是操作系统命令提示符,然后又是我的应用程序的控制台输出。

这是我的代码:

Program.cs

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = new HostBuilder()
            .ConfigureHostConfiguration(config =>
            {
                config.SetBasePath(AppContext.BaseDirectory);
                config.AddEnvironmentVariables(prefix: "ASPNETCORE_");
                config.AddJsonFile("hostsettings.json", optional: true);
            })
            .ConfigureAppConfiguration((context, config) =>
            {
                var env = context.HostingEnvironment;
                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
                config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
                if (env.IsProduction())
                    config.AddDockerSecrets();
                config.AddEnvironmentVariables();
            })
            .ConfigureServices((context, services) =>
            {
                services.AddLogging();
                services.AddHostedService<WorkerPoolHostedService>();
                // ... other services
            })
            .ConfigureLogging((context, logging) =>
            {
                if (context.HostingEnvironment.IsDevelopment())
                    logging.AddDebug();

                logging.AddSerilog(dispose: true);

                Log.Logger = new LoggerConfiguration()
                    .ReadFrom.Configuration(context.Configuration)
                    .CreateLogger();
            })
            .UseConsoleLifetime()
            .Build();

        await host.RunAsync();
    }
}

WorkerPoolHostedService.cs

internal class WorkerPoolHostedService : IHostedService
{
    private IList<VideoProcessingWorker> _workers;
    private CancellationTokenSource _stoppingCts = new CancellationTokenSource();

    protected WorkerPoolConfiguration WorkerPoolConfiguration { get; }
    protected RabbitMqConfiguration RabbitMqConfiguration { get; }
    protected IServiceProvider ServiceProvider { get; }
    protected ILogger<WorkerPoolHostedService> Logger { get; }

    public WorkerPoolHostedService(
        IConfiguration configuration,
        IServiceProvider serviceProvider,
        ILogger<WorkerPoolHostedService> logger)
    {
        this.WorkerPoolConfiguration = new WorkerPoolConfiguration(configuration);
        this.RabbitMqConfiguration = new RabbitMqConfiguration(configuration);
        this.ServiceProvider = serviceProvider;
        this.Logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var connectionFactory = new ConnectionFactory
        {
            AutomaticRecoveryEnabled = true,
            UserName = this.RabbitMqConfiguration.Username,
            Password = this.RabbitMqConfiguration.Password,
            HostName = this.RabbitMqConfiguration.Hostname,
            Port = this.RabbitMqConfiguration.Port,
            VirtualHost = this.RabbitMqConfiguration.VirtualHost
        };

        _workers = Enumerable.Range(0, this.WorkerPoolConfiguration.WorkerCount)
            .Select(i => new VideoProcessingWorker(
                connectionFactory: connectionFactory,
                serviceScopeFactory: this.ServiceProvider.GetRequiredService<IServiceScopeFactory>(),
                logger: this.ServiceProvider.GetRequiredService<ILogger<VideoProcessingWorker>>(),
                cancellationToken: _stoppingCts.Token))
            .ToList();

        this.Logger.LogInformation("Worker pool started with {0} workers.", this.WorkerPoolConfiguration.WorkerCount);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        this.Logger.LogInformation("Stopping working pool...");

        try
        {
            _stoppingCts.Cancel();
            await Task.WhenAll(_workers.SelectMany(w => w.ActiveTasks).ToArray());
        }
        catch (AggregateException ae)
        {
            ae.Handle((Exception exc) =>
            {
                this.Logger.LogError(exc, "Error while cancelling workers");
                return true;
            });
        }
        finally
        {
            if (_workers != null)
            {
                foreach (var worker in _workers)
                    worker.Dispose();
                _workers = null;
            }
        }
    }
}

VideoProcessingWorker.cs

internal class VideoProcessingWorker : IDisposable
{
    private readonly Guid _id = Guid.NewGuid();
    private bool _disposed = false;

    protected IConnection Connection { get; }
    protected IModel Channel { get; }
    protected IServiceScopeFactory ServiceScopeFactory { get; }
    protected ILogger<VideoProcessingWorker> Logger { get; }
    protected CancellationToken CancellationToken { get; }

    public VideoProcessingWorker(
        IConnectionFactory connectionFactory,
        IServiceScopeFactory serviceScopeFactory,
        ILogger<VideoProcessingWorker> logger,
        CancellationToken cancellationToken)
    {
        this.Connection = connectionFactory.CreateConnection();
        this.Channel = this.Connection.CreateModel();
        this.Channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
        this.ServiceScopeFactory = serviceScopeFactory;
        this.Logger = logger;
        this.CancellationToken = cancellationToken;

        #region [ Declare ]

        // ...

        #endregion

        #region [ Consume ]

        // ...

        #endregion
    }

    // ... worker logic ...

    public void Dispose()
    {
        if (!_disposed)
        {
            this.Channel.Close(200, "Goodbye");
            this.Channel.Dispose();
            this.Connection.Close();
            this.Connection.Dispose();
            this.Logger.LogDebug("Worker {0}: disposed.", _id);
        }
        _disposed = true;
    }
}

所以,当我按下 Ctrl+C 时,我会在控制台中看到以下输出(当没有请求处理时):

正在停止工作池...
命令提示符
工人 id:已处置。

如何优雅关机?

【问题讨论】:

  • 工人听取消令牌吗? // ... worker logic .. 中的代码应该定期检查this.CancellationToken 并在收到信号时退出
  • @PanagiotisKanavos 是的,当然
  • 目前还不清楚您在 VideoProcessingWorker 中究竟对令牌做了什么。您是在检查 IsCancellationRequested 还是正在传递令牌,以便一些任务可以通过抛出 TaskCancelledException 来取消。 StopAsyc 方法可以持续到无限期等待完成,因此您显示的代码看起来是正确的,问题似乎隐藏在未显示的部分中。如果您可以使用可以发布的更简单的代码重现该问题,那就太好了。

标签: c# .net asp.net-core .net-core


【解决方案1】:

您需要IApplicationLifetime。这为您提供了有关应用程序启动和关闭所需的所有信息。您甚至可以通过appLifetime.StopApplication(); 触发关机

https://github.com/aspnet/Docs/blob/66916c2ed3874ed9b000dfd1cab53ef68e84a0f7/aspnetcore/fundamentals/host/generic-host/samples/2.x/GenericHostSample/LifetimeEventsHostedService.cs

片段(如果链接失效):

public Task StartAsync(CancellationToken cancellationToken)
{
    appLifetime.ApplicationStarted.Register(OnStarted);
    appLifetime.ApplicationStopping.Register(OnStopping);
    appLifetime.ApplicationStopped.Register(OnStopped);

    return Task.CompletedTask;
}

【讨论】:

  • 谢谢,我已经阅读了有关此界面及其事件的信息。但不是所有的关闭逻辑都应该在 IHostedService.StopAsync 中吗?主机不应该等待所有托管服务的 StopAsync 方法完成吗?
  • 你是对的,我应该更仔细地阅读你的问题。我认为问题最终可能出在您的尝试中。您是否尝试过在 finally 中等待完成的任务(使用 Task.Delay(Timeout.Infinite, cancelToken))?
  • 不,不幸的是它没有帮助。当我在 finally 部分添加 Task.Delay 并尝试使用 Ctrl+C 停止主机时,它会打印“Stopping working pool”,然后出现命令提示符,然后挂起几秒钟并因 OperationCanceledException 而崩溃。我想当主机不等待正常关机并且即将开始“硬”关机时,StopAsync 方法中的 cancelToken 参数被取消。
  • 对不起,我的回答迟了。我没有收到通知。 Task.Delay 抛出 OperationCanceledException 是预期的行为。只需捕获并处理 OperationCanceledException。
  • 所以我在 OnStopping 中创建了一个断点,在 IISExpress 中选择了“停止网站”,一秒钟后到达断点,又过了 3 秒钟(当我查看调用堆栈时)进程刚刚得到被杀。不好,因为a)我的关机需要几秒钟,b)我必须调用异步代码。
【解决方案2】:

我将分享一些我认为非常适合非 WebHost 项目的模式。

namespace MyNamespace
{
    public class MyService : BackgroundService
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly IApplicationLifetime _appLifetime;

        public MyService(
            IServiceProvider serviceProvider,
            IApplicationLifetime appLifetime)
        {
            _serviceProvider = serviceProvider;
            _appLifetime = appLifetime;
        }

        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _appLifetime.ApplicationStopped.Register(OnStopped);

            return RunAsync(stoppingToken);
        }

        private async Task RunAsync(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                using (var scope = _serviceProvider.CreateScope())
                {
                    var runner = scope.ServiceProvider.GetRequiredService<IMyJobRunner>();
                    await runner.RunAsync();
                }
            }
        }

        public void OnStopped()
        {
            Log.Information("Window will close automatically in 20 seconds.");
            Task.Delay(20000).GetAwaiter().GetResult();
        }
    }
}

关于这门课的几点说明:

  1. 我正在使用 BackgroundService 抽象类来表示我的服务。它在 Microsoft.Extensions.Hosting.Abstractions 包中可用。我相信这是计划在 .NET Core 3.0 中开箱即用的。
  2. ExecuteAsync 方法需要返回一个表示正在运行 服务的任务。 注意:如果您有同步服务,请将“Run”方法包装在 Task.Run() 中。
  3. 如果您想为您的服务进行额外的设置或拆卸,您可以注入应用生命周期服务并挂钩到事件中。我添加了一个在服务完全停止后触发的事件。
  4. 因为您没有像在 MVC 项目中那样为每个 Web 请求创建新范围的自动魔法,所以您必须为范围服务创建自己的范围。将 IServiceProvider 注入到服务中来做到这一点。 所有作用域上的依赖都应该使用 AddScoped() 添加到 DI 容器中。

在 Main(string[] args) 中设置主机,以便在调用 CTRL+C / SIGTERM 时正常关闭:

IHost host = new HostBuilder()
    .ConfigureServices( ( hostContext, services ) =>
    {
        services.AddHostedService<MyService>();
    })
    .UseConsoleLifetime()
    .Build();

host.Run();  // use RunAsync() if you have access to async Main()

我发现这组模式在 ASP.NET 应用程序之外也能很好地工作。

请注意,Microsoft 已针对 .NET Standard 构建,因此您无需使用 .NET Core 即可利用这些新便利。如果您在 Framework 中工作,只需添加相关的 NuGet 包。该包是针对 .NET Standard 2.0 构建的,因此您需要使用 Framework 4.6.1 或更高版本。您可以在此处找到所有基础架构的代码,并随时查看您正在使用的所有抽象的实现:https://github.com/aspnet/Extensions

【讨论】:

【解决方案3】:

Startup.cs中,可以使用当前进程的Kill()方法终止应用:

        public void Configure(IHostApplicationLifetime appLifetime)
        {
            appLifetime.ApplicationStarted.Register(() =>
            {
                Console.WriteLine("Press Ctrl+C to shut down.");
            });

            appLifetime.ApplicationStopped.Register(() =>
            {
                Console.WriteLine("Shutting down...");
                System.Diagnostics.Process.GetCurrentProcess().Kill();
            });
        }

Program.cs

在构建主机时不要忘记使用UseConsoleLifetime()

Host.CreateDefaultBuilder(args).UseConsoleLifetime(opts => opts.SuppressStatusMessages = true);

【讨论】:

    猜你喜欢
    • 2019-04-13
    • 2017-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多