3、环境搭建:Spark + Delta Lake 本地开发环境配置

说实话,很多同学学 Delta Lake 卡住的第一步,不是概念难懂,而是环境没配好。

我见过太多人花了两天装 Spark,最后跑个 read.format("delta") 就报 ClassNotFoundException。嗯,这章我们就来解决这个问题。

我会带你从零搭一套能跑 Delta Lake 的本地环境。你跟着走一遍,后面所有实战都能直接跑。

3.1 环境总览:我们需要什么?

先看整体结构。本地开发环境说白了就三层:

  • 底层:Java 8/11 + Scala 2.12/2.13 —— Spark 跑在 JVM 上,这是基础。
  • 中间层:Apache Spark 3.x —— 计算引擎,Delta Lake 是它的扩展。
  • 上层:Delta Lake 库 —— 提供 ACID、时间旅行、Schema 强制等功能。

我个人的习惯是:用 Maven 或 SBT 管理依赖,而不是手动下载 Jar 包。为什么?因为版本匹配太容易出错了。你想想看,Spark 3.2 配 Delta 1.0 能跑,但 Spark 3.3 配 Delta 2.0 可能就报错。用构建工具,版本号写死,省心。

核心版本匹配原则:

  • Spark 3.2.x → Delta 1.0.x ~ 1.2.x
  • Spark 3.3.x → Delta 2.0.x ~ 2.2.x
  • Spark 3.4.x → Delta 2.3.x ~ 3.0.x
  • Spark 3.5.x → Delta 3.1.x+

建议:用最新稳定版。我写这章时推荐 Spark 3.5.0 + Delta 3.1.0。

下面这张图帮你理清各组件的关系:

Spark + Delta Lake 本地开发环境架构 你的代码(Scala / Python / SQL) DataFrame API | Spark SQL | DeltaTable API Delta Lake 核心库(delta-core_2.12) ACID 事务 | 时间旅行 | Schema 强制 | 数据版本管理 Apache Spark 3.5.0 Spark SQL | Catalyst 优化器 | Tungsten 执行引擎 本地文件系统 / HDFS / S3 / ADLS Delta 表以 Parquet + _delta_log 目录形式存储 开发环境建议 用 local[*] 模式调试

3.2 安装 Java 和 Scala

先检查你的机器上有没有 Java。打开终端:

java -version
# 应该输出类似:openjdk version "1.8.0_382" 或 "11.0.20"

如果没有,去 Adoptium 下载 JDK 8 或 11。我个人推荐 JDK 11,Spark 3.5 对 JDK 11 支持最好。

Scala 呢?其实你不需要单独装 Scala 编译器。Spark 自带了 Scala 运行时。但如果你要用 SBT 构建项目,那需要装 Scala。不过我们这章用 Maven,所以 Scala 版本由 Spark 决定——Spark 3.5 用的是 Scala 2.12。

小提示: 如果你用 IntelliJ IDEA,装好 Scala 插件就行。IDE 会自动下载 Scala SDK。别手动装 Scala 2.13,否则和 Spark 3.5 不兼容,你会遇到各种诡异的序列化错误。

3.3 安装 Apache Spark

这一步我踩过坑。以前我直接下载预编译包,结果 Hadoop 版本不对,读本地文件都报错。后来我学乖了:

  1. 去 Spark 官网 下载 spark-3.5.0-bin-hadoop3.tgz
  2. 解压到 /opt/spark 或 C:\spark
  3. 设置环境变量 SPARK_HOME 和 PATH

Linux/Mac 上:

export SPARK_HOME=/opt/spark/spark-3.5.0-bin-hadoop3
export PATH=$SPARK_HOME/bin:$PATH

Windows 上:在系统环境变量里加 SPARK_HOME,然后把 %SPARK_HOME%\bin 加到 Path。

验证安装:

spark-shell --version
# 应该输出 Spark version 3.5.0

注意: Windows 用户记得装 winutils.exe 和 hadoop.dll。否则 Spark 读本地文件会报 Failed to locate the winutils binary。去 GitHub 搜 "winutils hadoop3" 下载,放到 %SPARK_HOME%\bin 下。

3.4 创建 Maven 项目并引入 Delta Lake

我个人习惯用 Maven 管理依赖。创建一个标准 Maven 项目,在 pom.xml 里加这些:

<properties>
    <scala.version>2.12.18</scala.version>
    <spark.version>3.5.0</spark.version>
    <delta.version>3.1.0</delta.version>
</properties>

<dependencies>
    <!-- Spark Core -->
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_2.12</artifactId>
        <version>${spark.version}</version>
        <scope>provided</scope>
    </dependency>

    <!-- Spark SQL -->
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-sql_2.12</artifactId>
        <version>${spark.version}</version>
        <scope>provided</scope>
    </dependency>

    <!-- Delta Lake -->
    <dependency>
        <groupId>io.delta</groupId>
        <artifactId>delta-core_2.12</artifactId>
        <version>${delta.version}</version>
    </dependency>

    <!-- Delta Lake 独立表 API(可选) -->
    <dependency>
        <groupId>io.delta</groupId>
        <artifactId>delta-standalone_2.12</artifactId>
        <version>3.0.0</version>
    </dependency>
