的第 4 版 (V4) 适用于 .NET 的 AWS SDK 已经发布!
有关重大更改和迁移应用程序的信息,请参阅迁移主题。
本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
的性能最佳实践 适用于 .NET 的 AWS SDK
如何创建客户端、处理响应和配置应用程序对吞吐量、延迟和内存使用有很大影响。本主题介绍可帮助您的应用程序高效、可靠地运行的使用和配置模式。这些模式在高负载或资源受限的环境(例如容器和无服务器函数)中最为重要。遵循这些做法可以提高性能并防止常见问题,例如响应缓慢、挂起和内存使用率过高。
最有影响力的做法如下:
-
重复使用单个长期服务客户端,而不是为每个请求创建一个服务客户端。
-
处置响应和流,以便将网络连接释放回连接池。
-
await正确使用async/,切勿阻塞异步 SDK 调用。
-
为配置 .NET 垃圾收集 AWS Lambda 还有亚马逊 ECS为受限环境(例如 AWS Lambda 和 Amazon ECS)配置.NET 垃圾回收功能。
-
在高吞吐量下管理 HTTP 连接和连接限制。
重复使用单个长期服务客户端
诸如 Amazons3Client之类的服务客户端AmazonDynamoDBClient是线程安全的,构造成本相对较高且使用寿命长。 构建客户端可以解析区域和终端节点信息,并建立底层 HTTP 基础架构。为每项服务创建一个客户端,并在应用程序的整个生命周期中重复使用。如果您致电多个区域,请为每个区域创建一个单独的客户端。
警告
不要为每个请求或在循环内创建新的服务客户端。反复创建客户端会破坏底层 HTTP 基础架构,从而增加可衡量的延迟。它还会耗尽插座或手柄,导致请求失败或在负载下挂起。
在使用依赖注入的应用程序中,将客户端注册为单例。AWSSDK.Extensions.NETCore.SetupNuGet包中的AddAWSService扩展方法将客户端注册的默认生命周期为ServiceLifetime.Singleton。客户端是在第一次请求时创建的,并且在进程的整个生命周期中都会重复使用相同的实例。有关使用依赖注入注册 AWS
服务以及从配置中读取选项的更多信息,请参阅AWSSDK.Extensions.NETCore.Setup 和 iconfiguration。
您也可以手动将客户端注册为单例。
builder.Services.AddSingleton<IAmazonS3>(_ => new AmazonS3Client());
注意
因为默认情况下会将客户端AddAWSService注册为单例,所以不要处置它提供的客户端。如果您需要非默认生命周期,请将不同的ServiceLifetime值传递给的可选lifetime参数。AddAWSService
重复使用客户端并不意味着你应该停止处理每个操作的响应。在应用程序生命周期内重复使用客户端,但继续处置各个操作返回的响应和流,如中所述处置响应和直播。
处置响应和直播以释放连接
一些 SDK 响应对象带有网络直播流。最常见的示例是 GetObjectResponse,它通过其ResponseStream属性实现IDisposable和公开对象的内容。在完全读取流或处理响应之前,响应会保持开放的 HTTP 连接。如果您泄露了这些响应(例如,通过循环调用GetObject而不处置每个结果),则打开的连接会累积直到连接池耗尽,下一次调用就会阻塞。这是关于第 N 个对象上的下载 “随机挂起” 或停滞的报道背后的原因。
务必将直播响应打包在using声明中,并立即阅读或复制其直播。
using Amazon.S3; using Amazon.S3.Model; // s3Client is a reused, long-lived client. var request = new GetObjectRequest { BucketName = bucketName, Key = key }; using var response = await s3Client.GetObjectAsync(request); await response.WriteResponseStreamToFileAsync(filePath, append: false, CancellationToken.None);
处置响应和处置响应ResponseStream是等效的;任何一个都关闭底层网络流并将连接返回到池。
提示
如果您只需要对象元数据,例如大小、上次修改时间、内容类型或 ETag,请调用GetObjectMetadata或GetObjectMetadataAsync代替。GetObject元数据操作发出 HTTP HEAD 请求且不传输任何对象正文,因此无需管理任何内容流。
var metadata = await s3Client.GetObjectMetadataAsync(bucketName, key); Console.WriteLine($"Size: {metadata.ContentLength} bytes");
警告
响应和流类型(例如)GetObjectResponse不实现终结器,因此您不能依靠垃圾回收来为您释放它们的连接。您必须使用using或显式Dispose调用确定性地处理响应和流。
async/await 正确使用
在现代 .NET 上,中的服务操作 适用于 .NET 的 AWS SDK 是异步的,返回 a Task。在 .NET Framework 上,同步方法也存在,但异步调用的扩展性更好,建议使用。使用代码执行操作await,并async传播到代码的每一层,直至入口点。有关使用 SDK 进行异步编程的更多信息,请参阅异步编程。
警告
不要使用.Result、.Wait()或屏蔽异步 SDK 调用.GetAwaiter().GetResult()。这种异步同步模式是应用程序挂起的常见原因:
-
在负载下,阻塞调用消耗线程的速度快于线程池的增长速度,因此无法连续运行。这种线程池匮乏表现为无限期挂起,是现代 .NET(ASP.NET Core没有默认值)上的主要故障模式。
SynchronizationContext -
某些上下文捕获
SynchronizationContext,例如经典ASP.NET、Windows FormsWPFBlazor WebAssembly、和.NET 框架应用程序。在这些情况下,阻塞调用线程而延续需要相同的线程会导致死锁。SDK 中的操作在ConfigureAwait(false)内部使用,因此它们不会将其延续内容发布回捕获的上下文中。死锁是由调用链中其他地方的async代码造成的,这些代码确实捕获了死锁。自await始至终使用SDK调用可以完全避免问题。
以下方法阻塞异步调用,可能导致线程池死锁或耗尽。
// Anti-pattern: do not do this. public GetObjectResponse Get(GetObjectRequest request) { return s3Client.GetObjectAsync(request).Result; }
相反,进行方法async和await调用。
public async Task<GetObjectResponse> GetAsync(GetObjectRequest request) { return await s3Client.GetObjectAsync(request); }
异步代码的其他建议:
-
将一个传递
CancellationToken给每个操作,这样慢速或停顿的呼叫就可以取消,而不是挂起。有关更多信息,请参阅 使用 CancellationToken 参数配置超时。 -
不要在空
catch块中吞下异常。这样做会掩盖真正的失败,使挂机与错误无法区分。捕获特定的异常并记录它们。 -
重新抛出捕获的异常时,使用
throw;而不是 sthrow ex;o 来保留原始堆栈跟踪。 -
如果您必须从同步边界调用,则将其视为最后的手段,并将工作与捕获的上下文隔离开来,而不是将阻塞调用设为默认模式。
为配置 .NET 垃圾收集 AWS Lambda 还有亚马逊 ECS
当容器出现 “内存泄漏” 时,.NET 垃圾收集器 (GC) 可能会保留回收的内存以供重复使用。因此,即使托管堆没有增长,进程内存也可能显得很高且稳定。此外,GC 不会自动检测容器的内存限制(其 cgroup 限制)。在受限的环境中,堆可能会向主机的内存而不是容器的极限增长。这可能导致OutOfMemoryException或容器被终止。
为了帮助 GC 在受限的环境中正常运行:
-
在容器上设置明确的内存限制,以便 GC 遵守 cgroup 限制, and/or 设置
DOTNET_GCHeapHardLimit(绝对字节值,十六进制)或DOTNET_GCHeapHardLimitPercent环境变量来限制托管堆的上限。在报告的几个 Amazon ECS 内存不足案例中,设置硬内存限制解决了崩溃问题。 -
在小型 AWS Lambda 和 Amazon ECS 主机上,考虑禁用并发(后台)垃圾回收,这样收集器就不会预留额外内存;例如,将
DOTNET_gcConcurrent环境变量设置为0,或<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>在项目文件中设置。 -
绑定您的并发量。同时启动许多操作,例如调用
Task.WhenAll大量集合,会膨胀进程内存,并可能耗尽连接池。改为限制并行度。例如,避免这种无界模式:// Anti-pattern: starts one task per item with no limit. await Task.WhenAll(keys.Select(key => s3Client.GetObjectMetadataAsync(bucket, key)));取而代之的是,使用
Parallel.ForEachAsync以下方法限制并发量:var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; await Parallel.ForEachAsync(keys, options, async (key, token) => { await s3Client.GetObjectMetadataAsync(bucket, key, token); });
有关这些设置的更多信息,请参阅 learn.microsoft.com 上的垃圾收集
管理 HTTP 连接和连接限制
在高吞吐量下,两个与连接相关的问题很常见。首先是打开了太多短暂的连接。这会耗尽临时端口,留下套接字,并增加 TCP 和 TLS 握手延迟。TIME_WAIT第二个问题是可用的连接太少,这给并行性带来了瓶颈。重复使用单个长寿命客户端(参见重复使用服务客户端)是健康连接池的基础,因为池化取决于客户端的重复使用。
要调整每个端点的并发连接数,请在客户端配置上设置该MaxConnectionsPerServer属性。当此属性为null(默认值)时,基础HttpClientHandler默认值适用,这在现代 .NET 中实际上是无限的。仅当对同一端点的多个并发请求出现连接瓶颈时才提高。一个不错的起点是每个终端节点的预期并发请求的峰值数。在不提高吞吐量的情况下,将其设置得远高于工作负载需求会浪费套接字。
using Amazon.S3; var config = new AmazonS3Config { MaxConnectionsPerServer = 50 }; var s3Client = new AmazonS3Client(config);
如果您已经使用依赖注入,请通过配置客户端AWSSDK.Extensions.NETCore.Setup。当您使用 DI 或注册多个服务客户端时,推荐使用这种方法。它可以集中配置,使客户端易于注入和测试。您可以从应用程序的配置中设置配置值,而不是在代码中设置配置值。有关更多信息,请参阅 AWSSDK.Extensions.NETCore.Setup 和 iconfiguration。
最后,绑定自己的并行度,这样启动的并发操作就不会超过连接限制允许的次数。例如,根据您的连接限制SemaphoreSlim进行门禁呼叫:
var throttle = new SemaphoreSlim(50); // match MaxConnectionsPerServer await throttle.WaitAsync(token); try { await s3Client.GetObjectAsync(request, token); } finally { throttle.Release(); }
配置超时和重试
超时和重试直接影响感知性能。Timeout值过高会使停滞的请求长时间阻塞。当服务已经返回限制错误时,激进的重试策略会增加更多请求,并可能使限制变得更糟。选择符合应用程序对延迟和失败容忍度的重试策略,让真正的异常传播,而不是以掩盖挂起的方式重试。
注意
该Timeout属性不影响异步调用。如果您使用异步调用,请使用 CancellationToken 参数配置超时改为参见。
有关重试模式和Timeout属性(仅ReadWriteTimeout适用于.NET Framework)的更多信息以及如何设置它们的示例,请参阅重试和超时。MaxErrorRetry
优化串流和大对象传输 (Amazon S3)
要上传和下载大型对象或许多对象,请使用Amazon.S3.Transfer命名空间中的TransferUtility类。它使用分段传输并行上传和下载,并为您管理流、分段和连接。这比单流传输更快,也是移动大型对象的推荐方式。
-
并行分段下载。SDK 的早期版本以单个流的形式下载对象,而不是并行下载各个部分。从
AWSSDK.S3版本 4.0.17 开始,通过、和方法TransferUtility提供分段(并行)下载。DownloadWithResponseAsyncOpenStreamWithResponseAsyncDownloadDirectoryWithResponseAsync使用时OpenStreamWithResponseAsync,当您消耗返回的流时,对象的各个部分会缓冲在内存中。使用的MaxInMemoryParts属性控制缓冲了多少个部件。 TransferUtilityOpenStreamRequest 对于大多数转账,首选TransferUtility。仅在需要特定范围或自定义并行GetObjectRequest模式时,才使用ByteRange属性的方式自行下载字节范围。有关更多信息,请参阅 AWS 开发者工具博客上介绍适用于.NET Transfer Manager 的 AWS SDK 的分段下载支持。 -
上传的内容长度。Amazon S3
PUT需要已知的内容长度,默认情况下,开发工具包会在请求正文中计算校验和。当已知长度且可搜索流时,SDK 无需在内存中缓冲整个对象即可执行此操作。通过根据需要进行缓冲来为您TransferUtility处理不可搜索的流。如果您改为使用不可搜索的流(例如中的原始请求正文ASP.NET Core)PutObjectAsync直接调用,则请求可能会失败。提供可搜索的直播,明确设置内容长度,或配置 SDK 计算校验和的方式。有关更多信息,请参阅《软件开发工具包和 AWS 工具参考指南》中的数据完整性保护。 -
零件尺寸。Amazon S3 允许每次分段上传最多 10,000 个分段。当总长度已知时,SDK 会自动计算出保持在该限制范围内的分段大小。你主要需要为
PartSize事先不知道长度的直播做好准备。否则,较小的默认分段可能会超过超大上传的限制。更大的部件大小还可以减少每个部件的开销,但代价是每个部件需要更多的内存。
诊断性能问题
当您调查减速、挂起或明显泄漏时,以下信号可帮助您快速找到原因:
-
越来越多的
TIME_WAIT处于CLOSE_WAIT或状态(可见netstat)的套接字是未处理的响应或每个请求创建和丢弃的客户端的指纹。请参阅处置响应和直播和重复使用服务客户端。 -
启用请求指标和响应日志,以确认请求实际正在分发并测量延迟。在创建服务客户端之前,在 AWSConfigs 上设置LoggingConfig对象的属性。客户端捕获设置,例如
LogMetrics在构造时捕获设置。using Amazon; AWSConfigs.LoggingConfig.LogMetrics = true; AWSConfigs.LoggingConfig.LogResponses = ResponseLoggingOption.OnError; -
研究内存时,将全进程内存与托管堆区分开。高但稳定的进程内存通常是 GC 保存回收的内存,而不是泄漏。请参阅配置垃圾收集。