PAYATHON 2026

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 安全限制。

常见原因有:

  1. JAR 完成签名后,META-INF/services/java.sql.Driver 又被修改。
  2. JAR 在构建、缓存、复制、解压重打包或注入过程中损坏。
  3. 使用了未经验证、可能与目标 Runtime 不兼容的 JDK 或安全配置。
  4. 平台注入的文件与镜像层、挂载点或缓存发生异常。如果最小受支持镜像也能稳定复现,应由 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-storage JAR;
  • 写入 spark-env.sh 的 SPARK_DIST_CLASSPATH;
  • 自定义的 spark-defaults.conf。

然后以受支持的 Databricks 基础镜像为起点,只加入业务必需的操作系统库和普通应用依赖。如果仍然出现完全相同的 digest error,问题范围便可进一步缩小到特定 JDK、平台 JAR 注入或 Databricks Runtime 本身,不再优先怀疑两套 Spark 环境之间的冲突。

备注:内容仅供参考。