抢占式实例可能会因为价格因素或者市场供需变化而被强制回收。本文将以Alibaba Cloud SDK for Java为例,介绍如何通过Java代码监控到抢占式实例被回收,并在实例被完全回收前完成实例内的数据恢复。

前提条件

  • 已准备阿里云账号以及对应的访问密钥(AccessKey)。

    使用Alibaba Cloud SDK for Java时需要设置阿里云账号的AccessKey信息。AccessKey的获取方式,请参见获取AccessKey

  • 已在开发环境中安装Java SDK。
    您需要在Maven项目中添加以下依赖。具体操作,请参见安装Java SDK
    <dependencies>
           <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.68</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-ecs</artifactId>
                <version>4.23.10</version>
            </dependency>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>4.0.8</version>
            </dependency>
    </dependencies>

背景信息

您在使用抢占式实例时,实例可能会因为价格因素或者市场供需变化而被强制回收,在被完全回收前,实例会进入锁定状态,并触发抢占式实例的中断事件。您可以基于该事件设置监控机制,当接收到抢占式实例的中断事件后,通过Java代码自动为实例创建自定义镜像,并基于创建好的自定义镜像新建抢占式实例,以实现实例内的数据恢复。本文提供的示例场景中,运维工作流程图如下所示:抢占式实例中断与恢复流程图

注意事项

注意 本文提供的示例代码仅供参考,并不能保证您的实例一定会在5分钟内完成镜像的创建与数据的恢复。

抢占式实例会提前至少5分钟发送实例中断消息,但数据恢复的具体耗时取决于您实例的镜像类型与系统盘文件大小等因素。例如,系统盘文件越大,恢复时间越久。请您使用示例代码前务必自行进行评估与验证。

步骤一:创建抢占式实例

本步骤提供名为CreateSpotInstance的示例类,代码中主要通过ECS的RunInstances接口创建抢占式实例。

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.ecs.model.v20140526.RunInstancesRequest;
import com.aliyuncs.ecs.model.v20140526.RunInstancesResponse;
import com.aliyuncs.profile.DefaultProfile;

/**
 * 通过RunInstances创建抢占式实例。
 */
public class CreateSpotInstance {

    static IAcsClient client;
    // 指定地域ID。指定后您创建的ECS实例属于该地域内。
    static String regionId = "cn-hangzhou";
    // 指定可用区ID。指定后您创建的ECS实例属于该可用区内。
    static String zoneId = "cn-hangzhou-i";
    // 指定创建的ECS实例所使用的实例规格。
    static String instanceType = "ecs.s6-c1m1.small";
    // 指定创建的ECS实例所使用的镜像ID。
    static String imagesId = "centos_7_6_x64_20G_alibase_20211130.vhd";
    // 指定创建的ECS实例所属的交换机ID。
    static String vSwitchId = "<your-vSwitchId>";
    // 指定创建的ECS实例所属的安全组ID。
    static String securityGroupId = "<your-securityGroupId>";
    // 指定抢占策略。
    static String spotStrategy = "SpotAsPriceGo";
    // 修改您需要保留抢占式实例的时长。不能确定保留时长时,请设置为0。
    static Integer spotDuration = 0;
    // 指定ECS实例的登录密码。
    static String password = "<your-password>";

    public static void main(String[] args) {
        client = Initialization();
        createInstance();
    }

    private static IAcsClient Initialization() {
        /**
         * 初始化请求参数。
         * 其中变量<your-access-key-id>需要设置为您的阿里云账号的AccessKey ID。
         * <your-access-key-secret>需要设置为您的阿里云账号的AccessKey Secret。
         */
        DefaultProfile profile = DefaultProfile.getProfile(regionId,"<your-access-key-id>", "<your-access-key-secret>");
        return new DefaultAcsClient(profile);
    }

