抢占式实例可能会因为价格因素或者市场供需变化而被强制回收,如果您的业务对实例中断敏感,则需要注意及时感知抢占式实例的中断事件并做出响应处理。为了帮助您更好地开发中断事件处理程序,本文将指导您如何通过不同的方式感知中断事件。
感知中断事件
通过ECS的SDK查询
通过云服务器ECS的DescribeInstances接口查询实例信息,并根据返回的OperationLocks判断实例是否进入待回收状态。
- 若返回空值:实例可持续使用。 
- 若返回 - LockReason值为- Recycling:抢占式实例被中断,处于待回收状态。
SDK调用示例
准备工作
- 创建AccessKey - 由于阿里云账号(主账号)拥有资源的所有权限,其AccessKey一旦泄露风险巨大,所以建议您使用满足最小化权限需求的RAM用户的AccessKey。获取方法请参见创建AccessKey。 
- 为RAM用户授予ECS相关权限 - 给RAM用户授予操作云服务器ECS相关资源的权限。本文提供的示例代码需要创建查询实例信息,建议授予以下权限: - 云产品 - 授予权限 - 云服务器ECS - 本示例选择系统策略:AliyunECSFullAccess 
- 配置访问凭证 - 本文示例代码会在系统环境变量中读取AccessKey作为访问云服务的凭证,具体操作步骤请参见在Linux、macOS和Windows系统配置环境变量。 
- 安装ECS SDK - 获取ECS SDK,本文通过添加Maven依赖的方式来安装。更多安装方式,请参见安装ECS Java SDK。 
初始化客户端
阿里云SDK支持多种访问凭据用于初始化客户端,例如AccessKey和STS Token等,更多方式请参见管理访问凭据。本示例以通过AccessKey初始化客户端为例。
import com.aliyun.ecs20140526.Client;
import com.aliyun.teaopenapi.models.Config;
public class Sample {
    private static Client createClient() throws Exception {
        Config config = new Config()
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
                // Endpoint 请参考 https://api.aliyun.com/product/Ecs
                .setEndpoint("ecs.cn-hangzhou.aliyuncs.com");
        return new Client(config);
    }
}构建接口的请求对象
在构建请求对象之前,请查看该接口的API文档获取参数信息。
// 构造请求对象
DescribeInstancesRequest request = new DescribeInstancesRequest().setRegionId("cn-hangzhou");
发起调用
通过客户端调用OpenAPI时,支持设置运行时参数,例如超时配置、代理配置等,更多信息请查看进阶配置。
// 设置运行时参数
RuntimeOptions runtime = new RuntimeOptions();
// 调用 DescribeInstances 接口
DescribeInstancesResponse response = client.describeInstancesWithOptions(request, runtime);
System.out.println(response.body.toMap());异常处理
Java SDK将异常进行了细致的分类,主要划分为TeaUnretryableException和TeaException。
- TeaUnretryableException:主要是因为网络问题造成,一般是网络问题达到最大重试次数后抛出。 
- TeaException:主要以业务报错为主的异常。 
建议采取合理的措施来处理异常,比如合理地传播异常、记录日志、尝试恢复等,以确保系统的健壮性和稳定性。
完整示例
import com.aliyun.ecs20140526.Client;
import com.aliyun.ecs20140526.models.DescribeInstancesRequest;
import com.aliyun.ecs20140526.models.DescribeInstancesResponse;
import com.aliyun.ecs20140526.models.DescribeInstancesResponseBody;
import com.aliyun.tea.TeaException;
import com.aliyun.tea.TeaUnretryableException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.alibaba.fastjson.JSONArray;
import java.util.Arrays;
public class Sample {
    private static Client createClient() throws Exception {
        Config config = new Config()
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
                // Endpoint 请参考 https://api.aliyun.com/product/Ecs
                .setEndpoint("ecs.cn-hangzhou.aliyuncs.com");
        return new Client(config);
    }
    public static void main(String[] args) {
        try {
            Client client = Sample.createClient();
            // 构造请求对象
            // 设置待查询的一个或多个ECS实例ID。
            JSONArray instanceIds = new JSONArray();
            instanceIds.addAll(Arrays.asList("i-bp145cvd0exyqj****","i-bp1gehfgfrrk4lah****"));
            DescribeInstancesRequest request = new DescribeInstancesRequest()
                    .setRegionId("cn-hangzhou")
                    .setInstanceIds(instanceIds.toJSONString());
            // 设置运行时参数
            RuntimeOptions runtime = new RuntimeOptions();
            while (!instanceIds.isEmpty()) {
              // 调用 DescribeInstances 接口
              DescribeInstancesResponse response = client.describeInstancesWithOptions(request, runtime);
              // 获取实例相关的返回结果。
              DescribeInstancesResponseBody responseBody = response.getBody();
              DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstances instanceList = responseBody.getInstances();
              //获取实例信息,并根据lockReason返回值进行判断
              if (instanceList != null && instanceList.getInstance()!= null && !instanceList.getInstance().isEmpty()) {
                  for (DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstancesInstance instance : instanceList.getInstance()) {
                      // 输出被查询的实例ID与可用区信息。
                      System.out.println("result:instance:" + instance.getInstanceId() + ",az:" + instance.getZoneId());
                      if (instance.getOperationLocks() != null ) {
                          DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstancesInstanceOperationLocks operationLocks  = instance.getOperationLocks();
                          if(operationLocks.getLockReason()!=null && !operationLocks.getLockReason().isEmpty()){
                              for (DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstancesInstanceOperationLocksLockReason lockReason : operationLocks.getLockReason()) {
                                  // 如果实例被锁定,输出指定实例ID以及对应的锁定类型。
                                  System.out.println("instance:" + instance.getInstanceId() + "-->lockReason:" + lockReason.getLockReason() + ",vmStatus:" + instance.getStatus());
                                  if ("Recycling".equals(lockReason.getLockReason())) {
                                      // 输出即将被回收的实例ID信息。
                                      System.out.println("spot instance will be recycled immediately, instance id:" + instance.getInstanceId());
                                      instanceIds.remove(instance.getInstanceId());
                                  }
                              }
                          }
                      }
                  }
                  // 如果抢占式实例还未被锁定,将每隔两分钟查询一次。
                  System.out.print("try describeInstances again later ...");
                  Thread.sleep(2 * 60 * 1000);
              } else {
                  break;
              }
            }
        } catch (TeaUnretryableException ue) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            ue.printStackTrace();
            // 打印错误信息
            System.out.println(ue.getMessage());
            // 打印请求记录,查询错误发生时的请求信息
            System.out.println(ue.getLastRequest());
        } catch (TeaException e) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            e.printStackTrace();
            // 打印错误码
            System.out.println(e.getCode());
            // 打印错误信息,错误信息中包含 RequestId
            System.out.println(e.getMessage());
            // 打印服务端返回的具体错误内容
            System.out.println(e.getData());
        } catch (Exception e) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            e.printStackTrace();
        }
    }
}
返回结果
触发回收时输出结果如下:
result:instance:i-bp1i9c3qiv1qs6nc****,az:cn-hangzhou-i
instance:i-bp1i9c3qiv1qs6nc****-->lockReason:Recycling,vmStatus:Stopped
spot instance will be recycled immediately, instance id:i-bp1i9c3qiv1qs6nc****通过元数据在实例内部查询
您可以在ECS实例内部访问元数据服务(Metadata Service)获知抢占式实例的终止时间。更多关于实例元数据的信息,请参见实例元数据。
获取抢占式实例停机释放时间元数据:instance/spot/termination-time
返回结果:
- 如果返回404:实例可持续使用。 
- 如果返回类似 - 2015-01-05T18:02:00Z格式的信息(UTC时间):实例将于这个时间被回收。
调用示例:
Linux实例
# 获取元数据服务器的访问凭证用于鉴权
TOKEN=`curl -X PUT "http://100.100.100.200/latest/api/token" -H "X-aliyun-ecs-metadata-token-ttl-seconds:<元数据服务器访问凭证有效期>"`
# 查询抢占式实例是否被中断回收
curl -H "X-aliyun-ecs-metadata-token: $TOKEN" http://100.100.100.200/latest/meta-data/instance/spot/termination-timeWindows实例
# 获取元数据服务器的访问凭证用于鉴权
$token = Invoke-RestMethod -Headers @{"X-aliyun-ecs-metadata-token-ttl-seconds" = "<元数据服务器访问凭证有效期>"} -Method PUT -Uri http://100.100.100.200/latest/api/token
# 查询抢占式实例是否被中断回收
Invoke-RestMethod -Headers @{"X-aliyun-ecs-metadata-token" = $token} -Method GET -Uri http://100.100.100.200/latest/meta-data/instance/spot/termination-time通过云监控SDK查询
ECS实例相关的事件都会同步至云监控,您也可直接通过云监控的DescribeSystemEventAttribute接口查询抢占式实例中断事件Instance:PreemptibleInstanceInterruption,根据返回结果中的content字段返回值是否为delete判断抢占式实例是否触发中断回收。
SDK调用示例
准备工作
- 创建AccessKey - 由于阿里云账号(主账号)拥有资源的所有权限,其AccessKey一旦泄露风险巨大,所以建议您使用满足最小化权限需求的RAM用户的AccessKey。获取方法请参见创建AccessKey。 
- 为RAM用户授予CMS相关权限 - 给RAM用户授予操作云监控CMS的权限。本文提供的示例代码需要查询系统事件,建议授予以下权限: - 云产品 - 授予权限 - 云监控CMS - AliyunCloudMonitorFullAccess 
- 配置访问凭证 - 本文示例代码会在系统环境变量中读取AccessKey作为访问云服务的凭证,具体操作步骤请参见在Linux、macOS和Windows系统配置环境变量。 
- 安装CMS SDK - 获取CMS SDK,本文通过添加Maven依赖的方式来安装。更多安装方式,请参见安装CMS Java SDK。 
初始化客户端
阿里云SDK支持多种访问凭据用于初始化客户端,例如AccessKey和STS Token等,更多方式请参见管理访问凭据。本示例以通过AccessKey初始化客户端为例。
import com.aliyun.cms20190101.Client;
import com.aliyun.teaopenapi.models.Config;
public class Sample {
    private static Client createClient() throws Exception {
        Config config = new Config()
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
                // Endpoint 请参考 https://api.aliyun.com/product/Ecs
                .setEndpoint("metrics.cn-hangzhou.aliyuncs.com");
        return new Client(config);
    }
}构建接口的请求对象
配置抢占式实例中断事件的查询参数,更多参数请参见DescribeSystemEventAttribute。
| API | 参数 | 示例取值 | 
| Product | 产品名称缩写:ECS | |
| EventType | 事件类型:StatusNotification | |
| Name | 事件名称:Instance:PreemptibleInstanceInterruption | 
// 构造请求对象
DescribeSystemEventAttributeRequest request = new DescribeSystemEventAttributeRequest()
    .setProduct("ECS");
    .setEventType("StatusNotification");
     .setName("Instance:PreemptibleInstanceInterruption");
