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

.NET 6迁移后Azure App Service中Task.Run执行失败求助

问题

我正在将Web应用从.NET Core 2.2迁移至.NET 6,其中一段用Task.Run实现“即发即忘”的代码(用户点击页面按钮后,在VM上运行长时间PowerShell脚本)本地运行正常,日志完整,但部署到Azure App Service后,Task.Run内部的代码完全不执行,Application Insights仅记录到Task.Run调用前的内容。

已排查确认.NET版本间VM控制逻辑无差异;简化为循环日志测试后问题依旧:本地日志正常,Azure端无Task.Run内的日志。直接调用方法而非用Task.Run包裹时,代码在Azure上可正常运行。因任务耗时较长,希望保留异步执行方式,求问为何Task.Run在Azure App Service中无法执行?

相关代码示例

GenerateUsingTask方法

public IActionResult GenerateUsingTask(pageViewModel vm, string id)
{
    var virtualMachine = _configService.GetVm(id);
    if (virtualMachine == null)
    {
        return BadRequest();
    }
    
    _configService.SwitchOn(virtualMachine, User.Identity.Name, vm.Static).Wait();
    _logger.LogInformation("Going into RunTestTask");

    Task.Run(()=>_configService.RunTestTask(virtualMachine,User.Identity.Name));
    _logger.LogInformation("After RunTestTask");
    
    return Ok();
}

RunTestTask方法

public void RunTestTask(theVm vm, string user)
{
    _logger.LogInformation("Into test task");

    for (int i = 0; i < 100; i++)
    {
        if (i % 10 == 0) { 
            _logger.LogInformation("Logging " + i);
        }
    }
    _logger.LogInformation("Out of test task");
}

Generate方法

public IActionResult Generate(PageViewModel vm, string id)
{
    
    var virtualMachine = _configService.GetVm(id);
    if (virtualMachine == null)
    {
        return BadRequest();
    }
    
    _configService.SwitchOn(virtualMachine, User.Identity.Name, vm.Static).Wait();
    _logger.LogInformation("Going into RunScripts");

    try
    {
        Task.Run(()=>_configService.RunScripts(virtualMachine, User.Identity.Name, vm.Static, vm.Pending));
    }
    catch (Exception ex)
    {
        _logger.LogCritical("Error in Generate - message" + ex.Message);
        _logger.LogCritical("Error in Generate - stacktrace" + ex.StackTrace);
        throw;
    }

    return Ok();
}

RunScripts方法

public void RunScripts(ScriptingVm vm, string user, bool moduleDataSets, bool edits)
{
    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    var outputFolder = Environment.GetEnvironmentVariable("Scripts_OUTPUT_FOLDER");

    _logger.LogInformation("Environment is " + env);

    //simple powershell test
    //string sDate = DateTime.UtcNow.ToString("yyyy-MM-dd_HHmmss");
    //_logger.LogCritical("Want to create folder on " + vm.Id + " - " + sDate);
    //string folderName = "C:\\temp\\Test_" + sDate;
    //var result = _vmService.RunPowerShellScript(vm.Id,
    //    new List<string> { "param([string]$folderName) New-Item -ItemType Directory -Path $folderName" }, new List<RunCommandInputParameter>() { new RunCommandInputParameter("folderName", folderName) });
    //simple powershell test

    _logger.LogInformation("server :: " + Environment.GetEnvironmentVariable("Scripts_SERVER"));
    _logger.LogInformation("username :: " + Environment.GetEnvironmentVariable("Scripts_DB_USER"));
    //...logs more environment variables...
    try
    {
        var result = _vmService.RunPowerShellScript(vm.Id, new List<string>
        {
            "param(",
            "    [string]$server,",
            "    [string]$login,",
            //...more variables here...
            ")",
            $@"Set-Location {outputFolder}",
            @"& 'C:\Program Files\Git\bin\git.exe' pull $gitCredential",
            @"& 'C:\Program Files\Git\bin\git.exe' checkout $sourceBranch",
            @"& 'C:\Program Files\Git\bin\git.exe' checkout -B $branch"
        }.Concat((moduleDataSets ? DeleteFolders.Concat(ModuleDataSetDeleteFolders) : DeleteFolders)
            .Select(f => $"Remove-Item -path \"Config\\{f}\" -recurse")
            ).Concat(new List<string> {
            @"& 'C:\Python\Python36\python.exe' scripts\configscripts\master.py " + (moduleDataSets ? "-m " : "") + "-o $output -l $login -s $server, " +
                "-d $database -u $username -p $password -n $clientName -a $clientSecret -t $tenant -r $url",
            @"& 'C:\Program Files\Git\bin\git.exe' config user.email $gitEmail",
            @"& 'C:\Program Files\Git\bin\git.exe' config user.name $gitUser",
            @"& 'C:\Program Files\Git\bin\git.exe' add -A",
            @"& 'C:\Program Files\Git\bin\git.exe' commit -m $commitMsg",
            @"& 'C:\Program Files\Git\bin\git.exe' push $remote $branch --force",
            @"& 'C:\Program Files\Git\bin\git.exe' checkout $sourceBranch",
        }).ToList(),
        new List<RunCommandInputParameter>
        {
            new RunCommandInputParameter("server", Environment.GetEnvironmentVariable("Scripts_SERVER")),
            new RunCommandInputParameter("username", Environment.GetEnvironmentVariable("Scripts_DB_USER")),
            //...more variables here...
        });

        if (result.HasValue)
        {
            //what to report to user
            string loopOutput = "";

            foreach (var s in result.Value.Value)
            {
                loopOutput += s.Message + "/n";

            }
            _logger.LogInformation(loopOutput);

            //reported to user via database
            var report = new ScriptingReport
            {
                Date = DateTime.Now,
                User = user,
                Report = loopOutput
            };
            _repository.Create(report);

            vm.LastReportId = report.Id;
        }

    }
    catch (Exception ex)
    {
        _logger.LogCritical($"Error: {ex.Message}");

        var report = new ScriptingReport
        {
            Date = DateTime.Now,
            User = user,
            Report = ex.Message + "\n" + ex.StackTrace.ToString()
        };
        _repository.Create(report);
        vm.LastReportId = report.Id;
    }
    finally
    {
        //vm object has been updated with newly inserted LastReportId value to link vm to results

        SwitchOff(vm);
    }
}

