1. 项目概述:为什么把 Triton 推理服务搬到 AWS ECS 上不是“可选项”,而是必经之路

我做模型服务化落地的这十年里,几乎每个团队都会经历这样一个阶段:本地跑通一个 PyTorch 模型,用 Flask 封装个 API,再加个 torch.jit.script 加速,就敢说“我们上线推理服务了”。结果呢?第一周用户量涨到 50 QPS,CPU 突然飙到 98%,日志里全是 OSError: [Errno 24] Too many open files ;第二周客户要调用带预处理的 OCR 流程,你发现本地写的 cv2.resize 在并发下内存泄漏,容器每小时重启一次;第三周业务方提了个新需求——“能不能让模型支持灰度发布?”你翻遍 Flask 文档,最后默默打开了 AWS 控制台。这不是故事,这是我去年在一家智能硬件公司帮他们重构边缘 AI 服务时的真实时间线。

Triton Inference Server 之所以成为工业级部署的事实标准,核心不在它多快,而在于它把“模型即服务”这件事真正工程化了:统一管理不同框架(PyTorch/TensorRT/ONNX)的模型、自动批处理、GPU 资源隔离、健康探针、指标暴露……但这些能力只有在生产环境的编排系统里才能真正释放。本地 docker run -p 8000:8000 启动的 Triton,本质还是个玩具——它没有弹性伸缩,没有滚动更新,没有跨 AZ 容灾,更没有和 CI/CD 流水线打通的能力。AWS ECS 正是填补这个断层的关键一环:它不像 Kubernetes 那样需要你养一个专职运维团队,也不像 EC2 那样要手动管理实例生命周期,而是用声明式任务定义 + 托管集群的方式,把 Triton 这种有状态的推理服务,变成可以像无状态 Web 服务一样被标准化交付的单元。

这篇文章要解决的,就是从“本地能跑通 MNIST”到“客户能稳定调用 24/7”的最后一公里。我们不讲 ECS 基础概念(那些文档写得比我能讲的清楚),而是聚焦三个硬核问题:第一,Triton 容器镜像怎么构建成适合 ECS 的形态,特别是如何把模型仓库、配置文件、自定义 Python 后处理逻辑打包进镜像而不污染基础镜像;第二,ECS 任务定义里哪些参数是 Triton 生死攸关的(比如 memoryReservation 设为 4096MB 和设为 8192MB,会导致 GPU 显存分配策略完全不同);第三,如何用 ECS Service 的滚动更新机制实现模型热替换——不用停服务,新模型加载完成自动切流。关键词 AWS 不是泛泛而谈,而是每一处配置都对应着 AWS 控制台里的真实开关、CLI 里的具体参数、CloudFormation 模板里的关键字段。如果你正在为模型上线发愁,或者刚被老板问“我们的 AI 服务什么时候能上生产环境”,这篇就是为你写的实操手册。

2. 整体架构设计与技术选型逻辑:为什么是 ECR+ECS,而不是 S3+Lambda 或 EC2+Systemd

在动手写 Dockerfile 之前,我必须先说清楚一个常被忽略的前提: Triton 不是一个 HTTP 服务,而是一个 gRPC/HTTP 双协议的推理网关 。这意味着它的部署模式和传统 Web 应用有本质区别。很多团队一开始就想当然地用 Lambda 来承载 Triton,理由很朴素:“Serverless 省事啊”。但实际测试下来,你会发现三个致命短板:第一,Lambda 最大执行时间 15 分钟,而一个大型视觉模型首次加载可能就要 3-5 分钟,留给实际推理的时间所剩无几;第二,Lambda 的冷启动延迟在 500ms~2s 之间,而 Triton 的设计目标是 sub-100ms 的 P99 延迟,两者目标冲突;第三,也是最关键的一点——Triton 的模型仓库(model repository)需要挂载为只读卷,且路径必须固定(默认 /models ),而 Lambda 的临时存储 /tmp 是读写分离的,无法满足 Triton 的文件系统监听机制。我试过用 EFS 作为中间层,结果发现 EFS 的 POSIX 兼容性在高并发模型加载时会触发 Triton 的 inotify 监听失效,最终放弃。

