创建和使用 Spot 虚拟机


本页面介绍如何创建和管理 Spot 虚拟机,包括以下主题:

  • 如何创建、启动和识别 Spot 虚拟机
  • 如何检测、处理和测试 Spot 虚拟机的抢占
  • Spot 虚拟机的最佳做法

Spot 虚拟机是具有 Spot 配置模型的虚拟机 (VM) 实例。与标准虚拟机的价格相比,您可以最高按 60-91% 的折扣获得 Spot 虚拟机。但是,Compute Engine 可能会随时抢占 Spot 虚拟机来收回资源。建议只将 Spot 虚拟机用于可承受虚拟机抢占的容错应用。在您决定创建 Spot 虚拟机之前,请确保您的应用可以处理抢占

准备工作

  • 阅读 Spot 虚拟机的概念文档
    • 查看 Spot 虚拟机的限制价格
    • 为了防止 Spot 虚拟机消耗标准虚拟机的 CPU、GPU 和磁盘的配额,请考虑为 Spot 虚拟机申请抢占式配额
  • 如果您尚未设置身份验证,请进行设置。身份验证是通过其进行身份验证以访问 Google Cloud 服务和 API 的过程。如需从本地开发环境运行代码或示例,您可以选择以下任一选项向 Compute Engine 进行身份验证:

    Select the tab for how you plan to use the samples on this page:

    Console

    When you use the Google Cloud console to access Google Cloud services and APIs, you don't need to set up authentication.

    gcloud

    1. Install the Google Cloud CLI, then initialize it by running the following command:

      gcloud init
    2. Set a default region and zone.
    3. Terraform

      如需在本地开发环境中使用本页面上的 Terraform 示例,请安装并初始化 gcloud CLI,然后使用您的用户凭据设置应用默认凭据。

      1. Install the Google Cloud CLI.
      2. To initialize the gcloud CLI, run the following command:

        gcloud init
      3. If you're using a local shell, then create local authentication credentials for your user account:

        gcloud auth application-default login

        You don't need to do this if you're using Cloud Shell.

      如需了解详情,请参阅 Set up authentication for a local development environment

      REST

      如需在本地开发环境中使用本页面上的 REST API 示例,请使用您提供给 gcloud CLI 的凭据。

        Install the Google Cloud CLI, then initialize it by running the following command:

        gcloud init

      如需了解详情,请参阅 Google Cloud 身份验证文档中的使用 REST 时进行身份验证

创建 Spot 虚拟机

使用 Google Cloud 控制台、gcloud CLI 或 Compute Engine API 创建 Spot 虚拟机。Spot 虚拟机是指配置为使用 Spot 预配模型的任何虚拟机:

  • 在 Google Cloud 控制台中将虚拟机预配模型设置为 Spot
  • 在 gcloud CLI 中使用 --provisioning-model=SPOT
  • 在 Compute Engine API 中使用 "provisioningModel": "SPOT"

控制台

  1. 在 Google Cloud 控制台中,转到创建实例页面。

    转到“创建实例”

  2. 之后,执行以下操作:

    1. 可用性政策部分中,从虚拟机预配模型列表中选择 Spot。此设置会对虚拟机停用自动重启和主机维护选项,并启用终止操作选项。
    2. 可选:在虚拟机终止时列表中,选择要在 Compute Engine 抢占虚拟机时执行的操作:
      • 如需在抢占期间停止虚拟机,请选择停止(默认)。
      • 如需在抢占期间删除虚拟机,请选择删除
  3. 可选:指定其他虚拟机选项。如需了解详情,请参阅创建和启动虚拟机实例

  4. 要创建并启动该虚拟机,请点击创建

gcloud

如需通过 gcloud CLI 创建虚拟机,请使用 gcloud compute instances create 命令。如需创建 Spot 虚拟机,您必须添加 --provisioning-model=SPOT 标志。(可选)您也可以通过同时包括 --instance-termination-action 标志来为 Spot 虚拟机指定终止操作。

gcloud compute instances create VM_NAME \
    --provisioning-model=SPOT \
    --instance-termination-action=TERMINATION_ACTION

替换以下内容:

  • VM_NAME:新虚拟机的名称
  • TERMINATION_ACTION(可选):指定在 Compute Engine 抢占虚拟机时要执行的操作(STOP(默认行为)或 DELETE)。

如需详细了解您在创建虚拟机时可以指定的选项,请参阅创建和启动虚拟机实例。 例如,如需创建具有指定机器类型和映像的 Spot 虚拟机,请使用以下命令:

gcloud compute instances create VM_NAME \
    --provisioning-model=SPOT \
    [--image=IMAGE | --image-family=IMAGE_FAMILY] \
    --image-project=IMAGE_PROJECT \
    --machine-type=MACHINE_TYPE \
    --instance-termination-action=TERMINATION_ACTION

替换以下内容:

  • VM_NAME:新虚拟机的名称
  • IMAGE:指定以下其中一项:
    • IMAGE:公共映像或映像系列的特定版本。例如,特定映像为 --image=debian-10-buster-v20200309
    • 映像系列。此项表示通过最新的未弃用的操作系统映像创建虚拟机。例如,如果您指定 --image-family=debian-10,则 Compute Engine 会通过 Debian 10 映像系列中最新版本的操作系统映像创建虚拟机。
  • IMAGE_PROJECT:包含映像的项目。 例如,如果您将 debian-10 指定为映像系列,请将 debian-cloud 指定为映像项目。
  • MACHINE_TYPE:新虚拟机的预定义自定义机器类型。
  • TERMINATION_ACTION(可选):指定在 Compute Engine 抢占虚拟机时要执行的操作(STOP(默认行为)或 DELETE)。

    如需获取可用区中可用的机器类型列表,请使用带有 --zones 标志的 gcloud compute machine-types list 命令