发起调用
通过客户端调用OpenAPI时,支持设置运行时参数,例如超时配置、代理配置等,更多信息请查看进阶配置。
// 设置运行时参数
RuntimeOptions runtime = new RuntimeOptions();
// 调用 DescribeInstances 接口
DescribeSystemEventAttributeResponse response = client.describeSystemEventAttributeWithOptions(request, runtime);
System.out.println(response.body.toMap());
异常处理
Java SDK将异常进行了细致的分类,主要划分为TeaUnretryableException和TeaException。
- TeaUnretryableException:主要是因为网络问题造成,一般是网络问题达到最大重试次数后抛出。 
- TeaException:主要以业务报错为主的异常。 
建议采取合理的措施来处理异常,比如合理地传播异常、记录日志、尝试恢复等,以确保系统的健壮性和稳定性。
完整示例
import com.aliyun.cms20190101.Client;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.cms20190101.models.DescribeSystemEventAttributeRequest;
import com.aliyun.cms20190101.models.DescribeSystemEventAttributeResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.tea.TeaUnretryableException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
public class Sample {
    private static Client createClient() throws Exception {
        Config config = new Config()
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
                // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
                // Endpoint 请参考 https://api.aliyun.com/product/Ecs
                .setEndpoint("metrics.cn-hangzhou.aliyuncs.com");
        return new Client(config);
    }
    public static void main(String[] args) {
        try {
            Client client = Sample.createClient();
            // 构造请求对象
            DescribeSystemEventAttributeRequest request = new DescribeSystemEventAttributeRequest()
                .setProduct("ECS");
                .setEventType("StatusNotification");
                .setName("Instance:PreemptibleInstanceInterruption");
            // 设置运行时参数
            RuntimeOptions runtime = new RuntimeOptions();
            // 调用 DescribeSystemEventAttribute 接口
            DescribeSystemEventAttributeResponse response = client.describeSystemEventAttributeWithOptions(request, runtime);
            System.out.println(response.body.toMap());
        } catch (TeaUnretryableException ue) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            ue.printStackTrace();
            // 打印错误信息
            System.out.println(ue.getMessage());
            // 打印请求记录,查询错误发生时的请求信息
            System.out.println(ue.getLastRequest());
        } catch (TeaException e) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            e.printStackTrace();
            // 打印错误码
            System.out.println(e.getCode());
            // 打印错误信息,错误信息中包含 RequestId
            System.out.println(e.getMessage());
            // 打印服务端返回的具体错误内容
            System.out.println(e.getData());
        } catch (Exception e) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            e.printStackTrace();
        }
    }
}
返回结果
根据返回结果判断抢占式实例中断事件。
事件通知的JSON格式如下所示:
{
  "ver": "1.0",
  "id": "2256A988-0B26-4E2B-820A-8A********E5",
  "product": "ECS",
  "resourceId": "acs:ecs:cn-hangzhou:169070********30:instance/i-bp1ecr********5go2go",
  "level": "INFO",
  "name": "Instance:PreemptibleInstanceInterruption",
  "userId": "169070********30",
  "eventTime": "20190409T121826.922+0800",
  "regionId": "cn-hangzhou",
  "content": {
    "instanceId": "i-bp1ecr********5go2go",  
    "action": "delete"                       
  }
}content字段解释如下表所示。更多参数说明,请参见DescribeSystemEventAttribute。
| 字段 | 说明 | 示例值 | 
| instanceId | 抢占式实例的ID。 | i-bp1ecr********5go2go | 
| action | 抢占式实例的操作事件。取值delete时表示抢占式实例中断,将被强制回收。 | delete | 
订阅云监控系统事件
通过订阅云监控系统事件实时监控抢占式实例的中断事件,再通过短信、邮件、函数计算、消息队列、Webhook等不同的渠道推送。
工作流程
操作步骤
- 接收渠道准备 - 轻量消息队列- 在左侧导航栏,选择。 
- 在顶部菜单栏,选择地域。 
- 在队列列表页面,单击创建队列。 
- 在创建队列面板配置以下参数,然后单击确定。 - 名称:队列名称。 
- 消息最大长度:发送到队列的消息体的最大长度。 
- 长轮询时间:当队列中没有消息时,该队列的ReceiveMessage请求的最大等待时长。 
- 消息可见性超时时间:消息从队列中取出后从Active状态变成Inactive状态后的持续时间。 
- 消息保存时长:消息在队列中的最长存活时间。从发送到队列开始经过此参数指定的时间后,不论消息是否被取出都将被删除。 
- 消息延时时间:发送到队列的所有消息将延后此参数指定的时间后被消费。 
- 启用日志功能:是否开启日志管理功能。 
 - 队列列表页面目标队列已创建。 
 - Webhook重要- Webhook服务需要部署在开通公网的服务器上,注意服务器需要开启响应端口的访问权限。 - Java示例代码如下: - import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class WebhookController { @PostMapping("/callback") public ResponseEntity<String> receiveWebhook(@RequestBody String payload) { // 处理payload,例如记录日志或进行业务逻辑处理 System.out.println("Received webhook payload: " + payload); // 返回成功响应 return ResponseEntity.ok("Webhook received"); } }
- 创建订阅策略 - 登录云监控控制台。 
- 在左侧导航栏,选择。 
- 在订阅策略页签,单击创建订阅策略。 
- 在创建订阅策略页面,设置订阅策略的相关参数。 - 本示例仅说明订阅抢占式实例中断事件所涉及的参数,更多参数说明,请参见管理事件订阅(推荐)。 - 基本信息:输入订阅策略名称和描述。 
- 报警订阅: - 订阅类型:选择系统事件 
- 订阅范围: 
- 产品:选择云服务器ECS 
- 事件类型:选择状态通知 
- 事件名称:选择抢占式实例中断通知 
- 事件等级:选择告警(Warning) 
- 应用分组、事件内容和事件资源:均不设置,表示订阅本账号内所有应用分组中的所有ECS实例的系统事件抢占式实例中断通知。 
 
- 合并降噪:使用默认值。 
- 通知:自定义通知方式使用默认通知方式。 
- 推送与集成:点击添加渠道,弹出窗口中点击增加渠道,目标类型选择第1步准备的渠道,其他参数根据提示完成填写即可。更多推送渠道说明,请参见管理推送渠道。 
 
 
- 模拟中断事件 - 抢占式实例的中断事件为被动触发事件,当您在开发抢占式实例中断事件处理程序过程中,无法有效地进行代码调试。您可以借助调试事件订阅模拟抢占式实例中断事件。 - 在订阅策略页签,单击调试事件订阅。 
- 在创建事件调试面板,产品选择云服务器ECS,名称选择抢占式实例中断通知。 - 系统自动生成JSON格式的调试内容,您需要将JSON文件中资源相关的信息替换为待模拟中断事件的抢占式实例的信息。 - 阿里云账号UID变量需要替换为当前登录的阿里云账号UID。
- <resource-id>以及- <instanceId>需要替换为抢占式实例的实例ID。
- <地域ID>需要替换为抢占式实例所属的地域ID。- { "product": "ECS", "resourceId": "acs:ecs:cn-shanghai:阿里云账号UID:instance/<resource-id>", "level": "WARN", "instanceName": "instanceName", "regionId": "<地域ID>", "groupId": "0", "name": "Instance:PreemptibleInstanceInterruption", "content": { "instanceId": "i-2zeg014dfo4y892z***", "instanceName": "wor***b73", "action": "delete" }, "status": "Normal" }
 
- 单击确定。 - 系统提示操作成功,云监控自动根据订阅策略中的通知方式发送一条报警测试通知。 
 
- 接收和查看 - 轻量消息队列- 登录轻量消息队列(原 MNS)控制台,在左侧导航栏,单击队列列表。 
- 在顶部菜单栏,选择地域,然后在队列列表页面,找到目标队列,在其右侧操作列选择收发消息。 
- 可选:在收发消息快速体验页面,单击编辑接收消息参数,在编辑接收消息参数面板配置单次获取最大条数和轮询时间,然后单击确定。 
- 在队列收发消息快速体验页面,单击接收消息按钮,接收消息列表区域显示队列的消息列表。 
- 可选:在消息列表中找到目标消息,在其右侧操作列单击详情,在消息详情对话框中查看消息内容等信息。 
 - Webhook- 在部署了Webhook的程序中查看调用情况和通知内容。 
响应中断事件
如何响应中断,取决于您的实际业务场景和需求,强烈建议您对应用程序进行测试,确保它可以很好地应对抢占式实例的中断回收,下面给出一些思路和建议供您参考:
- 优雅处理中断 - 及时响应中断信号,保存任务处理进度并进行资源清理,终止任务执行。 
- 数据持久化和检查点 - 定期将任务处理进度和中间结果保存到持久化存储(如本地文件或数据库),确保业务重要数据和配置得到了保存。关于数据保留和恢复的配置,请参见抢占式实例数据保留和恢复。 
- 测试集成是否成功 - 在云监控中触发测试事件,检查程序是否正确响应。更多信息,请参见通过轻量消息队列感知并响应抢占式实例中断事件。