

 适用于 Java 的 Amazon SDK 1.x于2025年 end-of-support 12月31日达到。我们建议您迁移到 [Amazon SDK for Java 2.x](https://docs.amazonaws.cn/sdk-for-java/latest/developer-guide/home.html) 以继续获得新功能、可用性改进和安全更新。

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

# 教程：Amazon EC2 竞价型实例
<a name="tutorial-spot-instances-java"></a>

## 概览
<a name="tutor-spot-java-overview"></a>

与按需实例价格相比，通过 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 终止了您的实例，则您无需对任何不满一小时的使用时间付费。

本教程介绍如何使用适用于 Java 的 Amazon SDK 执行以下操作。
+ 提交一个 Spot 请求
+ 判定何时执行该 Spot 请求
+ 取消该 Spot 请求
+ 终止相关实例

## 先决条件
<a name="tutor-spot-java-prereq"></a>

要使用此指南，您必须已安装适用于 Java 的 Amazon SDK 并且已满足其基本安装先决条件。有关更多信息，请参阅[设置适用于 Java 的 Amazon SDK](setup-install.md)。

## 第 1 步：设置您的证书
<a name="tutor-spot-java-credentials"></a>

要开始使用此代码示例，您需要设置 Amazon 凭证。有关具体操作说明，请参阅[设置用于开发的 Amazon 凭证和区域](setup-credentials.md)。

**注意**  
建议您使用 IAM 用户凭证来提供这些值。有关更多信息，请参阅[注册 Amazon 并创建 IAM 用户](signup-create-iam-user.md)。

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

## 第 2 步：设置安全组
<a name="tutor-spot-java-sg"></a>

一个*安全组*可作为一个控制流量进入和流出实例组的防火墙。默认情况下，实例开始运行时没有配置任何安全组，这就意味着，从任何 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());
}
```

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

您还可以使用 Amazon Toolkit for Eclipse 创建安全组。有关更多信息，请参阅[通过 Amazon Cost Explorer 管理安全组](https://docs.amazonaws.cn/toolkit-for-eclipse/v1/user-guide/tke-sg.html)。

## 步骤 3：提交您的 Spot 请求
<a name="tutor-spot-java-submit"></a>

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

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

1. 打开 [Amazon EC2 管理控制台](https://console.amazonaws.cn/ec2/home)。

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

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

有很多方法可以竞价 Spot 实例，如要大致了解各种方法，您应当观看[对 Spot 实例出价](https://www.youtube.com/watch?v=WD9N73F3Fao&feature=player_embedded)视频。然而，为了入门，我们将介绍三种常见的策略：确保成本低于按需定价的竞价；基于所得计算值的竞价；以便尽可能快地获取计算能力的竞价。
+  *降低成本至低于按需实例*您需要进行花费数小时或数天的批处理工作。然而，您可以灵活调整启动和完成时间。您希望看到是否以较低的成本完成了按需实例。您可以通过使用 Amazon Web Services 管理控制台或 Amazon EC2 API 来检查各个类型实例的 Spot 价格历史记录。如需更多信息，请转到[查看 Spot 价格历史记录](https://docs.amazonaws.cn/AWSEC2/latest/UserGuide/using-spot-instances-history.html)。在您分析了给定可用区内所需实例类型的价格记录之后，您有两种可供选择的方法进行竞价：
  + 您可以在现货价格范围（这仍然低于按需定价）的上端竞价，预测您单次现货请求很有可能会达成，并运行足够的连续计算时间来完成此项工作。
  + 或者，您可以通过按需实例价格的百分比形式，指定您愿意为 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 竞价型实例请求管理](tutorial-spot-adv-java.md)或《适用于 Java 的 Amazon SDK API Reference》中的 [RequestSpotInstances](https://docs.amazonaws.cn/sdk-for-java/v1/reference/com/amazonaws/services/ec2/model/RequestSpotInstancesRequest.html) 类。

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

## 步骤 4：确定 Spot 请求的状态
<a name="tutor-spot-java-request-state"></a>

下一步是，要一直等到在进行最后一步之前、Spot 请求达到“活跃”状态时再创建代码。为了确定 Spot 请求的状态，我们轮询了[ describeSpotInstanceRequests](https://docs.amazonaws.cn/sdk-for-java/v1/reference/com/amazonaws/services/ec2/AmazonEC2Client.html#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 请求和实例
<a name="tutor-spot-java-cleaning-up"></a>

最后，我们需要清理请求和实例。重要的是，要取消所有未完成的请求*并*终止所有实例。只取消请求不会终止您的实例，这意味着您需要继续为它们支付费用。如果您终止了实例，那么 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());
}
```

## 综述
<a name="tutor-spot-java-bring-together"></a>

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

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

可在 [GitHub](https://github.com/aws/aws-sdk-java/tree/master/src/samples/AmazonEC2SpotInstances-GettingStarted) 查看和下载此示例的完整源代码。

恭喜您！您已经完成了用适用于 Java 的 Amazon SDK 开发 Spot 实例软件的入门教程。

## 后续步骤
<a name="tutor-spot-java-next"></a>

继续阅览[教程：高级 Amazon EC2 竞价型实例请求管理](tutorial-spot-adv-java.md)。