1. 这不是调包是亲手搭起AI工程的骨架“AI Engineering from Scratch”——看到这个标题我第一反应不是兴奋而是下意识摸了摸键盘边角被磨出的浅痕。过去三年我带过17个从零起步的工程师团队落地AI项目其中12个卡在同一个地方他们能跑通Hugging Face的demo能微调Llama3但当服务器内存突然飙到98%、当模型在生产环境里输出一串乱码、当客户指着报表问“为什么预测值连续三天偏离超15%”没人能立刻定位是数据管道的时区配置错了还是特征缓存的TTL没对齐业务更新节奏。这根本不是算法问题是AI工程能力的断层。AI Engineering这个词正在从论文里的概念变成招聘JD里硬性要求的“熟悉MLOps工具链”“具备端到端pipeline构建经验”而from scratch绝不是指从零写Transformer而是指不依赖现成黑盒平台亲手把数据采集、特征计算、模型训练、服务部署、监控告警这一整条链路像搭乐高一样一块块拼出来、拧紧每颗螺丝。它解决的不是“能不能出结果”而是“结果能不能稳、准、快、可解释、可追溯”。适合谁不是刚学完吴恩达课程的新手而是已经能调通模型、但一进真实业务就手足无措的中级工程师是技术负责人需要评估团队是否真具备自主交付AI能力也是产品经理想搞懂为什么一个“简单”的推荐功能开发周期从预估2周拉长到6周。接下来的内容没有一行代码是抄来的所有步骤都来自我们踩坑后重写的内部手册——比如为什么特征存储必须用Delta Lake而不是Parquet为什么模型注册表要强制包含数据集指纹为什么健康检查接口必须返回三个维度的延迟指标。这不是教程是实战日志。2. 内容整体设计与思路拆解为什么拒绝“一键部署”坚持从零构建2.1 核心设计哲学把AI当作一个需要持续运维的软件系统而非一次性的实验很多团队做AI工程化起点就错了。他们把模型训练当成核心把部署当成“最后一步”把监控当成“出了问题再加”。这种思路在Kaggle上能拿牌在生产环境里会死得很难看。我们的设计起点非常朴素AI系统 数据 代码 配置 环境 人。任何一个环节的变更都可能引发蝴蝶效应。比如上周一个电商项目线上A/B测试发现新模型转化率下降3%排查三天才发现是数据团队升级了用户行为埋点SDK新增了一个session_duration_ms字段而特征工程脚本里没处理这个新字段导致所有用户的该特征被填为NULL进而让模型误判用户活跃度。如果当时特征计算是封装在黑盒平台里这个NULL值可能被平台自动填充为0或均值问题会更隐蔽。而我们从scratch构建的方案强制要求每个特征的来源、计算逻辑、缺失值处理策略、版本号全部明文定义在YAML里并和代码一起提交到Git。这样当问题发生时回滚的不是整个模型而是某一个特征的定义文件。这就是“可追溯性”的价值——它不提升模型精度但能让你在故障时少掉一半头发。2.2 方案选型背后的硬核权衡为什么选Airflow不用Prefect为什么用MLflow不用DVC工具链选择不是赶时髦而是算一笔账时间成本、人力成本、长期维护成本。我们对比过主流方案工作流编排Prefect的动态DAG和Python原生语法确实优雅但它的Operator生态远不如Airflow成熟。当我们需要集成一个老旧的Oracle数据库只支持JDBC和一个定制化的风控API需要特定证书链Airflow的SqlAlchemyOperator和SimpleHttpOperator开箱即用而Prefect需要自己写适配器。更重要的是Airflow的Web UI对非Python工程师如数据分析师更友好他们能直接在UI里触发数据重跑、查看任务日志而Prefect的UI更偏向开发者。这笔账算下来Prefect节省的10%编码时间远低于它带来的额外培训和排障成本。模型生命周期管理DVC在Git版本控制上做得极好但它对模型元数据如训练参数、硬件信息、数据集摘要的支持是弱项。而MLflow的log_model接口能自动捕获PyTorch Lightning的Trainer配置、CUDA版本、甚至GPU显存占用峰值。有一次我们发现同一份代码在不同机器上训练结果有微小差异最终通过MLflow记录的cuda_version和cudnn_version发现是cudnn版本不一致导致的。DVC无法提供这种粒度的环境快照。特征存储我们放弃Feast选择自建基于Delta Lake的特征仓库。Feast的实时特征服务很强大但它的离线存储层BigQuery/Redshift成本极高。而Delta Lake的OPTIMIZE和VACUUM命令配合S3的分层存储策略能把历史特征数据的存储成本压到Feast方案的1/5。代价是我们需要自己实现特征在线服务的gRPC接口但这恰恰让我们对特征一致性有了绝对掌控——比如我们强制要求所有在线查询必须带上feature_version参数服务端会校验该版本是否已同步到在线存储避免了“离线训练用v2特征线上服务却返回v1结果”的经典灾难。这些选择背后是一个贯穿始终的原则宁可多写200行代码也不引入一个无法完全理解其内部机制的黑盒组件。因为AI工程的终极敌人从来不是技术难度而是不可知性。2.3 架构全景图五个核心模块如何咬合运转整个系统不是单体而是由五个松耦合、高内聚的模块组成它们通过定义清晰的契约Contract交互数据摄取层Ingestion Layer不追求“实时”而追求“可靠”。使用Debezium监听MySQL binlog将变更事件写入Kafka Topic。关键设计是每个Topic的Key必须是业务主键如user_idValue中必须包含event_timestamp业务事件发生时间和ingest_timestamp数据被摄入时间。这为后续的“事件时间 vs 处理时间”对齐打下基础。特征计算层Feature Computation Layer这是最耗脑力的部分。我们采用“批流一体”架构但不是用Flink统一处理而是用Airflow调度批处理T1全量特征用Flink处理实时特征如最近5分钟点击率。两者共享同一套特征定义DSLDomain Specific Language确保逻辑完全一致。例如一个“用户30天购买频次”特征在批处理中是COUNT(*) FROM orders WHERE user_id ? AND order_time NOW() - INTERVAL 30 days在实时处理中是Flink的Tumble窗口聚合。DSL编译器会自动生成两种SQL。模型训练层Training Layer核心是“确定性”。我们禁用所有随机种子以外的不确定性来源。PyTorch的torch.backends.cudnn.benchmark FalseTensorFlow的tf.config.experimental.enable_op_determinism()连NumPy的np.random.seed()都封装在一个全局初始化函数里。每次训练启动前系统会生成一个唯一的run_id并将其作为所有随机种子的输入。这样只要代码、数据、配置、环境完全一致两次训练的权重绝对相同。这是模型可复现性的基石。模型服务层Serving Layer拒绝“一刀切”。我们为不同场景提供三种服务模式Batch Serving对全量用户做预测写入结果表供BI工具查询。用Spark MLlib直接加载模型避免HTTP开销。Online Serving低延迟API用Triton Inference Server它原生支持TensorRT优化能将BERT-base的P99延迟从120ms压到35ms。Embedded Serving将轻量模型如XGBoost编译成C库嵌入到iOS App中实现完全离线的个性化推荐。可观测性层Observability Layer不只是看CPU和内存。我们定义了AI专属的黄金指标Golden SignalsData Drift用KS检验比较线上请求数据分布与训练数据分布阈值设为0.15。Prediction Drift监控预测值的均值和方差变化超过±5%触发告警。Feature Freshness每个特征都有SLA如user_profile_age必须≤2小时超时即告警。Model Latency区分P50/P90/P99P99突增往往意味着特征计算出现瓶颈。这五个模块像五个齿轮每一个的齿形接口都经过精密设计咬合时才能传递动力而不是互相磨损。3. 核心细节解析与实操要点从代码到生产的每一处暗礁3.1 数据摄取为什么binlog监听比定时SQL抽取更安全很多人觉得“每天凌晨跑个SQL把昨天数据捞出来”最简单。但这是生产环境的定时炸弹。想象一下一个订单表有10亿行凌晨2点执行SELECT * FROM orders WHERE create_time 2024-05-20这个查询会锁表吗会不会拖慢白天的交易如果SQL执行到一半失败怎么保证不漏数据、不重数据这些问题定时SQL都无法优雅回答。而Debezium监听binlog本质是MySQL的复制协议。它不产生任何查询负载不锁表天然支持Exactly-Once语义。关键实操要点在于配置# debezium-connector-mysql.properties database.history.kafka.bootstrap.servers: kafka:9092 database.history.kafka.topic: schema-changes.inventory # 这行是灵魂它告诉Debeziumschema变更也当普通事件发出去 include.schema.changes: true # 必须开启否则无法捕获大事务 snapshot.mode: initial # 关键设置合理的batch大小避免Kafka消息过大 max.batch.size: 1024提示include.schema.changes: true这个配置常被忽略。当DBA给用户表加了一个vip_level字段Debezium会先发一条schema change事件里面包含新字段的类型和默认值。我们的下游Flink作业会监听这个事件动态更新特征计算的Schema无需重启作业。这是实现“零停机”数据模型演进的关键。另一个暗礁是时区。MySQL的TIMESTAMP类型会自动转成UTC存储但DATETIME不会。我们的做法是在Debezium连接字符串里强制指定serverTimezoneUTC并在Kafka Producer里将所有时间字段序列化为ISO8601格式如2024-05-20T08:30:00.123Z彻底规避时区歧义。我亲眼见过一个金融项目因为没统一时区导致风控模型把“昨日收盘价”错当成“今日开盘价”损失惨重。3.2 特征计算DSL的设计如何让批流逻辑真正一致特征不一致是AI项目上线后最大的“背锅侠”。我们设计的特征DSL核心是抽象出“时间窗口”和“聚合函数”两个原语# features/user_purchase_frequency.yaml name: user_30d_purchase_count description: 用户过去30天购买订单数 entity: user window: type: time_range unit: days length: 30 # 关键定义时间锚点是事件时间还是处理时间 anchor: event_time aggregation: function: COUNT field: order_id filter: status paid这个YAML会被编译器翻译成两套代码批处理Spark SQLSELECT user_id, COUNT(order_id) AS user_30d_purchase_count FROM orders WHERE status paid AND order_time date_sub(current_date(), 30) GROUP BY user_id实时处理Flink SQLSELECT user_id, COUNT(order_id) AS user_30d_purchase_count FROM orders GROUP BY user_id, TUMBLING(ORDER BY order_time, INTERVAL 30 DAYS) HAVING status paid注意Flink的TUMBLING窗口必须基于order_time事件时间而不是处理时间。这就要求Kafka消息里必须有准确的event_timestamp。我们强制规定所有上游Producer在发送消息前必须调用System.currentTimeMillis()获取时间戳并写入消息Header。Flink Consumer会从Header里读取这个时间戳作为事件时间。这是保证批流结果一致的物理基础。实操心得我们曾遇到一个坑某个上游服务在写Kafka时把event_timestamp写成了System.nanoTime()纳秒级而Flink期望的是毫秒级。结果所有窗口都计算失败。解决方案是在Kafka Consumer端加一层校验中间件自动将纳秒转毫秒并记录日志。这个中间件现在成了我们所有项目的标配。3.3 模型训练确定性训练的七道关卡让训练100%可复现需要攻克七道关卡缺一不可关卡工具/配置为什么必须我们踩过的坑1. 随机种子seed_everything(42)初始化所有随机源忘记设PyTorch的torch.manual_seed()导致每次训练权重不同2. CUDA确定性torch.backends.cudnn.enabled False禁用cudnn的非确定性算法启用cudnn后同一份代码在不同GPU上结果有微小差异3. NumPy确定性np.random.seed(42)初始化NumPy随机数数据增强时用了np.random.choice()未设种子4. Dataloader确定性worker_init_fn,generator子进程随机数隔离多进程加载数据时各worker的随机种子相同导致数据重复5. 操作系统级export CUBLAS_WORKSPACE_CONFIG:4096:2强制cuBLAS使用确定性算法不设此环境变量torch.mm()结果不稳定6. Python哈希export PYTHONHASHSEED42字典遍历顺序确定训练时用字典存特征名顺序不同导致模型输入列错位7. 硬件一致性固定GPU型号、驱动版本避免硬件级浮点差异在A100上训练在V100上推理因FP16精度差异导致结果漂移这七道关卡我们封装成一个reproducible_training.py模块所有训练脚本的第一行就是from reproducible_training import seed_everything; seed_everything(42)。它不是一个可选项而是一条铁律。有一次算法同学说“这次训练效果特别好但没法复现”我们查日志发现他忘了设PYTHONHASHSEED导致特征工程阶段字典顺序随机输入张量列顺序错乱模型其实学到了错误的映射关系。这个教训让我们把所有训练环境都容器化镜像里预装了所有必需的环境变量。3.4 模型服务Triton的配置如何榨干GPU性能Triton不是装上就能用它的配置是门艺术。我们针对BERT类模型摸索出一套黄金配置# config.pbtxt name: bert_ranker platform: pytorch_libtorch max_batch_size: 32 input [ { name: input_ids data_type: TYPE_INT64 dims: [128] } ] output [ { name: logits data_type: TYPE_FP32 dims: [2] } ] # 关键启用TensorRT加速 optimization: { execution_accelerators: { gpu_execution_accelerator: [{ name: tensorrt }] } } # 关键设置动态批处理平衡延迟和吞吐 dynamic_batching: { max_queue_delay_microseconds: 100 } # 关键为不同batch size预编译避免运行时编译卡顿 instance_group [ { count: 2 kind: KIND_GPU } ]注意max_queue_delay_microseconds: 100这个值是反复压测出来的。设得太小如10μs会导致batch size经常为1吞吐上不去设得太大如1000μsP99延迟飙升。我们用locust模拟真实流量发现100μs是延迟和吞吐的最佳平衡点。另一个重要技巧是模型量化。我们不直接用PyTorch的torch.quantization而是用NVIDIA的pytorch_quantization库它能生成Triton原生支持的INT8模型。量化后BERT-base的GPU显存占用从1.8GB降到0.9GBP99延迟从120ms降到35ms精度损失仅0.3%在验证集上。这个精度损失在业务可接受范围内但性能提升是质的飞跃。4. 实操过程与核心环节实现从零开始搭建第一个端到端Pipeline4.1 环境准备用Docker Compose一键拉起本地沙盒所有操作都在一个隔离的Docker环境中进行避免污染本地环境。docker-compose.yml如下version: 3.8 services: zookeeper: image: confluentinc/cp-zookeeper:7.3.0 environment: ZOOKEEPER_CLIENT_PORT: 2181 ZOOKEEPER_TICK_TIME: 2000 kafka: image: confluentinc/cp-kafka:7.3.0 depends_on: [zookeeper] ports: [9092:9092] environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092 KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: demo_db ports: [3306:3306] volumes: - ./mysql-init.sql:/docker-entrypoint-initdb.d/init.sql airflow: build: ./airflow depends_on: [mysql, kafka] ports: [8080:8080] environment: AIRFLOW__CORE__EXECUTOR: CeleryExecutor AIRFLOW__CELERY__RESULT_BACKEND: dbmysql://root:rootmysql:3306/airflow AIRFLOW__CELERY__BROKER_URL: redis://redis:6379/1 AIRFLOW__CORE__SQL_ALCHEMY_CONN: mysql://root:rootmysql:3306/airflow redis: image: redis:7-alpine提示mysql-init.sql里预先创建好orders表并插入几条测试数据。这样当你docker-compose up -d后整个数据链路就活了。我们把这个Compose文件放在GitHub上新人入职第一天git clone docker-compose up10分钟内就能看到数据从MySQL流到Kafka再被Airflow消费全程可视化。这种“开箱即用”的体验极大降低了学习门槛。4.2 第一个特征实现“用户昨日订单数”的端到端流程目标计算每个用户在“昨天”相对于当前日期下的订单总数。这是一个典型的T1批处理特征。步骤1定义特征DSL# features/user_yesterday_order_count.yaml name: user_yesterday_order_count description: 用户昨日订单总数 entity: user window: type: time_range unit: days length: 1 anchor: event_time offset: -1 # 昨天 aggregation: function: COUNT field: order_id filter: 11 # 无过滤步骤2编写Airflow DAG# dags/feature_user_yesterday_order_count.py from airflow import DAG from airflow.operators.python import PythonOperator from datetime import datetime, timedelta import pandas as pd from sqlalchemy import create_engine default_args { owner: ai-engineer, depends_on_past: False, start_date: datetime(2024, 1, 1), email_on_failure: False, retries: 1, retry_delay: timedelta(minutes5), } dag DAG( feature_user_yesterday_order_count, default_argsdefault_args, descriptionCompute user yesterday order count, schedule_interval0 2 * * *, # 每天凌晨2点执行 catchupFalse, ) def compute_feature(**context): # 获取执行日期即“昨天”的日期 execution_date context[execution_date] yesterday (execution_date - timedelta(days1)).strftime(%Y-%m-%d) # 连接MySQL engine create_engine(mysqlpymysql://root:rootmysql:3306/demo_db) # 执行SQL sql f SELECT user_id, COUNT(*) AS user_yesterday_order_count FROM orders WHERE DATE(create_time) {yesterday} GROUP BY user_id df pd.read_sql(sql, engine) # 写入特征表这里简化为写入MySQL的features库 feature_engine create_engine(mysqlpymysql://root:rootmysql:3306/features) df.to_sql(user_yesterday_order_count, feature_engine, if_existsappend, indexFalse) compute_task PythonOperator( task_idcompute_feature, python_callablecompute_feature, dagdag, )步骤3注册到MLflow# 在compute_feature函数末尾添加 import mlflow mlflow.set_tracking_uri(http://mlflow:5000) mlflow.set_experiment(feature_computation) with mlflow.start_run(run_namefuser_yesterday_order_count_{yesterday}): mlflow.log_param(execution_date, str(execution_date)) mlflow.log_param(yesterday, yesterday) mlflow.log_metric(feature_count, len(df)) mlflow.log_artifact(features/user_yesterday_order_count.yaml)步骤4验证访问Airflow UI (http://localhost:8080)触发DAG。查看Task Log确认SQL执行成功。进入MySQL查询features.user_yesterday_order_count表确认数据正确。访问MLflow UI (http://localhost:5000)查看Run详情确认参数和指标已记录。这个看似简单的流程包含了AI Engineering的核心要素可调度、可追踪、可审计、可复现。它不是终点而是你亲手搭建的第一块基石。4.3 模型训练与部署用PyTorch Lightning训练一个二分类模型我们用一个简化的“用户是否会复购”预测任务来演示。步骤1数据准备从MySQL的features库中拉取user_yesterday_order_count和user_lifetime_value用户生命周期价值两个特征以及is_rebuy是否复购标签构成训练集。步骤2模型定义PyTorch Lightning# models/rebuy_predictor.py import pytorch_lightning as pl import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class RebuyDataset(Dataset): def __init__(self, X, y): self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.long) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] class RebuyPredictor(pl.LightningModule): def __init__(self, input_dim2, hidden_dim64, num_classes2): super().__init__() self.model nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden_dim, num_classes) ) self.loss_fn nn.CrossEntropyLoss() def forward(self, x): return self.model(x) def training_step(self, batch, batch_idx): x, y batch logits self(x) loss self.loss_fn(logits, y) self.log(train_loss, loss) return loss def configure_optimizers(self): return torch.optim.Adam(self.parameters(), lr1e-3)步骤3训练脚本# train.py import mlflow import pytorch_lightning as pl from pytorch_lightning.callbacks import ModelCheckpoint from rebuy_predictor import RebuyPredictor, RebuyDataset # 设置MLflow mlflow.set_tracking_uri(http://mlflow:5000) mlflow.set_experiment(rebuy_prediction) # 加载数据 X_train, y_train load_data_from_mysql() # 此函数省略 dataset RebuyDataset(X_train, y_train) dataloader DataLoader(dataset, batch_size32, shuffleTrue) # 创建模型 model RebuyPredictor(input_dim2) # MLflow回调 mlflow.pytorch.autolog() # 训练 trainer pl.Trainer( max_epochs10, callbacks[ModelCheckpoint(monitortrain_loss, modemin)], loggerFalse, # 使用MLflow logger ) trainer.fit(model, dataloader)步骤4导出为Triton模型# 将PyTorch模型转换为TorchScript python -c import torch from rebuy_predictor import RebuyPredictor model RebuyPredictor.load_from_checkpoint(lightning_logs/version_0/checkpoints/epoch9-step100.ckpt) model.eval() example_input torch.randn(1, 2) traced_model torch.jit.trace(model, example_input) traced_model.save(model.pt) # 创建Triton模型仓库结构 mkdir -p models/rebuy_predictor/1 cp model.pt models/rebuy_predictor/1/model.pt # 编写config.pbtxt内容同3.4节步骤5启动Triton服务并测试# 启动Triton docker run --rm -p8000:8000 -p8001:8001 -p8002:8002 -v $(pwd)/models:/models nvcr.io/nvidia/tritonserver:23.04-py3 tritonserver --model-repository/models # 测试 curl -d {inputs: [{name: input__0, shape: [1, 2], datatype: FP32, data: [[1.0, 5000.0]]}]} -X POST http://localhost:8000/v2/models/rebuy_predictor/infer这个流程从数据、特征、模型、训练、导出、服务全部打通。它证明了一件事AI Engineering from Scratch不是神话而是一系列可分解、可验证、可重复的操作。5. 常见问题与排查技巧实录那些只有踩过才知道的坑5.1 数据漂移Data Drift告警频繁但业务说“没问题”怎么办这是最经典的“告警疲劳”。我们曾有一个推荐模型每天早上9点准时触发Data Drift告警KS值高达0.3。运维同事第一反应是“模型坏了赶紧回滚”但算法同学检查后说“预测效果很好AUC没变”。深入排查发现根源在数据摄取层上游App的埋点SDK在每天9点会批量上报前一天的离线日志导致event_timestamp集中在9点而ingest_timestamp是9:00:01。这造成了数据分布的“尖峰”但业务逻辑上这批数据是合法的。排查技巧不要只看KS值要看分布图在告警通知里强制附上训练数据和线上数据的直方图对比图。我们用Plotly生成HTML直接嵌入Slack通知。分层采样分析对告警时段的数据按user_segment新用户/老用户、device_typeiOS/Android分层计算KS值。我们发现只有“新用户iOS”的组合KS值高其他都正常。这指向了新用户注册流程的iOS SDK Bug。建立“漂移白名单”对于已知的、业务可接受的漂移如节假日、大促在告警系统里配置白名单规则例如if event_time in [2024-05-01, 2024-10-01] then ignore。实操心得我们后来在数据摄取层加了一个“数据质量探针”它会实时计算每个Kafka Topic的event_timestamp和ingest_timestamp的差值分布。如果差值普遍大于1小时说明上游有积压这时才值得触发高级别告警。这把告警准确率从42%提升到了89%。5.2 模型服务延迟Latency突增P99从50ms飙到500ms如何快速定位延迟问题永远是“冰山一角”。表面是Triton慢根因可能在千里之外。标准排查路径5分钟法则第一步1分钟确认是Triton本身还是上游直接用curl绕过所有网关直连Triton的/v2/health/ready和/v2/health/live看是否响应正常。如果健康检查都慢说明是Triton或GPU问题。第二步1分钟检查GPU资源nvidia-smi看GPU利用率、显存占用、温度。我们曾遇到一个案例GPU温度达到92°C触发了降频保护计算速度直接腰斩。解决方案是调整服务器风扇策略。第三步2分钟检查Triton日志docker logs triton_container | grep -i error\|warn。重点看是否有OOM内存溢出或cudaErrorMemoryAllocation。如果有说明模型太大需要量化或减小batch size。第四步1分钟检查特征服务如果前三步都正常问题大概率在特征服务。用curl直接调用特征服务的健康检查接口并用time curl测延迟。我们曾发现特征服务的Redis连接池耗尽导致每个请求都要新建连接延迟暴涨。独家避坑技巧我们在Triton的config.pbtxt里加了一个metrics: { enable: true }然后用Prometheus抓取nv_inference_server指标。我们定义了一个SLOrate(triton_inference_request_success[5m]) / rate(triton_inference_request_total[5m]) 0.995。当这个SLO不满足时才触发深度排查。这避免了大量“偶发抖动”带来的无效排查。5.3 Airflow DAG执行失败日志显示“Connection refused”但MySQL明明在运行这是新手最常见的幻觉。Connection refused不代表MySQL挂了而代表Airflow Worker找不到MySQL。根本原因Docker网络。Airflow Worker运行在自己的容器里它访问mysql是访问Docker Compose定义的mysql服务名这个服务名只在Compose网络内有效。如果你在宿主机上ping mysql肯定不通因为宿主机不在那个网络里。排查与解决确认网络docker network ls找到你的Compose网络名通常是project_name_default然后docker network inspect network_name确认mysql和airflow-worker容器都在这个网络里。进入Worker容器调试docker exec -it airflow_worker_container_id bash然后ping mysql。如果通说明网络OK如果不通检查docker-compose.yml里airflow服务的depends_on是否写了mysql。检查连接字符串Airflow的SQL_ALCHEMY_CONN必须是mysql://root:rootmysql:3306/airflow注意是mysql:3306不是localhost:3306。localhost在容器里指向自己不是MySQL容器。提示我们把所有服务的连接字符串都定义在airflow/airflow.cfg的[connections]部分并用airflow connections add命令导入。这样所有DAG都可以用conn_idmysql_default来引用避免硬编码。5.4 MLflow模型注册后Triton加载失败报错“Unknown model format”这是格式陷阱。MLflow的log_model默认保存的是mlflow.pytorch格式这是一种包含Python代码的打包