最新n1n v2.0.1 正式上线!企业级大模型接口聚合平台 (LLM API Gateway),为您接入 500+ AI Models,价格低至 1 折, 立即尝试

使用AWS CDK编排SageMaker AI端点自动扩缩容

作者
  • avatar
    姓名
    Nino
    职业
    Senior Tech Editor

将机器学习模型部署到生产环境时,通常需要动态分配资源以应对波动的流量。在为SageMaker AI端点配置生产变体时,实施应用程序自动扩缩容(Application Auto Scaling)是确保高可用性和成本效益的最佳实践。然而,当尝试更新已经注册了活跃扩缩策略的端点时,开发者经常会遇到令人头疼的CloudFormation部署失败问题。这是因为底层CloudFormation资源约束阻止在外部扩缩目标保持对端点容量锁定时修改变体。

在这篇深入探讨中,我们将剖析这些部署死锁发生的原因,以及如何使用AWS CDK和CloudFormation自定义资源构建健壮且可复用的解决方案。通过将扩缩注册的生命周期与端点本身解耦,您可以在单次cdk deploy执行中实现无缝的零停机更新。

SageMaker自动扩缩容部署死锁的根本原因

当您在AWS CDK中定义CfnEndpoint以及ApplicationAutoScaling目标时,CloudFormation会将其作为独立资源进行管理。在更新期间——例如更改DeepSeek-V3或Claude 3.5 Sonnet托管配置的实例类型——CloudFormation尝试在更新扩缩目标的同时或紧接其前修改SageMaker端点配置。

由于应用程序自动扩缩服务对变体的期望容量持有活跃引用和执行锁,SageMaker会拒绝更新端点变体权重或实例数量。CloudFormation堆栈回滚,使您的部署处于失败状态,并需要在AWS管理控制台中进行手动干预,在重试之前注销可扩缩目标。

为了在架构上解决这个问题,我们需要一个拦截CloudFormation生命周期的机制:

  1. 更新前:自动从应用程序自动扩缩中注销SageMaker端点。
  2. 更新中:允许CloudFormation成功更新SageMaker端点和端点配置。
  3. 更新后:使用所需的最小和最大容量边界重新注册扩缩目标。

虽然您可以手动管理这些操作,但这违背了基础架构即代码(IaC)的初衷。相反,我们可以将此逻辑封装在由AWS Lambda和CloudFormation自定义资源驱动的自定义AWS CDK构造中。

设计自定义构造架构

我们的目标是构建一个名为AutoScalingSageMakerEndpoint的高级构造。该构造将包装标准的CfnEndpoint、CfnEndpointConfig以及自动扩缩策略,同时插入一个监听CloudFormation生命周期事件的自定义资源提供程序。

以下是组件在部署生命周期中的交互方式:

生命周期阶段CloudFormation操作自定义资源处理程序操作
创建供应端点,然后注册扩缩调用应用程序自动扩缩API以注册目标。
更新检测配置更改注销目标 → 更新端点 → 重新注册目标。
删除删除扩缩,然后删除端点清理扩缩策略和目标。

通过在Lambda支持的自定义资源中按顺序编排这些步骤,我们彻底消除了竞态条件。

在TypeScript中实现CDK构造

让我们编写此构造的完整、生产就绪的TypeScript代码。首先,确保您已初始化CDK项目并配置了AWS SDK v3依赖项。

import * as cdk from 'aws-cdk-lib';
import * as sagemaker from 'aws-cdk-lib/aws-sagemaker';
import * as appscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cr from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';

export interface AutoScalingSageMakerEndpointProps {
  readonly endpointName: string;
  readonly endpointConfigName: string;
  readonly variantName: string;
  readonly minCapacity: number;
  readonly maxCapacity: number;
  readonly targetValue: number;
  readonly scaleInCooldown?: number;
  readonly scaleOutCooldown?: number;
}

export class AutoScalingSageMakerEndpoint extends Construct {
  public readonly endpoint: sagemaker.CfnEndpoint;

