ASP.NET Core使用IHostedService时延长Azure App Service关闭超时
问题:Azure App Service中ASP.NET Core BackgroundService优雅终止失败
场景与目标
- 基于ASP.NET Core的应用,使用
BackgroundService(IHostedService)从Azure Service Bus Queue读取消息 - 部分消息处理耗时可达45秒
- 部署在Azure App Service中
- 目标:部署、槽交换或停止App Service时,让正在处理的消息完成后再关闭应用,避免突然失败
实际问题
本地运行时按Ctrl + C,应用会等待消息处理完成后再关闭,行为正常;但部署到Azure App Service后,应用会在5-15秒内被强制关闭,StopAsync()方法无法完成:
await _serviceBusProcessor.StopProcessingAsync()一直阻塞,StopAsync()的日志无法记录- 正在执行的
ProcessMessageAsync()(比如20次循环的模拟处理)被中断,无法完成
已尝试的配置
Program.cs设置
public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Host.ConfigureHostOptions(options => options.ShutdownTimeout = TimeSpan.FromSeconds(60)); builder.WebHost.UseShutdownTimeout(TimeSpan.FromSeconds(60)); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddHostedService<ServiceBusHostedService>(); var app = builder.Build(); app.UseStaticFiles(); app.UseSwagger(); app.UseSwaggerUI(options => { options.EnableTryItOutByDefault(); }); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); }
BackgroundService代码
using Azure.Identity; using Azure.Messaging.ServiceBus; namespace MyApp.API.ServiceBus { public class ServiceBusHostedService : BackgroundService { private readonly ILogger<ServiceBusHostedService> _logger; private readonly ServiceBusClient _serviceBusClient; private readonly ServiceBusProcessor _serviceBusProcessor; public ServiceBusHostedService( ILogger<ServiceBusHostedService> logger, ServiceBusConfiguration serviceBusConfiguration) { _logger = logger; _serviceBusClient = new ServiceBusClient(serviceBusConfiguration.Namespace, new DefaultAzureCredential()); _serviceBusProcessor = _serviceBusClient.CreateProcessor(serviceBusConfiguration.WorkQueueName, new ServiceBusProcessorOptions { MaxConcurrentCalls = serviceBusConfiguration.NumberOfThreads, AutoCompleteMessages = false, }); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("ServiceBusHostedService started"); _serviceBusProcessor.ProcessMessageAsync += ProcessMessageAsync; _serviceBusProcessor.ProcessErrorAsync += ProcessErrorAsync; await _serviceBusProcessor.StartProcessingAsync(stoppingToken); await Task.Delay(Timeout.Infinite, stoppingToken); } private async Task ProcessMessageAsync(ProcessMessageEventArgs args) { var message = args.Message; var sequenceNumber = message.SequenceNumber.ToString(); try { var messageTypeExists = message.ApplicationProperties.TryGetValue("MessageType", out var messageType); // 模拟20秒的消息处理 for (var i = 0; i < 20; i++) { _logger.LogInformation($"Sleeping... i = {i}"); Thread.Sleep(1000); } _logger.LogWarning(message.Body.ToString()); await args.CompleteMessageAsync(message); } catch (Exception ex) { _logger.LogError(ex, $"Error while processing message with sequence number {sequenceNumber} for {args.FullyQualifiedNamespace} and entity path {args.EntityPath}"); try { await args.AbandonMessageAsync(args.Message); } catch (Exception abandonEx) { _logger.LogCritical(abandonEx, "Failed to abandon message"); } } } private Task ProcessErrorAsync(ProcessErrorEventArgs args) { _logger.LogError($"Error in Service Bus Processor for {args.FullyQualifiedNamespace} at {args.ErrorSource} for entity path {args.EntityPath}: {args.Exception}"); return Task.CompletedTask; } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation($"{nameof(ServiceBusHostedService)} is stopping"); await _serviceBusProcessor.StopProcessingAsync(); // 等待正在处理的消息完成 while (_serviceBusProcessor.IsProcessing) { _logger.LogInformation("Waiting for ongoing message processing to complete.."); if (cancellationToken.IsCancellationRequested) { _logger.LogWarning("Shutdown was forcibly canceled"); break; } await Task.Delay(1000, cancellationToken); } _logger.LogInformation("All message processing completed. Cleaning up resources"); // 释放资源 await _serviceBusProcessor.DisposeAsync(); await _serviceBusClient.DisposeAsync(); await base.StopAsync(cancellationToken); _logger.LogInformation($"{nameof(ServiceBusHostedService)} stopped gracefully"); } } }
App Service环境变量设置
ASPNETCORE_SHUTDOWNTIMEOUTSECONDS= 60WEBSITE_WAIT_FOR_SHUTDOWN= 60
解决方案
1. 修正WEBSITE_WAIT_FOR_SHUTDOWN配置
WEBSITE_WAIT_FOR_SHUTDOWN是布尔型变量,正确值为1,而非数字60。设置为1后,App Service会等待应用完成优雅关闭逻辑,而非立即终止进程。
2. 调整关闭超时并传递取消令牌
将应用关闭超时设置为覆盖最长消息处理时间(比如90秒),同时在StopProcessingAsync中传入取消令牌,确保方法能响应关闭信号:
修改Program.cs超时配置
builder.Host.ConfigureHostOptions(options => options.ShutdownTimeout = TimeSpan.FromSeconds(90)); builder.WebHost.UseShutdownTimeout(TimeSpan.FromSeconds(90));
修改StopAsync方法
public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation($"{nameof(ServiceBusHostedService)} is stopping"); // 传入取消令牌,让StopProcessingAsync响应关闭信号 await _serviceBusProcessor.StopProcessingAsync(cancellationToken); // 等待消息处理完成,同时监听超时信号 while (!cancellationToken.IsCancellationRequested && _serviceBusProcessor.IsProcessing) { _logger.LogInformation("Waiting for ongoing message processing to complete.."); await Task.Delay(500, cancellationToken); } if (cancellationToken.IsCancellationRequested) { _logger.LogWarning("Shutdown timeout reached, forcing cleanup"); } _logger.LogInformation("Cleaning up resources"); await _serviceBusProcessor.DisposeAsync(); await _serviceBusClient.DisposeAsync(); await base.StopAsync(cancellationToken); _logger.LogInformation($"{nameof(ServiceBusHostedService)} stopped"); }
3. 替换阻塞调用为异步等待
ProcessMessageAsync中的Thread.Sleep是阻塞调用,会占用线程池线程并影响关闭响应性,替换为异步等待并监听取消令牌:
// 替换Thread.Sleep(1000)为 await Task.Delay(1000, args.CancellationToken); // 处理过程中监听取消信号(根据业务需求决定是否允许中断) for (var i = 0; i < 20; i++) { args.CancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation($"Processing step {i}"); await Task.Delay(1000, args.CancellationToken); }
4. 配置App Service优雅关闭超时
在Azure Portal的App Service配置中:
- 进入配置 > 常规设置
- 设置优雅关闭超时为90秒(或覆盖最长处理时间的值),该设置会覆盖默认的关闭等待时长
5. 验证日志流程
通过App Service日志流确认StopAsync日志是否正常输出,消息处理是否能完成。若仍出现强制关闭,排查是否有其他组件抢占关闭信号,或存在未处理的异步操作阻塞关闭流程。
内容的提问来源于stack exchange,提问作者chrisbuttacavoli
相关产品推荐
相关产品推荐

