

# 优化 Amazon ECS 服务自动扩缩
<a name="capacity-autoscaling-best-practice"></a>

Amazon ECS 服务是一个托管任务集合。每项服务都有关联的任务定义、所需的任务计数和可选的放置策略。

Amazon ECS 服务自动扩缩通过 Application Auto Scaling 服务实现。Application Auto Scaling 使用 CloudWatch 指标作为扩展指标的来源。它还使用 CloudWatch 警报来设置何时横向缩减或横向扩展服务的阈值。

您可以提供扩缩阈值。您可以设置指标目标，这称为*目标跟踪扩缩*。您还可以指定阈值，这称为*分步扩缩*。

配置 Application Auto Scaling 后，它会持续计算服务的相应所需任务数。它还会在所需的任务计数发生变化时通过横向扩展或横向缩减来通知 Amazon ECS。

要有效地使用服务自动扩缩，必须选择适当的扩缩指标。我们将在以下各节中讨论如何选择指标。

## 描述应用程序的特性
<a name="capacity-autoscaling-app"></a>

要正确扩缩应用程序，您需要知道应在何种条件下横向缩减应用程序，以及何时应横向扩展应用程序。

本质上，如果预计需求将超过容量，则应横向扩展应用程序。相反，当资源超过需求时，可以横向缩减应用程序以节省成本。

### 确定利用率指标
<a name="capacity-autoscaling-app-utilizationmetric"></a>

要有效地进行扩缩，必须确定一个表明利用率或饱和度的指标。此指标必须显示以下属性才能用于扩缩。
+ 该指标必须与需求相关联。当资源保持稳定，但需求发生变化时，指标值也必须发生变化。当需求增加或减少时，该指标也应增加或减少。
+ 指标值必须与容量成比例地横向缩减。当需求保持不变时，增加更多资源必须导致指标值按比例变化。因此，将任务数增加一倍应该会使该指标减少 50%。

标识利用率指标的最佳方法是在预生产环境（例如暂存环境）中进行负载测试。商业和开源负载测试解决方案广泛可用。这些解决方案通常可以生成合成负载，或模拟真实的用户流量。

要开始负载测试过程，应先为应用程序的利用率指标构建控制面板。这些指标包括 CPU 利用率、内存利用率、I/O 操作、I/O 队列深度和网络吞吐量。您可以使用 CloudWatch Container Insights 等服务收集这些指标。您还可以将 Amazon Managed Service for Prometheus 与 Amazon Managed Grafana 配合使用，以收集这些指标。在此过程中，请确保收集应用程序响应时间或工作完成率指标并绘制其图形。

进行负载测试时，请从小的请求或作业插入率开始。将此速率保持稳定几分钟，让您的应用程序进行预热。然后，慢慢提高速率并使其保持稳定几分钟。重复此周期，每次都提高速率，直到应用程序的响应或完成时间太慢而无法达到服务级目标（SLO）。

进行负载测试时，请检查每个利用率指标。随负载增加而增加的指标最适合作为最佳利用率指标。

接下来，识别已达到饱和度的资源。同时，还要检查利用率指标，看看哪个指标首先在高水平上趋于平缓。或者，检查哪个指标达到峰值，然后首先使应用程序崩溃。例如，如果随着负载增加，CPU 利用率从 0% 增加到 70-80%，然后在增加更多负载后仍保持在该水平，则可以肯定地说 CPU 已饱和。根据 CPU 架构的不同，它可能永远不会达到 100%。例如，假设内存利用率随负载的增加而增加，然后您的应用程序在达到任务或 Amazon EC2 实例内存限制时突然崩溃。这种情况下，很可能是内存已被完全消耗。您的应用程序可能会消耗多种资源。因此，请选择表示首先耗尽的资源的指标。

最后，在任务数或 Amazon EC2 实例数增加一倍后，再次尝试负载测试。假设关键指标的增加或减少速度是以前的一半。如果是这种情况，则指标与容量成正比。这是自动扩缩的一个很好的利用率指标。