Terraform

您可以通过 Terraform 资源使用调度块创建 Spot 实例


resource "google_compute_instance" "spot_vm_instance" {
  name         = "spot-instance-name"
  machine_type = "f1-micro"
  zone         = "us-central1-c"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  scheduling {
    preemptible                 = true
    automatic_restart           = false
    provisioning_model          = "SPOT"
    instance_termination_action = "STOP"
  }

  network_interface {
    # A default network is created for all GCP projects
    network = "default"
    access_config {
    }
  }
}

REST

如需通过 Compute Engine API 创建虚拟机,请使用 instances.insert 方法。您必须为虚拟机指定机器类型和名称。(可选)您也可以指定启动磁盘的映像。

如需创建 Spot 虚拟机,您必须添加 "provisioningModel": spot 字段。(可选)您也可以通过添加 "instanceTerminationAction" 字段来为 Spot 虚拟机指定终止操作。

POST https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances
{
 "machineType": "zones/ZONE/machineTypes/MACHINE_TYPE",
 "name": "VM_NAME",
 "disks": [
   {
     "initializeParams": {
       "sourceImage": "projects/IMAGE_PROJECT/global/images/IMAGE"
     },
     "boot": true
   }
 ]
 "scheduling":
 {
     "provisioningModel": "SPOT",
     "instanceTerminationAction": "TERMINATION_ACTION"
 },
 ...
}

替换以下内容:

  • PROJECT_ID:要在其中创建虚拟机的项目的 ID
  • ZONE:要在其中创建虚拟机的可用区。该可用区还必须支持要用于新虚拟机的机器类型。
  • MACHINE_TYPE:新虚拟机的预定义自定义机器类型。
  • VM_NAME:新虚拟机的名称
  • IMAGE_PROJECT:包含映像的项目。 例如,如果您将 family/debian-10 指定为映像系列,请将 debian-cloud 指定为映像项目。
  • IMAGE:指定以下其中一项:
    • 公共映像的特定版本。例如,特定映像为 "sourceImage": "projects/debian-cloud/global/images/debian-10-buster-v20200309",其中 debian-cloudIMAGE_PROJECT
    • 映像系列。此项表示通过最新的未弃用的操作系统映像创建虚拟机。例如,如果您指定 "sourceImage": "projects/debian-cloud/global/images/family/debian-10",其中 debian-cloudIMAGE_PROJECT,则 Compute Engine 会通过 Debian 10 映像系列中最新版本的操作系统映像创建虚拟机。
  • TERMINATION_ACTION(可选):指定在 Compute Engine 抢占虚拟机时要执行的操作(STOP(默认行为)或 DELETE)。

如需详细了解您在创建虚拟机时可以指定的选项,请参阅创建和启动虚拟机实例

Go


import (
	"context"
	"fmt"
	"io"

	compute "cloud.google.com/go/compute/apiv1"
	"cloud.google.com/go/compute/apiv1/computepb"
	"google.golang.org/protobuf/proto"
)

// createSpotInstance creates a new Spot VM instance with Debian 10 operating system.
func createSpotInstance(w io.Writer, projectID, zone, instanceName string) error {
	// projectID := "your_project_id"
	// zone := "europe-central2-b"
	// instanceName := "your_instance_name"

	ctx := context.Background()
	imagesClient, err := compute.NewImagesRESTClient(ctx)
	if err != nil {
		return fmt.Errorf("NewImagesRESTClient: %w", err)
	}
	defer imagesClient.Close()

	instancesClient, err := compute.NewInstancesRESTClient(ctx)
	if err != nil {
		return fmt.Errorf("NewInstancesRESTClient: %w", err)
	}
	defer instancesClient.Close()

	req := &computepb.GetFromFamilyImageRequest{
		Project: "debian-cloud",
		Family:  "debian-11",
	}

	image, err := imagesClient.GetFromFamily(ctx, req)
	if err != nil {
		return fmt.Errorf("getImageFromFamily: %w", err)
	}

	diskType := fmt.Sprintf("zones/%s/diskTypes/pd-standard", zone)
	disks := []*computepb.AttachedDisk{
		{
			AutoDelete: proto.Bool(true),
			Boot:       proto.Bool(true),
			InitializeParams: &computepb.AttachedDiskInitializeParams{
				DiskSizeGb:  proto.Int64(10),
				DiskType:    proto.String(diskType),
				SourceImage: proto.String(image.GetSelfLink()),
			},
			Type: proto.String(computepb.AttachedDisk_PERSISTENT.String()),
		},
	}

	req2 := &computepb.InsertInstanceRequest{
		Project: projectID,
		Zone:    zone,
		InstanceResource: &computepb.Instance{
			Name:        proto.String(instanceName),
			Disks:       disks,
			MachineType: proto.String(fmt.Sprintf("zones/%s/machineTypes/%s", zone, "n1-standard-1")),
			NetworkInterfaces: []*computepb.NetworkInterface{
				{
					Name: proto.String("global/networks/default"),
				},
			},
			Scheduling: &computepb.Scheduling{
				ProvisioningModel: proto.String(computepb.Scheduling_SPOT.String()),
			},
		},
	}
	op, err := instancesClient.Insert(ctx, req2)
	if err != nil {
		return fmt.Errorf("insert: %w", err)
	}

	if err = op.Wait(ctx); err != nil {
		return fmt.Errorf("unable to wait for the operation: %w", err)
	}

	instance, err := instancesClient.Get(ctx, &computepb.GetInstanceRequest{
		Project:  projectID,
		Zone:     zone,
		Instance: instanceName,
	})

	if err != nil {
		return fmt.Errorf("createInstance: %w", err)
	}

	fmt.Fprintf(w, "Instance created: %v\n", instance)
	return nil
}

