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

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() };
}
根因分析
  1. 异步方法同步阻塞导致请求异常:同步Action中调用异步方法UploadFileToTempPath时使用.Result,会阻塞ASP.NET Core的请求上下文,在Azure App Service的IIS托管环境下极易触发线程池饥饿、请求死锁,出现断点不命中、请求被直接跳过的问题;本地环境请求量小、线程资源充足,该问题不会复现。
  2. 多实例负载均衡导致会话丢失:Azure App Service默认是多实例部署,默认内存会话仅存储在当前处理请求的实例上,未开启会话亲和性时,同一个用户的连续请求可能被路由到不同实例,导致读不到之前写入的FOLDERSESSIONID,拿到旧值或空值。
  3. 本地临时目录跨实例不可共享:Path.GetTempPath()对应的是实例本地磁盘目录,不同实例的临时目录完全隔离,且实例重启、替换时临时目录内容会被清空,后续请求切换实例后既读不到之前的会话ID,也找不到之前上传的临时文件。
  4. 资源泄漏放大异常概率:读取上传文件的BinaryReader未使用using释放,高并发场景下会导致流资源无法及时回收,触发请求处理中断。
  5. 隐藏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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 08:25:31