现在考虑一下这个假设场景。假设您对应用程序进行负载测试，并发现每秒 100 个请求时 CPU 利用率最终达到 80%。当增加更多负载时，CPU 利用率不会再提高。但是，它确实会使您的应用程序的响应速度变慢。然后，您再次运行负载测试，将任务数增加一倍，但将速率保持在之前的峰值水平。如果您发现平均 CPU 利用率降至 40% 左右，则平均 CPU 利用率是扩缩指标的理想选择。另一方面，如果增加任务数后 CPU 利用率保持在 80%，则平均 CPU 利用率不是一个很好的扩缩指标。这种情况下，需要进行更多的研究才能找到合适的指标。

### 常见的应用程序模型和扩缩属性
<a name="capacity-autoscaling-app-common"></a>

各种软件都可以在 AWS 上运行。许多工作负载是自主开发的，而另一些工作负载则基于常见的开源软件。无论它们来自哪里，我们都观察到了一些常见的服务设计模式。如何有效扩缩很大程度上取决于模式。

#### 高效的 CPU 绑定服务器
<a name="capacity-autoscaling-app-common-cpu"></a>

除了 CPU 和网络吞吐量之外，高效的 CPU 绑定服务器几乎不利用任何资源。每个请求都可以由应用程序单独处理。请求不依赖于数据库等其他服务。应用程序可以处理成千上万个并发请求，并且可以高效地利用多个 CPU 来执行此操作。每个请求或由内存开销较低的专用线程提供服务，或者在为请求提供服务的每个 CPU 上运行一个异步事件循环。应用程序的每个副本都同样能够处理请求。可能在 CPU 之前耗尽的唯一资源是网络带宽。在 CPU 绑定服务中，即使在吞吐量峰值下，内存利用率也只是可用资源的一小部分。

可以将基于 CPU 的自动扩缩用于这种类型的应用程序。该应用程序在扩缩方面具有最大的灵活性。可以通过向其提供更大的 Amazon EC2 实例或 Fargate vCPU 来对其进行纵向扩缩。而且，还可以通过添加更多副本对其进行横向扩缩。添加更多副本或将实例大小增加一倍，可将平均 CPU 利用率（相对于容量）降低一半。

如果您为此应用程序使用 Amazon EC2 容量，则请考虑将其放置在计算优化型实例上，例如 `c5` 或 `c6g` 系列。

#### 高效的内存绑定服务器
<a name="capacity-autoscaling-app-common-memory"></a>

高效的内存绑定服务器为每个请求分配大量内存。在最大并发（但不一定是吞吐量）下，内存会在 CPU 资源耗尽之前耗尽。请求结束时，与请求关联的内存会被释放。只要有可用内存，就可以接受额外请求。

可以将基于内存的自动扩缩用于这种类型的应用程序。该应用程序在扩缩方面具有最大的灵活性。可以通过向其提供更大的 Amazon EC2 或 Fargate 内存资源来对其进行纵向扩缩。而且，还可以通过添加更多副本对其进行横向扩缩。添加更多副本或将实例大小增加一倍，可将平均内存利用率（相对于容量）降低一半。

如果您为此应用程序使用 Amazon EC2 容量，请考虑将其放置在内存优化型实例上，例如 `r5` 或 `r6g` 系列。

某些受内存限制的应用程序在某项请求结束时不会释放与该请求关联的内存，因此并发度的降低不会导致使用的内存减少。为此，不建议您使用基于内存的扩缩。

#### 基于工作线程的服务器
<a name="capacity-autoscaling-app-common-worker"></a>

基于工作线程的服务器一个接一个地处理每个工作线程的请求。工作线程可以是轻量级线程，例如 POSIX 线程。它们也可以是重量级线程，例如 UNIX 进程。无论它们是哪个线程，应用程序可以支持的最大并发度总是存在的。通常，并发限制根据可用的内存资源按比例设置。如果达到并发限制，应用程序会将其他请求放入积压队列中。如果积压队列溢出，应用程序会立即拒绝其他传入请求。符合这种模式的常见应用程序包括 Apache Web 服务器和 Gunicorn。

请求并发度通常是扩缩此应用程序的最佳指标。由于每个副本都有并发限制，因此务必在达到平均限制之前进行横向扩展。

获取请求并发指标的最佳方法是让您的应用程序将其报告给 CloudWatch。应用程序的每个副本都可以将并发请求数量作为自定义指标高频率发布。建议将频率设置为至少每分钟一次。收集多个报告后，您可以使用平均并发度作为扩缩指标。此指标通过将总并发度除以副本数计算得出。例如，如果总并发度为 1000，副本数为 10，则平均并发度为 100。