RunPowerShellScript方法

public ArmOperation<VirtualMachineRunCommandResult> RunPowerShellScript(string virtualMachineName, List<string> commands, List<RunCommandInputParameter> parameters)
{
    
    //used to be a single line (.Net Core 2)
    //return _azure.VirtualMachines.RunPowerShellScript(ResourceGroupName, virtualMachineName, commands, parameters);
    SubscriptionResource subsRes = _armClient.GetDefaultSubscription();
    ResourceGroupResource resGroup = subsRes.GetResourceGroup(ResourceGroupName);
    VirtualMachineCollection vmColl = resGroup.GetVirtualMachines();

    if (vmColl.Exists(virtualMachineName))
    {
        VirtualMachineResource vmR = vmColl.Get(virtualMachineName);
        RunCommandInput runCommInput = new RunCommandInput("RunPowerShellScript");

        //Script is readonly so can't directly pass in
        //pass in command
        string commandsString = "";
        foreach (var command in commands)
        {
            runCommInput.Script.Add(command);
            commandsString += command + ": " + "\n";

        }
        
        _logger.LogInformation(string.Format("Command list: {0}", commandsString));

        //pass in commmand parameters
        string paramsString = "";
        foreach (var parameter in parameters)
        {
            runCommInput.Parameters.Add(parameter);
            paramsString += parameter.Name + ": " + parameter.Value.ToString() + "\n";
        }
        _logger.LogInformation(string.Format("Parameter list: {0}", paramsString));

        var res = vmR.RunCommand(Azure.WaitUntil.Completed, runCommInput);
        return res;
    }
    else
    {
        return null;
    };
}
原因分析
  • Azure App Service请求生命周期限制:当HTTP请求结束(Controller返回Ok())后,App Service宿主环境会认为该请求关联的资源可回收,包括Task.Run启动的后台线程。.NET 6的GenericHost相比.NET Core 2.2的WebHost,对请求结束后的资源回收逻辑更严格,未绑定到请求上下文的“孤儿任务”会被直接终止。
  • Task.Run无生命周期托管:直接用Task.Run启动的任务不属于ASP.NET Core框架托管范畴,框架不会跟踪其执行状态,请求结束后这些任务失去运行依托,极易被宿主环境终止。
  • Azure平台资源回收机制:App Service存在自动回收规则,若后台任务耗时过长、应用处于空闲状态,或平台进行维护操作,进程可能被强制回收,导致任务中断。
解决方案

针对长时间后台任务,推荐以下几种可靠实现方式:

1. 使用ASP.NET Core官方后台任务托管(IHostedService/BackgroundService)

这是.NET Core/.NET 6官方推荐的后台任务管理方案,框架会负责任务的生命周期管控,即使请求结束,后台任务仍能持续执行。

实现步骤:

1.1 创建任务队列类

用于传递待执行的后台任务:

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
    Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, Task>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options);
    }

    public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _queue.Writer.TryWrite(workItem);
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);
        return workItem;
    }
}

1.2 创建后台服务类

持续监听并执行队列中的任务:

public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;
    private readonly IBackgroundTaskQueue _taskQueue;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
    {
        _taskQueue = taskQueue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued hosted service is starting.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred executing background task.");
            }
        }

        _logger.LogInformation("Queued hosted service is stopping.");
    }
}

1.3 注册服务到Program.cs

builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(capacity: 100));
builder.Services.AddHostedService<QueuedHostedService>();

1.4 修改Controller代码,将任务加入队列

public IActionResult GenerateUsingTask(pageViewModel vm, string id, IBackgroundTaskQueue taskQueue)
{
    var virtualMachine = _configService.GetVm(id);
    if (virtualMachine == null)
    {
        return BadRequest();
    }
    
    _configService.SwitchOn(virtualMachine, User.Identity.Name, vm.Static).Wait();
    _logger.LogInformation("Going into RunTestTask");

    // 将任务加入后台队列,由托管服务执行
    taskQueue.QueueBackgroundWorkItem(async token =>
    {
        _configService.RunTestTask(virtualMachine, User.Identity.Name);
    });
    
    _logger.LogInformation("After RunTestTask");
    
    return Ok();
}

2. 使用Azure专用服务处理长时间任务

如果任务耗时超过30分钟,建议将任务逻辑迁移到Azure Functions或Azure Logic Apps,App Service仅负责触发任务,具体执行逻辑由专用服务处理。这种方式完全规避App Service的回收限制,是长时间任务的最优解。

3. 临时测试方案(不推荐用于生产)

若仅需临时验证,可在Azure Portal调整App Service配置:

  • 开启“始终开启”功能(会增加成本)
  • 将“空闲超时”调整至最大值230分钟
  • 关闭“自动回收”规则

但该方案不可靠,平台仍可能因资源压力或维护操作回收进程,仅适合短期测试。

内容的提问来源于stack exchange,提问作者Pete

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 13:42:37