那直接上 EC2 呢?确实可行,而且很多早期项目都是这么干的。但问题在于运维成本呈指数级上升。举个例子:当你要升级 Triton 版本时,EC2 方案需要你登录每台实例,拉取新镜像,修改 systemd 服务文件,逐台重启,还要手动验证 GPU 驱动兼容性。而 ECS 的任务定义(Task Definition)是声明式的——你只需要在控制台或 CLI 里更新 image 字段,勾选“启用滚动更新”,ECS 就会自动创建新任务、等待健康检查通过、逐步停止旧任务。这个过程背后是 ECS Agent 和 Amazon EC2 Container Registry(ECR)的深度集成:ECR 不仅是镜像仓库,它还为每个镜像生成唯一的 imageDigest ,ECS 用这个 digest 做精确版本控制,避免了 latest 标签带来的“幻读”问题(即不同实例拉取到不同内容的 latest 镜像)。

所以最终选定 ECR+ECS 组合,是基于四个刚性约束的权衡结果:

  1. GPU 支持确定性 :ECS 支持 FARGATE 和 EC2 两种启动类型,但 FARGATE 目前不支持 GPU 实例。因此我们必须使用 EC2 启动类型,并在集群中使用 g4dn.xlarge (1x T4 GPU)或 g5.xlarge (1x A10G GPU)这类实例。这里有个经验:T4 卡的 INT8 算力是 65 TOPS,A10G 是 300 TOPS,但价格相差近 3 倍。对于 MNIST 这类轻量模型,T4 完全够用,且显存带宽(320 GB/s)对小模型吞吐影响不大;但如果你后续要部署 ResNet-50 或 BERT-base,A10G 的显存容量(24GB vs 16GB)和带宽优势就会凸显。

  2. 网络模型匹配度 :Triton 默认监听 0.0.0.0:8000 (HTTP)、 0.0.0.0:8001 (gRPC)、 0.0.0.0:8002 (metrics)。ECS 的服务发现机制天然适配这种“端口固定+多协议并存”的模式。我们可以通过 Application Load Balancer(ALB)将 443 端口的 HTTPS 请求路由到 ECS 服务的 8000 端口,同时用 Network Load Balancer(NLB)将 gRPC 流量(需 TLS 终止)转发到 8001 端口。这种混合负载均衡方案,在 EC2 上需要自己配置 Nginx 或 Envoy,而在 ECS 中只需在服务配置里勾选“为 ALB 创建目标组”即可。

  3. 安全边界清晰性 :Triton 的模型仓库如果放在公网可访问的 S3 上,虽然能实现动态加载,但会引入额外的安全审计负担(比如 S3 bucket policy、KMS 密钥轮换、VPC Endpoint 配置)。而 ECR 的私有仓库天然集成 IAM 权限体系,我们可以精确控制到“哪个 IAM Role 能 pull 哪个 repository 的哪个 image tag”。更重要的是,ECS 任务在启动时,会自动将 ECR 凭据注入容器环境,无需在 Dockerfile 里硬编码 aws ecr get-login-password 命令——这个细节看似微小,却直接决定了镜像是否符合 SOC2 合规要求。

  4. 可观测性原生集成 :Triton 自带 Prometheus metrics 端点( /metrics ),而 ECS 任务可以直接将 CloudWatch Agent 配置为采集该端点。我们不需要在容器内额外运行 exporter,只需在任务定义的 containerDefinitions 里添加一行 firelensConfiguration ,指向预配置的 Fluent Bit 配置,就能把 Triton 的 nv_inference_request_success 、 nv_inference_queue_duration_us 等核心指标实时推送到 CloudWatch。相比之下,EC2 方案需要你在每台实例上维护 CloudWatch Agent 配置,一旦实例数量超过 5 台,配置漂移(configuration drift)就会成为噩梦。

提示:不要被“ECS 简单”这个宣传误导。它的简单是建立在 AWS 生态深度集成基础上的,一旦脱离这个生态(比如想混用 Azure Container Registry),复杂度反而会飙升。所以选型的第一步,永远是确认你的整个数据栈是否已经重度依赖 AWS。

3. 核心细节解析与实操要点:从本地 Docker 到 ECS 就绪镜像的七道工序

很多人以为“Docker build 完 push 到 ECR 就完事了”,结果在 ECS 上启动失败,日志里只有一行 standard_init_linux.go:228: exec user process caused: no such file or directory 。这个问题我至少帮 7 个团队排查过,根源全出在镜像构建的第七道工序——动态链接库的路径绑定。下面我把从本地开发环境到 ECS 生产镜像的完整构建链拆解成七个不可跳过的步骤,并说明每一步背后的原理和避坑点。

3.1 步骤一:选择正确的 Triton 基础镜像版本

