.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
相关产品推荐
相关产品推荐

