教程:Amazon EC2 竞价型实例 - AWS SDK for Java 1.x

我们宣布了即将推出 end-of-support 的 AWS SDK for Java (v1)。建议您迁移到 AWS SDK for Java v2。有关日期、其他详细信息以及如何迁移的信息,请参阅链接的公告。

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

教程:Amazon EC2 竞价型实例

概述

与按需实例价格相比,通过 Spot 实例,您可以对未使用的 Amazon Elastic Compute Cloud (Amazon EC2) 容量进行出价(最高达 90%),并在出价高于当前 Spot 价格 时运行您购买的实例。根据供应和需求情况,Amazon EC2 会定期更改 Spot 价格;出价达到或超过 Spot 价格的客户可获得可用的 Spot 实例。就像按需实例和预留实例, Spot 实例为您提供了另一种获得更多计算能力的选择。

Spot 实例可以大幅降低您用于批量处理、科学研究、图像处理、视频编码、数据和 Web 检索、财务分析和测试的 Amazon EC2 成本。除此之外,在不急需容量的情况下, Spot 实例还能让您获得大量的附加容量。

如要使用 Spot 实例,您就需要置入一个 Spot 实例请求,以便指定您愿意支付的每个实例每小时的最高价格;这就是您的竞价。如果您的最高出价超出当前的 Spot 价格,则会满足您的请求,您的实例将会运行,直到您选择终止它们或 Spot 价格增长到高于您的最高价格(以先到者为准)。

请务必记住:

  • 您每小时支付的价格通常低于您的出价。随着请求的接收和现有供应的变化,Amazon EC2 会定期调整 Spot 价格。在该期间内,无论每个人的最高出价是否更高,它们支付的 Spot 价格都是相同的。因此,您的支付要低于您的出价,但永远不会支付超过您的出价。

  • 如果您正在运行 Spot 实例,而您的出价不再达到或高于当前的 Spot 价格,则您的实例将会终止。这意味着,您要确保工作负载和应用程序足够灵活,以便利用这一机会性的容量。

运行时,Spot 实例的操作方式与其他 Amazon EC2 实例完全相同,而且同其他 Amazon EC2 实例一样,当您不再需要 Spot 实例时可以终止它们。如果终止了实例,您需要为不满一小时的时间付费(与按需或预留实例相同)。不过,如果 Spot 价格超出您的最高价格,且 Amazon EC2 终止了您的实例,则您无需对任何不满一小时的使用时间付费。

本教程介绍如何使用 AWS SDK for Java 执行以下操作。

  • 提交一个 Spot 请求

  • 判定何时执行该 Spot 请求

  • 取消该 Spot 请求

  • 终止相关实例

先决条件

要使用此指南,您必须已安装 AWS SDK for Java 并且已满足其基本安装先决条件。有关更多信息,请参阅设置 AWS SDK for Java

第 1 步:设置您的证书

要开始使用此代码示例,您需要设置 AWS 凭证。有关具体操作说明,请参阅设置用于开发的 AWS 凭证和区域

注意

建议您使用 IAM 用户凭证来提供这些值。有关更多信息,请参阅注册 AWS 并创建 IAM 用户

您既然已配置好了您的设置,现在就可以使用示例中的代码开始了。

第 2 步:设置安全组

一个安全组可作为一个控制流量进入和流出实例组的防火墙。默认情况下,实例开始运行时没有配置任何安全组,这就意味着,从任何 TCP 端口传入的 IP 流量都将被拒绝。因此,在提交 Spot 请求前,我们会设置一个安全组,以允许必要的网络流量传入。出于本教程的目的,我们将创建一个名为“GettingStarted”的新安全组,以允许从您正在运行的应用程序的 IP 地址传入 Secure Shell (SSH) 流量。要设置一个新的安全组,需要包含或运行下列通过编程的方式来设置安全组的代码示例。