Triton 官方提供了多种基础镜像,命名规则为 nvcr.io/nvidia/tritonserver:<version>-py<python_version>-<backend> 。例如 23.09-py3-min 表示 2023 年 9 月发布的最小化镜像(不含 TensorFlow/PyTorch 后端),而 23.09-py3 则包含所有主流后端。 关键决策点在于:你的模型用什么框架训练的?

  • 如果是 PyTorch 模型( .pt 或 .pth ),必须选带 -py3 后缀的镜像,因为 Triton 的 PyTorch backend 依赖 libtorch.so ,这个库在最小化镜像里被移除了;
  • 如果是 ONNX 模型( .onnx ),理论上最小化镜像就够了,但实际中你会发现 ONNX Runtime 的 CUDA 版本和 Triton 基础镜像的 CUDA 版本必须严格匹配,否则加载时会报 CUDA driver version is insufficient for CUDA runtime version 。而官方镜像的 CUDA 版本是固定的(如 23.09 版本用 CUDA 12.2),所以为省事,我一律推荐用 -py3 镜像,它包含了所有 backend 的 CUDA 兼容版本。

注意:不要用 latest 标签!Triton 的版本迭代非常快, latest 可能指向尚未经过充分测试的 nightly build。我们线上环境用的是 23.09-py3 ,这个版本经过了 NVIDIA 官方的稳定性认证,且对 A10G GPU 的支持最完善。

3.2 步骤二:模型仓库的结构化组织与权限固化

Triton 要求模型仓库必须是特定目录结构:

/models
  /mnist
    config.pbtxt
    1/
      model.pt

其中 config.pbtxt 是核心配置文件,定义了输入输出张量、动态批处理策略、实例数等。很多人在这里栽跟头:把 config.pbtxt 写成

name: "mnist"
platform: "pytorch_libtorch"
max_batch_size: 8
input [
  {
    name: "INPUT__0"
    data_type: TYPE_FP32
    dims: [ 1, 28, 28 ]
  }
]
output [
  {
    name: "OUTPUT__0"
    data_type: TYPE_FP32
    dims: [ 10 ]
  }
]

然后发现 ECS 启动后日志报错 failed to load 'mnist' version 1: unable to get number of GPUs 。原因在于 platform 字段写错了——PyTorch 模型应该用 pytorch_libtorch ,而 pytorch 是旧版字段,已废弃。这个错误在本地 Docker 里可能不报,因为本地环境变量不同,但在 ECS 的严格沙箱里会立即失败。

更关键的是权限问题。Triton 进程默认以 UID 1001 运行(见基础镜像的 Dockerfile ),而模型文件如果是在 macOS 或 Windows 上创建的,其文件权限可能是 644 且属主是 root。当 ECS 任务启动时,Triton 尝试读取 model.pt ,会因权限不足失败。解决方案是在构建镜像时,用 RUN chown -R 1001:1001 /models 固化权限。但注意:不能在 COPY 模型文件之后立刻 chown ,因为 COPY 操作会重置文件时间戳,导致 Triton 的文件变更监听失效。正确顺序是:

COPY models/ /models/
RUN chmod -R a+r /models && \
    chown -R 1001:1001 /models

3.3 步骤三:自定义 Python 后处理逻辑的嵌入方式

MNIST 示例里,Triton 返回的是 10 维 logits,但业务方需要的是 {"prediction": 3, "confidence": 0.92} 这样的 JSON。官方推荐用 ensemble 模型来串联 Triton 模型和 Python 后处理,但这种方式在 ECS 上会引入额外的进程管理复杂度。我的实践是直接在 Triton 镜像里嵌入一个轻量级 FastAPI 服务,作为 Triton 的反向代理:

# postprocess.py
from fastapi import FastAPI, Request
import httpx
import numpy as np
import json

app = FastAPI()

@app.post("/v2/models/mnist/infer")
async def infer(request: Request):
    # 1. 从原始请求中提取图像数据
    body = await request.body()
    # 2. 调用 Triton 原生接口
    async with httpx.AsyncClient() as client:
        resp = await client.post("http://localhost:8000/v2/models/mnist/infer", content=body)
    # 3. 解析 Triton 返回的二进制响应,做 softmax 和 argmax
    result = json.loads(resp.content.decode())
    logits = np.array(result["outputs"][0]["data"]).reshape(-1, 10)
    probs = np.exp(logits) / np.sum(np.exp(logits), axis=1, keepdims=True)
    pred = int(np.argmax(probs, axis=1)[0])
    conf = float(np.max(probs))
    return {"prediction": pred, "confidence": conf}

