AWS ECS部署Triton推理服务:GPU调度、模型热加载与生产级健康检查

AWS ECS部署Triton推理服务:GPU调度、模型热加载与生产级健康检查

1. 项目概述:为什么在 AWS ECS 上部署 Triton 不是“选修课”,而是生产级 AI 服务的必经之路

Triton Inference Server 是 NVIDIA 官方主推的高性能、多框架、多模型统一推理服务引擎,它不是简单的模型加载器,而是一套完整的推理生命周期管理平台——支持 TensorFlow、PyTorch、ONNX、TensorRT、Python Backend 等十余种模型格式,原生集成动态批处理(Dynamic Batching)、并发模型执行(Concurrent Model Execution)、模型热更新(Model Repository Watch)、GPU 资源隔离(Instance Grouping)等关键企业级能力。当你手头有多个业务线共用一套 GPU 集群,或者需要在同一个 GPU 上同时跑推荐模型、CV 检测模型和 NLP 语义理解模型时,Triton 就不是“能用就行”,而是“非它不可”。

而 AWS ECS(Elastic Container Service)作为 AWS 原生容器编排服务,与 EC2 实例深度耦合、与 IAM 权限体系无缝集成、与 CloudWatch 日志监控天然打通,更重要的是——它不强制你学习 Kubernetes 的 Operator、CRD、Helm Chart 和 StatefulSet 生命周期管理。对于一支以快速交付 AI 服务为目标、而非专职运维 K8s 集群的工程团队来说,ECS 提供的是“足够好+足够稳+足够快”的确定性路径。这不是在回避 K8s,而是在权衡 ROI:把 3 天调试 Istio Sidecar 注入的时间,换成优化 Triton 的max_batch_sizepreferred_batch_size参数,让首字节延迟(Time to First Token)再降 12ms,对用户感知更实在。

本篇标题中 “Part (3/4)” 的定位非常关键——它意味着前两部分已完成了 Triton 容器镜像构建(含自定义 Python Backend 编译)、本地 Docker Compose 验证及性能基线测试;第四部分将聚焦灰度发布与 A/B 测试架构。而本篇的核心任务,是把经过验证的 Triton 服务,从单机 Docker 环境,可重复、可审计、可伸缩、可回滚地迁移到 ECS 生产集群。这不是一次“docker run”的简单平移,而是要解决真实生产环境中的四个刚性问题:GPU 资源如何被 ECS 正确识别与调度?Triton 的模型仓库(model repository)如何实现版本化、热加载与跨实例一致性?健康检查(Health Check)如何真正反映 Triton 内部模型加载状态,而非仅容器进程存活?日志与指标如何与现有 AWS 监控体系(CloudWatch Logs + Metrics)对齐,避免排查时在多个控制台间跳转?

我带过的三个 AI 工程团队,在首次落地 Triton+ECS 时,90% 的阻塞点都卡在这四个环节。有人用 EFS 挂载模型仓库,结果发现 NFS 协议下 Triton 的model_repository目录扫描耗时飙升至 47 秒;有人把健康检查写成curl http://localhost:8000/v2/health/ready,却没意识到该端点只校验 HTTP 服务是否启动,完全不检查模型是否已成功加载到 GPU 显存;还有人直接把本地config.pbtxt文件硬编码进镜像,导致每次模型参数微调都要重建镜像并重新部署——这些都不是文档里会写的“坑”,而是我在客户现场凌晨三点盯着 CloudWatch Logs 里反复出现的Failed to load model 'resnet50'报错,一条条翻 Triton 源码才定位到的实操细节。接下来的内容,就是把这些踩过的坑、验证过的解法、以及每一步背后的“为什么”,掰开揉碎讲清楚。

2. 整体架构设计与核心取舍:为什么放弃 Fargate,坚持 EC2 启动类型?

在 ECS 中部署 GPU 工作负载,首要决策是启动类型(Launch Type)的选择:Fargate 还是 EC2?官方文档对 Fargate 的描述很诱人:“无需管理服务器,按 vCPU 和内存付费”。但当你真把 Triton 镜像扔进 Fargate,会立刻撞上三堵墙:

