亚马逊AWS官方博客

基于 Auto Scaling 实现油气智慧基地 AI 视频日报系统的优雅扩缩容

摘要:本文聚集某油气行业客户在油气智慧基地平台上的 AI 视频日报系统优雅扩缩容的需求,提出了针对计算密集型工作负载在对成本和时效性都有较高要求的场景下,基于 Auto Scaling 实现的优雅扩缩容的解决方案。该方案在对原系统最小修改的原则下,基于自定义监控指标、Auto Scaling 的 Target Tracking 策略和 Lifecycle Hook 实现了系统的高峰期自动扩容、低谷期优雅缩容,避免了任务被中途强制终止,以达到成本和时效性的最优解。


1. 背景介绍

某客户为油气田、炼化厂等能源化工企业开发了一套油气智慧基地管理平台,该平台整合数据、优化资源,使基地生产管理更便捷、更精准、更安全、更及时,实现基地的数据驱动转型。该平台有一项功能,在每日凌晨,会将前一天监控摄像头检测到的违规事件,如:未戴安全头盔、未穿安全服、未登记车辆出入、禁烟区域吸烟、接打电话等事件视频,分类汇总合并成一个视频文件,以供基地管理人员进行查看,推进整改,该视频文件称为 AI 视频日报,相关系统称为 AI 视频日报系统,该系统具有以下特点:

  • 计算密集:单个视频的生成,数据源是几十上百个监控摄像头的事件视频,各摄像头码率、分辨率存在不一致的情况,同时需要添加所属基地、事件位置、事件类型等水印信息,转码过程需要消耗数分钟到数小时的 CPU/GPU 资源。
  • 任务时长不确定:根据事件多少、视频时长、分辨率、编码格式的不同,单个任务的执行时间从 30 秒到数小时不等。
  • 任务量不可预期:任务量与当天各基地出现违规事件的多少有关,无法准确预测
  • 时效性:每个 AI 视频日报需要在第二天 8 点前产出,以让管理人员查看,并进行跟进。

针对这些特点,为了保障任务的时效性和总体成本的最优,需要一套方案,既能实现根据负载自动弹性伸缩,又能在缩容时确保正在执行的任务不被中断——即”优雅扩缩容”。

根据过往经验,为实现“优雅扩缩容”,有多种方案可供选择,详细对比如下:

对比维度 EC2 + Auto Scaling ECS + Capacity Provider EKS + K EDA/Karpenter
扩容速度 1-3 分钟 Fargate: 30-60 秒;
EC2 模式: 1-3 分钟
已有 Node:秒级
新增 Node:1-3 分钟
缩容粒度 实例级(整个 EC2) Task 级(单个容器任务) Pod 级 + Node 级
优雅终止实现复杂度
优雅终止上限 最长 48 小时 最长 48 小时 无硬上限
自定义指标扩容 Lambda 发布自定义 CloudWatch 指标,根据该指标设置指标跟踪策略 Lambda 发布自定义 CloudWatch 指标,根据该指标设置 ECS 指标跟踪策略 KEDA ScaledObject 直接对接队列 Scaler
运维复杂度
学习曲线
管控平面费用 EKS 管控平面费用
Spot 支持 支持 支持 支持
多租户隔离
CI/CD 部署 AMI 更新与 Launch Template 版本 镜像推送 + ECS Service Rolling Update 镜像推送 + Deployment Rolling / Blue-Green
故障恢复 ASG 自动替换不健康实例 ECS 自动重启失败 Task + 替换不健康实例 Pod 重启 + Node 自愈 + PDB 保护
可迁移性

总结以上对比分析,EC2 + Auto Scaling 方案胜在实现与维护简单、成本低;ECS 方案提供容器化的便利和 Fargate Serverless 体验,但可迁移性差;EKS 方案提供最强大的编排能力和生态,但运维复杂度与成本最高。

2.方案选择

客户的 AI 视频日报系统,由两个子系统组成:

  • 任务管理系统:负责接收任务并维护任务状态;
  • 任务处理 Worker:定时拉取任务并上报任务处理状态。

整体架构图如下:

[图 1]

  • 各油气智慧基地管理平台,对监控摄像头的视频流进行 AI 识别,将识别到的事件在系统中进行提示并存储到对象存储中;
  • 油气智慧基地管理平台每日凌晨下发 AI 视频日报任务到任务管理系统;
  • 任务管理系统会将任务放入基于 Redis 构建的消息队列并写入数据库做持久化;
  • Worker 在空闲时会定时访问任务管理系统获取任务并进行 AI 视频日报生成,任务处理过程中会定时上报任务状态;
  • 任务结束时会将任务处理结果上报给任务管理系统,任务管理系统接收到任务结束请求时,将任务结果通过 API 回调给对应的油气智慧基地管理平台。

