新手友好:用Docker构建Vertex AI自定义流水线
1. 项目概述:为什么新手需要“定制化”的Vertex AI流水线,而不是直接抄模板?
如果你刚接触 Google Cloud 的 Vertex AI,大概率会卡在第一个真正动手的环节:怎么把本地写好的训练脚本,变成一个能在云端自动调度、容错运行、支持参数变更的 Pipeline?官方文档里那些 YAML 配置、 @component 装饰器、 PipelineJob 初始化参数,看起来像一套加密协议——你照着跑通了示例,但只要把数据路径从 gs://my-bucket/train.csv 换成 gs://my-prod-bucket/v2/features.parquet ,或者想加个模型版本号标签,整个流水线就报错退出,连日志都找不到在哪看。这不是你代码写得差,而是 Vertex AI Pipeline 的默认范式,天然偏向“中台团队封装好、业务方只填参数”的协作模式,对单人开发者或小团队来说,学习成本高、调试链路长、环境一致性差。
而这篇标题里的 Custom Vertex AI pipelines for beginners using Docker images ,核心价值恰恰在于“反向操作”:它不让你去啃 KFP(Kubeflow Pipelines)底层 DSL,也不逼你用 Python SDK 写一堆嵌套函数;而是把整个 pipeline 的每个 step(数据预处理、模型训练、评估、部署)全部打包进独立的 Docker 镜像里,再通过 Vertex AI 的 ContainerBasedComponent 或 PipelineJob 的 container_spec 直接调用。换句话说,你写的不是“KFP 流水线代码”,而是“能被流水线调用的命令行程序”。这带来三个实打实的好处:第一,本地开发和云端执行环境完全一致——你在 Mac 上 docker run -v $(pwd)/data:/data my-trainer:latest --epochs 50 --lr 0.001 跑通了,上云就几乎不会因依赖或路径出错;第二,调试极其简单——出问题?直接进容器 docker exec -it <container-id> /bin/bash ,查日志、改配置、重跑命令,全程秒级反馈;第三,复用性极强——同一个镜像,既能当 pipeline step,也能当 CLI 工具本地调试,还能集成进 CI/CD 做自动化测试。我带过 7 个刚转 AI 工程的新同事,平均上手时间从 3.2 天压缩到 8 小时,关键就是绕开了 KFP 的抽象层,先建立“可执行、可验证、可复现”的最小闭环。
这个 Part 2 的定位非常明确:Part 1 解决了“怎么把单个脚本塞进 Docker 并推到 Artifact Registry”,而 Part 2 要解决的是“怎么把这些 Docker 镜像串成有输入输出、能传参、能监控、能失败重试的完整流水线”。它不讲 Kubernetes Operator 原理,不展开 Argo Workflow 的 CRD 定义,所有内容都锚定在 Vertex AI 控制台能点、CLI 能敲、Python SDK 能调的范围内。适合三类人:一是刚学完机器学习课程、想把 Jupyter Notebook 里的模型训练流程工业化落地的学生;二是数据科学家转型 MLOps,需要快速交付可维护 pipeline 的工程师;三是中小团队技术负责人,希望用最低学习成本搭建起第一条生产级 AI 流水线。关键词 Vertex AI , Docker , pipeline , beginner , custom 不是泛泛而谈——它们对应着每一个实操环节的技术选型依据:为什么必须用 Artifact Registry 而不是 Container Registry?为什么 container_spec 的 command 字段不能写 python train.py 而要写 ["python", "train.py"] ?为什么 pipeline 参数必须定义为 ParameterType.STRING 而不是直接传 Python 字符串?这些细节,才是新手真正卡住的地方。
2. 整体设计思路:放弃“KFP 原生写法”,拥抱“Docker 为中心”的分层架构
很多教程教新手写 Vertex AI Pipeline,第一步就是让你 pip install kfp ,然后写一个带 @component 的函数,再用 @pipeline 包裹。这种写法看似“正宗”,实则埋了三个深坑:第一,本地调试困难。 @component 函数在本地执行时,KFP 会模拟一个轻量 runtime,但这个模拟和真实 Vertex AI 的容器环境差异极大——比如 os.environ.get("AIP_MODEL_DIR") 在本地是 None ,在云端是 /gcs/my-bucket/models/20240520/ ,你得反复修改代码来适配两种环境;第二,依赖管理混乱。 @component 函数里 import pandas as pd 看似简单,但实际运行时,KFP 会把整个当前 Python 环境打包上传,如果本地装了 pandas==2.2.0 ,而 Vertex AI 默认环境是 pandas==1.5.3 ,就会触发隐式降级,导致 .to_parquet() 行为不一致;第三,版本控制脆弱。一个 pipeline 文件里混着逻辑代码、参数定义、资源声明,Git diff 看不出哪次提交改了超参、哪次改了 GPU 类型,CI/CD 自动化也无从下手。
Part 2 的设计哲学,就是彻底解耦: 把“计算逻辑”锁死在 Docker 镜像里,把“编排逻辑”简化为 YAML 或 Python 字典驱动 。整个架构分三层,每层职责清晰,互不越界:
-
底层:Docker 镜像层 。每个 step 对应一个独立镜像,例如
us-central1-docker.pkg.dev/my-project/my-repo/data-prep:v1.2。镜像内只做一件事:接收命令行参数(如--input-path gs://my-bucket/raw/ --output-path gs://my-bucket/cleaned/),执行固定逻辑(读 Parquet → 清洗缺失值 → 写回 GCS),输出结构化结果(如{"status": "success", "rows_processed": 12456})。镜像构建用标准Dockerfile,基础镜像统一用python:3.10-slim,依赖通过requirements.txt显式声明,ENTRYPOINT固定为/app/entrypoint.sh,确保任何调用都走同一入口。 -
中层:组件定义层 。不再写
@component,而是用 Vertex AI 原生支持的ContainerBasedComponent(Python SDK)或containerSpec(YAML)。它只干三件事:声明镜像地址、定义命令行参数映射(把 pipeline 的input_data_uri参数,映射为容器内的--input-path)、指定资源规格(CPU/MEM/GPU)。这里的关键是“参数绑定”——不是把 Python 变量传进去,而是生成一条真实的 shell 命令,例如["python", "prep.py", "--input-path", "gs://my-bucket/raw/", "--output-path", "gs://my-bucket/cleaned/"]。这意味着,你完全可以脱离 Vertex AI,在本地用docker run模拟这条命令,验证参数传递是否正确。 -
顶层:流水线编排层 。用
PipelineJob类(Python SDK)或pipeline.yaml(YAML)描述 step 之间的依赖关系。例如train_step必须等prep_step成功后才启动,且train_step的--data-path参数,必须取prep_step输出的output_path字段值。Vertex AI 会自动解析这种依赖,并在 Argo Workflow 层生成对应的 DAG 图。重点在于: 所有跨 step 的数据传递,都通过 GCS 路径完成,而不是内存变量 。prep_step把清洗后的数据写到gs://my-bucket/cleaned/20240520/,train_step就从这个路径读——路径本身作为 pipeline 参数传递,既符合云原生存储最佳实践,又避免了 KFP 的 artifact 机制带来的序列化/反序列化陷阱。
这个分层设计,让新手能“分阶段攻克”:第一阶段,专注把单个脚本 Docker 化,确保 docker run 能跑通;第二阶段,把镜像注册到 Artifact Registry,用 gcloud 命令验证拉取;第三阶段,写最简 pipeline,只包含一个 step,验证参数传递;第四阶段,加入第二个 step,验证跨 step 数据流。每一步都有明确的成功标志(容器退出码 0、GCS 路径存在文件、pipeline 状态为 SUCCEEDED ),没有模糊地带。我见过太多人卡在“为什么 pipeline 显示 running 却没日志”,根源往往是 @component 函数里 print() 语句没被 KFP runtime 捕获,而用 Docker 方案, docker logs 一查便知。
3. 核心细节解析:Docker 镜像构建、Artifact Registry 推送与 pipeline 组件定义的硬核要点
3.1 Docker 镜像构建:为什么 requirements.txt 必须锁定版本,且 pip install 不能加 -U
新手最容易犯的错误,是在 Dockerfile 里写 RUN pip install pandas scikit-learn tensorflow ,以为这样最省事。实则这是生产环境大忌。原因有三:第一, pip install pandas 默认安装最新版,而 pandas==2.2.0 和 pandas==2.1.4 在处理 pd.read_parquet() 时,对某些嵌套 schema 的解析行为不同,可能导致训练数据列顺序错乱;第二, tensorflow 依赖 numpy ,如果 pip install tensorflow 同时升级了 numpy ,而你的训练脚本用了 numpy.ndarray.item() 这个在新版本已弃用的方法,容器就会在运行时崩溃;第三,Google Cloud 的 Artifact Registry 对镜像层有缓存机制,如果 requirements.txt 没锁定版本,每次 docker build 都会生成全新 layer,无法利用缓存,构建时间从 45 秒飙升到 6 分钟。
正确的做法是: 所有依赖必须显式锁定小版本号 。 requirements.txt 示例:
pandas==2.1.4
scikit-learn==1.3.2
tensorflow==2.15.0
google-cloud-storage==2.12.0
pyarrow==12.0.1
注意,这里没写 >= 或 ~= ,就是严格的 == 。构建时, Dockerfile 中的 RUN 指令必须加 --no-cache-dir 和 --upgrade-strategy only-if-needed :
FROM python:3.10-slim
# 创建非 root 用户,提升安全性
RUN useradd -m -u 1001 -G root -d /home/appuser appuser
USER appuser
WORKDIR /home/appuser
# 复制依赖文件并安装,禁用缓存,只升级必要包
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade-strategy only-if-needed -r requirements.txt
# 复制应用代码
COPY . .
# 设置入口脚本,确保可执行
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
entrypoint.sh 是关键枢纽,它负责解析命令行参数、设置环境变量、调用主程序:
#!/bin/bash
set -e # 任何命令失败立即退出
# 解析参数,使用 getopts 标准语法
INPUT_PATH=""
OUTPUT_PATH=""
while getopts "i:o:" opt; do
case $opt in
i) INPUT_PATH="$OPTARG" ;;
o) OUTPUT_PATH="$OPTARG" ;;
*) echo "Usage: $0 -i <input_path> -o <output_path>" >&2; exit 1 ;;
esac
done
# 验证必填参数
if [ -z "$INPUT_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Error: -i and -o are required" >&2
exit 1
fi
# 设置 GCS 认证(Vertex AI 自动挂载 service account)
export GOOGLE_APPLICATION_CREDENTIALS="/var/run/secrets/cloud.google.com/application_default_credentials.json"
# 执行主程序,传入参数
python prep.py --input-path "$INPUT_PATH" --output-path "$OUTPUT_PATH"
这个设计保证了:参数校验在容器启动时完成,错误信息直接输出到 docker logs ;GCS 认证由 Vertex AI 自动注入,无需手动管理密钥;主程序 prep.py 只需专注业务逻辑,不用管环境适配。
3.2 Artifact Registry 推送:为什么必须用 us-central1-docker.pkg.dev ,且镜像名要带 v{major}.{minor}
Artifact Registry 是 Google Cloud 官方推荐的容器镜像仓库,替代了老旧的 Container Registry。新手常忽略两个关键配置:区域(region)和镜像命名规范。
首先, 区域必须与 Vertex AI pipeline 运行区域一致 。假设你的 pipeline 在 us-central1 区域创建,那么镜像必须推送到 us-central1-docker.pkg.dev 。如果误推到 europe-west1-docker.pkg.dev ,Vertex AI 在拉取镜像时会报 Failed to pull image: rpc error: code = Unknown desc = failed to resolve reference "europe-west1-docker.pkg.dev/..." 。这不是网络问题,而是 Vertex AI 的 worker node 默认只配置了同 region 的 registry 认证。解决方案很简单:推送前,先用 gcloud config set project my-project 设置项目,再用 gcloud config set run/region us-central1 设置 region,最后执行:
# 启用 Artifact Registry API(一次)
gcloud services enable artifactregistry.googleapis.com
# 创建仓库(一次)
gcloud artifacts repositories create my-repo \
--repository-format=docker \
--location=us-central1 \
--description="Docker images for Vertex AI pipelines"
# 登录 registry(每次 shell 会话需执行)
gcloud auth configure-docker us-central1-docker.pkg.dev
# 构建并推送(注意镜像名格式)
docker build -t us-central1-docker.pkg.dev/my-project/my-repo/data-prep:v1.2 .
docker push us-central1-docker.pkg.dev/my-project/my-repo/data-prep:v1.2
其次, 镜像标签必须遵循语义化版本 v{major}.{minor} ,严禁用 latest 。原因在于: latest 标签不提供任何版本保障,今天推的镜像,明天可能被覆盖,导致 pipeline 某次运行成功、某次失败,排查难度指数级上升。 v1.2 则意味着这是一个稳定发布版,所有 v1.2 镜像的二进制内容完全一致。我在一个金融风控项目中,曾因同事误用 latest ,导致线上 pipeline 在周五下午突然开始输出异常预测结果,回溯发现是周三有人更新了 latest 镜像但未通知,最终花了 7 小时才定位到问题。从此团队强制规定:所有 pipeline 引用的镜像,必须用 v{major}.{minor} 标签,且 major 号变更需同步更新 pipeline YAML 中的镜像地址。
3.3 Pipeline 组件定义: ContainerBasedComponent 的 command 和 args 为何必须是列表,且不能含 shell 特性
当你用 Python SDK 定义一个基于容器的 component 时,核心是 ContainerBasedComponent 类:
from google.cloud.aiplatform import pipeline_jobs
from google.cloud.aiplatform.pipeline_jobs import ContainerBasedComponent
prep_component = ContainerBasedComponent(
display_name="Data Preprocessing",
container_image_uri="us-central1-docker.pkg.dev/my-project/my-repo/data-prep:v1.2",
command=["python", "prep.py"], # 关键:必须是列表,不能是字符串
args=[
"--input-path",
pipeline_parameter_values={"input_data_uri": "gs://my-bucket/raw/"},
"--output-path",
pipeline_parameter_values={"output_data_uri": "gs://my-bucket/cleaned/"}
],
machine_type="n1-standard-4",
accelerator_type="NVIDIA_TESLA_K80",
accelerator_count=1
)
这里有两个极易出错的点:第一, command 必须是字符串列表,如 ["python", "prep.py"] , 绝不能写成 "python prep.py" 。因为 Vertex AI 底层调用的是 execve() 系统调用,它需要精确的 argv[0] , argv[1] ... 如果传入字符串,系统会尝试用 /bin/sh -c 解析,而 /bin/sh 在 slim 镜像中可能不存在,或行为与 bash 不一致,导致 exec format error 。第二, args 列表中的参数, 不能包含 && 、 | 、 $() 等 shell 特性 。例如,你不能写 ["--output-path", "$(gsutil ls gs://my-bucket/cleaned/ | head -1)"] ,因为这不是 shell 执行,而是直接传给 prep.py 的 sys.argv 。所有路径拼接、命令组合,必须在 prep.py 内部用 Python 代码完成。
更关键的是参数绑定。 pipeline_parameter_values 不是简单的字符串替换,而是 Vertex AI 的参数解析引擎在 workflow runtime 时动态注入。 {"input_data_uri": "gs://my-bucket/raw/"} 这个字典,会被转换为 --input-path gs://my-bucket/raw/ 这条命令的一部分。因此,你的 prep.py 必须用标准 argparse 解析:
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input-path", type=str, required=True)
parser.add_argument("--output-path", type=str, required=True)
args = parser.parse_args()
# 业务逻辑
df = pd.read_parquet(args.input_path)
df_clean = df.dropna()
df_clean.to_parquet(args.output_path)
if __name__ == "__main__":
main()
required=True 是安全底线——如果 Vertex AI 因配置错误没传参数, argparse 会直接报错退出,并在日志中清晰显示 error: the following arguments are required: --input-path ,而不是让程序在后续 read_parquet(None) 时崩溃,让你在海量日志里大海捞针。
4. 实操过程:从零构建一个两步 pipeline(数据清洗 + XGBoost 训练),含完整代码与调试技巧
4.1 步骤一:准备本地开发环境与基础镜像
我们以一个真实场景为例:每天凌晨 2 点,从 gs://my-raw-data/banking/ 下载用户交易 CSV,清洗后存入 gs://my-clean-data/banking/ ,再用清洗数据训练一个 XGBoost 模型,预测用户流失风险。整个 pipeline 分两步: data-prep 和 xgb-train 。
首先,初始化本地目录结构:
mkdir -p vertex-pipeline-demo/{data-prep,xgb-train}
cd vertex-pipeline-demo
data-prep 目录下创建文件:
Dockerfile(内容见 3.1 节)requirements.txt(pandas==2.1.4,pyarrow==12.0.1,google-cloud-storage==2.12.0)entrypoint.sh(内容见 3.1 节)prep.py:
import argparse
import pandas as pd
from google.cloud import storage
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input-path", type=str, required=True)
parser.add_argument("--output-path", type=str, required=True)
args = parser.parse_args()
# 读取原始 CSV(注意:GCS 路径需转为 gsutil 兼容格式)
client = storage.Client()
bucket_name, blob_path = args.input_path.replace("gs://", "").split("/", 1)
bucket = client.bucket(bucket_name)
blob = bucket.blob(blob_path)
content = blob.download_as_string()
# 用 pandas 读取 CSV 字符串
import io
df = pd.read_csv(io.StringIO(content.decode("utf-8")))
# 清洗逻辑:删除空行,填充数值列均值,分类列用 'unknown'
df = df.dropna(how="all")
numeric_cols = df.select_dtypes(include=["number"]).columns
for col in numeric_cols:
df[col].fillna(df[col].mean(), inplace=True)
categorical_cols = df.select_dtypes(include=["object"]).columns
for col in categorical_cols:
df[col].fillna("unknown", inplace=True)
# 写回 GCS,使用 parquet 格式提升后续训练效率
output_buffer = io.BytesIO()
df.to_parquet(output_buffer, index=False)
output_buffer.seek(0)
output_bucket_name, output_blob_path = args.output_path.replace("gs://", "").split("/", 1)
output_bucket = client.bucket(output_bucket_name)
output_blob = output_bucket.blob(output_blob_path)
output_blob.upload_from_file(output_buffer, content_type="application/octet-stream")
if __name__ == "__main__":
main()
注意:这里没用 pd.read_csv(args.input_path) ,因为 pandas 的 read_csv 对 GCS 路径支持不稳定,直接用 google-cloud-storage SDK 下载再解析,100% 可控。
构建并本地测试 data-prep 镜像:
cd data-prep
# 创建测试数据
echo "user_id,amount,category,timestamp
1,150.0,groceries,2024-05-20T00:00:00Z
2,,electronics,2024-05-20T00:01:00Z
3,89.5,,2024-05-20T00:02:00Z" > test.csv
# 启动本地 GCS 模拟(用 fake-gcs-server)
docker run -d -p 4443:4443 -e "STORAGE_ROOT=/data" --name fake-gcs fake-gcs-server
# 上传测试数据到模拟 GCS
gsutil -o "Credentials:gs_service_key=" -m cp test.csv gs://my-bucket/raw/test.csv
# 构建镜像
docker build -t data-prep-local:v1.0 .
# 运行测试(注意:--network host 让容器能访问 localhost:4443)
docker run --network host -v $(pwd):/workspace data-prep-local:v1.0 \
--input-path gs://my-bucket/raw/test.csv \
--output-path gs://my-bucket/cleaned/test.parquet
如果看到容器退出码为 0,且 gsutil ls gs://my-bucket/cleaned/ 能列出 test.parquet ,说明本地环节 100% 通过。
4.2 步骤二:推送镜像到 Artifact Registry 并定义 pipeline
按 3.2 节步骤,将 data-prep-local:v1.0 推送到 us-central1-docker.pkg.dev/my-project/my-repo/data-prep:v1.0 。接着,创建 xgb-train 目录,结构类似, train.py 核心逻辑:
import argparse
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
import joblib
import os
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--data-path", type=str, required=True)
parser.add_argument("--model-output-path", type=str, required=True)
parser.add_argument("--n-estimators", type=int, default=100)
args = parser.parse_args()
# 读取清洗后的 parquet
df = pd.read_parquet(args.data-path)
# 特征工程(简化版)
X = df[["amount", "category_encoded"]] # category_encoded 需提前做好
y = df["is_churn"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 训练
model = xgb.XGBClassifier(n_estimators=args.n_estimators)
model.fit(X_train, y_train)
# 评估
y_pred_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_pred_proba)
print(f"Test AUC: {auc:.4f}")
# 保存模型
os.makedirs(os.path.dirname(args.model_output_path), exist_ok=True)
joblib.dump(model, args.model_output_path)
if __name__ == "__main__":
main()
构建 xgb-train:v1.0 并推送。
现在,用 Python SDK 编写完整 pipeline:
from google.cloud import aiplatform
from google.cloud.aiplatform import pipeline_jobs
from google.cloud.aiplatform.pipeline_jobs import ContainerBasedComponent
# 初始化 Vertex AI
aiplatform.init(project="my-project", location="us-central1", staging_bucket="gs://my-staging-bucket")
# 定义 data-prep component
prep_component = ContainerBasedComponent(
display_name="Data Preprocessing",
container_image_uri="us-central1-docker.pkg.dev/my-project/my-repo/data-prep:v1.0",
command=["python", "prep.py"],
args=[
"--input-path",
"gs://my-raw-data/banking/daily_20240520.csv",
"--output-path",
"gs://my-clean-data/banking/daily_20240520.parquet"
],
machine_type="n1-standard-4"
)
# 定义 xgb-train component,依赖 prep_component 的输出
train_component = ContainerBasedComponent(
display_name="XGBoost Training",
container_image_uri="us-central1-docker.pkg.dev/my-project/my-repo/xgb-train:v1.0",
command=["python", "train.py"],
args=[
"--data-path",
"gs://my-clean-data/banking/daily_20240520.parquet", # 注意:这里硬编码,实际应动态传入
"--model-output-path",
"gs://my-models/xgb_v1_20240520.joblib",
"--n-estimators",
"200"
],
machine_type="n1-standard-8",
accelerator_type="NVIDIA_TESLA_K80",
accelerator_count=1
)
# 创建 pipeline job
pipeline_job = pipeline_jobs.PipelineJob(
display_name="banking-churn-pipeline",
template_path=None, # 我们不用 YAML,直接用 Python 构建
parameter_values={},
enable_caching=False # 新手建议关闭缓存,避免状态混淆
)
# 添加 components(注意:add_component 返回的是 step 对象,可用于依赖设置)
prep_step = pipeline_job.add_component(prep_component)
train_step = pipeline_job.add_component(train_component)
# 设置依赖:train_step 必须等 prep_step 完成
train_step.after(prep_step)
# 提交 pipeline
pipeline_job.submit()
提交后,进入 Vertex AI Console 的 Pipelines 页面,找到刚提交的 job,点击进入。此时你会看到 DAG 图: Data Preprocessing 节点,下方连接 XGBoost Training 节点。点击 Data Preprocessing 节点,选择 Logs 标签页,就能实时看到容器日志,如 Downloaded 12456 rows , Saved to gs://my-clean-data/... 。如果出错,日志会直接显示 ValueError: cannot convert float NaN to integer ,你立刻知道是清洗逻辑没处理好 amount 列的空值。
4.3 步骤三:调试与监控实战技巧——如何 5 分钟定位 pipeline 失败根因
Pipeline 失败时,新手常陷入“日志太多,不知看哪”的困境。我的经验是,按以下四步,5 分钟内必定位:
第一步:看 pipeline 总体状态 。在 Console 的 pipeline job 页面,顶部状态栏显示 FAILED ,旁边有红色感叹号。点击感叹号,弹出 Error details ,这里会显示最高层错误,如 Step 'XGBoost Training' failed with exit code 1 。这告诉你失败发生在哪个 step,而非整个 pipeline。
第二步:进失败 step 查 Execution log 。点击 XGBoost Training 节点,切换到 Execution log 。这里显示的是 Argo Workflow 层的日志,开头几行通常是 Starting step , Pulling image ... 。如果卡在这里,说明镜像拉取失败,检查 container_image_uri 是否拼写错误,或 Artifact Registry 权限是否开启(需给 aiplatform.worker 角色)。
第三步:查 Container log 。在 Execution log 下方,有 Container log 标签页。这才是你的 train.py 输出。如果看到 Traceback (most recent call last): ... FileNotFoundError: [Errno 2] No such file or directory: 'gs://my-clean-data/banking/daily_20240520.parquet' ,说明 data-prep step 没成功写入,或路径不匹配。此时立刻切回 Data Preprocessing 节点,查它的 Container log ,确认是否有 Saved to gs://... 日志。
第四步:用 gcloud 快速验证 GCS 路径 。如果日志显示路径存在,但 train.py 读不到,可能是权限问题。在 Cloud Shell 中执行:
# 模拟 pipeline worker 的身份
gcloud auth application-default login # 确保用服务账号登录
gsutil ls gs://my-clean-data/banking/daily_20240520.parquet
如果返回 AccessDeniedException ,说明你的 pipeline service account(默认是 PROJECT_NUMBER-compute@developer.gserviceaccount.com )没被授予 roles/storage.objectViewer 角色。在 IAM 页面,搜索该账号,添加角色即可。
这个调试流程,我总结成一张速查表:
| 现象 | 最可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
Execution log 显示 Failed to pull image |
镜像 URI 错误或权限不足 | gcloud artifacts docker images list us-central1-docker.pkg.dev/my-project/my-repo |
检查拼写,确认 service account 有 artifactregistry.repositories.downloadArtifacts 权限 |
Container log 显示 ModuleNotFoundError: No module named 'xgboost' |
requirements.txt 未生效或镜像未重新推送 |
docker run --rm us-central1-docker.pkg.dev/my-project/my-repo/xgb-train:v1.0 pip list | grep xgboost |
重新构建推送,确认 Dockerfile 中 pip install 无误 |
Container log 显示 OSError: Unable to open file |
GCS 路径不存在或权限不足 | gsutil ls gs://my-clean-data/banking/daily_20240520.parquet |
检查 data-prep step 日志,确认写入成功;检查 IAM 权限 |
Container log 显示 Killed |
内存溢出(OOM) | 查看 Execution log 中 Memory usage 字段 |
升级 machine_type ,如从 n1-standard-4 改为 n1-standard-8 |
记住,Vertex AI 的日志是分层的: Execution log 是基础设施层, Container log 是应用层。永远先看后者,它是你代码的真实输出。
5. 常见问题与排查技巧实录:新手踩过的 7 个典型坑及独家避坑指南
5.1 问题一: container_spec 中 command 写成字符串,导致 exec format error
现象 :pipeline 提交后, Execution log 显示 Error: failed to start container: exec format error , Container log 为空。
根因分析 : command: "python train.py" 被解释为 sh -c "python train.py" ,而 python:3.10-slim 镜像中没有 /bin/sh (只有 /bin/bash ), execve() 调用失败。这是 Docker 和 Linux exec 机制的底层限制,与 Python 无关。
独家避坑指南 :在写 ContainerBasedComponent 或 YAML 时,强制自己默念三遍:“ command 是列表, command 是列表, command 是列表”。写完立刻用 print(type(command)) 验证。更进一步,可以在 Dockerfile 中加一行 RUN ls -l /bin/ ,确认镜像里到底有没有 sh 。 slim 镜像默认不带 sh ,这是 Google 官方文档里都没明说的细节。
5.2 问题二: args 中传入 gs:// 路径,但 train.py 里 pd.read_parquet() 报错
现象 : Container log 显示 pyarrow.lib.ArrowInvalid: Cannot parse URI: 'gs://my-bucket/data.parquet' 。
根因分析 : pandas.read_parquet() 默认不支持 GCS URI,它需要 pyarrow 的 GCS filesystem 支持,而这依赖 gcsfs 库。但 gcsfs 没在 requirements.txt 中,导致运行时找不到 GCS driver。
独家避坑指南 :永远在 requirements.txt 中加入 gcsfs==2023.10.0 (版本需与 pyarrow 兼容)。更稳妥的做法,是像 4.1 节那样, 彻底放弃 pandas.read_parquet("gs://...") ,改用 google-cloud-storage SDK 下载到内存再解析 。因为 gcsfs 的行为受环境变量影响(如 GCSFS_TOKEN ),而 google-cloud-storage SDK 的认证由 Vertex AI 自动注入,100% 可靠。这是我在线上项目中强制推行的规范。
5.3 问题三:pipeline 提交后一直 Pending , Execution log 无任何输出
现象 :pipeline job 状态卡在 Pending 超过 10 分钟, Execution log 空白。
根因分析 :Vertex AI 的 worker node 需要从 Artifact Registry 拉取镜像,而拉取动作由 `aiplatform.worker
更多推荐
所有评论(0)