C#在Lambda中实现S3同桶多对象打zip转存的方法
C# Lambda 实现S3目录打包ZIP上传方案
现有代码可优化点
- 检查BatchDownload目录是否存在的逻辑放在对象遍历循环内,会重复发起S3请求,无意义消耗接口配额和执行时间,该逻辑只需在打包前执行1次
- 调用异步接口时用
.Result同步等待,在Lambda运行环境下有概率出现线程池死锁,建议全程用await异步写法 - 单次调用
ListObjectsAsync最多返回1000个对象,Download目录下文件数超过阈值时会出现漏打包,需要兼容分页遍历逻辑 - 提前拉取所有对象键存入列表再处理,会产生不必要的内存开销,可在遍历对象时直接做流处理
核心实现思路
不要将S3对象全量下载到Lambda本地临时目录后再打包,全程使用流对接S3响应流、ZIP写入流、S3上传流,可大幅降低内存占用和执行耗时。注意Lambda内存配置需要大于待打包文件的总大小,若单包预期超过1G,建议改用S3 Batch Operation做离线打包,不要用同步Lambda执行。
完整可运行代码如下,不需要单独缓存所有对象键,边遍历S3对象边写入ZIP:
using Amazon.S3; using Amazon.S3.Model; using System.IO.Compression; // 配置项,建议放到Lambda环境变量里 private const string BucketName = "你的存储桶名"; private const string SourcePrefix = "Download/"; private const string TargetPrefix = "BatchDownload/"; private readonly IAmazonS3 _s3Client = new AmazonS3Client(); // Lambda环境下默认用执行角色权限,不需要手动传密钥 public async Task<string> Handler() { // 1. 提前检查目标目录是否存在,不存在则创建(S3目录本质是0字节前缀对象) var listTargetDirReq = new ListObjectsV2Request { BucketName = BucketName, Prefix = TargetPrefix, MaxKeys = 1 }; var targetDirResp = await _s3Client.ListObjectsV2Async(listTargetDirReq); if (targetDirResp.S3Objects.Count == 0) { await _s3Client.PutObjectAsync(new PutObjectRequest { BucketName = BucketName, Key = TargetPrefix }); } // 2. 初始化ZIP流,总文件大小小于500M用MemoryStream即可,超过的话替换为FileStream写到/tmp目录 var zipFileName = $"download-bundle-{DateTime.UtcNow:yyyyMMddHHmmss}.zip"; using var zipOutputStream = new MemoryStream(); using (var zipArchive = new ZipArchive(zipOutputStream, ZipArchiveMode.Create, leaveOpen: true)) { // 3. 分页遍历源目录下所有对象,不需要提前存对象键列表 var listSourceReq = new ListObjectsV2Request { BucketName = BucketName, Prefix = SourcePrefix }; do { var sourceResp = await _s3Client.ListObjectsV2Async(listSourceReq); foreach (var s3Object in sourceResp.S3Objects) { // 跳过目录本身(Key以/结尾的对象是目录占位符,不需要打包) if (s3Object.Key.EndsWith("/")) continue; // 拉取当前对象的流 var getObjResp = await _s3Client.GetObjectAsync(BucketName, s3Object.Key); // ZIP内的文件名去掉源前缀,避免ZIP里套一层Download目录 var entryName = s3Object.Key.Substring(SourcePrefix.Length); var zipEntry = zipArchive.CreateEntry(entryName, CompressionLevel.Fastest); await using var entryStream = zipEntry.Open(); // 直接把S3响应流复制到ZIP条目流,不落地到本地磁盘 await getObjResp.ResponseStream.CopyToAsync(entryStream); } listSourceReq.ContinuationToken = sourceResp.NextContinuationToken; } while (listSourceReq.IsTruncated); // 还有分页的话继续遍历 } // 4. 流位置重置到开头,上传ZIP到目标目录 zipOutputStream.Position = 0; var putZipReq = new PutObjectRequest { BucketName = BucketName, Key = $"{TargetPrefix}{zipFileName}", InputStream = zipOutputStream, ContentType = "application/zip" }; await _s3Client.PutObjectAsync(putZipReq); return $"打包完成,ZIP路径:s3://{BucketName}/{TargetPrefix}{zipFileName}"; }
额外注意事项
- 代码用
ListObjectsV2替代旧版ListObjects接口,是AWS官方推荐的版本,性能和稳定性更好 - 压缩级别用
Fastest平衡压缩率和执行速度,Lambda按执行时长计费,压缩级别太高会拉长执行时间增加成本 - 如果需要打包后删除源Download目录下的文件,在ZIP上传成功后遍历源对象批量调用
DeleteObjectAsync即可,不要在打包前删除避免文件丢失 - Lambda默认临时目录
/tmp有512M~10G的存储空间(和内存配置挂钩),打包大文件时把MemoryStream替换为new FileStream("/tmp/temp.zip", FileMode.Create)即可,内存占用会更低
内容的提问来源于stack exchange,提问作者Coding_ninja
相关产品推荐
相关产品推荐