创建 AmazonEC2 客户端数据元之后,我们会创建一个名为“GettingStarted”的CreateSecurityGroupRequest数据元以及对安全组的描述。接下来,我们将调用ec2.createSecurityGroup API 来创建安全组。

为访问安全组,我们将使用本地电脑子网的 CIDR 表示的 IP 地址范围创建一个 ipPermission 数据元,IP 地址的后缀“/10”指明了该指定 IP 地址的子网。我们还为 ipPermission 数据元配置了 TCP 协议和端口 22 (SSH)。最后一步是使用我们的安全组名称和 ec2.authorizeSecurityGroupIngress 数据元来调用 ipPermission

// Create the AmazonEC2 client so we can call various APIs. AmazonEC2 ec2 = AmazonEC2ClientBuilder.defaultClient(); // Create a new security group. try { CreateSecurityGroupRequest securityGroupRequest = new CreateSecurityGroupRequest("GettingStartedGroup", "Getting Started Security Group"); ec2.createSecurityGroup(securityGroupRequest); } catch (AmazonServiceException ase) { // Likely this means that the group is already created, so ignore. System.out.println(ase.getMessage()); } String ipAddr = "0.0.0.0/0"; // Get the IP of the current host, so that we can limit the Security // Group by default to the ip range associated with your subnet. try { InetAddress addr = InetAddress.getLocalHost(); // Get IP Address ipAddr = addr.getHostAddress()+"/10"; } catch (UnknownHostException e) { } // Create a range that you would like to populate. ArrayList<String> ipRanges = new ArrayList<String>(); ipRanges.add(ipAddr); // Open up port 22 for TCP traffic to the associated IP // from above (e.g. ssh traffic). ArrayList<IpPermission> ipPermissions = new ArrayList<IpPermission> (); IpPermission ipPermission = new IpPermission(); ipPermission.setIpProtocol("tcp"); ipPermission.setFromPort(new Integer(22)); ipPermission.setToPort(new Integer(22)); ipPermission.setIpRanges(ipRanges); ipPermissions.add(ipPermission); try { // Authorize the ports to the used. AuthorizeSecurityGroupIngressRequest ingressRequest = new AuthorizeSecurityGroupIngressRequest("GettingStartedGroup",ipPermissions); ec2.authorizeSecurityGroupIngress(ingressRequest); } catch (AmazonServiceException ase) { // Ignore because this likely means the zone has // already been authorized. System.out.println(ase.getMessage()); }

请注意,要创建一个新的安全组,您只需要运行一次此应用程序。

您还可以使用 AWS Toolkit for Eclipse 创建安全组。有关更多信息,请参阅通过 AWS Cost Explorer 管理安全组

步骤 3:提交您的 Spot 请求

为了提交一个 Spot 请求,您首先需要确定该实例类型,Amazon 系统映像 (AMI),和您要使用的最高出价。还须包括我们先前配置好的安全组,这样一来,如果需要的话,您就可以登录到该实例中了。

有几个实例类型可供选择;请转到 Amazon EC2 实例类型获取完整列表。在本教程中,我们将使用最便宜的实例类型 t1.micro。下一步是确定我们想用的 AMI 类型。在本教程中,我们使用的是最新版的 Amazon Linux AMI,即 ami-a9d09ed1。最新的 AMI 可能会随时间而改变,但您始终可以通过执行以下步骤来确定最新版的 AMI:

  1. 打开 Amazon EC2 控制台

  2. 选择 Launch Instance (启动实例) 按钮。

  3. 第一个窗口将显示可用的 AMI。每个 AMI 标题旁边都列出了 AMI ID。或者,您也可以使用 DescribeImages API,但该命令的使用不在本教程的范围之内。

