You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 = 60
  • WEBSITE_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 04:09:57