Java


import com.google.cloud.compute.v1.AccessConfig;
import com.google.cloud.compute.v1.AccessConfig.Type;
import com.google.cloud.compute.v1.Address.NetworkTier;
import com.google.cloud.compute.v1.AttachedDisk;
import com.google.cloud.compute.v1.AttachedDiskInitializeParams;
import com.google.cloud.compute.v1.ImagesClient;
import com.google.cloud.compute.v1.InsertInstanceRequest;
import com.google.cloud.compute.v1.Instance;
import com.google.cloud.compute.v1.InstancesClient;
import com.google.cloud.compute.v1.NetworkInterface;
import com.google.cloud.compute.v1.Scheduling;
import com.google.cloud.compute.v1.Scheduling.ProvisioningModel;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class CreateSpotVm {
  public static void main(String[] args)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    // TODO(developer): Replace these variables before running the sample.
    // Project ID or project number of the Google Cloud project you want to use.
    String projectId = "your-project-id";
    // Name of the virtual machine to check.
    String instanceName = "your-instance-name";
    // Name of the zone you want to use. For example: "us-west3-b"
    String zone = "your-zone";

    createSpotInstance(projectId, instanceName, zone);
  }

  // Create a new Spot VM instance with Debian 11 operating system.
  public static Instance createSpotInstance(String projectId, String instanceName, String zone)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    String image;
    // Initialize client that will be used to send requests. This client only needs to be created
    // once, and can be reused for multiple requests.
    try (ImagesClient imagesClient = ImagesClient.create()) {
      image = imagesClient.getFromFamily("debian-cloud", "debian-11").getSelfLink();
    }
    AttachedDisk attachedDisk = buildAttachedDisk(image, zone);
    String machineTypes = String.format("zones/%s/machineTypes/%s", zone, "n1-standard-1");

    // Send an instance creation request to the Compute Engine API and wait for it to complete.
    Instance instance =
            createInstance(projectId, zone, instanceName, attachedDisk, true, machineTypes, false);

    System.out.printf("Spot instance '%s' has been created successfully", instance.getName());

    return instance;
  }

  // disks: a list of compute_v1.AttachedDisk objects describing the disks
  //     you want to attach to your new instance.
  // machine_type: machine type of the VM being created. This value uses the
  //     following format: "zones/{zone}/machineTypes/{type_name}".
  //     For example: "zones/europe-west3-c/machineTypes/f1-micro"
  // external_access: boolean flag indicating if the instance should have an external IPv4
  //     address assigned.
  // spot: boolean value indicating if the new instance should be a Spot VM or not.
  private static Instance createInstance(String projectId, String zone, String instanceName,
                                         AttachedDisk disk, boolean isSpot, String machineType,
                                         boolean externalAccess)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    // Initialize client that will be used to send requests. This client only needs to be created
    // once, and can be reused for multiple requests.
    try (InstancesClient client = InstancesClient.create()) {
      Instance instanceResource =
              buildInstanceResource(instanceName, disk, machineType, externalAccess, isSpot);

      InsertInstanceRequest build = InsertInstanceRequest.newBuilder()
              .setProject(projectId)
              .setRequestId(UUID.randomUUID().toString())
              .setZone(zone)
              .setInstanceResource(instanceResource)
              .build();
      client.insertCallable().futureCall(build).get(60, TimeUnit.SECONDS);

      return client.get(projectId, zone, instanceName);
    }
  }

  private static Instance buildInstanceResource(String instanceName, AttachedDisk disk,
                                                String machineType, boolean externalAccess,
                                                boolean isSpot) {
    NetworkInterface networkInterface =
            networkInterface(externalAccess);
    Instance.Builder builder = Instance.newBuilder()
            .setName(instanceName)
            .addDisks(disk)
            .setMachineType(machineType)
            .addNetworkInterfaces(networkInterface);

    if (isSpot) {
      // Set the Spot VM setting
      Scheduling.Builder scheduling = builder.getScheduling()
              .toBuilder()
              .setProvisioningModel(ProvisioningModel.SPOT.name())
              .setInstanceTerminationAction("STOP");
      builder.setScheduling(scheduling);
    }

    return builder.build();
  }

  private static NetworkInterface networkInterface(boolean externalAccess) {
    NetworkInterface.Builder build = NetworkInterface.newBuilder()
            .setNetwork("global/networks/default");

    if (externalAccess) {
      AccessConfig.Builder accessConfig = AccessConfig.newBuilder()
              .setType(Type.ONE_TO_ONE_NAT.name())
              .setName("External NAT")
              .setNetworkTier(NetworkTier.PREMIUM.name());
      build.addAccessConfigs(accessConfig.build());
    }

    return build.build();
  }

  private static AttachedDisk buildAttachedDisk(String sourceImage, String zone) {
    AttachedDiskInitializeParams initializeParams = AttachedDiskInitializeParams.newBuilder()
            .setSourceImage(sourceImage)
            .setDiskSizeGb(10)
            .setDiskType(String.format("zones/%s/diskTypes/pd-standard", zone))
            .build();
    return AttachedDisk.newBuilder()
            .setInitializeParams(initializeParams)
            // Remember to set auto_delete to True if you want the disk to be deleted
            // when you delete your VM instance.
            .setAutoDelete(true)
            .setBoot(true)
            .build();
  }
}