有很多方法可以竞价 Spot 实例,如要大致了解各种方法,您应当观看对 Spot 实例出价视频。然而,为了入门,我们将介绍三种常见的策略:确保成本低于按需定价的竞价;基于所得计算值的竞价;以便尽可能快地获取计算能力的竞价。

  • 降低成本至低于按需实例您需要进行花费数小时或数天的批处理工作。然而,您可以灵活调整启动和完成时间。您希望看到是否以较低的成本完成了按需实例。您可以通过使用 AWS Management Console或 Amazon EC2 API 来检查各个类型实例的 Spot 价格历史记录。如需更多信息,请转到查看 Spot 价格历史记录。在您分析了给定可用区内所需实例类型的价格记录之后,您有两种可供选择的方法进行竞价:

    • 您可以在现货价格范围(这仍然低于按需定价)的上端竞价,预测您单次现货请求很有可能会达成,并运行足够的连续计算时间来完成此项工作。

    • 或者,您可以通过按需实例价格的百分比形式,指定您愿意为 Spot 实例支付的金额,并计划将持久请求期间启动的许多实例结合起来。如果超过指定价格,则 Spot 实例将终止。(在本教程之后我们会介绍如何自动运行该任务。)

  • 支付不超过该结果的值您需要进行数据处理工作。您将会对该工作的结果有一个很好的了解,以便于能够让您知道在计算成本方面它们的价值。当您分析了实例类型的 Spot 价格记录之后,选择一个计算时间成本不高于该工作结果成本的竞价。由于 Spot 价格的波动,该价格可能会达到或低于您的竞价,所以您要创建一个持久出价,并允许它间歇运行。

  • 快速获取计算容量您对附加容量有一个无法预料的短期需求,该容量不能通过按需实例获取。当您分析了实例类型的 Spot 价格记录之后,您出价高于历史最高价格,以便提供一个高的能很快执行实例的可能性,并继续计算,直到完成实例。

在选择竞价之后,您可以请求一个 Spot 实例。考虑到本教程的目的,我们将以按需定价来出价 (0.03 US),以便能最大化执行出价的机率。您可以通过进入 Amazon EC2 定价页面来确定可用实例的类型和这些实例的按需价格。当 Spot 实例在运行时,您将支付实例运行期间生效的 Spot 价格。Spot 实例的价格由 Amazon EC2 设置,并根据 Spot 实例容量的长期供求趋势逐步调整。您还可以指定您愿意为 Spot 实例支付的金额作为按需实例价格的百分比。要请求 Spot 实例,您只需使用先前选择的参数来构建请求。首先,我们创建一个RequestSpotInstanceRequest数据元。数据元的请求需要要启动的实例数量及其竞价。此外,您还需要设置LaunchSpecification该请求,其中包括实例类型、AMI ID,和要使用的安全组。在填写好该请求后,您可以调用该数据元上的requestSpotInstances方法AmazonEC2Client。以下示例演示了如何请求一个 Spot 实例。

// Create the AmazonEC2 client so we can call various APIs. AmazonEC2 ec2 = AmazonEC2ClientBuilder.defaultClient(); // Initializes a Spot Instance Request RequestSpotInstancesRequest requestRequest = new RequestSpotInstancesRequest(); // Request 1 x t1.micro instance with a bid price of $0.03. requestRequest.setSpotPrice("0.03"); requestRequest.setInstanceCount(Integer.valueOf(1)); // Setup the specifications of the launch. This includes the // instance type (e.g. t1.micro) and the latest Amazon Linux // AMI id available. Note, you should always use the latest // Amazon Linux AMI id or another of your choosing. LaunchSpecification launchSpecification = new LaunchSpecification(); launchSpecification.setImageId("ami-a9d09ed1"); launchSpecification.setInstanceType(InstanceType.T1Micro); // Add the security group to the request. ArrayList<String> securityGroups = new ArrayList<String>(); securityGroups.add("GettingStartedGroup"); launchSpecification.setSecurityGroups(securityGroups); // Add the launch specifications to the request. requestRequest.setLaunchSpecification(launchSpecification); // Call the RequestSpotInstance API. RequestSpotInstancesResult requestResult = ec2.requestSpotInstances(requestRequest);