第一堵墙是GPU 支持的缺失。截至 2024 年中,AWS Fargate完全不支持任何 GPU 实例类型。Fargate 的底层资源池由 AWS 统一维护,其计算单元(Fargate Task CPU/Memory 配置)全部基于 CPU 架构设计。即使你尝试在 task definition 中指定nvidia/cuda:11.8-runtime-ubuntu20.04镜像,并设置--gpus all,ECS 控制台会直接报错FARGATE_UNSUPPORTED_GPU_RESOURCE。这不是配置问题,而是服务边界限制——Fargate 的设计哲学是抽象掉所有硬件细节,而 GPU 推理恰恰是最依赖硬件特性的场景。所以,这个选项在项目启动第一天就必须被划掉。

第二堵墙是EC2 实例选型的硬约束。既然必须用 EC2 启动类型,就得直面 AWS EC2 GPU 实例族的选择。我们实测过 p3.2xlarge(V100)、g4dn.xlarge(T4)、p4d.24xlarge(A100)三类实例在 Triton 下的吞吐表现。结论很明确:T4 是性价比最优解,但仅适用于 batch_size ≤ 32 的中低并发场景;V100 在 batch_size=64 时吞吐稳定,显存带宽成为瓶颈;A100 则在高并发、大模型(如 Llama-2-13B FP16)场景下展现出碾压级优势。但选择 A100 意味着单实例成本飙升至 $3.92/小时(按需),而 g4dn.xlarge 仅 $0.526/小时。我们的取舍逻辑是:用 4 台 g4dn.xlarge(共 4×16GB 显存)替代 1 台 p4d.24xlarge(40GB 显存),通过 ECS Service 的 Auto Scaling Group 动态扩缩容,把固定成本转化为弹性成本。当夜间流量跌至谷底时,自动缩容至 1 台;早高峰前 15 分钟,根据 CloudWatch 的CPUUtilization和自定义指标triton_model_load_success_rate触发扩容。这种模式下,月均 GPU 成本下降 63%,且故障域被分散——单台 g4dn.xlarge 故障,只影响 25% 的服务能力,而非整个集群宕机。

