使用系统盘快照创建自定义镜像
抢占式实例可能会因为价格因素或者市场供需变化而被强制回收。本文将以Alibaba Cloud SDK for Java为例,介绍如何通过Java代码监控到抢占式实例被回收的中断事件后,系统自动创建实例的系统盘快照并使用快照创建自定义镜像,您可以使用该镜像新建抢占式实例完成实例内的数据恢复。
前提条件
已准备阿里云账号以及对应的访问密钥(AccessKey)。
使用Alibaba Cloud SDK for Java时需要设置阿里云账号的AccessKey信息。AccessKey的获取方式,请参见创建AccessKey。
已在开发环境中安装Java SDK。
您需要在Maven项目中添加以下依赖。具体操作,请参见安装Java SDK。
<project> <modelVersion>4.0.0</modelVersion> <groupId>java.demo</groupId> <artifactId>test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</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> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>20.0</version> </dependency> </dependencies> </project>
背景信息
您在使用抢占式实例时,实例可能会因为价格因素或者市场供需变化而被强制回收,在被完全回收前,实例会进入锁定状态,并触发抢占式实例的中断事件。
您可以基于该事件设置监控机制,并在实例正常运行过程中设置系统盘不随实例一起释放,当接收到抢占式实例的中断事件后,系统通过Java代码自动为系统盘创建快照,再根据系统盘快照自动创建自定义镜像,您可以使用创建好的自定义镜像新建抢占式实例,实现实例内的数据恢复。
设置系统盘不随实例一起释放后,即使抢占式实例被释放,创建系统盘快照、创建自定义镜像等工作不受影响。
本文提供的示例场景中,运维工作流程图如下所示:
注意事项
本文提供的示例代码仅供参考,并不能保证您的实例一定会在5分钟内完成镜像的创建与数据的恢复。
抢占式实例会提前至少5分钟发送实例中断消息,但数据恢复的具体耗时取决于您实例的镜像类型与系统盘文件大小等因素。例如,系统盘文件越大,恢复时间越久。请您使用示例代码前务必自行进行评估与验证。
使用限制
抢占式实例被回收时的数据恢复过程中,存在如下限制:
为了保证磁盘快照不丢失,建议关闭快照随磁盘一起释放的功能。
如果抢占式实例中含有数据盘,且数据盘中有重要数据,建议数据盘设置为不随实例释放。具体操作,请参见步骤二:设置系统盘不随实例一起释放中的《方式二:通过ECS控制台实现》。
当您调用ModifyDiskAttribute接口时设置了不随实例释放(DeleteWithInstance=false)属性,一旦磁盘挂载的ECS实例被安全锁定且OperationLocks中标记了
"LockReason" : "security"
的锁定状态,释放实例时会忽略磁盘的DeleteWithInstance属性而被同时释放。说明您可以设置
DiskIds.N
参数批量修改多个块存储的名称、描述、是否随实例释放等属性。
步骤一:创建抢占式实例
本步骤提供名为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() {
/**
* 初始化请求参数。
* 请确保代码运行环境设置了环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
* 代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取AccessKey的方式进行调用,建议使用更安全的STS方式。
*/
DefaultProfile profile = DefaultProfile.getProfile(regionId, System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_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;
}
}
步骤二:设置系统盘不随实例一起释放
方式一:通过Java代码实现
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.ecs.model.v20140526.DescribeDisksRequest;
import com.aliyuncs.ecs.model.v20140526.DescribeDisksResponse;
import com.aliyuncs.ecs.model.v20140526.DescribeDisksResponse.Disk;
import com.aliyuncs.ecs.model.v20140526.ModifyDiskAttributeRequest;
import com.aliyuncs.ecs.model.v20140526.ModifyDiskAttributeResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import org.apache.commons.collections4.CollectionUtils;
public class DiskRelated {
static IAcsClient client;
/**
* 请将regionId修改为您的抢占式实例所属的地域ID。
*/
static String regionId = "cn-hangzhou";
/**
* 抢占式实例的实例ID。
*/
static String instanceId = "<your-instance-id>";
public static void main(String[] args) {
client = initialization();
Disk disk = getDisks();
if(null == disk){
System.out.println("disk not exist");
return;
}
String diskId = disk.getDiskId();
modifyDiskAttribute(diskId);
Boolean b = diskNotDeleteWithInstance();
if(b){
//如果第一次设置系统盘不随实例一起释放失败,则重新设置一次。
modifyDiskAttribute(diskId);
}
}
/**
* 查询系统盘详情。
*/
public static Disk getDisks(){
DescribeDisksRequest request = new DescribeDisksRequest();
request.setSysRegionId(regionId);
request.setInstanceId(instanceId);
request.setDiskType("system");
try {
DescribeDisksResponse response = client.getAcsResponse(request);
if(CollectionUtils.isEmpty(response.getDisks())){
System.out.println(("disk not exist. instanceId: " + instanceId));
return null;
}
Disk disk = response.getDisks().get(0);
return disk;
} catch (ClientException e) {
e.printStackTrace();
}
return null;
}
/**
* 设置系统盘不随实例一起释放。
*/
public static void modifyDiskAttribute(String diskId){
ModifyDiskAttributeRequest request = new ModifyDiskAttributeRequest();
request.setDeleteWithInstance(false);
request.setSysRegionId(regionId);
request.setDiskId(diskId);
try {
ModifyDiskAttributeResponse response = client.getAcsResponse(request);
System.out.println(response.getRequestId());
} catch (ClientException e) {
e.printStackTrace();
}
}
/**
* 查询系统盘是否随实例一起释放。
*/
public static Boolean diskNotDeleteWithInstance(){
Disk disks = getDisks();
if (disks.getDeleteWithInstance()){
System.out.println(("disk is delete with instance"));
}else {
System.out.println(("disk not delete with instance"));
}
return disks.getDeleteWithInstance();
}
private static IAcsClient initialization() {
/**
* 初始化请求参数。
* 请确保代码运行环境设置了环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
* 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取AccessKey的方式进行调用,建议使用更安全的STS方式。
*/
DefaultProfile profile = DefaultProfile.getProfile(regionId, System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
return new DefaultAcsClient(profile);
}
}
方式二:通过ECS控制台实现
- 登录ECS管理控制台。
- 在左侧导航栏,选择 。
- 在页面左侧顶部,选择目标资源所在的地域。
找到目标抢占式实例,单击实例ID。
在实例详情页,单击云盘页签。
在目标云盘的操作列中,单击
图标,选择编辑属性。
取消勾选云盘随实例释放,然后单击确定。
步骤三:监控到中断事件后自动创建自定义镜像
当监控到抢占式实例产生中断事件后,系统通过Java代码自动为系统盘创建快照,再根据系统盘快照自动创建自定义镜像。
本步骤提供名为CreateSpotImage
的示例类,代码中依次调用了以下接口分别实现功能:
调用DescribeInstances监控抢占式实例的状态。
当监控到抢占式实例产生中断事件后,先调用CreateSnapshot创建系统盘快照,再调用DescribeSnapshots查询快照状态。
创建系统盘快照后,调用CreateImage,根据已创建的系统盘快照创建自定义镜像。
创建自定义镜像后,调用DescribeImages监控自定义镜像的状态,当镜像变为可用状态时,返回提示信息。
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson.JSON;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.ecs.model.v20140526.CreateImageRequest;
import com.aliyuncs.ecs.model.v20140526.CreateImageResponse;
import com.aliyuncs.ecs.model.v20140526.CreateSnapshotRequest;
import com.aliyuncs.ecs.model.v20140526.CreateSnapshotResponse;
import com.aliyuncs.ecs.model.v20140526.DescribeDisksRequest;
import com.aliyuncs.ecs.model.v20140526.DescribeDisksResponse;
import com.aliyuncs.ecs.model.v20140526.DescribeDisksResponse.Disk;
import com.aliyuncs.ecs.model.v20140526.DescribeImagesRequest;
import com.aliyuncs.ecs.model.v20140526.DescribeImagesResponse;
import com.aliyuncs.ecs.model.v20140526.DescribeInstancesRequest;
import com.aliyuncs.ecs.model.v20140526.DescribeInstancesResponse;
import com.aliyuncs.ecs.model.v20140526.DescribeInstancesResponse.Instance;
import com.aliyuncs.ecs.model.v20140526.DescribeSnapshotsRequest;
import com.aliyuncs.ecs.model.v20140526.DescribeSnapshotsResponse;
import com.aliyuncs.ecs.model.v20140526.DescribeSnapshotsResponse.Snapshot;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.google.common.collect.Lists;
/**
* 监控抢占式实例的中断事件。当中断事件发生时自动创建系统盘快照,再根据系统盘快照创建自定义镜像。
* 代码中将会调用以下ECS API:
* DescribeInstances:查询实例信息。
* CreateSnapshot:创建系统盘快照。
* DescribeSnapshots:查询系统盘快照状态。
* CreateImage:创建自定义镜像。
* DescribeImages:查询自定义镜像的状态。
*/
public class CreateSpotImage {
static IAcsClient client;
/**
* 请将regionId修改为您的抢占式实例所属的地域ID。
*/
static String regionId = "cn-hangzhou";
/**
* 抢占式实例的实例ID。
*/
static String instanceId = "<your-instance-id>";
public void main(String[] args) {
client = initialization();
// 步骤一:等待抢占式实例到待回收状态,并产生中断事件。
waitForInstanceMarked();
String diskId = getDiskId();
// 步骤二:当抢占式实例产生中断事件时,自动创建系统盘快照。
String snapshotId = createSnapshot(diskId);
// 步骤三:等待系统盘快照创建成功。
waitCreateSnapshotSuccess(snapshotId);
// 步骤四:根据系统盘快照创建自定义镜像。
String imageId = createImage(snapshotId);
// 步骤五:等待自定义镜像创建成功。
waitCreateImageSuccess(imageId);
}
/**
* 监控抢占式实例的状态,当产生中断事件时,输出实例相关信息。
*/
public void waitForInstanceMarked() {
// 将对象转化为JSON字符串。
ArrayList<String> instanceIds = new ArrayList();
DescribeInstancesResponse.Instance instance = null;
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<Instance> instanceList = response.getInstances();
// 如果未查询到实例信息,则跳出循环。
if (instanceList == null || instanceList.isEmpty()) {
break;
}
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 getDiskId(){
String diskId = null;
DescribeDisksRequest request = new DescribeDisksRequest();
request.setSysRegionId(regionId);
request.setInstanceId(instanceId);
request.setDiskType("system");
try {
DescribeDisksResponse response = client.getAcsResponse(request);
List<Disk> disks = response.getDisks();
if (null == disks || 0 == disks.size() ){
System.out.println("disk not exist. instance: " + instanceId);
return null;
}
Disk disk = disks.get(0);
diskId = disk.getDiskId();
} catch (ClientException e) {
e.printStackTrace();
}
return diskId;
}
/**
* 创建系统盘快照。
*/
public static String createSnapshot(String diskId){
CreateSnapshotRequest request = new CreateSnapshotRequest();
request.setDiskId(diskId);
request.setSnapshotName("disk_test");
CreateSnapshotResponse response = null;
try {
response = client.getAcsResponse(request);
System.out.println(JSON.toJSONString(response));
System.out.println(response.getSnapshotId());
return response.getSnapshotId();
} catch (ClientException e) {
e.printStackTrace();
}
return response.getSnapshotId();
}
/**
* 查询系统盘快照创建是否成功。
*/
public static void waitCreateSnapshotSuccess(String snapshotId){
boolean isSuccess = false;
while (!isSuccess) {
Snapshot snapshot = describeSnapshots(snapshotId);
if (null == snapshot) {
System.err.println("image not exist. imageId: " + snapshotId);
break;
}
if("accomplished".equals(snapshot.getStatus())){
System.out.println("snapshot created successfully.");
isSuccess = true;
}
}
}
/**
* 调用DescribeSnapshots查询系统盘快照状态。
*/
public static Snapshot describeSnapshots(String snapshotId){
DescribeSnapshotsRequest request = new DescribeSnapshotsRequest();
request.setSysRegionId(regionId);
List<String> snapshotIds = Lists.newArrayList(snapshotId);
String s = JSON.toJSONString(snapshotIds);
request.setSnapshotIds(s);
try {
DescribeSnapshotsResponse response = client.getAcsResponse(request);
if (null == response.getSnapshots() || response.getSnapshots().isEmpty()) {
return null;
}
return response.getSnapshots().get(0);
} catch (ClientException e) {
e.printStackTrace();
}
return null;
}
/**
* 创建自定义镜像。
*/
public static String createImage(String snapshotId) {
try {
// 设置CreateImage参数,发送请求。
CreateImageRequest request = new CreateImageRequest();
request.setRegionId(regionId);
request.setSnapshotId(snapshotId);
request.setImageName("image_test");
// 接收调用的返回结果,并输出已创建的自定义镜像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;
}
private static IAcsClient initialization() {
/**
* 初始化请求参数。
* 请确保代码运行环境设置了环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
* 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取AccessKey的方式进行调用,建议使用更安全的STS方式。
*/
DefaultProfile profile = DefaultProfile.getProfile(regionId, System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
return new DefaultAcsClient(profile);
}
}
步骤四:使用自定义镜像新建抢占式实例实现数据恢复
本步骤提供名为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-hangzhou-i";
/**
* 指定创建的ECS实例所使用的实例规格。
*/
static String instanceType = "ecs.s6-c1m1.small";
/**
* 指定步骤三:监控到中断事件后自动创建自定义镜像中创建的自定义镜像ID。
*/
static String imagesId = "<your-image-id>";
/**
* 指定创建的ECS实例所属的交换机ID。
*/
static String vSwitchId = "<your-vsw-id>";
/**
* 指定创建的ECS实例所属的安全组ID。
*/
static String securityGroupId = "<your-sg-id>";
/**
* 指定抢占策略。
*/
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() {
/**
* 初始化请求参数。
* 请确保代码运行环境设置了环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
* 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取AccessKey的方式进行调用,建议使用更安全的STS方式。
*/
DefaultProfile profile = DefaultProfile.getProfile(regionId, System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_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;
}
}