Android Map | Article Map
Terraform @ Scale - 第 5a 部分:理解API限制

Color logo   no background

    Terraform @ Scale - 第 5a 部分:理解API限制

    现在是一个普通星期二下午的14:30。一家瑞士金融服务商的DevOps团队例行启动其Terraform流水线,执行每月的灾难恢复测试(Disaster-Recovery-Testing)。计划在备用区域(Backup-Region)中部署300台虚拟机、150个负载均衡器后端、500条DNS记录以及无数网络规则。
    5分钟后,流水线中断。HTTP 429: Too Many Requests
    接下来的3个小时,团队花费大量时间手动清理部分已创建的资源,而管理层则紧张地盯着时钟。
    灾备测试还未真正开始就已经宣告失败。

    发生了什么?团队踩中了云端自动化中最隐蔽的陷阱之一:API速率限制(API Rate Limits)。当Terraform尝试并行创建数百个资源时,云服务提供商在收到前100个每分钟的请求后,就触发了紧急限流。一个在部署10台虚拟机时从未出现的问题,在部署300台虚拟机时却变成了一堵无法逾越的高墙。

    看不见的墙:理解API限制

    所有大型云服务提供商都会实施API速率限制,以保证其服务的稳定性。这些限制并非人为刁难,而是必要的保护机制。没有这些限制,一个有缺陷的Terraform运行就可能导致该提供商所有客户的API端点瘫痪。

    Oracle Cloud Infrastructure 实施了API速率限制,当IAM检测到超出限制时会返回429错误码。具体的限制因服务而异,并非统一公布,而是按服务类型分别实施。OCI会为每个租户设定服务限制,这些限制要么是在采购时与Oracle销售代表商定,要么在标准/试用情况下自动设定。

    AWS在EC2 API限流中采用令牌桶(Token-Bucket)机制。对于RunInstances操作,其资源令牌桶容量为1000个令牌,补充速率为每秒两个令牌。这意味着您可以一次性启动1000台实例,之后每秒最多再启动两台。API操作会按类别分组,其中Non-mutating Actions(Describe*、List*、Search*、Get*)通常拥有最高的API限流阈值。
    [注:Non-mutating Actions是指不会改变云端状态、仅进行数据读取的API操作。AWS(以及其他提供商)通常会对这类操作设置更高的速率限制,因为它们不会创建、修改或删除资源,因此对后端系统稳定性的影响较小。]

    Terraform本身会因为并行化而加剧这一问题。默认情况下,Terraform会同时尝试创建或修改最多10个资源。在复杂的依赖关系图以及大量独立资源的情况下,这很快会导致数十个同时进行的API调用。如果再叠加我们在本系列前文中讨论过的Data Sources,就会形成一个所谓的“完美风暴”(perfect storm)API请求模式。

    并行化悖论

    Terraform的默认并行化是一把双刃剑。一方面,它能显著加快部署速度,没有人愿意在资源按顺序逐个创建时等待数小时;但另一方面,正是这种优化在大规模基础设施中带来了问题。

    让我们来看一个典型场景:一个用于多层应用(Multi-Tier Application)的Terraform配置,包括Web层、应用层和数据库层。每一层都有多个实例,分布在不同的可用域(Availability Domains)中:


    # A seemingly harmless root module, which will turn into an API bomb
    module "web_tier" {
      source = "./modules/compute-cluster"
    
      instance_count = 50
      instance_config = {
        shape  = "VM.Standard.E4.Flex"
        ocpus  = 2
        memory_in_gbs = 16
      }
    
      availability_domains = data.oci_identity_availability_domains.ads.availability_domains
      subnet_ids           = module.network.web_subnet_ids
    
      load_balancer_backend_sets = {
        for idx, val in range(50) :
        "web-${idx}" => module.load_balancer.backend_set_name
      }
    }
    
    module "app_tier" {
      source = "./modules/compute-cluster"
    
      instance_count = 30
      # ... similar configuration
    }
    
    module "monitoring" {
      source = "./modules/monitoring"
    
      # Create monitoring alarms for each instance
      monitored_instances = concat(
        module.web_tier.instance_ids,
        module.app_tier.instance_ids
      )
    
      alarms_per_instance = 5  # CPU, Memory, Disk, Network, Application
    }

    这个看似无害的配置在执行时会触发:

    • 50次API调用用于创建Web层实例
    • 30次API调用用于创建应用层实例
    • 50次API调用用于负载均衡器后端注册
    • 400次API调用用于创建监控告警,80个实例 × 每实例5个告警

    总计530次API调用,Terraform会尽量并行执行。在每秒仅允许10个写操作的限制下,即使完美串行化,也要接近一分钟才能完成。

    而在实际情况中,限流会导致重试循环(Retry Loop)、指数退避(Exponential Backoff),并在最糟糕的情况下触发超时(Timeout)和执行中断,留下部分已创建的资源需要手动清理。

    Terraform -target:错误的解决方案

    在走投无路时,许多团队会选择使用terraform apply -target,以选择性地创建资源。计划是这样的:“我们先部署网络,再部署计算实例,最后部署监控”。但这是一个比解决问题带来更多麻烦的危险做法。

    -target参数是为紧急情况下的外科手术式变更而设计的,不是用于常规部署。

    它会绕过Terraform的依赖管理,可能导致状态不一致(State Inconsistency)。

    更糟糕的是,它无法扩展。当资源数量达到数百时,您要么必须单独针对每种资源类型进行target操作(使复杂性急剧上升),要么编写复杂的脚本,让Terraform用不同的Target多次运行。

    以下是我在实践中见过的一种反模式:


    #!/bin/bash
    # DON'T TRY THIS AT HOME - THIS IS AN ANTIPATTERN!
    # Note: This script requires GNU grep.
    
    # Phase 1: Network
    terraform apply -target=module.network -auto-approve
    
    # Phase 2: Compute (in Batches)
    for i in {0..4}; do
      start=$((i*10))
      end=$((start+9))
      for j in $(seq "$start" "$end"); do
        terraform apply -target="module.web_tier.oci_core_instance.this[${j}]" -auto-approve
      done
      sleep 30  # "Cooldown" period
    done
    
    # Phase 3: Monitoring
    terraform apply -target=module.monitoring -auto-approve

    这个脚本也许能运行,但它脆弱、难以维护且容易出错:

    1. 它治标不治本。
    2. 此外,您会失去Terraform的幂等性(Idempotenz) - 如果脚本中途停止,将无法确定哪些资源已创建。

    正确的解决方案:控制并行度

    解决API限制问题最优雅的方法是控制Terraform的并行化。Terraform为此提供了-parallelism参数:


     terraform apply -parallelism=5
    

    这会将同时运行的操作数从10减少到5。在极度敏感的环境中,这个值甚至可以进一步降低:


    terraform apply -parallelism=1 
    

    此时,所有资源都会严格按顺序依次创建。是的,这会非常耗时,因此这里只是学术示例。

    而且,这通常只是冰山一角。在生产环境中,您可能需要更为精细的策略。

    以下是一个几近“偏执”的Wrapper脚本示例,它会根据需要创建的资源数量动态调整并行度:


    #!/bin/bash
    # Intelligent control of parallelism based on the number of planned operations
    # Requires: jq, GNU bash
    
    set -euo pipefail
    
    # Create a fresh plan and capture exit code (0=no changes, 2=changes, 1=error)
    create_plan() {
      if terraform plan -out=tfplan -detailed-exitcode; then
        return 0
      fi
      local ec=$?
      if [ "$ec" -eq 2 ]; then
        return 0
      fi
      echo "Plan failed." >&2
      exit "$ec"
    }
    
    # Retrieve the number of operations from the JSON plan
    get_resource_count() {
      # JSON output, no colors, parse via jq
      local count
      count=$(terraform show -no-color -json tfplan 2>/dev/null \
        | jq '[.resource_changes[] 
                | select(.change.actions[] != "no-op")] 
               | length')
      echo "${count:-0}"
    }
    
    # Compute provider-aware parallelism (heuristic)
    calculate_parallelism() {
      local resource_count=$1
      local cloud_provider=${CLOUD_PROVIDER:-oci} # Set default value "oci" if unset
      cloud_provider="${cloud_provider,,}"  # Allow Case-insensitive value
    
      case "$cloud_provider" in
        oci)
          if   [ "$resource_count" -lt 20  ]; then echo 10
          elif [ "$resource_count" -lt 50  ]; then echo 5
          elif [ "$resource_count" -lt 100 ]; then echo 3
          else                                   echo 1
          fi
          ;;
        aws)
          if   [ "$resource_count" -lt 50  ]; then echo 10
          elif [ "$resource_count" -lt 100 ]; then echo 5
          else                                   echo 2
          fi
          ;;
        *)
          echo 5
          ;;
      esac
    }
    
    echo "Analyzing Terraform plan..."
    create_plan
    resource_count=$(get_resource_count)
    echo "Planned resource operations: ${resource_count}"
    
    if [ "$resource_count" -eq 0 ]; then
      echo "No changes necessary."
      rm -f tfplan
      exit 0
    fi
    
    parallelism=$(calculate_parallelism "$resource_count")
    echo "Using parallelism: ${parallelism}"
    
    # Execute apply against the saved plan with computed parallelism
    terraform apply -parallelism="${parallelism}" tfplan
    
    # Clean up plan file
    rm -f tfplan

    Provider级别的限流与重试机制

    对于使用官方OCI SDK自行开发的OCI工具,您可以通过环境变量控制其行为。SDK支持如下配置:


    export OCI_SDK_DEFAULT_RETRY_ENABLED=true
    export OCI_SDK_DEFAULT_RETRY_MAX_ATTEMPTS=5
    export OCI_SDK_DEFAULT_RETRY_MAX_WAIT_TIME=30
    

    这些设置并不能替代良好的架构设计,但它们能帮助缓冲临时的限流高峰。

    OCI Terraform Provider中的重试选项

    OCI Terraform Provider在Provider块中支持两个配置选项,用于控制在特定API错误发生时自动重试的行为:

    disable_auto_retries(boolean):如果设置为true,将禁用Provider的所有自动重试,无论retry_duration_seconds的值是多少。默认值:false。

    retry_duration_seconds(integer):设置在特定HTTP错误下Provider执行重试的最小时间窗口(单位:秒)。此选项仅在HTTP状态码429和500时生效。所配置的时间被解释为最小值,由于采用了带全随机抖动(full jitter)的平方退避(quadratic backoff)机制,实际持续时间可能更长。如果disable_auto_retries为true,则此值会被忽略。默认值:600秒。

    默认行为:在默认设置下(disable_auto_retries = falseretry_duration_seconds = 600),Provider会在收到HTTP 429和HTTP 500响应时自动进行重试。其他HTTP错误(如400、401、403、404或409)不会触发该机制的重试。

    配置示例:


    provider "oci" {
      # Automatic retries are enabled by default
      disable_auto_retries   = false
      retry_duration_seconds = 600  # Minimum window for retries (in seconds)
    }

     

    AWS Terraform Provider中的重试配置

    在AWS中,该机制因服务不同而差异较大。Provider中的全局配置用于限制最大重试次数;回退(Backoff)策略的模式由AWS SDK决定。实际用例如下:


    provider "aws" {
      region = var.region
      # Maximum number of retry attempts if an API call fails
      max_retries = 5
      # Retry strategy:
      # "adaptive": adjusts dynamically to latency and error rates (good for throttling)
      # "standard": deterministic exponential backoff
      retry_mode = "adaptive"
    }

     

    架构模式:Resource Batching

    避免触发API限制的一种高级技巧是Resource Batching(资源分批)。其核心是将资源分组为逻辑单元并按顺序部署。在下面的示例中,我们有意使用time Provider及其time_sleep资源类型,在批次之间插入人工延迟。这样可以确保下一个批次仅在延迟时间结束后才开始执行。


    terraform {
      required_version = ">= 1.5.0"
    
      required_providers {
        oci  = { source = "oracle/oci" }
        time = { source = "hashicorp/time" }
      }
    }
    
    variable "batch_size" {
      type        = number
      default     = 10
      description = "Number of instances per batch"
    }
    
    variable "batch_delay_seconds" {
      type        = number
      default     = 30
      description = "Wait time between batches (seconds)"
    }
    
    variable "total_instances" {
      type        = number
      description = "Total number of instances to create"
    }
    
    variable "name_prefix" {
      type        = string
      description = "Prefix for instance display_name"
    }
    
    variable "availability_domains" {
      type        = list(string)
      description = "ADs to spread instances across"
    }
    
    variable "subnet_ids" {
      type        = list(string)
      description = "Subnets to distribute NICs across"
    }
    
    variable "compartment_id" {
      type        = string
    }
    
    variable "instance_shape" {
      type        = string
    }
    
    variable "instance_ocpus" {
      type        = number
    }
    
    variable "instance_memory" {
      type        = number
    }
    
    variable "image_id" {
      type        = string
    }
    
    locals {
      batch_count = ceil(var.total_instances / var.batch_size)
    
      # Build batches with instance index metadata
      batches = {
        for batch_idx in range(local.batch_count) :
        "batch-${batch_idx}" => {
          instances = {
            for inst_idx in range(
              batch_idx * var.batch_size,
              min((batch_idx + 1) * var.batch_size, var.total_instances)
            ) :
            "instance-${inst_idx}" => {
              index = inst_idx
              batch = batch_idx
            }
          }
        }
      }
    }
    
    # Artificial delay resources for batch sequencing
    resource "time_sleep" "batch_delay" {
      for_each = { for b, _ in local.batches : b => b }
    
      create_duration = each.key == "batch-0" ? "0s" : "${var.batch_delay_seconds}s"
    
      # Ensure delay starts only after the previous batch instances are created
      depends_on = [
        oci_core_instance.batch
      ]
    }
    
    resource "oci_core_instance" "batch" {
      for_each = merge([
        for _, batch_data in local.batches : batch_data.instances
      ]...)
    
      display_name        = "${var.name_prefix}-${each.key}"
      availability_domain = var.availability_domains[each.value.index % length(var.availability_domains)]
      compartment_id      = var.compartment_id
      shape               = var.instance_shape
    
      shape_config {
        ocpus         = var.instance_ocpus
        memory_in_gbs = var.instance_memory
      }
    
      source_details {
        source_type = "image"
        source_id   = var.image_id
      }
    
      create_vnic_details {
        subnet_id = var.subnet_ids[each.value.index % length(var.subnet_ids)]
      }
    
      # Ensure this instance respects its batch delay
      depends_on = [
        time_sleep.batch_delay["batch-${each.value.batch}"]
      ]
    
      lifecycle {
        precondition {
          condition     = each.value.index < var.total_instances
          error_message = "Instance index ${each.value.index} exceeds total_instances ${var.total_instances}"
        }
      }
    }
    
    data "oci_core_instance" "health_check" {
      for_each    = oci_core_instance.batch
      instance_id = each.value.id
    }
    
    check "instance_health" {
      assert {
        condition = alltrue([
          for _, d in data.oci_core_instance.health_check :
          d.state == "RUNNING"
        ])
        error_message = "Not all instances reached RUNNING state"
      }
    }

    这种模式允许将大型部署拆分成可管理的小批次,而无需使用-target。批次会自动带有延迟,以确保遵守API限制。关键在于将高成本的API操作分时执行,而不必人为拆分架构或模块。

    至此,本文内容足以在遭遇API限制问题时提供初步的应对方法和解决思路。不过,到目前为止我们依然处于高度被动、甚至略显“偏执”的状态。另外,像批处理(Batching)这种机制,其实优秀的Provider本应在内部自动处理——至少作为终端用户通常会下意识地这么认为。
    在下一篇文章中,我们将探讨高级方法,如API Gateway、使用Ephemeral进行测试以及Sentinel策略,如何主动诊断这些限制。