客户的整套系统完全自研,由于团队无专职 K8S 运维人员,所以整体系统均是直接部署在虚拟机上。考虑到团队人员技术栈情况,同时避免对系统进行过多改造,兼顾运营成本与时间成本的考量,客户最终选择 EC2、Auto Scaling 的方案。

3. 方案架构

3.1 架构图

[图 2]

本方案基于客户系统现状,使用 Amazon EC2、Auto Scaling 和 Amazon CloudWatch 等组件构建了一套完整的优雅扩缩容的方案。

3.2 核心组件说明

组件 职责说明
Amazon S3 对象存储系统,用于存储源视频文件和转码后的输出文件。
Auto Scaling 自动伸缩服务,可根据业务需求动态调整计算资源。
Auto Scaling Group 自动伸缩组,是一个逻辑组,自动伸缩服务进行扩缩操作的实体。
Amazon CloudWatch 云监控,用于存储自定义指标、触发自动伸缩策略,配置告警通知。
EventBridge 事件总线,用于定时触发 Lambda 函数。
Amazon Lambda 调用 API 获取队列长度,计算并发布自定义 CloudWatch 指标。
Lifecycle Hook 自动伸缩组中实例的生命周期钩子,用于实现优雅退出。
Launch Template 启动模板,用于定义自动伸缩组中启动实例的类型与启动过程,由 AMI 和 UserData 组成。
AMI 机器映像,用于快速启动具备相同功能的 EC2 实例。
UserData 启动模板中的一个配置项,用于指定在实例启动时执行特定任务。

3.3 核心流程

视频日报生成流程,与原流程完全一致,只是对像存储换为 Amazon S3

任务处理集群优雅扩缩容流程:

  1. EventBridge 定时调用 Lambda 函数;
  2. Lambda 函数调用 API 获取队列长度,并计算自定义指标发布到 CloudWatch;
  3. CloudWatch 触发目标跟踪策略;
  4. Auto Scaling 动态调整 ASG(Auto Scaling Group) 实例数:
    • 如需扩容,Auto Scaling 通过启动模板启动新实例加入到 ASG 中;
    • 如需缩容,Lifecycle Hook 拦截关机操作,保障 Worker 完成当前任务后再关机。

3.4 控制模型

3.4.1 扩容策略

| 参考:使用带有动态 SQS 目标的目标跟踪扩展 ASG

本方案采用自定义 CloudWatch 指标驱动的目标跟踪策略,核心指标为:

PendingJobsPerInstance = 自建队列长度 / RunningInstanceCount

该指标表示当前每个 Worker 实例平均待处理的任务数。当该值超过目标值(如:5)时,Auto Scaling 触发扩容,低于目标值时触发缩容。

使用“每实例平均负载”而非原始队列长度的优势:

  • 避免了队列中有 100 条消息但已有 50 个实例在处理时仍然触发扩容的问题;
  • 在实例数较多时自然降低指标值,避免过度扩容;
  • 当队列为空且指标值为 0 时,自动触发缩容至最小实例数。

3.4.2 缩容策略与优雅终止

缩容的核心挑战在于:如何确保被选中终止的实例已经完成当前任务?

本方案的解决思路:

  • Lifecycle Hook:在 ASG 上配置 autoscaling:EC2_INSTANCE_TERMINATING 类型的 Lifecycle Hook。当 Auto Scaling 决定终止某个实例时,实例进入 Terminating:Wait 状态,而不是立即被终止。
  • Worker 自检机制:运行 Worker 进程的 EC2 实例上,执行定时脚本定期查询本实例的生命周期状态。一旦检测到即将被终止,立即通知 Worker 进行停止拉取新任务,等待当前任务完成后自动退出。
  • 发送 CONTINUE:当前 Worker 进程退出后,定时脚本调用 complete-lifecycle-action API 发送 CONTINUE,允许 Auto Scaling 安全终止该实例。

3.4.3 Lifecycle Hook 工作流