此代码的运行将启动一个新的 Spot 实例请求。还有其他可用来配置 Spot 请求的选择。要了解更多信息,请访问教程:高级 Amazon EC2 竞价型实例请求管理或《AWS SDK for Java API Reference》中的 RequestSpotInstances 类。

注意

您需为任何已启动的 Spot 实例付费,因此,请确保您取消了任何请求并终止了任何已启动的实例,以便减少所有相关费用。

步骤 4:确定 Spot 请求的状态

下一步是,要一直等到在进行最后一步之前、Spot 请求达到“活跃”状态时再创建代码。为了确定 Spot 请求的状态,我们轮询了 describeSpotInstanceRequests方法来确定要监视的 Spot 请求 ID 的状态。

第 2 步中创建的请求 ID 内嵌在该requestSpotInstances请求响应中。以下示例代码显示了如何从requestSpotInstances响应中收集请求 ID 和如何用它们填写一个ArrayList

// Call the RequestSpotInstance API. RequestSpotInstancesResult requestResult = ec2.requestSpotInstances(requestRequest); List<SpotInstanceRequest> requestResponses = requestResult.getSpotInstanceRequests(); // Setup an arraylist to collect all of the request ids we want to // watch hit the running state. ArrayList<String> spotInstanceRequestIds = new ArrayList<String>(); // Add all of the request ids to the hashset, so we can determine when they hit the // active state. for (SpotInstanceRequest requestResponse : requestResponses) { System.out.println("Created Spot Request: "+requestResponse.getSpotInstanceRequestId()); spotInstanceRequestIds.add(requestResponse.getSpotInstanceRequestId()); }

为哦了监控您的请求 ID,请调用describeSpotInstanceRequests方法来确定该请求的状态。然后循环,直到该请求不处于“打开”的状态。请注意,我们监控的是“打开”这一状态,而不是“活跃”状态,因为如果请求参数有问题,该请求可以直接“关闭”。以下代码示例提供了如何完成此项任务的详细信息。

// Create a variable that will track whether there are any // requests still in the open state. boolean anyOpen; do { // Create the describeRequest object with all of the request ids // to monitor (e.g. that we started). DescribeSpotInstanceRequestsRequest describeRequest = new DescribeSpotInstanceRequestsRequest(); describeRequest.setSpotInstanceRequestIds(spotInstanceRequestIds); // Initialize the anyOpen variable to false - which assumes there // are no requests open unless we find one that is still open. anyOpen=false; try { // Retrieve all of the requests we want to monitor. DescribeSpotInstanceRequestsResult describeResult = ec2.describeSpotInstanceRequests(describeRequest); List<SpotInstanceRequest> describeResponses = describeResult.getSpotInstanceRequests(); // Look through each request and determine if they are all in // the active state. for (SpotInstanceRequest describeResponse : describeResponses) { // If the state is open, it hasn't changed since we attempted // to request it. There is the potential for it to transition // almost immediately to closed or cancelled so we compare // against open instead of active. if (describeResponse.getState().equals("open")) { anyOpen = true; break; } } } catch (AmazonServiceException e) { // If we have an exception, ensure we don't break out of // the loop. This prevents the scenario where there was // blip on the wire. anyOpen = true; } try { // Sleep for 60 seconds. Thread.sleep(60*1000); } catch (Exception e) { // Do nothing because it woke up early. } } while (anyOpen);

运行此代码后, Spot 实例请求会完成或失败,如果失败,将输出一个错误提示到屏幕上。在任一情况下,我们都可以进行下一步,以便清理任何已活跃请求并终止任何正在运行的实例。

步骤 5:清理 Spot 请求和实例

最后,我们需要清理请求和实例。重要的是,要取消所有未完成的请求终止所有实例。只取消请求不会终止您的实例,这意味着您需要继续为它们支付费用。如果您终止了实例,那么 Spot 请求可能会被取消,但在某些情况下,例如,如果您使用的是持久出价,那么终止实例则不足以阻止请求重新执行。因此,最好的做法是取消所有已活跃出价并终止所有正在运行的实例。