第三堵墙是模型仓库(Model Repository)的存储架构设计。Triton 要求模型文件以特定目录结构存放(<model_name>/<version>/model.planmodel.py),且启动时通过--model-repository参数指定根路径。这个路径必须对所有运行 Triton 的容器实例可见、一致、低延迟。我们评估了三种方案:

  • 方案 A:EFS(Elastic File System)挂载。优点是跨 AZ 共享、自动扩缩容;缺点是 NFS 协议在高频小文件读取(Triton 启动时需扫描每个模型的config.pbtxtmodel.py)时,平均延迟达 120ms,导致单次模型加载耗时超过 3 秒,无法满足 SLA。
  • 方案 B:S3 + S3 Mount(如 s3fs-fuse)。优点是成本极低、无限容量;缺点是 fuse 层引入额外延迟,且 Triton 对文件锁(flock)的支持不完善,多实例同时热更新模型时偶发Permission denied错误。
  • 方案 C:EC2 实例本地 NVMe SSD + S3 同步机制。这是我们最终采用的方案:每台 EC2 实例挂载一块 1TB NVMe SSD(如/mnt/triton-models),作为 Triton 的本地模型仓库;通过一个轻量级守护进程(我们用 Go 编写,约 200 行代码),监听 S3 存储桶(s3://my-ai-models/prod/)的ObjectCreated事件,一旦检测到新模型 ZIP 包上传,立即下载、解压、校验 SHA256、原子性替换/mnt/triton-models/<model_name>目录,并向 Triton 发送POST /v2/repository/models/<model_name>/load请求触发热加载。整个流程平均耗时 840ms,P99 < 1.2s,且完全规避了网络文件系统的一致性风险。

这个架构取舍背后,是典型的“用确定性换复杂度”思维:宁可在 EC2 实例上多维护一个同步进程,也不愿为了一点开发便利性,把整个推理服务的稳定性押注在 NFS 或 S3 Fuse 这类非专为 AI 场景优化的中间件上。这就像你不会用 MySQL 存放视频文件一样——存储选型必须匹配访问模式。Triton 对模型仓库的访问模式是“启动时批量扫描 + 运行时按需加载 + 更新时原子切换”,NVMe SSD 完美匹配这一模式。

3. 核心细节解析:GPU 资源声明、健康检查与日志治理的魔鬼细节

3.1 GPU 资源声明:不是加个--gpus all就完事

在 ECS EC2 启动类型下,让容器使用 GPU,远不止在 Dockerfile 里装nvidia-container-toolkit那么简单。它是一个横跨 AWS 底层、NVIDIA 驱动、Docker 引擎、ECS Agent 四个层级的链式信任体系。任何一个环节断开,你都会看到容器日志里反复刷屏的nvidia-smi: command not foundFailed to initialize NVML

第一步,是EC2 实例的 AMI 选择与驱动预装。我们不使用 AWS 官方的Deep Learning AMI (DLAMI),因为其预装的驱动版本(如 515.65.01)与 Triton 24.04 版本要求的 CUDA 12.2 兼容性不佳。我们基于Amazon Linux 2自定义 AMI:在 Packer 构建脚本中,明确指定安装nvidia-driver-535.104.05-1.amzn2.x86_64.rpm(对应 CUDA 12.2),并执行sudo nvidia-smi -L验证驱动加载成功。关键点在于:驱动必须在 ECS Agent 启动前就绪。我们通过systemd服务依赖关系确保:nvidia-persistenced.servicedocker.serviceecs.service,避免 ECS Agent 启动时因驱动未就绪而忽略 GPU 设备。

第二步,是Docker Daemon 的 GPU 支持配置。在/etc/docker/daemon.json中,必须包含:

{ "runtimes": { "nvidia": { "path": "/usr/bin/nvidia-container-runtime", "runtimeArgs": [] } }, "default-runtime": "runc" }

注意,这里default-runtime不能设为nvidia,否则所有无 GPU 需求的 sidecar 容器(如 log forwarder)也会被强制绑定 GPU,造成资源浪费。正确的做法是在 ECS Task Definition 的 container definition 中,显式声明:

"linuxParameters": { "devices": [ { "hostPath": "/dev/nvidiactl", "containerPath": "/dev/nvidiactl", "permissions": ["read", "write"] }, { "hostPath": "/dev/nvidia-uvm", "containerPath": "/dev/nvidia-uvm", "permissions": ["read", "write"] }, { "hostPath": "/dev/nvidia0", "containerPath": "/dev/nvidia0", "permissions": ["read", "write"] } ] }, "environment": [ { "name": "NVIDIA_VISIBLE_DEVICES", "value": "all" } ]

这段配置的含义是:将宿主机上的 NVIDIA 控制设备(nvidiactl)、统一虚拟内存设备(nvidia-uvm)和第一个 GPU 设备(nvidia0)映射进容器,并通过环境变量NVIDIA_VISIBLE_DEVICES=all告知容器内应用“所有 GPU 可见”。这是 ECS 官方推荐的、最细粒度的 GPU 设备控制方式,比旧版的--gpus all更安全、更可控。

第三步,是Triton 容器内的 CUDA 工具包验证。我们在 Triton 镜像的ENTRYPOINT脚本中,加入启动前自检:

#!/bin/bash # pre-start.sh if ! command -v nvidia-smi &> /dev/null; then echo "ERROR: nvidia-smi not found. GPU drivers not loaded in container." exit 1 fi if ! nvidia-smi -q | grep "Product Name" | grep -q "Tesla T4"; then echo "WARNING: Expected Tesla T4, got $(nvidia-smi -q | grep 'Product Name' | awk -F': ' '{print $2}')" fi # Verify CUDA version matches Triton requirement CUDA_VERSION=$(cat /usr/local/cuda/version.txt 2>/dev/null | head -n1 | cut -d' ' -f3) if [[ "$CUDA_VERSION" != "12.2"* ]]; then echo "ERROR: CUDA version mismatch. Expected 12.2, got $CUDA_VERSION" exit 1 fi exec "$@"

这个脚本会在 Triton 主进程启动前执行,任何一项失败都会导致容器退出,并在 ECS 控制台的Stopped reason中清晰显示错误信息,极大缩短故障定位时间。这比等 Triton 启动后报CUDA driver version is insufficient for CUDA runtime version再去查日志,效率高出一个数量级。

3.2 健康检查:从“容器存活”到“模型就绪”的质变

ECS 的健康检查(Health Check)默认只检查容器进程是否存活(CMD-SHELL ["curl -f http://localhost:8000/v2/health/live || exit 1"]),这对 Triton 是严重失焦。/v2/health/live端点只确认 HTTP 服务监听正常,而 Triton 的核心价值在于“模型是否已加载到 GPU 并可响应推理请求”。一个常见场景是:模型文件损坏、config.pbtxt语法错误、或 GPU 显存不足导致模型加载失败,此时/v2/health/live仍返回 200,但所有inference请求都会得到400 Bad Request或超时。ECS 会认为服务健康,拒绝触发替换任务,导致线上服务静默降级。

我们的解决方案是:自定义健康检查脚本,深度探针 Triton 的内部状态。在 ECS Task Definition 的 container definition 中,将健康检查改为:

"healthCheck": { "command": ["CMD-SHELL", "python3 /opt/check_triton_health.py"], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 120 }

对应的/opt/check_triton_health.py脚本内容如下:

#!/usr/bin/env python3 import requests import sys import json def main(): # Step 1: Check if Triton server is alive try: resp = requests.get("http://localhost:8000/v2/health/live", timeout=2) if resp.status_code != 200: print(f"Live check failed: {resp.status_code}") return False except Exception as e: print(f"Live check exception: {e}") return False # Step 2: Check if models are loaded and ready try: resp = requests.get("http://localhost:8000/v2/repository/index", timeout=2) if resp.status_code != 200: print(f"Repository index check failed: {resp.status_code}") return False models = resp.json() if len(models) == 0: print("No models found in repository") return False # Check each model's state for model in models: model_name = model["name"] model_version = model["version"] try: state_resp = requests.get( f"http://localhost:8000/v2/models/{model_name}/versions/{model_version}/state", timeout=2 ) if state_resp.status_code != 200: print(f"Model {model_name} state check failed: {state_resp.status_code}") return False state_data = state_resp.json() if state_data.get("state") != "READY": print(f"Model {model_name} is not READY, state: {state_data.get('state')}") return False except Exception as e: print(f"Model {model_name} state check exception: {e}") return False except Exception as e: print(f"Repository check exception: {e}") return False # Step 3: Optional - Run a lightweight inference test try: # Use a tiny dummy input for resnet50 (1x3x224x224) dummy_input = {"inputs": [{"name": "INPUT__0", "shape": [1, 3, 224, 224], "datatype": "FP32", "data": [0.0] * 150528}]} infer_resp = requests.post( "http://localhost:8000/v2/models/resnet50/infer", json=dummy_input, timeout=5 ) if infer_resp.status_code != 200: print(f"Inference test failed: {infer_resp.status_code}") return False except Exception as e: print(f"Inference test exception: {e}") return False print("All health checks passed") return True if __name__ == "__main__": sys.exit(0 if main() else 1)

这个脚本实现了三层健康验证:

  1. 基础连通性:确认 Triton HTTP 服务进程存活;
  2. 模型加载状态:调用/v2/repository/index获取所有模型列表,再逐个调用/v2/models/{name}/versions/{version}/state确认每个模型的状态为READY(而非UNAVAILABLELOADING);
  3. 端到端功能:对一个已知可用的模型(如resnet50)发起一次最小化推理请求,验证从网络层到 GPU 计算的全链路畅通。

startPeriod: 120的设置至关重要——它告诉 ECS,在容器启动后的前 120 秒内,健康检查失败不计入重试次数。这是因为 Triton 加载一个大型模型(如 BERT-Large)可能需要 45 秒,而模型仓库扫描又需 15 秒,总计 60 秒是常态。没有这个宽限期,ECS 会在模型加载完成前就判定任务失败并反复重启,形成“启动风暴”。

3.3 日志与指标治理:让每一行日志都可追溯,每一个指标都可告警

Triton 默认日志输出到stdout/stderr,但在 ECS 环境下,这远远不够。我们需要的是:日志按模型、按请求 ID、按错误类型结构化归类;指标能精确到每个模型的 P95 延迟、每秒请求数(RPS)、GPU 显存占用率

首先,是日志结构化。我们在 Triton 启动命令中,强制启用 JSON 格式日志:

tritonserver \ --model-repository=/mnt/triton-models \ --log-verbose=1 \ --log-format=json \ # 关键!开启 JSON 日志 --strict-model-config=false \ --grpc-port=8001 \ --http-port=8000 \ --metrics-port=8002

JSON 日志的关键字段包括:"level"(INFO/WARNING/ERROR)、"model_name"(当前操作的模型名)、"request_id"(唯一请求标识)、"message"(人类可读消息)、"timestamp"(ISO8601 时间戳)。这使得后续用 Fluent Bit 采集时,可以轻松提取model_name作为日志流标签,发送到 CloudWatch Logs 的不同 Log Group(如/ecs/triton/resnet50-errors/ecs/triton/bert-inference)。

其次,是指标采集与聚合。Triton 内置 Prometheus metrics 端点(/metrics),暴露了数百个指标,但其中 90% 对生产运维无意义。我们通过一个轻量级metrics-exportersidecar 容器(基于 Prometheus client_python),只抓取最关键的 12 个指标:

  • nv_gpu_utilization{gpu="0",modelName="resnet50"}:GPU 0 的利用率
  • nv_gpu_memory_used_bytes{gpu="0",modelName="resnet50"}:GPU 0 已用显存
  • triton_inference_request_success{model_name="resnet50",version="1"}:成功请求数
  • triton_inference_request_failure{model_name="resnet50",version="1"}:失败请求数
  • triton_inference_request_duration_us{model_name="resnet50",version="1",quantile="0.95"}:P95 延迟(微秒)

这个 sidecar 容器将这些指标拉取后,通过 CloudWatch Agent 的Embedded Metric Format (EMF)协议,直接推送至 CloudWatch Metrics。EMF 的优势在于:它允许你在单个PutMetricDataAPI 调用中,发送多个维度(model_name,version,gpu)的指标,且数据点免费(CloudWatch Metrics 按数据点收费,EMF 是唯一免费的数据点类型)。我们为每个模型配置独立的告警规则,例如:

  • triton_inference_request_failure5 分钟内 > 10 次,触发PAGERDUTY告警;
  • nv_gpu_memory_used_bytes> 95% 持续 10 分钟,触发自动扩容 ECS Service。

最后,是日志与指标的关联分析。这是提升 MTTR(平均修复时间)的核心。我们在 Triton 的config.pbtxt中,为每个模型启用dynamic_batching并设置max_queue_delay_microseconds: 100000(100ms),这意味着如果请求排队超过 100ms,Triton 会记录一条WARNING级别日志,包含request_idqueue_time_us。与此同时,metrics-exporter会采集triton_inference_queue_duration_us指标。当我们在 CloudWatch Logs Insights 中搜索request_id="abc123",就能看到完整的请求生命周期日志;再切换到 CloudWatch Metrics,输入相同的request_id作为维度过滤器,就能看到该请求对应的队列等待时间、GPU 计算时间、网络传输时间。这种“日志-指标-链路追踪”三位一体的可观测性,是我们在线上将平均故障定位时间从 47 分钟压缩到 6 分钟的关键。

4. 实操过程详解:从 ECS Task Definition 到 Service 部署的完整流水线

4.1 Task Definition 编写:一份可审计、可复用、可参数化的声明式蓝图

ECS Task Definition 是整个部署的基石,它必须是纯文本、版本化、可自动化生成的。我们摒弃了 AWS 控制台的手动创建方式,全部通过 Terraform 模块管理。以下是一个精简但生产可用的triton-task-definition.tf核心片段:

resource "aws_ecs_task_definition" "triton" { family = "triton-prod" network_mode = "awsvpc" requires_compatibilities = ["EC2"] cpu = "4096" # 4 vCPU memory = "16384" # 16 GB RAM execution_role_arn = aws_iam_role.ecs_task_execution.arn task_role_arn = aws_iam_role.ecs_task_role.arn # GPU resource specification - critical! cpu_architecture = "X86_64" os_family = "LINUX" container_definitions = jsonencode([ { name = "triton-server" image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/triton:24.04-cuda12.2" essential = true portMappings = [ { containerPort = 8000, hostPort = 8000, protocol = "tcp" }, { containerPort = 8001, hostPort = 8001, protocol = "tcp" }, { containerPort = 8002, hostPort = 8002, protocol = "tcp" } ] linuxParameters = { devices = [ { hostPath = "/dev/nvidiactl" containerPath = "/dev/nvidiactl" permissions = ["read", "write"] }, { hostPath = "/dev/nvidia-uvm" containerPath = "/dev/nvidia-uvm" permissions = ["read", "write"] }, { hostPath = "/dev/nvidia0" containerPath = "/dev/nvidia0" permissions = ["read", "write"] } ] } environment = [ { name = "NVIDIA_VISIBLE_DEVICES", value = "all" }, { name = "TRITON_MODEL_REPOSITORY", value = "/mnt/triton-models" } ] mountPoints = [ { sourceVolume = "triton-models" containerPath = "/mnt/triton-models" readOnly = true } ] healthCheck = { command = ["CMD-SHELL", "python3 /opt/check_triton_health.py"] interval = 30 timeout = 5 retries = 3 startPeriod = 120 } logConfiguration = { logDriver = "awslogs" options = { "awslogs-group" = "/ecs/triton-prod" "awslogs-region" = "us-east-1" "awslogs-stream-prefix" = "ecs" } } }, { name = "metrics-exporter" image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/metrics-exporter:1.2" essential = true environment = [ { name = "TRITON_METRICS_URL", value = "http://localhost:8002/metrics" } ] dependsOn = [ { containerName = "triton-server", condition = "HEALTHY" } ] } ]) volume = [ { name = "triton-models" host = { sourcePath = "/mnt/triton-models" } } ] }

这份定义的关键设计点在于:

  • requires_compatibilities = ["EC2"]:明确声明只兼容 EC2 启动类型,避免误用于 Fargate;
  • cpumemory的设定:不是随意填写,而是基于实测。我们用wrk工具对 Triton 进行压力测试,发现当并发连接数 > 200 时,Triton 主进程的 CPU 使用率会触及 3.8 vCPU,因此预留 4 vCPU 保证余量;内存则根据模型大小(ResNet50 约 180MB,BERT-Large 约 2.1GB)和预期并发数(峰值 50 QPS)计算得出,16GB 是安全下限;
  • mountPointsvolume的配对sourcePath = "/mnt/triton-models"必须与 EC2 实例上 NVMe SSD 的挂载点完全一致,这是本地模型仓库生效的前提;
  • dependsOn的使用metrics-exporter容器明确依赖triton-serverHEALTHY状态,确保指标采集总在 Triton 完全就绪后开始,避免采集到空指标或错误指标。

4.2 Service 部署与 Auto Scaling:让服务像呼吸一样自然伸缩

Task Definition 定义了“单个任务长什么样”,而 Service 定义了“我要运行多少个这样的任务”。我们的triton-service.tf如下:

resource "aws_ecs_service" "triton" { name = "triton-prod" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.triton.arn desired_count = 1 launch_type = "EC2" scheduling_strategy = "REPLICA" # Network configuration for ALB integration network_configuration { subnets = module.vpc.private_subnets security_groups = [aws_security_group.ecs_service.id] } load_balancer { target_group_arn = aws_lb_target_group.triton.arn container_name = "triton-server" container_port = 8000 } # Auto Scaling configuration dynamic "capacity_provider_strategy" { for_each = var.enable_auto_scaling ? [1] : [] content { capacity_provider = "EC2" weight = 1 base = 1 } } # CloudWatch alarms for scaling dynamic "alarms" { for_each = var.enable_auto_scaling ? [1] : [] content { alarm_name = "triton-cpu-high" alarm_description = "Scale out when CPU > 70%" metric_name = "CPUUtilization" namespace = "AWS/ECS" statistic = "Average" period = 300 evaluation_periods = 2 threshold = 70 comparison_operator = "GreaterThanThreshold" dimensions = { ClusterName = aws_ecs_cluster.main.name ServiceName = "triton-prod" } } } } # Application Load Balancer Target Group resource "aws_lb_target_group" "triton" { name = "triton-prod-tg" port = 8000 protocol = "HTTP" vpc_id = module.vpc.vpc_id health_check { path = "/v2/health/ready" healthy_threshold = 3 unhealthy_threshold = 2 timeout = 5 interval = 30 } }

这里有几个极易被忽视的细节:

  • ALB 的 Health Check Path:虽然我们在容器内实现了深度健康检查,但 ALB 的健康检查路径仍设为/v2/health/ready,而非自定义脚本。这是因为 ALB 无法执行容器内的 Python 脚本,它只能做 HTTP 探针。/v2/health/ready端点的意义是“Triton 服务已准备好接收请求”,它比/v2/health/live更严格,会检查模型加载状态。这是一个分层健康检查的设计:容器内脚本保障“模型就绪”,ALB 探针保障“服务可接入流量”,两者缺一不可。
  • Auto Scaling 的触发维度:我们没有使用 ECS 原生的TargetTrackingScaling(基于 CPU),而是采用CloudWatch 自定义指标 + Step Scaling。原因在于:CPU 利用率对 Triton 不是敏感指标。一个模型可能因数据预处理瓶颈(CPU 密集)而 CPU 高,但 GPU 利用率只有 20%;反之,一个纯 GPU 计算的模型,CPU 可能很低,但 GPU 已满载。因此,我们创建了一个自定义 CloudWatch 指标triton_gpu_utilization_percent,由metrics-exporter每 30 秒上报一次,然后配置 Step Scaling 策略:当triton_gpu_utilization_percent > 85%持续 5 分钟,增加 1 个任务;当< 40%持续 15 分钟,减少 1 个任务。这种基于 GPU 的伸缩,才是真正匹配 Triton 工作负载的。
  • desired_count = 1的深意:这并非表示我们只运行一个实例,而是将初始规模设为最小值。真正的规模由 Auto Scaling 策略动态决定。将desired_count设为 1,可以确保在没有任何流量时,Service 不会维持一个空跑的实例,从而节省成本。

4.3 模型仓库同步守护进程:S3 到 NVMe 的原子化管道

前面提到的模型仓库同步,是整个架构的“神经末梢”。我们用一个极简的 Go 程序model-sync-daemon实现,其核心逻辑如下(伪代码):

func main() { // 1. 初始化 S3 客户端和本地文件系统 s3Client := s3.New(session.Must(session.NewSession())) localFS := afero.NewOsFs() // 2. 启动 S3 事件监听(使用 S3 EventBridge + Lambda 作为触发器) // Lambda 函数收到 S3:ObjectCreated:* 事件后,调用此 daemon 的 HTTP webhook http.HandleFunc("/sync", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 3. 解析事件,获取 S3 key (e.g., "prod/resnet50-v2.1.zip") var event map[string]interface{} json.NewDecoder(r.Body).Decode(&event) s3Key := event["key"].(string) bucket := event["bucket"].(string) // 4. 下载 ZIP 包到临时目录 tmpFile, _ := ioutil.TempFile("", "model-*.zip") defer os.Remove(tmpFile.Name()) s3Client.GetObject(&s3.GetObjectInput{ Bucket: &bucket, Key: &s3Key, }).WriteTo(tmpFile) // 5. 校验 SHA256(从 S3 Object Tag 中读取) expectedSHA := getTagValue(s