Python

from __future__ import annotations

import re
import sys
from typing import Any
import warnings

from google.api_core.extended_operation import ExtendedOperation
from google.cloud import compute_v1


def get_image_from_family(project: str, family: str) -> compute_v1.Image:
    """
    Retrieve the newest image that is part of a given family in a project.

    Args:
        project: project ID or project number of the Cloud project you want to get image from.
        family: name of the image family you want to get image from.

    Returns:
        An Image object.
    """
    image_client = compute_v1.ImagesClient()
    # List of public operating system (OS) images: https://cloud.google.com/compute/docs/images/os-details
    newest_image = image_client.get_from_family(project=project, family=family)
    return newest_image


def disk_from_image(
    disk_type: str,
    disk_size_gb: int,
    boot: bool,
    source_image: str,
    auto_delete: bool = True,
) -> compute_v1.AttachedDisk:
    """
    Create an AttachedDisk object to be used in VM instance creation. Uses an image as the
    source for the new disk.

    Args:
         disk_type: the type of disk you want to create. This value uses the following format:
            "zones/{zone}/diskTypes/(pd-standard|pd-ssd|pd-balanced|pd-extreme)".
            For example: "zones/us-west3-b/diskTypes/pd-ssd"
        disk_size_gb: size of the new disk in gigabytes
        boot: boolean flag indicating whether this disk should be used as a boot disk of an instance
        source_image: source image to use when creating this disk. You must have read access to this disk. This can be one
            of the publicly available images or an image from one of your projects.
            This value uses the following format: "projects/{project_name}/global/images/{image_name}"
        auto_delete: boolean flag indicating whether this disk should be deleted with the VM that uses it

    Returns:
        AttachedDisk object configured to be created using the specified image.
    """
    boot_disk = compute_v1.AttachedDisk()
    initialize_params = compute_v1.AttachedDiskInitializeParams()
    initialize_params.source_image = source_image
    initialize_params.disk_size_gb = disk_size_gb
    initialize_params.disk_type = disk_type
    boot_disk.initialize_params = initialize_params
    # Remember to set auto_delete to True if you want the disk to be deleted when you delete
    # your VM instance.
    boot_disk.auto_delete = auto_delete
    boot_disk.boot = boot
    return boot_disk


def wait_for_extended_operation(
    operation: ExtendedOperation, verbose_name: str = "operation", timeout: int = 300
) -> Any:
    """
    Waits for the extended (long-running) operation to complete.

    If the operation is successful, it will return its result.
    If the operation ends with an error, an exception will be raised.
    If there were any warnings during the execution of the operation
    they will be printed to sys.stderr.

    Args:
        operation: a long-running operation you want to wait on.
        verbose_name: (optional) a more verbose name of the operation,
            used only during error and warning reporting.
        timeout: how long (in seconds) to wait for operation to finish.
            If None, wait indefinitely.

    Returns:
        Whatever the operation.result() returns.

    Raises:
        This method will raise the exception received from `operation.exception()`
        or RuntimeError if there is no exception set, but there is an `error_code`
        set for the `operation`.

        In case of an operation taking longer than `timeout` seconds to complete,
        a `concurrent.futures.TimeoutError` will be raised.
    """
    result = operation.result(timeout=timeout)

    if operation.error_code:
        print(
            f"Error during {verbose_name}: [Code: {operation.error_code}]: {operation.error_message}",
            file=sys.stderr,
            flush=True,
        )
        print(f"Operation ID: {operation.name}", file=sys.stderr, flush=True)
        raise operation.exception() or RuntimeError(operation.error_message)

    if operation.warnings:
        print(f"Warnings during {verbose_name}:\n", file=sys.stderr, flush=True)
        for warning in operation.warnings:
            print(f" - {warning.code}: {warning.message}", file=sys.stderr, flush=True)

    return result