</dependencies>

注意 scope=provided 的意思是:编译时需要,但运行时由 Spark 提供。这样打包时不会把 Spark 的 Jar 打进去,避免冲突。

3.5 编写第一个 Delta Lake 程序

环境搭好了,我们来写个最简单的程序验证一下。创建一个 Scala 对象:

import org.apache.spark.sql.SparkSession

object DeltaQuickStart {
  def main(args: Array[String]): Unit = {
    // 创建 SparkSession,启用 Delta Lake 扩展
    val spark = SparkSession.builder()
      .appName("DeltaQuickStart")
      .master("local[*]")
      .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
      .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
      .getOrCreate()

    import spark.implicits._

    // 1. 创建 DataFrame
    val data = Seq(
      ("Alice", 34),
      ("Bob", 45),
      ("Charlie", 28)
    ).toDF("name", "age")

    // 2. 保存为 Delta 表
    data.write.format("delta").save("/tmp/delta_table")

    // 3. 读取 Delta 表
    val df = spark.read.format("delta").load("/tmp/delta_table")
    df.show()

    // 4. 追加数据
    val moreData = Seq(("Diana", 31)).toDF("name", "age")
    moreData.write.format("delta").mode("append").save("/tmp/delta_table")

    // 5. 读取历史版本
    val historyDf = spark.read.format("delta")
      .option("versionAsOf", 0)
      .load("/tmp/delta_table")
    historyDf.show()

    spark.stop()
  }
}

运行这个程序,你会看到:

  • 第一次 show() 输出 3 条记录
  • 追加后第二次读取输出 4 条记录
  • 时间旅行读取版本 0,又回到 3 条记录

嗯,到这里你的环境就完全配好了。

3.6 常见问题与避坑指南

我曾经帮一个同事排查环境问题,搞了整整一下午。最后发现是他 pom.xml 里 Delta 版本写成了 2.0.0,但 Spark 是 3.5.0,不兼容。这里我把常见问题列出来:

错误现象 根本原因 解决方案
ClassNotFoundException: io.delta.sql.DeltaSparkSessionExtension Delta Jar 没在 classpath 中 检查 Maven 依赖是否下载成功;mvn dependency:tree 查看
IllegalArgumentException: requirement failed: Delta Lake 1.0+ requires Spark 3.0+ Spark 版本太老 升级 Spark 到 3.2+;或降级 Delta 到匹配版本
AnalysisException: Incompatible format Delta 表被非 Delta 引擎写入过 检查目录下是否有非 Parquet 文件;用 FSCK REPAIR TABLE 修复
SparkException: Task not serializable 在闭包中引用了不可序列化的对象 用 map 时只传简单类型;或使用 foreach 替代
Windows 上 Failed to locate the winutils binary 缺少 Hadoop Windows 二进制文件 下载 winutils.exe 和 hadoop.dll 放到 %SPARK_HOME%\bin

调试小技巧: 在 spark-shell 里直接测试 Delta 功能,比写完整项目快得多。启动时加上:

spark-shell --packages io.delta:delta-core_2.12:3.1.0 \
  --conf "spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension" \
  --conf "spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog"

这样就能在交互式环境里直接 spark.read.format("delta") 了。

3.7 验证环境:跑一个完整的 Delta 事务

最后,我建议你跑一个稍微复杂点的场景,验证 ACID 特性是否生效。写两个线程同时往同一个 Delta 表写数据:

import org.apache.spark.sql.SparkSession

object ConcurrentWriteTest {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .master("local[2]")
      .appName("ConcurrentWriteTest")
      .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
      .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
      .getOrCreate()

    import spark.implicits._

    // 线程 1:写 1-100
    val t1 = new Thread(() => {
      val data = (1 to 100).map(i => (s"user_$i", i)).toDF("name", "id")
      data.write.format("delta").mode("append").save("/tmp/delta_concurrent")
    })

    // 线程 2:写 101-200
    val t2 = new Thread(() => {
      val data = (101 to 200).map(i => (s"user_$i", i)).toDF("name", "id")
      data.write.format("delta").mode("append").save("/tmp/delta_concurrent")
    })

    t1.start()
    t2.start()
    t1.join()
    t2.join()

    // 验证:总记录数应该是 200
    val count = spark.read.format("delta").load("/tmp/delta_concurrent").count()
    println(s"Total records: $count")  // 应该输出 200

    spark.stop()
  }
}

如果输出是 200,说明 Delta Lake 的并发写入没问题。如果少于 200,那可能是你的环境有问题——不过 Delta Lake 的 ACID 保证不会丢数据,所以大概率是你代码写错了。

好了,环境搭好了,后面我们就可以放心地玩 Delta Lake 的各种特性了。