完整的 Lifecycle Hook 工作流程如下:

  1. Auto Scaling 决定终止实例 i-xxxxx;
  2. 实例进入 Terminating:Wait 状态(最长等待时间可配置);
  3. 定时脚本检测到生命周期状态变更,调用 Worker 进程的 stop 脚本;
  4. Worker 停止从队列拉取新消息,等待当前转码任务完成后退出;
  5. 定时脚本,根据 Worker 进程的状态,判断操作:
    • 如果 Worker 进程已经退出,调用 complete-lifecycle-action API,发送 CONTINUE;
    • 如果 Worker 进程未退出,且超过了一定时长,调用 record-lifecycle-action-heartbeat API,延长等待时间;
  6. 实例进入 Terminating:Proceed 状态,被安全终止。

3.4.4 心跳机制与超时处理

为了处理定时脚本出错、超长任务等情况,方案设计了多层保护机制:

  • Lifecycle Hook 超时:配置 HeartbeatTimeout(如 3600 秒),如果 EC2 实例在超时时间内未发送 CONTINUE,Auto Scaling 将执行默认动作(ABANDON,即强制终止)。
  • 心跳续期:对于超长转码任务,定时脚本可定期调用 record-lifecycle-action-heartbeat API,延长等待时间。

4. 具体实施步骤

4.1 Step 1:构建转码系统 AMI 镜像

通过 aws cli 命令创建 AMI 镜像:

aws ec2 create-image \
  --instance-id {已配置好的转码 worker 实例 ID} \
  --name "transcode-worker-base-$(date +%Y%m%d)" \
  --description "Video transcode worker with FFmpeg and lifecycle monitor" \
  --no-reboot \
  --tag-specifications \
'ResourceType=image,Tags=[{Key=Name,Value=transcode-worker-base},{Key=Version,Value=v1}]'

也可通过控制台创建:

[图 3]

查看镜像列表:

[图 4]

4.2 Step 2:配置 EC2 启动模板

通过 aws cli 命令,创建启动模板,包含转码软件安装和 Worker 服务启动脚本:

aws ec2 create-launch-template \
  --launch-template-name video-transcode-worker \
  --version-description "v1" \
  --launch-template-data '{
    "ImageId": "ami-0abcdef1234567890",
"InstanceType": "<实例类型>",
"IamInstanceProfile": {
"Arn":"arn:aws:iam::<AWS账号ID>:instance-profile/<具有autoscaling 相关权限的角色>"
},
    "UserData": "<base64-encoded-userdata>"
  }'

User Data 脚本示例(编码前):

#!/bin/bash

# 确保 aws cli 已安装
which aws || {
  yum install -y aws-cli
}

# 启动 Worker 服务
systemctl enable transcode-worker
systemctl start transcode-worker

# 安装并启动 corntab
yum install -y cronie
systemctl start crond

# 每分钟执行一次生命周期监控脚本
chmod +x /opt/scripts/lifecycle-check.sh
echo "* * * * * root /opt/scripts/lifecycle-check.sh >> /var/log/lifecycle.log 2>&1" \
  > /etc/cron.d/lifecycle-monitor

chmod 644 /etc/cron.d/lifecycle-monitor

lifecycle-check.sh 脚本示例:

#!/bin/bash
# 配合 crontab 定时执行的生命周期检查脚本(每次执行一次检查,非持续运行)
# 不修改原转码系统代码,通过原系统管理脚本实现优雅退出

set -uo pipefail

# ===== 配置 =====
ASG_NAME="video-transcode-asg"
LIFECYCLE_HOOK_NAME="graceful-terminate-hook"
HEARTBEAT_INTERVAL=300

# 原转码系统管理脚本
TRANSCODE_STOP="/opt/transcode/bin/stop.sh"
TRANSCODE_STATUS="/opt/transcode/bin/status.sh"

# 状态文件(用于跨 cron 执行保持状态)
STATE_FILE="/var/run/lifecycle-monitor.state"
HEARTBEAT_FILE="/var/run/lifecycle-monitor.heartbeat"
LOG_FILE="/var/log/lifecycle-monitor.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] \$1" >> "${LOG_FILE}"
}

# ===== 获取实例元数据 =====
IMDS_TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 60" 2>/dev/null)
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: ${IMDS_TOKEN}" \
  http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null)
REGION=$(curl -s -H "X-aws-ec2-metadata-token: ${IMDS_TOKEN}" \
  http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null)

if [ -z "${INSTANCE_ID}" ]; then
    log "ERROR: Failed to get instance ID"
    exit 1
fi

# ===== 检查生命周期状态 =====
STATE=$(aws autoscaling describe-auto-scaling-instances \
    --instance-ids "${INSTANCE_ID}" \
    --region "${REGION}" \
    --query "AutoScalingInstances[0].LifecycleState" \
    --output text 2>/dev/null)

# ===== 状态机逻辑 =====

