ASP.NET Core 6部署Azure App Service后控制器隔次调用不命中问题
问题背景
- 实现了名为
AddDocumentsToTempFolder的控制器接口,支持单份/多份文件存入临时文件夹,供后续统一保存选中文件使用,功能在本地运行环境完全正常 - 项目发布到Azure App Service后,接口出现每2次调用就有1次被跳过的异常:系统会创建携带新会话ID的临时文件夹,但下次新增文件时会读取到上一次的会话ID,最终表单提交时获取到错误ID,无法保存全部已上传文件
- Visual Studio远程调试确认:返回错误会话ID时,控制器内设置的断点不会命中;由于接口调用被跳过,后续保存文件时
Request.Form.Files会缺失部分已上传文件
问题代码
[HttpPost] public IActionResult AddDocumentsToTempFolder(IList<IFormFile> files, BoardType targetBoardType) { List<Document> DocList = new(); Document doc = default; PageType targetedController = new(); if (string.IsNullOrEmpty(_httpContextAccessor.HttpContext.Session.GetString(PredefineMessages.FOLDERSESSIONID))) _httpContextAccessor.HttpContext.Session.SetString(PredefineMessages.FOLDERSESSIONID, _httpContextAccessor.HttpContext.Session.Id); foreach (IFormFile file in files) { if (file.Length > 0) { BinaryReader reader = new(file.OpenReadStream()); var data = reader.ReadBytes((int)file.Length); DocList.Add(new Document() { Name = file.FileName, ContentType = file.ContentType, FileContent = data, DocId = new Guid(_httpContextAccessor.HttpContext.Session.GetString(PredefineMessages.FOLDERSESSIONID)) }); switch (targetBoardType) { case BoardType.Topic: targetedController = PageType.Board; break; case BoardType.Task: targetedController = PageType.Board; break; case BoardType.Decision: targetedController = PageType.Board; break; case BoardType.Information: targetedController = PageType.Board; break; case BoardType.Project: targetedController = PageType.Project; break; case BoardType.Meeting: targetedController = PageType.Meetings; break; } doc = _commonHelper.UploadFileToTempPath(file, Path.Combine(Path.GetTempPath(), _httpContextAccessor.HttpContext.Session.GetString(PredefineMessages.FOLDERSESSIONID), targetBoardType.ToString())).Result; var tempFile = DocList.FirstOrDefault(x => x.FilePath is null); tempFile.FilePath = doc.FilePath; tempFile.Id = doc.Id; } } var fileData = new FileViewModel { Documents = DocList, TargetedController = targetedController, BoardType = targetBoardType }; return PartialView(PredefineMessages.DOCUMENTLISTPARTIALVIEWSTRING, fileData); } public async Task<Document> UploadFileToTempPath(IFormFile file, string tempFolderPath) { var fileName = string.Format("{0}{1}{2}", file.FileName, "_", _random.Next(0, 1000)); string filePath = Path.GetFullPath(Path.Combine(tempFolderPath, fileName)); if (!Directory.Exists(tempFolderPath)) Directory.CreateDirectory(tempFolderPath); using Stream fileStream = new FileStream(filePath, FileMode.Create); await file.CopyToAsync(fileStream); return new Document { FilePath = filePath, Id = fileName.Split('_').LastOrDefault() }; }
根因分析
- 异步方法同步阻塞导致请求异常:同步Action中调用异步方法
UploadFileToTempPath时使用.Result,会阻塞ASP.NET Core的请求上下文,在Azure App Service的IIS托管环境下极易触发线程池饥饿、请求死锁,出现断点不命中、请求被直接跳过的问题;本地环境请求量小、线程资源充足,该问题不会复现。 - 多实例负载均衡导致会话丢失:Azure App Service默认是多实例部署,默认内存会话仅存储在当前处理请求的实例上,未开启会话亲和性时,同一个用户的连续请求可能被路由到不同实例,导致读不到之前写入的
FOLDERSESSIONID,拿到旧值或空值。 - 本地临时目录跨实例不可共享:
Path.GetTempPath()对应的是实例本地磁盘目录,不同实例的临时目录完全隔离,且实例重启、替换时临时目录内容会被清空,后续请求切换实例后既读不到之前的会话ID,也找不到之前上传的临时文件。 - 资源泄漏放大异常概率:读取上传文件的
BinaryReader未使用using释放,高并发场景下会导致流资源无法及时回收,触发请求处理中断。 - 隐藏bug:Random实例线程不安全:如果
_random不是静态全局实例,多线程并发调用时会生成重复随机数,导致临时文件名冲突、文件覆盖。
修复方案
1. 修正异步代码写法,移除阻塞调用
将Action改为全异步实现,用await代替.Result,统一释放流资源,修复空引用风险:
// 全局Random改为静态只读,保证线程安全 private static readonly Random _random = new Random(); [HttpPost] public async Task<IActionResult> AddDocumentsToTempFolder(IList<IFormFile> files, BoardType targetBoardType) { List<Document> DocList = new(); Document doc = default; PageType targetedController = new(); var session = _httpContextAccessor.HttpContext.Session; // 一次性读取会话ID,避免多次访问HttpContext出现上下文丢失 string folderSessionId = session.GetString(PredefineMessages.FOLDERSESSIONID); if (string.IsNullOrEmpty(folderSessionId)) { folderSessionId = session.Id; session.SetString(PredefineMessages.FOLDERSESSIONID, folderSessionId); } foreach (IFormFile file in files) { if (file.Length <= 0) continue; // 用using释放流资源 using var reader = new BinaryReader(file.OpenReadStream()); var data = reader.ReadBytes((int)file.Length); DocList.Add(new Document() { Name = file.FileName, ContentType = file.ContentType, FileContent = data, DocId = new Guid(folderSessionId) }); // 合并重复switch分支 targetedController = targetBoardType switch { BoardType.Topic or BoardType.Task or BoardType.Decision or BoardType.Information => PageType.Board, BoardType.Project => PageType.Project, BoardType.Meeting => PageType.Meetings, _ => targetedController }; string tempFolder = Path.Combine(Path.GetTempPath(), folderSessionId, targetBoardType.ToString()); // await异步调用,避免同步阻塞 doc = await _commonHelper.UploadFileToTempPath(file, tempFolder); var tempFile = DocList.FirstOrDefault(x => x.FilePath is null); // 增加空判断避免空引用异常 if (tempFile != null) { tempFile.FilePath = doc.FilePath; tempFile.Id = doc.Id; } } var fileData = new FileViewModel { Documents = DocList, TargetedController = targetedController, BoardType = targetBoardType }; return PartialView(PredefineMessages.DOCUMENTLISTPARTIALVIEWSTRING, fileData); } public async Task<Document> UploadFileToTempPath(IFormFile file, string tempFolderPath) { var fileName = $"{file.FileName}_{_random.Next(0, 1000)}"; string filePath = Path.GetFullPath(Path.Combine(tempFolderPath, fileName)); if (!Directory.Exists(tempFolderPath)) Directory.CreateDirectory(tempFolderPath); using Stream fileStream = new FileStream(filePath, FileMode.Create); await file.CopyToAsync(fileStream); return new Document { FilePath = filePath, Id = fileName.Split('_').LastOrDefault() }; }
2. 临时快速修复:开启会话亲和性
进入Azure Portal对应App Service的配置页,在「常规设置」中开启ARR亲和性,负载均衡会将同一用户的所有请求固定路由到同一个实例,解决多实例下的会话和本地文件访问问题,适合快速验证修复。
3. 长期稳定方案:替换临时存储+分布式会话
- 不要用实例本地临时目录存储业务临时文件,改用Blob存储的专用临时容器存储上传文件,会话中仅存储文件对应的Blob标识,保证任意实例都能访问到已上传文件,也不会因为实例重启丢失文件。
- 如果需要跨实例共享会话数据,替换默认内存会话为Redis等分布式会话存储,保证所有实例读取到的会话数据一致。
内容的提问来源于stack exchange,提问作者Andrej
相关产品推荐
相关产品推荐