如果您的应用程序位于应用程序负载均衡器之后，则您也可以使用负载均衡器的 `ActiveConnectionCount` 指标作为扩缩指标中的一个因子。必须将 `ActiveConnectionCount` 指标除以副本数才能得出平均值。必须使用平均值进行扩缩，而不是使用原始计数值。

为了使该设计发挥最佳效果，在低请求速率下，响应延迟的标准差应该较小。建议在需求低迷时期，在短时间内答复大多数请求，并且没有多少请求需要的时间比平均响应时间长得多。平均响应时间应接近第 95 个百分位响应时间。否则，可能会导致发生队列溢出。这会导致错误。建议您在必要时提供其他副本，以缓解溢出风险。

#### 等待服务器
<a name="capacity-autoscaling-app-common-waiting"></a>

等待服务器会对每个请求进行一些处理，但它高度依赖于一个或多个下游服务运行。容器应用程序经常会大量使用下游服务，例如数据库和其他 API 服务。这些服务可能需要一些时间才能做出响应，尤其是在高容量或高并发场景中。这是因为这些应用程序往往使用很少的 CPU 资源，并利用可用内存方面的最大并发性。

等待服务既适用于内存绑定的服务器模式，也适用于基于工作线程的服务器模式，具体取决于应用程序的设计方式。如果应用程序的并发度仅受内存限制，则应使用平均内存利用率作为扩缩指标。如果应用程序的并发度基于工作线程限制，则应使用平均并发度作为扩缩指标。

#### 基于 Java 的服务器
<a name="capacity-autoscaling-app-common-java"></a>

如果您的基于 Java 的服务器受 CPU 约束，并且与 CPU 资源成比例地扩展，则它可能适合高效的 CPU 绑定服务器模式。如果是这种情况，则平均 CPU 利用率可能适合作为扩缩指标。但是，许多 Java 应用程序不受 CPU 限制，因此难以扩展。

为了获得最佳性能，建议您为 Java 虚拟机（JVM）堆分配尽可能多的内存。最新版本的 JVM（包括 Java 8 更新 191 或更高版本）会自动将堆大小设置得尽可能大，以容纳到容器中。这意味着，在 Java 中，内存利用率很少与应用程序利用率成正比。随着请求速率和并发度提高，内存利用率保持不变。因此，不建议根据内存利用率扩缩基于 Java 的服务器。相反，通常建议按 CPU 利用率扩缩。

某些情况下，基于 Java 的服务器在耗尽 CPU 之前会遇到堆耗尽的情况。如果您的应用程序在高并发时容易出现堆耗尽的情况，则平均连接数是最好的扩缩指标。如果您的应用程序在高吞吐量时容易出现堆耗尽的情况，则平均请求率是最好的扩缩指标。

#### 使用其他垃圾回收运行时的服务器
<a name="capacity-autoscaling-app-common-garbage"></a>

许多服务器应用程序都基于执行垃圾回收的运行时，例如 .NET 和 Ruby。这些服务器应用程序可能符合前面描述的模式之一。但是，与 Java 一样，不建议根据内存扩缩这些应用程序，因为它们观察到的平均内存利用率通常与吞吐量或并发度不相关。

对于这些应用程序，如果应用程序受 CPU 限制，建议您按 CPU 利用率扩展。否则，建议您根据负载测试结果按平均吞吐量或平均并发度扩展。

#### 作业处理器
<a name="capacity-autoscaling-app-common-job"></a>

许多工作负载都涉及异步作业处理。它们包括不实时接收请求，而是通过订阅工作队列来接收作业的应用程序。对于这些类型的应用程序，正确的扩缩指标几乎总是队列深度。队列增长表明待处理的工作超过了处理能力，而空队列则表示容量大于要做的工作。

诸如 Amazon SQS 和 Amazon Kinesis Data Streams 之类的 AWS 消息收发服务提供了可用于扩缩的 CloudWatch 指标。对于 Amazon SQS，`ApproximateNumberOfMessagesVisible` 是最好的指标。对于 Kinesis Data Streams，可以考虑使用 `MillisBehindLatest` Kinesis Client Library（KCL）发布的指标。在使用该指标进行扩缩之前，应在所有使用者之间求它的平均值。