# 状态1:正常运行中,未触发缩容
if [ "${STATE}" != "Terminating:Wait" ]; then
    # 如果之前有 state 文件(异常残留),清理掉
    if [ -f "${STATE_FILE}" ]; then
        log "WARN: State file exists but instance not terminating, cleaning up"
        rm -f "${STATE_FILE}" "${HEARTBEAT_FILE}"
    fi
    exit 0
fi

# ===== 以下为 Terminating:Wait 状态处理 =====

# 状态2:首次检测到缩容,触发停止
if [ ! -f "${STATE_FILE}" ]; then
    log "DETECTED: Instance ${INSTANCE_ID} entering Terminating:Wait"
    echo "STOPPING" > "${STATE_FILE}"
    date +%s > "${HEARTBEAT_FILE}"

    # 调用原系统停止脚本
    log "Calling ${TRANSCODE_STOP}..."
    ${TRANSCODE_STOP} >> "${LOG_FILE}" 2>&1
    log "stop.sh called, waiting for process to exit"
    exit 0
fi

# 状态3:已触发停止,等待进程退出
PROC_STATUS=$(${TRANSCODE_STATUS} 2>/dev/null || echo "unknown")
log "Process status: ${PROC_STATUS}"

if [ "${PROC_STATUS}" == "stopped" ] || [ "${PROC_STATUS}" == "idle" ]; then
    # 进程已退出,发送 CONTINUE
    log "Process stopped. Sending CONTINUE..."
    aws autoscaling complete-lifecycle-action \
        --auto-scaling-group-name "${ASG_NAME}" \
        --lifecycle-hook-name "${LIFECYCLE_HOOK_NAME}" \
        --instance-id "${INSTANCE_ID}" \
        --lifecycle-action-result CONTINUE \
        --region "${REGION}" >> "${LOG_FILE}" 2>&1

    if [ $? -eq 0 ]; then
        log "SUCCESS: CONTINUE sent. Instance will be terminated."
    else
        log "ERROR: Failed to send CONTINUE"
    fi

    # 清理状态文件
    rm -f "${STATE_FILE}" "${HEARTBEAT_FILE}"
    exit 0
fi

# 状态4:进程仍在运行,检查是否需要发送心跳
if [ -f "${HEARTBEAT_FILE}" ]; then
    LAST_HEARTBEAT=$(cat "${HEARTBEAT_FILE}")
    NOW=$(date +%s)
    ELAPSED=$((NOW - LAST_HEARTBEAT))

    if [ ${ELAPSED} -ge ${HEARTBEAT_INTERVAL} ]; then
        log "Sending heartbeat (elapsed: ${ELAPSED}s)..."
        aws autoscaling record-lifecycle-action-heartbeat \
            --auto-scaling-group-name "${ASG_NAME}" \
            --lifecycle-hook-name "${LIFECYCLE_HOOK_NAME}" \
            --instance-id "${INSTANCE_ID}" \
            --region "${REGION}" >> "${LOG_FILE}" 2>&1
        date +%s > "${HEARTBEAT_FILE}"
    fi
fi

log "Still waiting for process to finish (status: ${PROC_STATUS})"
exit 0

控制台创建启动模板:

[图 5]

“高级详细信息”页面下配置“用户数据”:

[图 6]

查看启动模板:

[图 7]

4.3 Step 3:创建 Auto Scaling Group

通过 aws cli 命令,创建 Auto Scaling Group:

aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name video-transcode-asg \
  --launch-template LaunchTemplateName=video-transcode-worker,Version='$Latest' \
  --min-size 1 --max-size 5 --desired-capacity 1 \
  --vpc-zone-identifier "subnet-aaa,subnet-bbb" \
  --health-check-type EC2 \
  --health-check-grace-period 300

也可通过控制台创建

[图 8]

查看 Auto Scaling 组:

[图 9]

4.4 Step 4:创建自定义指标发布 Lambda

Lambda 函数通过 HTTP API 查询自建队列长度,计算每实例待处理任务数并发布至 CloudWatch:

# lambda_function.py
import boto3, json, urllib3

http = urllib3.PoolManager()

# 自建队列 API 地址(返回 JSON: {"queue_length": N})
QUEUE_API_URL = 'http://test.example.com/api/queues/transcode/length'
ASG_NAME = 'video-transcode-asg'

cw = boto3.client('cloudwatch')
asg_client = boto3.client('autoscaling')

