View a markdown version of this page

的性能最佳实践 适用于 .NET 的 AWS SDK - 适用于 .NET 的 AWS SDK (V4)

的第 4 版 (V4) 适用于 .NET 的 AWS SDK 已经发布!

有关重大更改和迁移应用程序的信息,请参阅迁移主题

Orange button with text "Click here for details".

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

的性能最佳实践 适用于 .NET 的 AWS SDK

如何创建客户端、处理响应和配置应用程序对吞吐量、延迟和内存使用有很大影响。本主题介绍可帮助您的应用程序高效、可靠地运行的使用和配置模式。这些模式在高负载或资源受限的环境(例如容器和无服务器函数)中最为重要。遵循这些做法可以提高性能并防止常见问题,例如响应缓慢、挂起和内存使用率过高。

最有影响力的做法如下:

在开始之前,请务必设置环境配置项目

重复使用单个长期服务客户端

诸如 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,请调用GetObjectMetadataGetObjectMetadataAsync代替。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; }

相反,进行方法asyncawait调用。

public async Task<GetObjectResponse> GetAsync(GetObjectRequest request) { return await s3Client.GetObjectAsync(request); }

异步代码的其他建议:

  • 将一个传递CancellationToken给每个操作,这样慢速或停顿的呼叫就可以取消,而不是挂起。有关更多信息,请参阅 使用 CancellationToken 参数配置超时

  • 不要在空catch块中吞下异常。这样做会掩盖真正的失败,使挂机与错误无法区分。捕获特定的异常并记录它们。

  • 重新抛出捕获的异常时,使用throw;而不是 s throw 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 上的垃圾收集运行时配置选项。有关 AWS 计算的特定指导,请参阅 AWS 开发者工具博客文章为 Amazon ECS 配置.NET 垃圾回收和 AWS Lambda

管理 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提供分段(并行)下载。DownloadWithResponseAsync OpenStreamWithResponseAsync DownloadDirectoryWithResponseAsync使用时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 保存回收的内存,而不是泄漏。请参阅配置垃圾收集