以下代码演示了如何取消您的请求。

try { // Cancel requests. CancelSpotInstanceRequestsRequest cancelRequest = new CancelSpotInstanceRequestsRequest(spotInstanceRequestIds); ec2.cancelSpotInstanceRequests(cancelRequest); } catch (AmazonServiceException e) { // Write out any exceptions that may have occurred. System.out.println("Error cancelling instances"); System.out.println("Caught Exception: " + e.getMessage()); System.out.println("Reponse Status Code: " + e.getStatusCode()); System.out.println("Error Code: " + e.getErrorCode()); System.out.println("Request ID: " + e.getRequestId()); }

要终止所有挂起的实例,您需要实例 ID 和启动它们的请求。以下代码示例采用了原代码来监控这些实例,并增加了一个存储这些实例 ID 和相关联的ArrayList响应的describeInstance

// Create a variable that will track whether there are any requests // still in the open state. boolean anyOpen; // Initialize variables. ArrayList<String> instanceIds = new ArrayList<String>(); do { // Create the describeRequest with all of the request ids to // monitor (e.g. that we started). DescribeSpotInstanceRequestsRequest describeRequest = new DescribeSpotInstanceRequestsRequest(); describeRequest.setSpotInstanceRequestIds(spotInstanceRequestIds); // Initialize the anyOpen variable to false, which assumes there // are no requests open unless we find one that is still open. anyOpen = false; try { // Retrieve all of the requests we want to monitor. DescribeSpotInstanceRequestsResult describeResult = ec2.describeSpotInstanceRequests(describeRequest); List<SpotInstanceRequest> describeResponses = describeResult.getSpotInstanceRequests(); // Look through each request and determine if they are all // in the active state. for (SpotInstanceRequest describeResponse : describeResponses) { // If the state is open, it hasn't changed since we // attempted to request it. There is the potential for // it to transition almost immediately to closed or // cancelled so we compare against open instead of active. if (describeResponse.getState().equals("open")) { anyOpen = true; break; } // Add the instance id to the list we will // eventually terminate. instanceIds.add(describeResponse.getInstanceId()); } } catch (AmazonServiceException e) { // If we have an exception, ensure we don't break out // of the loop. This prevents the scenario where there // was blip on the wire. anyOpen = true; } try { // Sleep for 60 seconds. Thread.sleep(60*1000); } catch (Exception e) { // Do nothing because it woke up early. } } while (anyOpen);

使用存储在ArrayList中的实例 ID,通过使用以下代码片段来终止任何正在运行的实例。

try { // Terminate instances. TerminateInstancesRequest terminateRequest = new TerminateInstancesRequest(instanceIds); ec2.terminateInstances(terminateRequest); } catch (AmazonServiceException e) { // Write out any exceptions that may have occurred. System.out.println("Error terminating instances"); System.out.println("Caught Exception: " + e.getMessage()); System.out.println("Reponse Status Code: " + e.getStatusCode()); System.out.println("Error Code: " + e.getErrorCode()); System.out.println("Request ID: " + e.getRequestId()); }

综述

为了将所有内容组合在一起,我们提供了一个更加面向数据元的方法,该方法结合了上文所示步骤:初始化 EC2 客户端,提交 Spot 请求,确定何时 Spot 请求不再处于开放状态,并清理所有延迟的 Spot 请求和相关实例。我们建立一个执行这些操作的类别,命名为Requests

我们还创建了一个 GettingStartedApp 类,为我们执行高级函数调用提供主要方法。具体地,我们对之前所述的数据元Requests进行初始化。提交 Spot 实例请求。然后等待 Spot 请求达到“有效”状态。最后,清理这些请求和实例。

可在 GitHub 查看和下载此示例的完整源代码。

恭喜您!您已经完成了用 AWS SDK for Java 开发 Spot 实例软件的入门教程。

后续步骤

继续阅览教程:高级 Amazon EC2 竞价型实例请求管理