  constructor(scope: Construct, id: string, props: AutoScalingSageMakerEndpointProps) {
    super(scope, id);

    // 1. 定义标准的SageMaker端点配置和端点
    const endpointConfig = new sagemaker.CfnEndpointConfig(this, 'EndpointConfig', {
      endpointConfigName: props.endpointConfigName,
      productionVariants: [
        {
          initialInstanceCount: props.minCapacity,
          instanceType: 'ml.m5.xlarge',
          variantName: props.variantName,
          modelName: 'your-model-name',
        },
      ],
    });

    this.endpoint = new sagemaker.CfnEndpoint(this, 'Endpoint', {
      endpointName: props.endpointName,
      endpointConfigName: endpointConfig.attrEndpointConfigName,
    });

    this.endpoint.addDependency(endpointConfig);

    // 2. 用于管理扩缩注册生命周期的Lambda处理程序
    const scalingManagerLambda = new lambda.SingletonFunction(this, 'ScalingManagerLambda', {
      uuid: 'a1b2c3d4-e5f6-7890-abcd-ef0123456789',
      code: lambda.Code.fromInline(`
        const { ApplicationAutoScalingClient, RegisterScalableTargetCommand, DeregisterScalableTargetCommand, PutScalingPolicyCommand } = require('@aws-sdk/client-application-auto-scaling');
        const client = new ApplicationAutoScalingClient();

        exports.handler = async (event) => {
          const props = event.ResourceProperties;
          const resourceId = \`endpoint/\${props.EndpointName}/variant/\${props.VariantName}\`;
          
          if (event.RequestType === 'Delete') {
            await client.send(new DeregisterScalableTargetCommand({
              ServiceNamespace: 'sagemaker',
              ResourceId: resourceId,
              ScalableDimension: 'sagemaker:variant:DesiredInstanceCount',
            }));
            return { PhysicalResourceId: resourceId };
          }

          if (event.RequestType === 'Update' || event.RequestType === 'Create') {
            // 抢先注销以避免更新期间的锁定冲突
            try {
              await client.send(new DeregisterScalableTargetCommand({
                ServiceNamespace: 'sagemaker',
                ResourceId: resourceId,
                ScalableDimension: 'sagemaker:variant:DesiredInstanceCount',
              }));
            } catch (e) {
              console.log('目标尚未注册,继续执行。');
            }

            // 注册可扩缩目标
            await client.send(new RegisterScalableTargetCommand({
              ServiceNamespace: 'sagemaker',
              ResourceId: resourceId,
              ScalableDimension: 'sagemaker:variant:DesiredInstanceCount',
              MinCapacity: parseInt(props.MinCapacity),
              MaxCapacity: parseInt(props.MaxCapacity),
            }));

            // 设置扩缩策略
            await client.send(new PutScalingPolicyCommand({
              PolicyName: \`sagemaker-target-tracking-\${props.VariantName}\`,
              ServiceNamespace: 'sagemaker',
              ResourceId: resourceId,
              ScalableDimension: 'sagemaker:variant:DesiredInstanceCount',
              PolicyType: 'TargetTrackingScaling',
              TargetTrackingScalingPolicyConfiguration: {
                TargetValue: parseFloat(props.TargetValue),
                PredefinedMetricSpecification: {
                  PredefinedMetricType: 'SageMakerVariantInvocationsPerInstance',
                },
              },
            }));
          }

          return { PhysicalResourceId: resourceId };
        };
      `),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(30),
    });

    scalingManagerLambda.addToRolePolicy(new iam.PolicyStatement({
      actions: [
        'application-autoscaling:RegisterScalableTarget',
        'application-autoscaling:DeregisterScalableTarget',
        'application-autoscaling:PutScalingPolicy',
        'application-autoscaling:DeleteScalingPolicy',
      ],
      resources: ['*'],
    }));

    // 3. 自定义资源提供程序绑定
    const provider = new cr.Provider(this, 'ScalingProvider', {
      onEventHandler: scalingManagerLambda,
    });

    const customResource = new cdk.CustomResource(this, 'ScalingCustomResource', {
      serviceToken: provider.serviceToken,
      properties: {
        EndpointName: this.endpoint.endpointName,
        VariantName: props.variantName,
        MinCapacity: props.minCapacity,
        MaxCapacity: props.maxCapacity,
        TargetValue: props.targetValue,
        // 强制在配置更改时重新执行
        EndpointConfigName: endpointConfig.attrEndpointConfigName,
      },
    });

    customResource.node.addDependency(this.endpoint);
  }
}

与AI工作流和外部API集成

在运营大规模AI应用程序时,通过CDK管理基础架构只是成功的一半。您还需要管理通往LLM推理端的高吞吐量连接。许多工程团队利用聚合器平台(例如 n1n.ai)在自定义SageMaker部署和托管的基础模型之间无缝路由流量。

通过将CDK中的自动化基础架构构造与通过 n1n.ai 的健壮API路由层相结合,开发者可以实现一个弹性架构,在基础架构层和应用程序网关层都能实现动态扩展。例如,如果您的SageMaker端点经历扩缩延迟,n1n.ai 可以智能地在替代端点之间进行故障转移或负载均衡,确保终端用户的零停机体验。

测试和验证部署

为了验证您新创建的构造是否正确处理更新,请在终端中执行以下步骤:

# 1. 综合CDK堆栈以检查模板有效性
cdk synth

# 2. 部署包含SageMaker端点和自动扩缩规则的初始堆栈
cdk deploy --require-approval never

# 3. 修改端点配置(例如更改实例数量或模型参数)并重新部署
cdk deploy --require-approval never

在第二次部署期间,CloudFormation将触发自定义资源的更新处理程序。Lambda函数将执行DeregisterScalableTargetCommand,允许CloudFormation成功更新SageMaker端点而不会抛出资源锁定异常。更新后,自定义资源将自动重新建立扩缩目标和目标跟踪策略。

结论

在AWS CDK中使用应用程序自动扩缩管理SageMaker AI端点不再需要成为部署摩擦的来源。通过将注册生命周期封装在CloudFormation自定义资源中,您可以将脆弱的基础架构代码转化为可靠、可复用的构造。

要进一步优化您的AI基础架构管道并无缝集成多个LLM提供商,请探索 n1n.ai 上提供的先进路由功能。

Get a free API key at n1n.ai