Databricks 通用计算为何无法启动自定义 Docker 镜像?
结论
自定义镜像无须复制或仿造 /databricks 目录。Databricks 会在计算资源启动时注入该目录及其中的 Spark、Hadoop、Databricks JAR 和配置,并通过显式 classpath 启动 DriverDaemon。镜像中的 PATH、SPARK_HOME 和 /opt/spark 都不会替换这套运行时。
当前镜像把自己构建成了一套完整的 Spark/Delta 发行环境,而 Databricks Custom Container 主要用于定制操作系统和依赖层。镜像额外安装的 Java、Spark、Hadoop、PySpark 和 Delta JAR 可能影响环境变量、Python 包或后续进程的 classpath,导致两套运行时混用。
SHA1 digest error for META-INF/services/java.sql.Driver 表示 JVM 校验某个已签名 JAR 时,发现文件的实际内容与 META-INF/MANIFEST.MF 中记录的摘要不一致。常见情况是 JAR 已损坏、被重新打包,或读取它的 Java 环境不兼容。多个 JDBC Driver 并存通常会引发类冲突或 ServiceLoader 问题,一般不会导致 digest error。
现有日志还不足以确定具体损坏了哪个 JAR。Dockerfile 中应优先排查自带的旧版 Zulu Java 8,以及整套上游 Spark/Hadoop/Delta 环境。
/databricks 是如何使用的
启动命令表明,DriverDaemon 使用的是 Databricks 注入的 classpath:
/databricks/hadoop-safety-jars/*
/databricks/spark/dbconf/...
/databricks/hive/conf
/databricks/jars/*
其中没有 /opt/spark/jars/* 或 /opt/hadoop/*。因此:
- 不要在镜像中创建或填充
/databricks。 - 不要把自行下载的 Spark、Hadoop 或 Delta 移到
/databricks。 CMD和ENTRYPOINT不是 Databricks 启动 Spark 的入口。PATH只影响命令查找,无法改写java -cp ...中明确指定的 classpath。- 自定义容器提供基础用户空间,不能整体替换 Databricks Runtime。
如果启动命令中已经出现 /databricks/jars/*,说明目录注入基本成功。错误发生在 JVM 读取其中某个 JAR 时。
当前 Dockerfile 中的冲突点
镜像接管了 Java
日志显示实际执行的是:
/opt/java8/bin/java
这是镜像安装的 Zulu Java 8,无法确认它是否与目标 Databricks Runtime 配套,而且 8u192 已经非常旧。
Databricks Runtime 对 Java 的要求可能涉及发行版、更新级别、安全策略和签名校验行为,并非主版本同为 Java 8 就一定兼容。
镜像还同时设置了:
ENV JAVA_HOME=/opt/java8
ENV SPARK_JAVA_HOME=/opt/java17
不同进程可能因此分别使用 Java 8 和 Java 17。即使 DriverDaemon 能够启动,后续 Spark 进程仍可能遇到其他兼容性问题。
镜像内又安装了一套 Spark 和 Hadoop
以下设置可能影响 Databricks 启动后的子进程:
ENV SPARK_HOME=/opt/spark
ENV HADOOP_HOME=/opt/hadoop
ENV SPARK_CONF_DIR=/opt/spark/conf
ENV PATH="$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH"
SPARK_DIST_CLASSPATH 还会把 /opt/hadoop 下的大量 JAR 加入 Spark 环境。DriverDaemon 启动时可能使用 /databricks/jars,用户代码或 executor 随后却加载 /opt/spark 和 /opt/hadoop。两套环境混用后,容易出现 Scala、Hadoop、Jackson、Netty、JDBC Driver 或 Delta Lake 的版本冲突。
安装同版本的开源组件无法还原 Databricks Runtime
即使 Spark 版本号相同,下面两套环境也不能直接互换:
Apache Spark 3.5.2 + Hadoop 3.3.6 + Delta Lake 3.3.0
Databricks Runtime 16.3
Databricks Runtime 还包含经过修改的组件、专有 JAR、固定的依赖组合及自身的启动配置。公开版本清单可以用来判断兼容范围,不能作为从零重建 Databricks Runtime 的配方。
将 pyspark 安装到系统 Python 后,还可能遮蔽 Databricks 提供的 PySpark Python 模块,导致 Python 端和 JVM 端使用不同的 Spark 发行物。
基础系统也要确认
该镜像基于:
FROM ubuntu:noble-20241015
Ubuntu 24.04 是否受支持,应以当前云平台、区域和目标 Databricks Runtime 的 Custom Container 文档为准。如果文档只列出了特定 Ubuntu 版本或官方基础镜像,容器能够拉取并启动也不能说明整个运行时受支持。
SHA1 digest error 的原因
已签名 JAR 的 META-INF/MANIFEST.MF 会记录条目摘要,例如:
Name: META-INF/services/java.sql.Driver
SHA1-Digest: ...
JVM 读取该条目时会重新计算摘要。如果计算结果与清单中的记录不同,就会抛出:
java.lang.SecurityException:
SHA1 digest error for META-INF/services/java.sql.Driver
这里的 “SHA1” 是 JAR 清单所用摘要算法的名称,与 Dockerfile 对 Spark、Hadoop 压缩包执行的 sha512sum 无关,也不表示应该关闭 Java 的 SHA-1 安全限制。
常见原因有:
- JAR 完成签名后,
META-INF/services/java.sql.Driver又被修改。 - JAR 在构建、缓存、复制、解压重打包或注入过程中损坏。
- 使用了未经验证、可能与目标 Runtime 不兼容的 JDK 或安全配置。
- 平台注入的文件与镜像层、挂载点或缓存发生异常。如果最小受支持镜像也能稳定复现,应由 Databricks 支持团队排查。
某个 JAR 包含 META-INF/services/java.sql.Driver 本身不能说明它有问题。该文件属于 JDBC 4 自动发现机制,正常的 JDBC Driver 通常都会包含它。
建议的解决步骤
1. 先构建最小镜像
使用当前 Databricks 文档为目标云平台和 Runtime 推荐的基础镜像。第一版不要安装 Spark、Hadoop、PySpark、Delta JAR 或自定义 JDK。
可以通过构建参数传入基础镜像,以免示例依赖可能变化的镜像标签:
ARG DBX_BASE_IMAGE
FROM ${DBX_BASE_IMAGE}
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
libpq-dev \
libcairo2-dev \
gobject-introspection \
libgirepository1.0-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 不设置 JAVA_HOME、SPARK_HOME、HADOOP_HOME 或 SPARK_CONF_DIR。
# 不向镜像添加 Spark、Hadoop、PySpark 或 Delta JAR。
构建时传入文档列出的基础镜像:
docker build \
--build-arg DBX_BASE_IMAGE='<supported-image-from-current-databricks-docs>' \
-t example/databricks-runtime-env:test .
最小镜像可以启动后,再分批加入系统库和业务依赖。每加入一批就重新启动 cluster,以便确定问题是由哪一层引入的。
2. 分开构建本地开发镜像和 Databricks 镜像
本地开发需要完整的 Spark、Hadoop 和 Delta 环境,Databricks 容器则应使用平台注入的 Runtime。两者可以共用一个 Dockerfile,但需要使用独立的 target:
FROM ubuntu:noble-20241015 AS local-dev
# 本地开发所需的 Java、Spark、Hadoop、PySpark 和 Delta 安装在这里。
# 这一 target 不用于 Databricks cluster。
ARG DBX_BASE_IMAGE
FROM ${DBX_BASE_IMAGE} AS databricks
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
libpq-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 只添加应用依赖和所需的操作系统库。
分别构建:
docker build --target local-dev -t example/project:local .
docker build \
--target databricks \
--build-arg DBX_BASE_IMAGE='<supported-image-from-current-databricks-docs>' \
-t example/project:databricks .
业务代码和普通 Python 依赖可以共用版本文件。pyspark、delta-spark 等运行时包应从 Databricks 依赖列表中排除,除非对应 Runtime 文档明确要求自行安装。
3. 确认实际使用的 Java
在可以执行诊断命令的环境中记录:
command -v java
readlink -f "$(command -v java)"
java -version
printf '%s\n' "$JAVA_HOME"
同时检查可能影响 JVM 的环境变量:
env | sort | grep -E '^(JAVA|JDK|SPARK|HADOOP|CLASSPATH|PYSPARK)_'
Databricks 目标镜像不应自行覆盖基础镜像提供的 JAVA_HOME 和 Java alternatives。如果必须使用自定义 JDK,需要让 Databricks 确认该发行版和更新级别受到目标 Runtime 支持。
4. 找到实际报错的 JAR
如果能够在故障环境中执行 shell,可以逐个验证 /databricks/jars,不要根据文件名推测:
for jar_file in /databricks/jars/*.jar; do
verify_log="$(mktemp)"
if ! jarsigner -verify -strict "$jar_file" >"$verify_log" 2>&1; then
if grep -Eqi 'digest error|SecurityException|invalid.*signature' "$verify_log"; then
printf 'Signature verification failed: %s\n' "$jar_file"
sed -n '1,120p' "$verify_log"
fi
fi
rm -f "$verify_log"
done
也可以只检查包含 JDBC Service Provider 文件的 JAR:
for jar_file in /databricks/jars/*.jar; do
if unzip -l "$jar_file" META-INF/services/java.sql.Driver 2>/dev/null \
| grep -q 'META-INF/services/java.sql.Driver'; then
printf '%s\n' "$jar_file"
jarsigner -verify -verbose "$jar_file" 2>&1 \
| grep -E 'java.sql.Driver|digest error|SecurityException'
fi
done
证书过期、弱算法或未签名 JAR 也可能导致 jarsigner -strict 返回非零状态。因此需要查看完整输出,不能把所有非零结果都判断为内容摘要损坏。
必要时可以临时设置:
ENV JAVA_TOOL_OPTIONS="-Djava.security.debug=jar"
这个配置会产生大量日志,只适合临时诊断,确认问题后应当删除。
5. 对比文件摘要
在云平台、Databricks Runtime 完整版本和系统架构均相同的条件下,分别计算正常 cluster 与自定义容器中可疑 JAR 的摘要:
sha256sum /databricks/jars/<suspect-jar>
可按以下结果判断:
- 摘要不同:两边使用的不是同一份 JAR,或者文件在注入后发生了变化。
- 摘要相同,但只有自定义镜像报错:优先比较 JDK、
java.security、系统架构和环境变量。 - 官方最小基础镜像也出现相同错误:保留 cluster event log、driver log、镜像 digest、Runtime 完整版本及可疑 JAR 的 SHA-256,然后提交给 Databricks 支持。
- 最小镜像正常,加入某一层后开始失败:检查该层新增的 JDK、环境变量、Python 包和系统组件。
不建议采用的修复方式
不要通过删除 JAR 中的 META-INF/*.SF、*.RSA 或 *.DSA 文件绕过校验。这会破坏签名,还可能违反平台对运行时完整性的要求。
以下做法也应避免:
- 手工修改
/databricks/jars中的文件; - 将上游 Spark JAR 混入
/databricks/jars; - 通过
SPARK_DIST_CLASSPATH全量加入另一套 Hadoop; - 同时设置 Java 8 和 Java 17,由不同启动环节自行选择;
- 只根据 Spark、Python 或 Delta 的版本号相同,就认定二进制兼容。
最可能有效的调整
先移除以下设置:
ENV JAVA_HOME=/opt/java8
ENV SPARK_JAVA_HOME=/opt/java17
ENV SPARK_HOME=/opt/spark
ENV HADOOP_HOME=/opt/hadoop
ENV SPARK_CONF_DIR=/opt/spark/conf
同时移除:
- 自行下载的 Java 8 和 Java 17;
/opt/spark;/opt/hadoop;- 系统 Python 中的
pyspark; delta-spark_2.12和delta-storageJAR;- 写入
spark-env.sh的SPARK_DIST_CLASSPATH; - 自定义的
spark-defaults.conf。
然后以受支持的 Databricks 基础镜像为起点,只加入业务必需的操作系统库和普通应用依赖。如果仍然出现完全相同的 digest error,问题范围便可进一步缩小到特定 JDK、平台 JAR 注入或 Databricks Runtime 本身,不再优先怀疑两套 Spark 环境之间的冲突。
备注:内容仅供参考。