def create_instance(
    project_id: str,
    zone: str,
    instance_name: str,
    disks: list[compute_v1.AttachedDisk],
    machine_type: str = "n1-standard-1",
    network_link: str = "global/networks/default",
    subnetwork_link: str = None,
    internal_ip: str = None,
    external_access: bool = False,
    external_ipv4: str = None,
    accelerators: list[compute_v1.AcceleratorConfig] = None,
    preemptible: bool = False,
    spot: bool = False,
    instance_termination_action: str = "STOP",
    custom_hostname: str = None,
    delete_protection: bool = False,
) -> compute_v1.Instance:
    """
    Send an instance creation request to the Compute Engine API and wait for it to complete.

    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone to create the instance in. For example: "us-west3-b"
        instance_name: name of the new virtual machine (VM) instance.
        disks: a list of compute_v1.AttachedDisk objects describing the disks
            you want to attach to your new instance.
        machine_type: machine type of the VM being created. This value uses the
            following format: "zones/{zone}/machineTypes/{type_name}".
            For example: "zones/europe-west3-c/machineTypes/f1-micro"
        network_link: name of the network you want the new instance to use.
            For example: "global/networks/default" represents the network
            named "default", which is created automatically for each project.
        subnetwork_link: name of the subnetwork you want the new instance to use.
            This value uses the following format:
            "regions/{region}/subnetworks/{subnetwork_name}"
        internal_ip: internal IP address you want to assign to the new instance.
            By default, a free address from the pool of available internal IP addresses of
            used subnet will be used.
        external_access: boolean flag indicating if the instance should have an external IPv4
            address assigned.
        external_ipv4: external IPv4 address to be assigned to this instance. If you specify
            an external IP address, it must live in the same region as the zone of the instance.
            This setting requires `external_access` to be set to True to work.
        accelerators: a list of AcceleratorConfig objects describing the accelerators that will
            be attached to the new instance.
        preemptible: boolean value indicating if the new instance should be preemptible
            or not. Preemptible VMs have been deprecated and you should now use Spot VMs.
        spot: boolean value indicating if the new instance should be a Spot VM or not.
        instance_termination_action: What action should be taken once a Spot VM is terminated.
            Possible values: "STOP", "DELETE"
        custom_hostname: Custom hostname of the new VM instance.
            Custom hostnames must conform to RFC 1035 requirements for valid hostnames.
        delete_protection: boolean value indicating if the new virtual machine should be
            protected against deletion or not.
    Returns:
        Instance object.
    """
    instance_client = compute_v1.InstancesClient()

    # Use the network interface provided in the network_link argument.
    network_interface = compute_v1.NetworkInterface()
    network_interface.network = network_link
    if subnetwork_link:
        network_interface.subnetwork = subnetwork_link

    if internal_ip:
        network_interface.network_i_p = internal_ip

    if external_access:
        access = compute_v1.AccessConfig()
        access.type_ = compute_v1.AccessConfig.Type.ONE_TO_ONE_NAT.name
        access.name = "External NAT"
        access.network_tier = access.NetworkTier.PREMIUM.name
        if external_ipv4:
            access.nat_i_p = external_ipv4
        network_interface.access_configs = [access]

    # Collect information into the Instance object.
    instance = compute_v1.Instance()
    instance.network_interfaces = [network_interface]
    instance.name = instance_name
    instance.disks = disks
    if re.match(r"^zones/[a-z\d\-]+/machineTypes/[a-z\d\-]+$", machine_type):
        instance.machine_type = machine_type
    else:
        instance.machine_type = f"zones/{zone}/machineTypes/{machine_type}"

    instance.scheduling = compute_v1.Scheduling()
    if accelerators:
        instance.guest_accelerators = accelerators
        instance.scheduling.on_host_maintenance = (
            compute_v1.Scheduling.OnHostMaintenance.TERMINATE.name
        )

    if preemptible:
        # Set the preemptible setting
        warnings.warn(
            "Preemptible VMs are being replaced by Spot VMs.", DeprecationWarning
        )
        instance.scheduling = compute_v1.Scheduling()
        instance.scheduling.preemptible = True

    if spot:
        # Set the Spot VM setting
        instance.scheduling.provisioning_model = (
            compute_v1.Scheduling.ProvisioningModel.SPOT.name
        )
        instance.scheduling.instance_termination_action = instance_termination_action

    if custom_hostname is not None:
        # Set the custom hostname for the instance
        instance.hostname = custom_hostname

    if delete_protection:
        # Set the delete protection bit
        instance.deletion_protection = True

    # Prepare the request to insert an instance.
    request = compute_v1.InsertInstanceRequest()
    request.zone = zone
    request.project = project_id
    request.instance_resource = instance

    # Wait for the create operation to complete.
    print(f"Creating the {instance_name} instance in {zone}...")

    operation = instance_client.insert(request=request)

    wait_for_extended_operation(operation, "instance creation")

    print(f"Instance {instance_name} created.")
    return instance_client.get(project=project_id, zone=zone, instance=instance_name)


def create_spot_instance(
    project_id: str, zone: str, instance_name: str
) -> compute_v1.Instance:
    """
    Create a new Spot VM instance with Debian 10 operating system.

    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone to create the instance in. For example: "us-west3-b"
        instance_name: name of the new virtual machine (VM) instance.

    Returns:
        Instance object.
    """
    newest_debian = get_image_from_family(project="debian-cloud", family="debian-11")
    disk_type = f"zones/{zone}/diskTypes/pd-standard"
    disks = [disk_from_image(disk_type, 10, True, newest_debian.self_link)]
    instance = create_instance(project_id, zone, instance_name, disks, spot=True)
    return instance

如需创建多个具有相同属性的 Spot 虚拟机,您可以执行以下操作:创建实例模板,然后使用该模板创建代管式实例组 (MIG)。如需了解详情,请参阅最佳做法

启动 Spot 虚拟机

与其他虚拟机一样,Spot 虚拟机会在创建后启动。同样,如果停止 Spot 虚拟机,您可以重启虚拟机以恢复 RUNNING 状态。只要有容量,您就可以根据需要停止和重启抢占式 Spot 虚拟机。如需了解详情,请参阅虚拟机实例生命周期

如果 Compute Engine 停止自动扩缩托管式实例组 (MIG) 或 Google Kubernetes Engine (GKE) 集群中的一个或多个 Spot 虚拟机,则实例组会在资源再次可用时重启虚拟机。

确定虚拟机的预配模型和终止操作

确定虚拟机的预配模型,以查看它是标准虚拟机、Spot 虚拟机还是抢占式虚拟机。对于 Spot 虚拟机,您还可以识别终止操作。您可以使用 Google Cloud 控制台、gcloud CLI 或 Compute Engine API 识别虚拟机的预配模型和终止操作。

控制台

  1. 转到虚拟机实例页面。

    转到“虚拟机实例”页面

  2. 点击要识别的虚拟机的名称虚拟机实例详情页面随即打开。

  3. 转到页面底部的管理部分。在可用性政策子部分中,检查以下选项:

    • 如果虚拟机预配模型设置为 Spot,则虚拟机为 Spot 虚拟机。
      • 虚拟机终止时表示在 Compute Engine 抢占虚拟机时要执行的操作(停止虚拟机或删除虚拟机)。
    • 否则,如果虚拟机预配模型设置为标准-
      • 如果可抢占性选项设置为开启,则虚拟机为抢占式虚拟机。
      • 否则,该虚拟机为标准虚拟机。

