创建抢占式实例的自定义镜像并基于该镜像创建实例以恢复被回收实例上的数据

抢占式实例可能会因为价格因素或者市场供需变化而被强制回收。如果您的抢占式实例上保存了重要数据,为避免数据丢失,您可以为该抢占式实例创建自定义镜像,并基于抢占式实例中断事件设置监控机制,接收到抢占式实例的中断事件后,系统自动使用该镜像新建抢占式实例,实现实例内数据恢复。

工作原理

使用抢占式实例时,实例可能会因为价格因素或者市场供需变化而被强制回收,在被完全回收前,实例会进入锁定状态,并触发抢占式实例的中断事件。

您可以基于该事件设置监控机制,当接收到抢占式实例的中断事件后,通过Java SDK 2.0代码自动为实例创建自定义镜像,并基于创建好的自定义镜像新建抢占式实例,以实现实例内的数据恢复。

本文提供的示例场景中,运维工作流程图如下所示:

image

前提条件

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

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

  • 已配置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。具体操作,请参见配置环境变量

  • 已在开发环境中安装ECS Java SDK 2.0。

    您需要在Maven项目中添加以下依赖。具体操作,请参见集成SDK

    <dependencies>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>ecs20140526</artifactId>
            <version>5.1.8</version>
        </dependency>
    
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>20.0</version>
        </dependency>
    
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
    
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
    
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    
    </dependencies>

注意事项

重要

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

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

步骤一:创建抢占式实例

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

import com.aliyun.ecs20140526.Client;
import com.aliyun.ecs20140526.models.RunInstancesRequest;
import com.aliyun.ecs20140526.models.RunInstancesResponse;

public class CreateSpotInstance {
    static Client client;
    // 指定地域ID。指定后您创建的ECS实例属于该地域内。
    static String regionId = "cn-hangzhou";
    // 指定可用区ID。指定后您创建的ECS实例属于该可用区内。
    static String zoneId = "cn-hangzhou-i";
    // 指定创建的ECS实例所使用的实例规格。
    static String instanceType = "ecs.e-c1m1.large";
    // 指定创建的ECS实例所使用的镜像ID。
    static String imagesId = "aliyun_3_9_x64_20G_alibase_20231219.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) throws Exception {
        client = createClient();
        createInstance();
    }

    private static Client createClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.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
        config.endpoint = "ecs-cn-hangzhou.aliyuncs.com";
        return new Client(config);
    }

    //创建实例。
    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.runInstances(request);
            if (null == response.getBody().getInstanceIdSets() || response.getBody().getInstanceIdSets().getInstanceIdSet().isEmpty()) {
                return null;
            }
            String instanceId = response.getBody().getInstanceIdSets().getInstanceIdSet().get(0);
            System.out.println("创建的实例ID:" + instanceId);
            return instanceId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

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

本步骤提供名为CreateSpotImage 的示例类,代码中依次调用了以下接口分别实现功能:

  • 调用DescribeInstances监控抢占式实例的状态。

  • 当监控到抢占式实例产生中断事件后,调用CreateImage为指定的抢占式实例创建自定义镜像。

  • 创建自定义镜像后,调用DescribeImages监控自定义镜像的状态,当镜像变为可用状态时,返回提示信息。

import com.aliyun.ecs20140526.Client;
import com.aliyun.ecs20140526.models.CreateImageRequest;
import com.aliyun.ecs20140526.models.CreateImageResponse;
import com.aliyun.ecs20140526.models.DescribeImagesRequest;
import com.aliyun.ecs20140526.models.DescribeImagesResponse;
import com.aliyun.ecs20140526.models.DescribeImagesResponseBody.DescribeImagesResponseBodyImagesImage;
import com.aliyun.ecs20140526.models.DescribeInstancesRequest;
import com.aliyun.ecs20140526.models.DescribeInstancesResponse;
import com.aliyun.ecs20140526.models.DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstances;
import com.aliyun.ecs20140526.models.DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstancesInstance;
import com.aliyun.ecs20140526.models.DescribeInstancesResponseBody.DescribeInstancesResponseBodyInstancesInstanceOperationLocksLockReason;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.List;

import com.alibaba.fastjson.JSON;

public class CreateSpotImage {
    static Client client;
    // 请将regionId修改为您的抢占式实例所属的地域ID。
    static String regionId = "cn-hangzhou";
    // 抢占式实例的实例ID。
    static String instanceId = "<your-instanceId>";

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

    private static Client createClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.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
        config.endpoint = "ecs-cn-hangzhou.aliyuncs.com";
        return new Client(config);
    }

    // 监控抢占式实例的状态,当产生中断事件时,输出实例相关信息。
    public static void waitForInstanceMarked() {
        // 将对象转化为JSON字符串。
        List<String> instanceIds = Lists.newArrayList();
        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.describeInstances(request);
                // 获取抢占式实例相关的返回结果。
                DescribeInstancesResponseBodyInstances instances = response.getBody().getInstances();
                // 如果未查询到实例信息,则跳出循环。
                if (CollectionUtils.isEmpty(instances.getInstance())) {
                    break;
                }
                DescribeInstancesResponseBodyInstancesInstance instance = instances.getInstance().get(0);
                // 如果查询到的实例没有被中断,则重新开始循环。
                if (instance.getOperationLocks() == null || instance.getOperationLocks().getLockReason().size() == 0) {
                    continue;
                }
                for (DescribeInstancesResponseBodyInstancesInstanceOperationLocksLockReason lockReason : instance
                    .getOperationLocks().getLockReason()) {
                    // 如果查询到的实例被中断,则输出指定实例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.createImage(request);
            System.out.println("imageID:" + response.getBody().getImageId());
            return response.getBody().getImageId();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // 查询镜像创建是否成功。
    public static void waitCreateImageSuccess(String imageId) {
        boolean isSuccess = false;
        while (!isSuccess) {
            DescribeImagesResponseBodyImagesImage 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 DescribeImagesResponseBodyImagesImage 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.describeImages(imagesRequest);
            if (null == imagesResponse.getBody().getImages() || CollectionUtils.isEmpty(imagesResponse.getBody().getImages().getImage())) {
                return null;
            }
            return imagesResponse.getBody().getImages().getImage().get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

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

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

import com.aliyun.ecs20140526.Client;
import com.aliyun.ecs20140526.models.RunInstancesRequest;
import com.aliyun.ecs20140526.models.RunInstancesResponse;

public class CreateSpotInstanceFromImage {
    static Client client;
    // 指定实例所属的地域ID。建议与源抢占式实例所属地域保持一致。
    static String regionId = "cn-hangzhou";
    // 指定实例所属的可用区ID。建议与源抢占式实例所属可用区保持一致。
    static String zoneId = "cn-hangzhou-i";
    // 指定创建的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-password>";


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

    private static Client createClient() throws Exception {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.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
        config.endpoint = "ecs-cn-hangzhou.aliyuncs.com";
        return new Client(config);
    }

    //调用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.runInstances(request);
            if (null == response.getBody().getInstanceIdSets() || response.getBody().getInstanceIdSets().getInstanceIdSet().isEmpty()) {
                return null;
            }
            String instanceId = response.getBody().getInstanceIdSets().getInstanceIdSet().get(0);
            System.out.println("创建的实例ID:" + instanceId);;
            return instanceId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}