Azure Block Blob压缩大于2GB文件时报指定块列表无效异常如何解决
异常产生原因
- Azure Blob 分块上传特性与ZipArchive写入逻辑冲突
你使用的Microsoft.Azure.Storage.Blob 11.2.2版本中,CloudBlockBlob.OpenWrite()返回的CloudBlobStream属于只进流,默认不支持随机查找(Seek)操作。同时该版本默认的单Blob上传阈值为2GB,小于2GB的文件会走单块直接上传逻辑,不会触发分块;超过2GB后会自动切换为分块上传,每个块默认大小为4MB,最多允许50000个分块。
而ZipArchive在Create模式下,当所有压缩条目写入完成、释放ZipArchive对象时,会尝试回退到流的起始位置写入Zip文件的头部元数据,以及在流末尾写入中央目录记录,这个操作依赖流的查找能力。当流不支持查找时,这部分写入操作会被当作新的内容追加到上传流中,导致分块的顺序、长度和预期不符,最终提交给Azure存储服务的块列表不符合规范,抛出"The specified block list is invalid."异常。 - 隐式问题:你的代码中没有显式设置分块大小,默认4MB的分块虽然理论上可以支持到近200GB的文件,但和ZipArchive的查找操作冲突后,刚好在超过2GB触发分块上传的阈值时暴露问题。
解决方案
方案1:使用本地临时文件作为中间缓冲(推荐,大文件场景内存占用最低)
先生成完整的Zip文件到本地临时磁盘,等Zip完全写完后,再把整个文件上传到Blob存储,从根源避免直接写Blob流的查找冲突问题。
修改后的代码逻辑示例:
public async Task DoWork(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { await _messageSemaphore.WaitAsync(cancellationToken); MyModel model = await _queue.PeekMessage(); if(model != null) { try { var zipBlockBlob = await _storageAccount.GetFileBlobReference(_configuration[ConfigKeys.ContainerName], model.Filename, model.FileRelativePath); // 生成临时文件路径 var tempZipPath = Path.GetTempFileName(); try { // 先写Zip到本地临时文件 using (var tempFileStream = File.Open(tempZipPath, FileMode.Create)) { using (var archive = new ZipArchive(tempFileStream, ZipArchiveMode.Create, false)) { foreach (var fileUri in Files) { var file = new Uri(fileUri); var cloudBlockBlob = blobContainer.GetBlockBlobReference(file); var zipEntry = archive.CreateEntry(model.Filename, CompressionLevel.Fastest); using (var blobStream = await cloudBlockBlob.OpenReadAsync()) using (var zipEntryStream = zipEntry.Open()) { await blobStream.CopyToAsync(zipEntryStream, cancellationToken); } } } } // 临时文件写完后上传到Blob using (var uploadStream = File.OpenRead(tempZipPath)) { // 显式设置分块大小为100MB,最大支持50000*100MB=5TB的Zip文件 zipBlockBlob.StreamWriteSizeInBytes = 100 * 1024 * 1024; await zipBlockBlob.UploadFromStreamAsync(uploadStream, cancellationToken); } } finally { // 上传完成后删除临时文件 if (File.Exists(tempZipPath)) File.Delete(tempZipPath); } } catch (FileNotFoundException e) { Console.WriteLine($"Download Error: {e.Message}"); } catch (Microsoft.Azure.Storage.StorageException e) { Console.WriteLine($"Storage Exception Error: {e.Message}"); } finally { _messageSemaphore.Release(); } } else { _messageSemaphore.Release(); await Task.Delay(_sleepTime, cancellationToken); } } }
方案2:使用可查找的内存流作为中间缓冲(适合磁盘IO受限、文件不会过大的场景)
如果你的服务有足够的内存资源,可以使用可回收内存流代替本地临时文件,逻辑和方案1一致,只是把中间存储从磁盘换成内存,可以避免磁盘IO开销。
额外优化点
你现有的DoWork方法用了async void,属于不安全的异步写法,建议改成async Task返回值,避免未捕获的异常直接终止进程。
内容的提问来源于stack exchange,提问作者ImposterX1
相关产品推荐
相关产品推荐