    //创建实例。
    public static String createInstance() {
        try {
            // 设置RunInstances参数,发送请求。
            RunInstancesRequest request = new RunInstancesRequest();
            request.setRegionId(regionId);
            request.setZoneId(zoneId);
            request.setInstanceType(instanceType);
            request.setSpotDuration(spotDuration);
            request.setSpotStrategy(spotStrategy);
            request.setImageId(imagesId);
            request.setVSwitchId(vSwitchId);
            request.setSecurityGroupId(securityGroupId);
            // InstanceChargeType取值为PostPaid时才会生效抢占策略。
            request.setInstanceChargeType("PostPaid");
            request.setPassword(password);
            request.setInternetMaxBandwidthOut(1);
            // 接收调用的返回结果,并输出已创建的ECS实例ID。
            RunInstancesResponse response = client.getAcsResponse(request);
            if (null == response.getInstanceIdSets() || response.getInstanceIdSets().isEmpty()) {
                return null;
            }
            String instanceId = response.getInstanceIdSets().get(0);
            System.out.println("创建的实例ID:" + instanceId);
            return instanceId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

步骤二:监控到中断事件后自动创建自定义镜像

本步骤提供名为CreateSpotImage 的示例类,代码中依次调用了以下接口分别实现功能:
  • 调用DescribeInstances监控抢占式实例的状态。
  • 当监控到抢占式实例产生中断事件后,调用CreateImage为指定的抢占式实例创建自定义镜像。
  • 创建自定义镜像后,调用DescribeImages监控自定义镜像的状态,当镜像变为可用状态时,返回提示信息。
import com.alibaba.fastjson.JSON;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.ecs.model.v20140526.*;
import com.aliyuncs.profile.DefaultProfile;
import java.util.ArrayList;
import java.util.List;

/**
 * 监控抢占式实例的中断事件。当中断事件发生时自动为实例创建自定义镜像。
 * 代码中将会调用以下ECS API:
 * DescribeInstances:查询实例信息
 * CreateImage:创建自定义镜像
 * DescribeImages:查询自定义镜像的状态
 */
public class CreateSpotImage {
    static IAcsClient client;
    // 请将regionId修改为您的抢占式实例所属的地域ID。
    static String regionId = "cn-hangzhou";
    // 抢占式实例的实例ID。
    static String instanceId = "<your-instanceId>";

    public static void main(String[] args) {
        client = Initialization();
        // 步骤一:等待抢占式实例到待回收状态,并产生中断事件。
        waitForInstanceMarked();
        System.out.println("spot instance will be recycled immediately, instance id:" + instanceId);
        // 步骤二:当抢占式实例产生中断事件时,自动为实例创建自定义镜像。
        String image1 = createImage();
        // 步骤三:等待自定义镜像创建成功。
        waitCreateImageSuccess(image1);
    }

    private static IAcsClient Initialization() {
        /**
         * 初始化请求参数。
         * 其中变量<your-access-key-id>需要设置为您的阿里云账号的AccessKey ID。
         * <your-access-key-secret>需要设置为您的阿里云账号的AccessKey Secret。
         */
        DefaultProfile profile = DefaultProfile.getProfile(regionId,"<your-access-key-id>", "<your-access-key-secret>");
        return new DefaultAcsClient(profile);
    }

    // 监控抢占式实例的状态,当产生中断事件时,输出实例相关信息。
    public static void waitForInstanceMarked() {
        // 将对象转化为JSON字符串。
        ArrayList<String> instanceIds = new ArrayList();
        instanceIds.add(instanceId);
        String instanceIdStr = JSON.toJSONString(instanceIds);
        boolean isMarked = false;
        // 判断抢占式实例是否产生中断事件。
        while (!isMarked) {
            try {
                // 设置DescribeInstances参数,发送请求。
                DescribeInstancesRequest request = new DescribeInstancesRequest();
                // 指定抢占式实例所在的地域。
                request.setRegionId(regionId);
                // 指定抢占式实例ID查询。
                request.setInstanceIds(instanceIdStr);
                // 接收调用的返回结果。
                DescribeInstancesResponse response = client.getAcsResponse(request);
                // 获取抢占式实例相关的返回结果。
                List<DescribeInstancesResponse.Instance> instanceList = response.getInstances();
                // 如果未查询到实例信息,则跳出循环。
                if (instanceList == null || instanceList.isEmpty()) {
                    break;
                }
                DescribeInstancesResponse.Instance instance = instanceList.get(0);
                // 如果查询到的实例没有被中断,则重新开始循环。
                if (instance.getOperationLocks() == null || instance.getOperationLocks().size() == 0) {
                    continue;
                }
                for (DescribeInstancesResponse.Instance.LockReason lockReason : instance.getOperationLocks()) {
                    // 如果查询到的实例被中断,则输出指定实例ID以及造成中断的原因。
                    System.out.println("instance:" + instance.getInstanceId() + "-->lockReason:" + lockReason.getLockReason() + ",vmStatus:" + instance.getStatus());
                    if ("Recycling".equals(lockReason.getLockReason())) {
                        isMarked = true;
                    }
                }
                Thread.sleep(2 * 1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 创建自定义镜像。
    public static String createImage() {
        try {
            // 设置CreateImage参数,发送请求。
            CreateImageRequest request = new CreateImageRequest();
            request.setRegionId(regionId);
            request.setInstanceId(instanceId);
            // 接收调用的返回结果,并输出已创建的自定义镜像ID。
            CreateImageResponse response = client.getAcsResponse(request);
            System.out.println("imageID:" + response.getImageId());
            return response.getImageId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // 查询镜像创建是否成功。
    public static void waitCreateImageSuccess(String imageId) {
        boolean isSuccess = false;
        while (!isSuccess) {
            DescribeImagesResponse.Image image = describeImage(imageId);
            if (null == image) {
                System.err.println("image not exist. imageId: " + imageId);
                break;
            }
            if ("Available".equals(image.getStatus())) {
                System.out.println("Image created successfully.");
                isSuccess = true;
            }
        }
    }

    // 调用DescribeImages监控镜像状态。
    public static DescribeImagesResponse.Image describeImage(String imageId) {
        try {
            Thread.sleep(6 * 60 * 1000);
            DescribeImagesRequest imagesRequest = new DescribeImagesRequest();
            imagesRequest.setRegionId(regionId);
            imagesRequest.setImageId(imageId);
            imagesRequest.setPageSize(100);
            DescribeImagesResponse imagesResponse = client.getAcsResponse(imagesRequest);
            if (null == imagesResponse.getImages() || imagesResponse.getImages().isEmpty()) {
                return null;
            }
            return imagesResponse.getImages().get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

步骤三:使用自定义镜像新建抢占式实例实现数据恢复

本步骤提供名为CreateSpotInstanceFromImage 的示例类,代码中调用ECS的RunInstances接口,指定已创建的自定义镜像新建抢占式实例。

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.ecs.model.v20140526.RunInstancesRequest;
import com.aliyuncs.ecs.model.v20140526.RunInstancesResponse;
import com.aliyuncs.profile.DefaultProfile;

/**
 *通过RunInstances创建抢占式实例。
 */
public class CreateSpotInstanceFromImage {

    static IAcsClient client;
    // 指定实例所属的地域ID。建议与源抢占式实例所属地域保持一致。
    static String regionId = "cn-hangzhou";
    // 指定实例所属的可用区ID。建议与源抢占式实例所属可用区保持一致。
    static String zoneId = "cn-shanghai-l";
    // 指定创建的ECS实例所使用的实例规格。
    static String instanceType = "ecs.s6-c1m1.small";
    // 指定已创建的自定义镜像ID。
    static String imagesId = "<your-imagesId>";
    // 指定创建的ECS实例所属的交换机ID。
    static String vSwitchId = "<your-vSwitchId>";
    // 指定创建的ECS实例所属的安全组ID。
    static String securityGroupId = "<your-securityGroupId>";
    // 指定抢占策略。
    static String spotStrategy = "SpotAsPriceGo";
    // 修改您需要保留抢占式实例的时长。不能确定保留时长时,请设置为0。
    static Integer spotDuration = 0;
    // 指定ECS实例的登录密码。
    static String password = "<your-passwd>";

    public static void main(String[] args) {
        client = Initialization();
        createInstance();
    }

    private static IAcsClient Initialization() {
        /**
         * 初始化请求参数。
         * 其中变量<your-access-key-id>需要设置为您的阿里云账号的AccessKey ID。
         * <your-access-key-secret>需要设置为您的阿里云账号的AccessKey Secret。
         */
        DefaultProfile profile = DefaultProfile.getProfile(regionId,"<your-access-key-id>", "<your-access-key-secret>");
        return new DefaultAcsClient(profile);
    }

    //调用RunInstances创建实例。
    public static String createInstance() {
        try {
            RunInstancesRequest request = new RunInstancesRequest();
            request.setRegionId(regionId);
            request.setZoneId(zoneId);
            request.setInstanceType(instanceType);
            request.setSpotDuration(spotDuration);
            request.setSpotStrategy(spotStrategy);
            request.setImageId(imagesId);
            request.setVSwitchId(vSwitchId);
            request.setSecurityGroupId(securityGroupId);
            request.setInstanceChargeType("PostPaid");
            request.setPassword(password);
            request.setInternetMaxBandwidthOut(1);
            RunInstancesResponse response = client.getAcsResponse(request);
            if (null == response.getInstanceIdSets() || response.getInstanceIdSets().isEmpty()) {
                return null;
            }
            String instanceId = response.getInstanceIdSets().get(0);
            System.out.println("创建的实例ID: " + instanceId);
            return instanceId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}