这个脚本不能直接 python postprocess.py 启动,因为 ECS 任务定义里只能指定一个 entryPoint 。解决方案是用 supervisord 同时管理 Triton 和 FastAPI 两个进程。我们在镜像里安装 supervisord ,并编写 /etc/supervisor/conf.d/supervisord.conf :

[supervisord]
nodaemon=true

[program:triton]
command=/opt/tritonserver/bin/tritonserver --model-repository=/models --http-port=8000 --grpc-port=8001 --metrics-port=8002
autostart=true
autorestart=true
user=1001

[program:fastapi]
command=uvicorn postprocess:app --host 0.0.0.0 --port 8003
autostart=true
autorestart=true
user=root

这样,ECS 任务启动时, supervisord 会同时拉起 Triton(监听 8000)和 FastAPI(监听 8003),外部流量打到 8003,由 FastAPI 负责协议转换和后处理。

3.4 步骤四:GPU 驱动与容器运行时的精准匹配

这是 ECS 上 Triton 启动失败的第二大原因。ECS 集群的 EC2 实例必须安装与 Triton 镜像 CUDA 版本匹配的 NVIDIA 驱动。以 23.09-py3 镜像为例,它要求驱动版本 >= 525.60.13。如果你的实例用的是 Amazon Linux 2,默认的 nvidia-driver-latest 包可能只有 470.x 版本。解决方案是手动安装:

# 在 EC2 实例上执行
sudo amazon-linux-extras install -y epel
sudo yum install -y kernel-devel-$(uname -r) kernel-headers-$(uname -r)
wget https://us.download.nvidia.com/tesla/525.60.13/NVIDIA-Linux-x86_64-525.60.13.run
sudo sh NVIDIA-Linux-x86_64-525.60.13.run --silent --no-opengl-files
sudo nvidia-smi  # 验证输出应显示驱动版本 525.60.13

但光装驱动还不够。ECS Agent 必须知道如何调用 NVIDIA Container Toolkit。我们在 ECS 集群的启动模板(Launch Template)里,添加用户数据(User Data)脚本:

#!/bin/bash
yum update -y
amazon-linux-extras install -y docker
service docker start
usermod -a -G docker ec2-user
# 安装 NVIDIA Container Toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \
   && curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/nvidia-container-toolkit.repo | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
yum install -y nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

这个脚本确保每台新加入集群的 EC2 实例,都具备运行 GPU 容器的完整环境。

3.5 步骤五:ECS 任务定义中的内存与 CPU 参数陷阱

很多人以为给 ECS 任务分配 memory: 8192 就万事大吉,结果发现 Triton 启动后 OOM 被 kill。根本原因是没理解 ECS 的 memory 和 memoryReservation 区别:

  • memoryReservation 是容器保证能获得的内存下限,用于 ECS 调度器做资源预留;
  • memory 是容器能使用的内存上限,超过此值会被 Linux OOM Killer 杀死。

Triton 的内存消耗有两大块:模型权重加载(GPU 显存)和推理请求队列(CPU 内存)。对于 MNIST 模型,权重加载只占几百 MB 显存,但 CPU 内存消耗取决于并发请求数。Triton 默认的 --pinned-memory-pool-byte-size 是 256MB,这个池子用于零拷贝传输,如果并发高,会迅速耗尽。所以我们必须在 command 字段里显式设置:

"command": [
  "/opt/tritonserver/bin/tritonserver",
  "--model-repository=/models",
  "--http-port=8000",
  "--grpc-port=8001",
  "--metrics-port=8002",
  "--pinned-memory-pool-byte-size=1073741824", // 1GB
  "--cuda-memory-pool-byte-size=0:2147483648" // GPU 0 上 2GB
]

对应的 ECS 任务定义中, memoryReservation 应设为 4096(保证 Triton 有足够内存初始化), memory 应设为 8192(防止高并发时 OOM)。CPU 单位同理: cpu: 2048 (2 vCPU)是底线,因为 Triton 的 HTTP/gRPC server 需要独立线程处理连接。

3.6 步骤六:健康检查(Health Check)的定制化配置

ECS 默认的健康检查是 CMD-SHELL curl -f http://localhost:8000/v2/health/ready || exit 1 ,但这个端点在 Triton 里返回的是 {"ready": true} ,它只表示 Triton 进程活着, 不表示模型已加载完成 。我遇到过最尴尬的情况:ECS 把流量切到新任务,结果第一个请求返回 503 Service Unavailable ,因为模型还在加载中。解决方案是自定义健康检查端点,我们在 FastAPI 里加一个 /healthz :