gcloud

如需通过 gcloud CLI 描述虚拟机,请使用 gcloud compute instances describe 命令

gcloud compute instances describe VM_NAME

其中 VM_NAME 是您要检查的虚拟机的名称

在输出中,检查 scheduling 字段以识别虚拟机:

  • 如果输出包含设置为 SPOTprovisioningModel 字段(类似于以下内容),则该虚拟机是 Spot 虚拟机。

    ...
    scheduling:
    ...
    provisioningModel: SPOT
    instanceTerminationAction: TERMINATION_ACTION
    ...
    

    其中,TERMINATION_ACTION 表示在 Compute Engine 抢占虚拟机时要执行的操作(停止 (STOP) 虚拟机或删除 (DELETE) 虚拟机)。如果缺少 instanceTerminationAction 字段,则默认值为 STOP

  • 否则,如果输出包含设置为 standardprovisioningModel 字段或者如果输出省略 provisioningModel 字段:

    • 如果输出包含设置为 truepreemptible 字段,则虚拟机为抢占式虚拟机。
    • 否则,该虚拟机为标准虚拟机。

REST

如需通过 Compute Engine API 描述虚拟机,请使用 instances.get 方法

GET https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/VM_NAME

替换以下内容:

  • PROJECT_ID:虚拟机所属项目的 ID
  • ZONE:虚拟机所在的可用区
  • VM_NAME:您要检查的虚拟机的名称

在输出中,检查 scheduling 字段以识别虚拟机:

  • 如果输出包含设置为 SPOTprovisioningModel 字段(类似于以下内容),则该虚拟机是 Spot 虚拟机。

    {
      ...
      "scheduling":
      {
         ...
         "provisioningModel": "SPOT",
         "instanceTerminationAction": "TERMINATION_ACTION"
         ...
      },
      ...
    }
    

    其中,TERMINATION_ACTION 表示在 Compute Engine 抢占虚拟机时要执行的操作(停止 (STOP) 虚拟机或删除 (DELETE) 虚拟机)。如果缺少 instanceTerminationAction 字段,则默认值为 STOP

  • 否则,如果输出包含设置为 standardprovisioningModel 字段或者如果输出省略 provisioningModel 字段:

    • 如果输出包含设置为 truepreemptible 字段,则虚拟机为抢占式虚拟机。
    • 否则,该虚拟机为标准虚拟机。

Go


import (
	"context"
	"fmt"
	"io"

	compute "cloud.google.com/go/compute/apiv1"
	"cloud.google.com/go/compute/apiv1/computepb"
)

// isSpotVM checks if a given instance is a Spot VM or not.
func isSpotVM(w io.Writer, projectID, zone, instanceName string) (bool, error) {
	// projectID := "your_project_id"
	// zone := "europe-central2-b"
	// instanceName := "your_instance_name"
	ctx := context.Background()
	client, err := compute.NewInstancesRESTClient(ctx)
	if err != nil {
		return false, fmt.Errorf("NewInstancesRESTClient: %w", err)
	}
	defer client.Close()

	req := &computepb.GetInstanceRequest{
		Project:  projectID,
		Zone:     zone,
		Instance: instanceName,
	}

	instance, err := client.Get(ctx, req)
	if err != nil {
		return false, fmt.Errorf("GetInstance: %w", err)
	}

	isSpot := instance.GetScheduling().GetProvisioningModel() == computepb.Scheduling_SPOT.String()

	var isSpotMessage string
	if !isSpot {
		isSpotMessage = " not"
	}
	fmt.Fprintf(w, "Instance %s is%s spot\n", instanceName, isSpotMessage)

	return instance.GetScheduling().GetProvisioningModel() == computepb.Scheduling_SPOT.String(), nil
}

Java


import com.google.cloud.compute.v1.Instance;
import com.google.cloud.compute.v1.InstancesClient;
import com.google.cloud.compute.v1.Scheduling;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

public class CheckIsSpotVm {
  public static void main(String[] args)
          throws IOException, ExecutionException, InterruptedException, TimeoutException {
    // TODO(developer): Replace these variables before running the sample.
    // Project ID or project number of the Google Cloud project you want to use.
    String projectId = "your-project-id";
    // Name of the virtual machine to check.
    String instanceName = "your-route-name";
    // Name of the zone you want to use. For example: "us-west3-b"
    String zone = "your-zone";

    boolean isSpotVm = isSpotVm(projectId, instanceName, zone);
    System.out.printf("Is %s spot VM instance - %s", instanceName, isSpotVm);
  }

  // Check if a given instance is Spot VM or not.
  public static boolean isSpotVm(String projectId, String instanceName, String zone)
          throws IOException {
    // Initialize client that will be used to send requests. This client only needs to be created
    // once, and can be reused for multiple requests.
    try (InstancesClient client = InstancesClient.create()) {
      Instance instance = client.get(projectId, zone, instanceName);

      return instance.getScheduling().getProvisioningModel()
              .equals(Scheduling.ProvisioningModel.SPOT.name());
    }
  }
}

Python

from google.cloud import compute_v1


def is_spot_vm(project_id: str, zone: str, instance_name: str) -> bool:
    """
    Check if a given instance is Spot VM or not.
    Args:
        project_id: project ID or project number of the Cloud project you want to use.
        zone: name of the zone you want to use. For example: "us-west3-b"
        instance_name: name of the virtual machine to check.
    Returns:
        The Spot VM status of the instance.
    """
    instance_client = compute_v1.InstancesClient()
    instance = instance_client.get(
        project=project_id, zone=zone, instance=instance_name
    )
    return (
        instance.scheduling.provisioning_model
        == compute_v1.Scheduling.ProvisioningModel.SPOT.name
    )