def lambda_handler(event, context):
    # 1. 查询自建队列长度
    resp = http.request(‘GET’, QUEUE_API_URL, timeout=5)
    queue_length = json.loads(resp.data.decode(‘utf-8’))['queue_length']

    # 2. 获取 ASG 当前运行实例数
    asg_resp = asg_client.describe_auto_scaling_groups(
        AutoScalingGroupNames=[ASG_NAME]
    )
    instances = asg_resp['AutoScalingGroups'][0]['Instances']
    running = len([i for i in instances
                   if i['LifecycleState'] == 'InService'])

    # 3. 计算每实例待处理任务数
    metric_value = queue_length / max(running, 1)

    # 4. 发布自定义 CloudWatch 指标
    cw.put_metric_data(
        Namespace='Custom/VideoTranscode',
        MetricData=[{
            'MetricName': 'PendingJobsPerInstance',
            'Value': metric_value,
            'Unit': 'Count'
        }]
    )
    return {'statusCode': 200, 'metric': metric_value}

通过 aws cli 命令,配置 EventBridge 每分钟触发此 Lambda:

aws events put-rule \
  --name publish-transcode-metric \
  --schedule-expression "rate(1 minute)"
aws events put-targets \
  --rule publish-transcode-metric \
  --targets "Id"="1","Arn"="arn:aws:lambda:us-east-1:<AWS 账号 ID>:function:<Lambda 函数名称>"

通过控制台创建 Lambda 函数并设置触发器:

[图 10]

[图 11]

注:需要给执行 Lambda 的角色赋予获取 ASG 描述信息和设置自定义 CloudWatch 监控指标的权限,参考示例如下:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*"
}]
}

4.5 Step 5:配置 Target Tracking 扩缩容策略

通过 aws cli 命令,配置 Target Tracking 扩缩容策略:

aws autoscaling put-scaling-policy \
  --auto-scaling-group-name video-transcode-asg \
  --policy-name target-tracking-pending-jobs \
  --policy-type TargetTrackingScaling \
  --target-tracking-configuration '{
    "CustomizedMetricSpecification": {
      "MetricName": "PendingJobsPerInstance",
      "Namespace": "Custom/VideoTranscode",
      "Statistic": "Average"
    },
    "TargetValue": 5.0,
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
  }'

参数说明:

  • TargetValue=5.0:目标是每个实例平均处理 5 个待办任务。超过此值触发扩容,低于此值触发缩容。
  • ScaleOutCooldown=60:扩容冷却 60 秒,确保快速响应突发流量。
  • ScaleInCooldown=300:缩容冷却 300 秒,避免频繁缩容导致的抖动。

控制台设置界面:

[图 12]

4.6 Step 6:配置 Lifecycle Hook

通过 aws cli 命令,配置生命周期钩子:

aws autoscaling put-lifecycle-hook \
  --auto-scaling-group-name video-transcode-asg \
  --lifecycle-hook-name graceful-terminate-hook \
  --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING \
  --heartbeat-timeout 7200 \
  --default-result ABANDON

参数说明:

  • heartbeat-timeout=7200:最长等待 2 小时,足够覆盖大多数转码任务的执行时间,可合理调整具体时长。
  • default-result=ABANDON:如果超时未收到 CONTINUE(可能 Worker 崩溃),则强制终止实例,避免实例永远卡在 Wait 状态。

控制台界面:

[图 13]

4.7 Step 7:最终效果

当指标为 10 时,ASG 自动扩容到 2 台:

[图 14]

[图 15]

当指标小于 5 时,自动缩容到 1 台:

[图 16]

[图 17]

5. 总结

本方案在最小修改原则下,实现了油气智慧基地管理平台 AI 视频日报系统的优雅扩缩容方案:

  • 自动扩容:基于自定义指标“每实例平均负载”的目标跟踪策略能够精确感知业务负载,实现分钟级的弹性响应;
  • 优雅缩容:通过 Lifecycle Hook 机制,确保缩容时不会中断正在执行的转码任务,实现任务零中断,避免了计算资源的浪费同时保障了任务的时效性。

该方案适用于大多数计算密集型业务场景,如:无人机拍摄视频的分析与处理、电商推广视频的多分辨率编解码、安防领域的视频 AI 分析等场景,这些场景的相关系统均可进行少量改动即可基于该方案实现系统的优雅扩缩容。针对任务时间较短或是对时效性要求不高的场景,还可以考虑通过使用 Spot 实例来进一步优化成本。

➡️ 下一步行动:

相关产品:

相关文章:

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

董文新

西云数据解决方案架构师。13年+ 软件研发和架构经验,深耕分发网络、即时通讯、大数据领域。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用