文章目录
-
- 起因
- 一、问题背景
- [二、问题分析:Kubeflow 如何处理容器启动命令](#二、问题分析:Kubeflow 如何处理容器启动命令)
- 三、三种解决方案
-
- [方案一:在 Dockerfile 中提前设置 CLASSPATH(不推荐)](#方案一:在 Dockerfile 中提前设置 CLASSPATH(不推荐))
- [方案二:修改 Kubeflow 组件,保持 ENTRYPOINT 不被覆盖(推荐【后续证明还有问题】)](#方案二:修改 Kubeflow 组件,保持 ENTRYPOINT 不被覆盖(推荐【后续证明还有问题】))
- [方案三:在 ENTRYPOINT 中支持自检测逻辑(增强版,不推荐,太麻烦)](#方案三:在 ENTRYPOINT 中支持自检测逻辑(增强版,不推荐,太麻烦))
- 四、完整示例
-
- [1. Dockerfile](#1. Dockerfile)
- [2. Kubeflow 组件定义](#2. Kubeflow 组件定义)
- [五、QA 常见问题](#五、QA 常见问题)
- 六、参考资料
- 七、总结
- 八、kubeflow去掉command后还是覆盖ENTRYPOINT的问题
-
- [一、从逻辑上确认:Kubeflow Pipelines 的执行机制](#一、从逻辑上确认:Kubeflow Pipelines 的执行机制)
- [二、从运行层面确认:如何验证实际 command](#二、从运行层面确认:如何验证实际 command)
- 三、源码级确认(官方实现)
- [✅ 五、结论](#✅ 五、结论)
- 九、笔者最后的方案
起因
笔者最近要将一个模型的处理流程通过kubeflow来部署,其中一个模型上线环节(将模型推送到HDFS上)需要访问HDFS,这一步需要容器里配置好CLASSPATH环境变量,我通过ENTRYPOINT来配置,单机测试的时候完全正常,但部署到kubeflow上时就出错(CLASSPATH未配置导致的错误)
一、问题背景
在日常使用 Kubeflow Pipelines (KFP) 部署机器学习任务时,我们通常会以容器镜像的方式封装好任务的运行逻辑。在一些涉及 HDFS 或 Java 组件 的场景中,镜像启动时需要动态设置环境变量,例如 CLASSPATH。
在笔者的案例中,我们的镜像通过 ENTRYPOINT 脚本动态生成 Hadoop Classpath:
bash
#!/bin/bash
set -e
export CLASSPATH=$(${HADOOP_HOME}/bin/hdfs classpath --glob)
exec "$@"
Dockerfile 指定入口:
dockerfile
ENTRYPOINT ["/opt/docker/entrypoint.sh"]
在宿主机中直接运行如下命令可以正常执行:
bash
docker run -it --rm myimage bash -c "bash /opt/run.sh -t online"
但在 Kubeflow 环境中执行同样的命令却失败,报错如下:
Environment variable CLASSPATH not set!
getJNIEnv: getGlobalJNIEnv failed
也就是说:
通过 Kubeflow 启动时,
entrypoint.sh并没有被执行,导致环境变量未设置。
二、问题分析:Kubeflow 如何处理容器启动命令
Kubeflow Pipelines 在执行组件时会自动生成 PodSpec,将你在组件定义中的参数注入到 container.command 和 container.args 字段中。
而根据 Kubernetes 的容器启动规则:
- 若在容器定义中设置了
command(相当于 Docker 的ENTRYPOINT),
则会覆盖镜像原有的ENTRYPOINT。 - 若在容器定义中设置了
args(相当于 Docker 的CMD),
则会替换镜像的CMD,但不会覆盖ENTRYPOINT。
因此,当笔者在 Kubeflow 组件中这样写时:
python
spec = dsl.ContainerSpec(
image="myimage",
command=["/bin/bash", "-c"],
args=["bash /opt/run.sh -t online"],
)
实际上,Kubeflow 会生成如下容器配置:
yaml
containers:
- image: myimage
command: ["/bin/bash", "-c"]
args: ["bash /opt/run.sh -t online"]
这会直接覆盖掉镜像原有的 ENTRYPOINT /opt/docker/entrypoint.sh ,
因此,CLASSPATH 自然不会被设置。
在 K8s 或 Kubeflow Pipeline 的 Pod 里启动容器时,有以下几种调用方式差异:
| 方式 | 实际执行命令 | ENTRYPOINT 是否执行 | CMD 是否执行 |
|---|---|---|---|
| Docker CLI 默认 | ENTRYPOINT + CMD | ✅ 是 | ✅ 是 |
| K8s container.spec.command 未设置 | ENTRYPOINT + CMD | ✅ 是 | ✅ 是 |
| K8s container.spec.command 设置了值 | 覆盖 ENTRYPOINT | ❌ 否 | ✅(或被 command 替代) |
| K8s container.spec.args 设置了值 | 传给 ENTRYPOINT | ✅ 是 | ✅(传参) |
也就是说:
只要 Kubeflow 里设置了
command:(或 pipeline 组件里指定了 command),ENTRYPOINT 就不会执行。
三、三种解决方案
方案一:在 Dockerfile 中提前设置 CLASSPATH(不推荐)
如果 CLASSPATH 是固定的,可以直接写入 Dockerfile:
dockerfile
ENV CLASSPATH="/opt/hadoop/share/hadoop/common/*:/opt/hadoop/share/hadoop/hdfs/*"
但在实际生产环境中,Hadoop Classpath 通常是通过命令动态生成的,因此这种方法局限性大。
方案二:修改 Kubeflow 组件,保持 ENTRYPOINT 不被覆盖(推荐【后续证明还有问题】)
后续补充:这个方案还是有问题,详见文章最后的说明
关键点是:不要在 Kubeflow 中指定 command ,只指定 args。
修改前:
python
spec = dsl.ContainerSpec(
image="myimage",
command=["/bin/bash", "-c"],
args=["bash /opt/run.sh -t online"],
)
修改后:
python
spec = dsl.ContainerSpec(
image="myimage",
args=["bash", "/opt/run.sh", "-t", "online"],
)
这样生成的 YAML 为:
yaml
containers:
- image: myimage
args: ["bash", "/opt/run.sh", "-t", "online"]
此时 Kubeflow 不再覆盖 ENTRYPOINT,容器会按以下逻辑执行:
ENTRYPOINT ["/opt/docker/entrypoint.sh"]
CMD ["bash", "/opt/run.sh", "-t", "online"]
最终执行顺序为:
/opt/docker/entrypoint.sh bash /opt/run.sh -t online
✅ 这样既能执行 entrypoint.sh 来设置环境变量,又能正确运行业务逻辑脚本。
方案三:在 ENTRYPOINT 中支持自检测逻辑(增强版,不推荐,太麻烦)
如果你希望镜像在被 Kubeflow 覆盖 ENTRYPOINT 时仍能自恢复,可以修改 entrypoint.sh:
bash
#!/bin/bash
set -e
# 检查 CLASSPATH 是否已经存在
if [ -z "$CLASSPATH" ]; then
export CLASSPATH=$(${HADOOP_HOME}/bin/hdfs classpath --glob)
echo "[INFO] CLASSPATH set dynamically"
else
echo "[INFO] CLASSPATH already set"
fi
# 如果参数中不包含 bash/run.sh,可强制执行默认命令
if [[ "$1" == "bash" && "$2" == "/opt/run.sh" ]]; then
exec "$@"
else
exec /bin/bash -c "$@"
fi
这种方法可以在一定程度上兼容被 KFP 修改的启动行为。
四、完整示例
1. Dockerfile
dockerfile
FROM openjdk:8-jdk
ENV HADOOP_HOME=/opt/hadoop
COPY entrypoint.sh /opt/docker/entrypoint.sh
COPY run.sh /opt/run.sh
RUN chmod +x /opt/docker/entrypoint.sh /opt/run.sh
ENTRYPOINT ["/opt/docker/entrypoint.sh"]
CMD ["bash", "/opt/run.sh", "-t", "online"]
2. Kubeflow 组件定义
python
@dsl.container_component
def OnlineOp(online_docker: str):
return dsl.ContainerSpec(
image=online_docker,
args=["bash", "/opt/run.sh", "-t", "online"],
)
运行后日志输出:
[INFO] CLASSPATH set dynamically
HDFS classpath configured successfully.
Running online mode...
五、QA 常见问题
Q1:为什么我在 docker run 里加了 /bin/bash -c 可以?
A1:因为在 Docker 命令中指定 -c 时,并不会覆盖 ENTRYPOINT,而是在 ENTRYPOINT 之后追加执行。
Q2:KFP 为什么默认要覆盖 ENTRYPOINT?
A2:Kubeflow 设计上假定用户的镜像是通用基础镜像,而实际执行逻辑通过 command/args 指定,因此会自动生成完整 command。
Q3:能否在 Kubeflow YAML 层修改?
A3:可以,但在 KFP SDK 层面修改 ContainerSpec 更可控、更方便。
六、参考资料
- Kubernetes 官方文档 - Container EntryPoint and Command
- Kubeflow Pipelines SDK v2 ContainerSpec 文档
- Docker 官方文档 - ENTRYPOINT vs CMD
七、总结
| 项目 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 环境变量 CLASSPATH 未设置 | Kubeflow 报错 Environment variable CLASSPATH not set | Kubeflow 覆盖了 ENTRYPOINT | 在 Kubeflow 中不指定 command,只指定 args |
| 想要兼容 Kubeflow 与本地运行 | 两种模式行为不同 | ENTRYPOINT 被覆盖 | 在 entrypoint.sh 中增加容错逻辑 |
| CLASSPATH 固定场景 | 只需静态设置 | 无需动态生成 | 直接在 Dockerfile 中 ENV |
✅ 推荐做法总结:
在 Kubeflow 组件中只使用
args,不要覆盖镜像 ENTRYPOINT。这样既保留了镜像启动逻辑,又能兼容动态环境变量设置,是最干净的解决方式。
八、kubeflow去掉command后还是覆盖ENTRYPOINT的问题
✅ 一句话总结根因:Kubeflow Pipelines v2 默认会将你的 args 统一包在 /bin/sh -c
中执行,从而覆盖镜像的 ENTRYPOINT
一、从逻辑上确认:Kubeflow Pipelines 的执行机制
在 Kubeflow Pipelines v2(即你用的 DSL 2.0)中,组件最终由 KFP launcher/executor 生成 Kubernetes Pod YAML 。
无论你写不写 command=["/bin/bash","-c"],KFP 都会将你的 args 经过封装:
yaml
command:
- sh
- -c
args:
- <这里是完整的 args 串接出来的命令>
这是因为:
- Kubeflow 要在容器启动时自动注入 输入参数、MLMD artifact、executor metadata;
- 所以执行逻辑必须先经由一层 shell 解释器;
- 这层包装逻辑由
kfp-launcher或kfp-v2-launcher自动生成。
换句话说,即使 DSL 中不定义 command,KFP 仍然自动生成 /bin/sh -c 来启动容器。
二、从运行层面确认:如何验证实际 command
你可以在 Kubeflow 集群中直接验证,100% 确认。
假设运行后 pipeline 失败了,找到 online 这一步的 Pod 名称:
bash
kubectl get pods -n kubeflow
然后执行:
bash
kubectl get pod <pod-name> -n kubeflow -o yaml | grep -A 10 "containers:"
你将会看到类似:
yaml
containers:
- name: main
image: image
command:
- sh
- -c
args:
- bash /opt/run.sh -j /data3/... -t online
✅ 这就说明:
command明确是sh -c- 而镜像原始 ENTRYPOINT(
/opt/docker/entrypoint.sh)被完全替换掉
三、源码级确认(官方实现)
KFP v2 的 executor 源码(kfp/kubernetes/executor.py)中有类似逻辑:
python
container.command = ['sh', '-c']
container.args = [generate_command_string(user_command)]
这一行明确说明了:无论用户是否设置 command,最终执行时都会走
sh -c。
你也可以从生成的 YAML 文件中观察到它。以下为笔者个人服务器上获取的结果:
从中可以明显看到command命令
bash
spec:
containers:
- command:
- argoexec
- wait
- --loglevel
- info
- --log-format
- text
- --gloglevel
- "0"
- args:
- /bin/bash
- -c
- '{{$.inputs.parameters[''online_cmd'']}}'
command:
- /var/run/argo/argoexec
- emissary
- --loglevel
- info
- --log-format
- text
- --gloglevel
- "0"
- --
- /kfp-launcher/launch
- --pipeline_name
- train-platform-pipeline
- --run_id
- 1a248387-e5da-4364-aad2-0400fbda58d0
- --execution_id
- "57"
- --
Kubeflow Pipeline 的执行底层基于 Argo Workflow。在 Argo Workflow 的执行模型中,每个容器步骤都会由 argoexec 注入一层 wrapper(封装)
✅ 五、结论
| 环境 | 实际执行命令 | ENTRYPOINT 是否保留 | CLASSPATH 是否设置 |
|---|---|---|---|
| docker run | /opt/docker/entrypoint.sh bash -c ... |
✅ 保留 | ✅ 有效 |
| kubeflow pipeline | sh -c "cd /opt/... && bash ..." |
❌ 被覆盖 | ❌ 丢失 |
所以:
✅ 可以 100% 确认 :Kubeflow executor 默认会在容器中设置 command 为
["sh", "-c"],覆盖掉镜像原始 ENTRYPOINT。
九、笔者最后的方案
直接在容器内部的启动脚本里导出环境变量,不再管镜像本身了。简单粗暴,完美解决,但只是没那么优雅
bash
# /opt/run.sh
export CLASSPATH=$(${HADOOP_HOME}/bin/hdfs classpath --glob)
cmdline="python ${MAIN_SCRIPT} ${TASKINFO_FILE}"
echo $cmdline
$cmdline
exit $?