使用关停脚本处理抢占

如果 Compute Engine 抢占 Spot 虚拟机,您可以使用关停脚本尝试在虚拟机被抢占前执行清理操作。例如,您可以正常停止正在运行的进程,并将检查点文件复制到 Cloud Storage。 值得注意的是,相对于用户发起的关停,抢占通知的关停时长上限更短。如需详细了解抢占通知的关停期,请参阅 Spot 虚拟机概念文档中的抢占过程

下面是一个关停脚本示例,您可以将其添加到正在运行的 Spot 虚拟机中,或者在创建新的 Spot 虚拟机时添加。该脚本会在虚拟机开始关停时运行,然后操作系统的常规 kill 命令会停止所有剩余进程。在正常停止所需程序后,该脚本会将检查点文件并行上传到 Cloud Storage 存储桶。

#!/bin/bash

MY_PROGRAM="PROGRAM_NAME" # For example, "apache2" or "nginx"
MY_USER="LOCAL_USER"
CHECKPOINT="/home/$MY_USER/checkpoint.out"
BUCKET_NAME="BUCKET_NAME" # For example, "my-checkpoint-files" (without gs://)

echo "Shutting down!  Seeing if ${MY_PROGRAM} is running."

# Find the newest copy of $MY_PROGRAM
PID="$(pgrep -n "$MY_PROGRAM")"

if [[ "$?" -ne 0 ]]; then
  echo "${MY_PROGRAM} not running, shutting down immediately."
  exit 0
fi

echo "Sending SIGINT to $PID"
kill -2 "$PID"

# Portable waitpid equivalent
while kill -0 "$PID"; do
   sleep 1
done

echo "$PID is done, copying ${CHECKPOINT} to gs://${BUCKET_NAME} as ${MY_USER}"

su "${MY_USER}" -c "gcloud storage cp $CHECKPOINT gs://${BUCKET_NAME}/"

echo "Done uploading, shutting down."

此脚本假定您满足以下条件:

  • 您已创建至少具备 Cloud Storage 读写权限的虚拟机。如需了解如何创建具有适当范围的虚拟机,请参阅身份验证文档

  • 您已有一个 Cloud Storage 存储桶,且拥有其写入权限。

如需将此脚本添加到虚拟机中,请将此脚本配置为与虚拟机上的应用搭配使用,并将其添加到虚拟机的元数据中。

  1. 复制或下载关停脚本:

    • 在替换以下内容后,复制上述关停脚本:

      • PROGRAM_NAME 是您要关停的进程或程序的名称,例如 apache2nginx
      • LOCAL_USER 是您用于登录虚拟机的用户名。
      • BUCKET_NAME 是您要用于保存程序检查点文件的 Cloud Storage 存储桶的名称。请注意,本例中的存储桶名称不是以 gs:// 开头。
    • 下载关停脚本到本地工作站,然后替换文件中的以下变量:

      • [PROGRAM_NAME] 是您要关停的进程或程序的名称,例如 apache2nginx
      • [LOCAL_USER] 是您用于登录虚拟机的用户名。
      • [BUCKET_NAME] 是您要用于保存程序检查点文件的 Cloud Storage 存储桶的名称。请注意,本例中的存储桶名称不是以 gs:// 开头。
  2. 将关停脚本添加到新虚拟机现有虚拟机

检测 Spot 虚拟机的抢占

使用 Google Cloud 控制台gcloud CLICompute Engine API 确定 Spot 虚拟机是否已被 Compute Engine 抢占。

控制台

您可以通过检查系统活动日志来检查虚拟机是否已被抢占。

  1. 在 Google Cloud 控制台中,转到日志页面。

    转到“日志”

  2. 选择您的项目并点击继续

  3. compute.instances.preempted 添加到按标签过滤或搜索文字字段。

  4. (可选)如果您要查看特定虚拟机的抢占操作,还可以输入虚拟机名称。

  5. 按 Enter 键以应用指定的过滤条件。Google Cloud 控制台会更新日志列表以仅显示虚拟机被抢占的操作。

  6. 在列表中选择一项操作,查看被抢占虚拟机的相关详细信息。

gcloud

gcloud compute operations list 命令过滤条件参数结合使用可以获取您的项目中的抢占事件列表。

gcloud compute operations list \
    --filter="operationType=compute.instances.preempted"

(可选)您可以使用其他过滤条件参数来进一步限定结果的范围。例如,如需仅查看托管式实例组中实例的抢占事件,请使用以下命令:

gcloud compute operations list \
    --filter="operationType=compute.instances.preempted AND targetLink:instances/BASE_INSTANCE_NAME"

其中,BASE_INSTANCE_NAME 是指定为此托管式实例组中所有虚拟机的名称前缀的基本名称。

输出内容类似如下:

NAME                  TYPE                         TARGET                                        HTTP_STATUS STATUS TIMESTAMP
systemevent-xxxxxxxx  compute.instances.preempted  us-central1-f/instances/example-instance-xxx  200         DONE   2015-04-02T12:12:10.881-07:00

操作类型 compute.instances.preempted 表示虚拟机实例已被抢占。您可以使用 gcloud compute operations describe 命令来获取特定抢占操作的相关详细信息。

gcloud compute operations describe SYSTEM_EVENT \
    --zone=ZONE

请替换以下内容:

  • SYSTEM_EVENTgcloud compute operations list 命令输出中的系统事件,例如 systemevent-xxxxxxxx
  • ZONE:系统事件的可用区,例如 us-central1-f