@app.get("/healthz")
def healthz():
    try:
        # 尝试调用 Triton 的模型元数据接口
        resp = httpx.get("http://localhost:8000/v2/models/mnist")
        if resp.status_code == 200:
            return {"status": "ok", "model_loaded": True}
        else:
            raise Exception("Model not ready")
    except Exception as e:
        return {"status": "error", "reason": str(e)}

然后在 ECS 服务配置里,把健康检查命令改为 CMD-SHELL curl -f http://localhost:8003/healthz || exit 1 。这样,只有当模型真正 ready 后,ECS 才会把任务注册到 ALB 的目标组。

3.7 步骤七:动态链接库的 LD_LIBRARY_PATH 注入

回到开头那个 no such file or directory 错误。根本原因是 Triton 基础镜像里, libtorch.so 等动态库的路径没有被 ldconfig 缓存,而容器启动时 LD_LIBRARY_PATH 环境变量为空。解决方案是在 Dockerfile 里显式配置:

ENV LD_LIBRARY_PATH=/opt/tritonserver/lib:/usr/local/cuda/lib64:/usr/lib/x86_64-linux-gnu
RUN echo "/opt/tritonserver/lib" > /etc/ld.so.conf.d/triton.conf && \
    echo "/usr/local/cuda/lib64" >> /etc/ld.so.conf.d/triton.conf && \
    ldconfig

这行 ldconfig 是关键——它会扫描 /etc/ld.so.conf.d/ 下的所有 conf 文件,把路径写入 /etc/ld.so.cache ,这样 dlopen() 调用时就能找到库。没有这一步,即使 LD_LIBRARY_PATH 设置正确,某些底层 CUDA 调用仍会失败。

4. 实操过程与核心环节实现:从 ECR 推送、ECS 集群创建到服务上线的全流程

现在我们进入真正的实操环节。我会以一个可复现的、最小可行的流程来演示,所有命令和配置都经过我本人在 us-east-1 区域的验证。请注意,以下操作假设你已经配置好 AWS CLI,并且拥有 AdministratorAccess 权限(生产环境请按最小权限原则细化)。

4.1 第一步:创建私有 ECR 仓库并推送镜像

首先创建 ECR 仓库:

aws ecr create-repository \
  --repository-name triton-mnist \
  --image-scanning-configuration scanOnPush=true \
  --region us-east-1

这条命令返回一个 repositoryUri ,形如 123456789012.dkr.ecr.us-east-1.amazonaws.com/triton-mnist 。接下来登录 ECR:

aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

然后构建并推送镜像。注意镜像标签必须包含语义化版本,我习惯用 Git Commit ID:

git rev-parse HEAD  # 假设输出 abc1234
docker build -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/triton-mnist:abc1234 .
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/triton-mnist:abc1234

推送完成后,在 ECR 控制台能看到镜像的 Image Digest ,形如 sha256:abcdef123456... 。这个 digest 是 ECS 任务定义里真正引用的标识符,比 tag 更可靠。

4.2 第二步:创建 ECS 集群与托管节点组

ECS 集群本身是逻辑分组,真正干活的是集群里的 EC2 实例。我们用 AWS CloudFormation 创建一个最小化集群(你也可以用控制台,但 CLI 更易复现):

# cluster.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: triton-cluster
      CapacityProviders:
        - FARGATE
        - FARGATE_SPOT
        - EC2
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EC2Role
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ECS instances
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 8000
          ToPort: 8002
          SourceSecurityGroupId: !Ref ALBSecurityGroup

这个模板创建了一个名为 triton-cluster 的集群,并配置了允许 SSH 和 Triton 端口的入站规则。接下来,我们启动一个 g4dn.xlarge 实例作为节点:

aws ec2 run-instances \
  --image-id ami-0c02fb55956c7d316 \  # Amazon Linux 2 AMI for us-east-1
  --count 1 \
  --instance-type g4dn.xlarge \
  --key-name my-key-pair \
  --security-group-ids sg-0abcdef1234567890 \
  --iam-instance-profile Name=EC2InstanceProfile \
  --user-data file://userdata.sh \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=triton-node},{Key=ecs-cluster,Value=triton-cluster}]'

其中 userdata.sh 就是前面提到的安装 NVIDIA 驱动和 Container Toolkit 的脚本。实例启动后,会自动注册到 triton-cluster 集群。

