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

ASP.NET Core上传大文件至Azure Blob Storage时如何容忍网络错误?

大文件分块上传至Azure Blob Storage(断点续传/部分内容保留)优化方案

需求背景

我有一个ASP.NET Core应用,通过API接收MP4文件上传,需要从输入流中分块读取并实时写入Azure Blob Storage。文件可能极大,要求网络出错时至少保留已写入的内容——但使用标准BlobClient.UploadAsync(stream)方法时,哪怕上传到99%失败,最终Blob中也只会保留0字节。我用本地文件做了概念验证,以下代码能实现需求,但想了解是否有更优方案:

var blobUriWithSas = new Uri($"{storageAccountUrl}/?{sasToken}");
var blobServiceClient = new BlobServiceClient(blobUriWithSas);
var blobContainerClient = blobServiceClient.GetBlobContainerClient("container");
var blockBlobClient = blobContainerClient.GetBlockBlobClient("file.mp4");

var blockIds = new List<string>();

using var fileStream = File.OpenRead("file.mp4");
var buffer = new byte[2048*10];
int bytesRead;
int blockId = 0;

while ((bytesRead = await fileStream.ReadAsync(buffer)) > 0)
{
    var blockIdBase64 = Convert.ToBase64String(BitConverter.GetBytes(blockId++));
    blockIds.Add(blockIdBase64);

    using var memoryStream = new MemoryStream(buffer, 0, bytesRead);

    // Upload the block
    await blockBlobClient.StageBlockAsync(blockIdBase64, memoryStream);

    if (blockId % 50 == 0)
    {
        // Commit the blocks now and then
        await blockBlobClient.CommitBlockListAsync(blockIds);
    }
}

// Commit the blocks when all is done
await blockBlobClient.CommitBlockListAsync(blockIds);

现有方案分析与优化建议

你的核心思路(手动分块上传+定期提交块列表)完全符合Azure Blob分块存储的设计逻辑,能满足"失败时保留已上传内容"的核心需求。以下是几个关键优化方向:

1. 块ID生成规范化

当前用BitConverter.GetBytes(blockId++)生成块ID,当blockId超过单字节范围后,字节长度会变化,虽然Base64编码后不影响唯一性,但会导致块ID长度不一致,可能影响后续Commit时的排序可靠性。建议使用固定长度的字符串格式生成块ID:

// 用6位补零的序号生成块ID,转Base64后长度固定
var blockIdBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(blockId.ToString("D6")));
blockId++;

2. 减少内存拷贝,优化性能

当前代码将读取到的buffer转存到MemoryStream再上传,存在额外的内存分配和拷贝。可以直接使用StageBlockAsync的ReadOnlyMemory<byte>重载,省去中间流:

// 无需创建MemoryStream,直接传递buffer片段
await blockBlobClient.StageBlockAsync(blockIdBase64, new Memory<byte>(buffer, 0, bytesRead));

3. 调整Commit频率,平衡性能与可靠性

Azure Blob允许最多保留50000个未提交的块,每50块Commit一次的频率偏高,会增加网络请求开销。可以根据块大小调整(比如每上传1GB内容Commit一次),或者仅在全部块上传完成后Commit——但如果中途失败,未提交的块会在7天后自动清理。若需要中途保留可访问的Blob,定期Commit是必要的,建议根据业务场景调整间隔。

4. 增加断点续传支持

如果需要客户端断开后重新上传,可以在上传前获取已上传的未提交块,跳过重复上传:

// 获取已上传的所有未提交块
var existingBlocks = await blockBlobClient.GetBlockListAsync(BlockListFilter.Uncommitted);
var uploadedBlockIds = existingBlocks.UncommittedBlocks.Select(b => b.Name).ToHashSet();

// 计算已上传的最大块ID,从下一个ID开始上传
if (uploadedBlockIds.Any())
{
    blockId = uploadedBlockIds
        .Select(id => int.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(id))))
        .Max() + 1;
}

5. ASP.NET Core场景下的流处理优化

在ASP.NET Core接收上传时,推荐使用PipeReader处理请求体流,避免手动管理buffer的复杂度,提升流处理效率:

var pipeReader = PipeReader.Create(Request.Body);
var blockId = 0;
var blockIds = new List<string>();

while (true)
{
    var result = await pipeReader.ReadAsync();
    var buffer = result.Buffer;

    foreach (var segment in buffer)
    {
        var blockIdBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(blockId.ToString("D6")));
        await blockBlobClient.StageBlockAsync(blockIdBase64, segment);
        blockIds.Add(blockIdBase64);
        blockId++;

        // 达到阈值时提交块列表
        if (blockId % 500 == 0)
        {
            await blockBlobClient.CommitBlockListAsync(blockIds);
        }
    }

    pipeReader.AdvanceTo(buffer.End, buffer.End);

    if (result.IsCompleted)
        break;
}

// 最终提交所有块
await blockBlobClient.CommitBlockListAsync(blockIds);

6. 错误处理与重试

对StageBlockAsync和CommitBlockListAsync添加重试逻辑(比如用Polly库),处理网络波动导致的临时错误,避免单次失败中断整个上传流程:

// 示例:用Polly配置重试策略
var retryPolicy = Policy
    .Handle<RequestFailedException>(ex => ex.Status == HttpStatusCode.RequestTimeout)
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

await retryPolicy.ExecuteAsync(async () =>
{
    await blockBlobClient.StageBlockAsync(blockIdBase64, new Memory<byte>(buffer, 0, bytesRead));
});

替代方案说明

如果你的需求只是客户端断点续传(不需要中途访问部分上传的Blob),可以使用Azure.Storage.Blobs库提供的高级封装,通过StorageTransferOptions配置分块上传,默认支持客户端断点续传:

var transferOptions = new StorageTransferOptions
{
    InitialTransferSize = 20 * 1024 * 1024, // 20MB分块大小
    MaximumConcurrency = 5 // 并发上传数
};

await blockBlobClient.UploadAsync(fileStream, transferOptions);

但注意:该方案的断点信息保存在客户端本地,若客户端进程重启则断点丢失;且上传过程中Blob不可访问,只有全部完成后才会Commit,无法满足"失败时保留已写入内容"的核心需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 16:47:07