输出类似于以下内容:

...
operationType: compute.instances.preempted
progress: 100
selfLink: https://compute.googleapis.com/compute/v1/projects/my-project/zones/us-central1-f/operations/systemevent-xxxxxxxx
startTime: '2015-04-02T12:12:10.881-07:00'
status: DONE
statusMessage: Instance was preempted.
...

REST

如需获取特定项目和可用区最近的系统操作列表,请使用 zoneOperations.get 方法

GET https://compute.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/operations

替换以下内容:

(可选)如需将响应范围限定为仅显示抢占操作,您可以在 API 请求中添加过滤条件:

operationType="compute.instances.preempted"

或者,如需查看特定虚拟机的抢占操作,请为过滤条件添加 targetLink 参数:

operationType="compute.instances.preempted" AND
targetLink="https://www.googleapis.com/compute/v1/projects/PROJECT_ID/zones/ZONE/instances/VM_NAME

替换以下内容:+ PROJECT_ID项目 ID。+ ZONE可用区。+ VM_NAME:此可用区和项目中特定虚拟机的名称。

该响应包含最近的操作列表。例如,抢占类似于以下内容:

{
  "kind": "compute#operation",
  "id": "15041793718812375371",
  "name": "systemevent-xxxxxxxx",
  "zone": "https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-f",
  "operationType": "compute.instances.preempted",
  "targetLink": "https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-f/instances/example-instance",
  "targetId": "12820389800990687210",
  "status": "DONE",
  "statusMessage": "Instance was preempted.",
  ...
}

或者,您也可以通过虚拟机本身来确定虚拟机是否已被抢占。如果您要处理因 Compute Engine 抢占而导致的关停事件(与处理因关停脚本而导致的正常关停事件的方式不同),那么这种做法会非常实用。为此,只需在元数据服务器中检查虚拟机的默认元数据中的 preempted 值即可。

例如,在虚拟机中使用 curl 获取 preempted 的值:

curl "http://metadata.google.internal/computeMetadata/v1/instance/preempted" -H "Metadata-Flavor: Google"
TRUE

如果此值为 TRUE,则表示虚拟机已被 Compute Engine 抢占;否则此值为 FALSE

如果您要在关停脚本外部使用此命令,则可以将 ?wait_for_change=true 附加到该网址。这将执行挂起的 HTTP GET 请求,该请求仅在元数据更改并且虚拟机已被抢占时才会返回。

curl "http://metadata.google.internal/computeMetadata/v1/instance/preempted?wait_for_change=true" -H "Metadata-Flavor: Google"
TRUE

如何测试抢占设置

您可以在虚拟机上运行模拟维护事件来强制进行抢占。使用此功能可以测试应用如何处理 Spot 虚拟机。请参阅模拟主机维护事件,了解如何在实例上测试维护事件。

您还可以通过停止虚拟机实例来模拟虚拟机抢占,这样做不但可以省去模拟维护事件的操作,还可避免配额限制。

最佳做法

以下是一些可帮助您充分利用 Spot 虚拟机的最佳做法。

  • 使用实例模板。您可以使用实例模板创建具有相同属性的多个 Spot 虚拟机,而不是一次创建一个 Spot 虚拟机。使用 MIG 需要实例模板。或者,您还可以使用批量实例 API 创建多个 Spot 虚拟机。

  • 使用 MIG 按区域分布并自动重新创建 Spot 虚拟机。使用 MIG 可使 Spot 虚拟机上的工作负载更具灵活性和弹性。例如,您可以使用区域级 MIG 在多个可用区中分布虚拟机,这有助于减少资源可用性错误。此外,请使用自动修复在 Spot 虚拟机被抢占后自动重新创建这些虚拟机。

  • 选择较小的机器类型。Spot 虚拟机的资源来自于额外及备用的 Google Cloud 容量。对于较小的机器类型,Spot 虚拟机的容量通常更容易获取,因为这些机器类型所需的 vCPU 和内存等资源也较少。您可能会发现,通过选择较小的自定义机器类型可以增加 Spot 虚拟机的容量;但对于较小的预定义机器类型,容量可能会更大。例如,与 n2-standard-32 预定义机器类型的容量相比,n2-custom-24-96 自定义机器类型的容量可能更大,但 n2-standard-16 预定义机器类型的容量可能会比前者还要大。

  • 在非高峰时段运行大型 Spot 虚拟机集群。Google Cloud 数据中心的负载因地点和时段而异,但通常夜晚和周末的负载最低。因此,夜晚和周末是运行大型 Spot 虚拟机集群的最佳时间。

  • 将您的应用设计成容错且容抢占型应用。请务必应对以下情况:抢占模式会随着时间点的不同而发生变化。例如,如果某个可用区受到部分中断影响,则大量 Spot 虚拟机可能会被抢占,以便为需要在恢复过程中迁移的标准虚拟机腾出空间。在这一小段时间内,抢占率会看起来与其他任何一天完全不同。如果您的应用假设抢占始终以小组形式完成,您可能无法应对此类事件。

  • 重新尝试创建已被抢占的 Spot 虚拟机。如果您的 Spot 虚拟机已被抢占,建议先尝试创建新的 Spot 虚拟机一到两次,然后再恢复为标准虚拟机。建议您根据具体要求在集群中结合使用标准虚拟机和 Spot 虚拟机,以确保工作能够按照适当的速度继续执行。

  • 使用关停脚本。使用可保存作业进度的关停脚本来管理关停和抢占通知,以便作业可以接续上次进度执行,而不用从头开始执行。

后续步骤