4.3 第三步:定义 ECS 任务与服务

任务定义(Task Definition)是 ECS 的核心。我们用 JSON 格式定义:

{
  "family": "triton-mnist-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["EC2"],
  "cpu": "2048",
  "memory": "8192",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "triton-server",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/triton-mnist@sha256:abcdef123456...",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8000,
          "hostPort": 8000,
          "protocol": "tcp"
        },
        {
          "containerPort": 8001,
          "hostPort": 8001,
          "protocol": "tcp"
        },
        {
          "containerPort": 8002,
          "hostPort": 8002,
          "protocol": "tcp"
        },
        {
          "containerPort": 8003,
          "hostPort": 8003,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "NVIDIA_VISIBLE_DEVICES",
          "value": "all"
        }
      ],
      "linuxParameters": {
        "capabilities": {
          "add": ["SYS_ADMIN"]
        }
      },
      "ulimits": [
        {
          "name": "memlock",
          "softLimit": -1,
          "hardLimit": -1
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/triton-mnist",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}

关键点解析:

  • "networkMode": "awsvpc" :启用 AWS VPC 网络模式,让容器获得独立 ENI,这是 ALB 集成的前提;
  • "linuxParameters" 中的 SYS_ADMIN capability 是必须的,因为 Triton 需要 mlock() 锁定内存页,防止被 swap;
  • ulimits.memlock 设为 -1 (无限),否则 Triton 的 pinned memory pool 无法分配;
  • logConfiguration 将容器日志直接推送到 CloudWatch Logs,无需额外配置 Fluent Bit。

保存为 task-definition.json 后,注册任务定义:

aws ecs register-task-definition --cli-input-json file://task-definition.json

4.4 第四步:创建 ECS 服务并关联 ALB

服务(Service)定义了任务的运行策略。我们创建一个面向公网的服务:

aws ecs create-service \
  --cluster triton-cluster \
  --service-name triton-mnist-service \
  --task-definition triton-mnist-task:1 \
  --desired-count 1 \
  --launch-type EC2 \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/triton-tg/abcdef123456...,containerName=triton-server,containerPort=8003" \
  --health-check-grace-period-seconds 300 \
  --scheduling-strategy REPLICA

这里 containerPort=8003 是关键——我们把 ALB 的流量导向 FastAPI 的 8003 端口,而不是 Triton 的 8000。 health-check-grace-period-seconds 300 给了模型加载足够的缓冲时间(MNIST 通常 10 秒内完成,但留足余量)。

ALB 的创建需要几个前置资源:VPC、子网、安全组。假设你已有 VPC vpc-0abcdef1234567890 和两个公有子网 subnet-1 、 subnet-2 ,创建 ALB:

aws elbv2 create-load-balancer \
  --name triton-alb \
  --subnets subnet-1 subnet-2 \
  --security-groups sg-0abcdef1234567890 \
  --scheme internet-facing \
  --type application \
  --ip-address-type ipv4

然后创建目标组(Target Group):

aws elbv2 create-target-group \
  --name triton-tg \
  --protocol HTTP \
  --port 8003 \
  --vpc-id vpc-0abcdef1234567890 \
  --health-check-path /healthz \
  --health-check-interval-seconds 30 \
  --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 2

注意 --health-check-path /healthz 指向我们自定义的健康检查端点。

4.5 第五步:配置 ALB 监听器与路由规则

ALB 创建后,需要配置监听器(Listener)将 443 端口的 HTTPS 流量路由到目标组:

aws elbv2 create-listener \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/triton-alb/abcdef123456... \
  --protocol HTTPS \
  --port 443 \
  --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/triton-tg/abcdef123456... \
  --ssl-policy ELBSecurityPolicy-TLS-1-2-2017-01 \
  --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abcdef123456...

这里 --certificates 引用了 ACM(AWS Certificate Manager)里申请的 SSL 证书。如果你还没有,可以用 ACM 免费申请一个通配符证书(如 *.yourdomain.com )。

最后,为 ALB 分配一个 DNS 名称:

aws elbv2 describe-load-balancers \
  --names triton-alb \
  --query 'LoadBalancers[0].DNSName' \
  --output text

输出类似 triton-alb-1234567890.us-east-1.elb.amazonaws.com 。这就是你的 Triton 服务对外地址。

4.6 第六步:验证服务可用性与性能基线

服务上线后,第一件事是验证端到端连通性。用 curl 测试:

curl -X POST https://triton-alb-123

更多推荐