Azure Blob存储批量并行调用过载问题求助
问题原因分析
前端无限制并发请求过载:
前端通过两层嵌套的Promise.all发起图片上传请求,所有图片请求会瞬间并行发送。当章节和图片数量较多时,短时间内会产生大量请求(你提到的200+次),直接导致后端线程池被占满,CPU使用率飙升;同时数据库请求因线程资源不足排队等待,出现耗时极久的情况。后端不必要的资源重复创建:
- 每次调用
GetUploadUrl时,都重复调用_blobServiceClient.GetBlobContainerClient创建容器客户端,虽然该操作轻量,但高频调用下累计开销不可忽视。 - 每次都实例化
StorageSharedKeyCredential,该对象涉及密钥加密运算,高频创建会大幅增加CPU负载,这是CPU飙升的核心原因之一。
- 每次调用
连接池与线程池耗尽:
大量并发请求耗尽后端HTTP线程池和数据库连接池,即使重启应用,若前端仍发起大量并发请求,会再次触发资源耗尽,导致请求挂起。
优化方案
前端优化:限制并发请求数量
替换无限制的Promise.all,改为带并发数限制的批量处理,避免瞬间压垮后端。示例如下(使用p-limit库,也可自行实现并发控制):
import pLimit from 'p-limit'; // 限制同时最多5个请求并发 const limit = pLimit(5); const processedChapters = await Promise.all( chapters.map(async (chapter) => { const search_language = getSearchLanguage(diplomaSettings.languages); if (chapter.images && chapter.images.length > 0) { // 对图片上传请求添加并发限制 const uploadedImages = await Promise.all( chapter.images.map((imageDataUrl, index) => limit(async () => { if (typeof imageDataUrl === "string" && imageDataUrl.startsWith("data:")) { const imageUrl = await uploadImageToBlobStorage( imageDataUrl, chapter.id, index ); return imageUrl; } return imageDataUrl; })) ); return { ...chapter, search_language, images: uploadedImages.filter((url) => url !== null), }; } return { ...chapter, search_language, }; }) );
后端优化:复用资源+异步化+限流
1. 复用BlobContainerClient和StorageSharedKeyCredential
将容器客户端和密钥凭证改为单例,在服务启动时初始化,避免重复创建:
// 在服务注入时初始化单例 private readonly BlobContainerClient _blobContainerClient; private readonly StorageSharedKeyCredential _storageSharedKeyCredential; // 构造函数注入 public YourBlobService(IConfiguration configuration, BlobServiceClient blobServiceClient) { _imagesContainerName = configuration["ImagesContainerName"]; _accountName = configuration["StorageAccountName"]; _accountKey = configuration["StorageAccountKey"]; // 初始化单例容器客户端 _blobContainerClient = blobServiceClient.GetBlobContainerClient(_imagesContainerName); // 初始化单例密钥凭证 _storageSharedKeyCredential = new StorageSharedKeyCredential(_accountName, _accountKey); } public async Task<string> GetUploadUrlAsync(string fileName) { var uniqueFileName = $"{Path.GetFileNameWithoutExtension(fileName)}-{Guid.NewGuid()}{Path.GetExtension(fileName)}"; // 复用已初始化的容器客户端 var blobClient = _blobContainerClient.GetBlobClient(uniqueFileName); var sasBuilder = new BlobSasBuilder { BlobContainerName = _imagesContainerName, BlobName = uniqueFileName, Resource = "b", ExpiresOn = DateTime.UtcNow.AddMinutes(15) }; sasBuilder.SetPermissions(BlobSasPermissions.Write | BlobSasPermissions.Create | BlobSasPermissions.Read); // 复用密钥凭证 var sasQueryParameters = sasBuilder.ToSasQueryParameters(_storageSharedKeyCredential); var sasUrl = $"{blobClient.Uri}?{sasQueryParameters}"; return sasUrl; }
2. 异步化方法
将同步方法改为异步Task<string>,避免阻塞线程池线程,提升后端处理并发请求的能力。
3. 添加接口限流
在ASP.NET Core中使用内置的Rate Limiting中间件,限制该接口的请求频率,防止瞬间大量请求:
// Program.cs中配置限流 builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("imageUpload", opt => { opt.Window = TimeSpan.FromSeconds(10); opt.PermitLimit = 10; // 10秒内最多允许10个请求 opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; opt.QueueLimit = 5; // 排队最多5个请求 }); }); // 在控制器接口上应用限流 [EnableRateLimiting("imageUpload")] public async Task<IActionResult> GetUploadUrl([FromQuery] string fileName) { var sasUrl = await _blobService.GetUploadUrlAsync(fileName); return Ok(sasUrl); }
内容的提问来源于stack exchange,提问作者Vadym Yesipov
相关产品推荐
相关产品推荐

