Apache Spark 在数据交换中的角色:作为统一计算引擎的 Spark

聊到 Lakehouse 架构,有个东西绕不开——Spark。

说实话,我见过不少团队,数据湖用 Hive,ETL 用 MapReduce,实时用 Flink,机器学习又另起一套。最后搞出四五套计算引擎,运维成本高得吓人。而 Lakehouse 的核心思路之一,就是让 Spark 成为那个「统一计算引擎」。为什么是它?我慢慢讲。

为什么是 Spark,而不是别的?

你想想看,一个数据交换方案,要面对的场景有多杂:

  • 批处理:每天几百 TB 的日志清洗
  • 流处理:实时接入 Kafka 的点击流
  • 交互式查询:分析师临时跑个 SQL
  • 机器学习:特征工程 + 模型训练
  • 数据湖写入:Parquet / Delta / Iceberg 格式转换

这些活,Spark 一个引擎全包了。我在项目中遇到过一家金融客户,原来用 Tez 跑批、用 Impala 做查询、用 Flink 做实时,三个集群各自维护。后来统一到 Spark 3.x + Delta Lake,集群数量从 3 个减到 1 个,运维人力直接砍半。

核心观点:Spark 的统一性不在于「什么都能做」,而在于「用同一套 API、同一个执行模型,覆盖批、流、查、学四种负载」。

Spark 在数据交换中的三个关键角色

我把 Spark 在数据交换里的作用拆成三层,画了张图帮你理解:

Spark 在 Lakehouse 数据交换中的三层角色 统一计算入口 SQL / DataFrame / RDD / MLlib 批处理引擎 ETL / 数据导入导出 流处理引擎 Structured Streaming 查询与分析 Spark SQL / JDBC 统一存储接口层 Delta Lake / Apache Iceberg / Apache Hudi 对象存储 / HDFS / 本地盘 Spark 通过 DataSource API 统一对接多种表格式,实现计算与存储的解耦

角色一:批处理与 ETL 的核心引擎

数据交换最重的活,就是 ETL。每天几百张表、几十 TB 的数据,从源系统拉到数据湖,再清洗、转换、分发。

Spark 在这块的优势很明显:

  • 内存计算:比 MapReduce 快 10-100 倍,尤其适合多阶段 ETL
  • 统一 API:一套 DataFrame 代码,既能跑在 10 台机器上,也能跑在 1000 台机器上
  • 丰富的连接器:JDBC、Kafka、HDFS、S3、Delta Lake……开箱即用

我的经验:做数据交换时,尽量用 Spark 的 DataFrame API 而不是 RDD。DataFrame 有 Catalyst 优化器,自动做谓词下推、列裁剪、分区剪枝。我曾经把一个 RDD 写的 ETL 改成 DataFrame,同样的逻辑,执行时间从 47 分钟降到 12 分钟。

举个实际例子,从 MySQL 同步一张订单表到 Delta Lake:

// 读取 MySQL 表
val jdbcDF = spark.read
  .format("jdbc")
  .option("url", "jdbc:mysql://host:3306/db")
  .option("dbtable", "orders")
  .option("user", "user")
  .option("password", "pass")
  .load()

// 简单清洗:过滤无效订单
val cleanedDF = jdbcDF
  .filter($"status" =!= "deleted")
  .withColumn("etl_time", current_timestamp())

// 写入 Delta Lake
cleanedDF.write
  .format("delta")
  .mode("append")
  .save("/data/lake/orders")

这段代码,生产环境每天跑一次,处理 2 亿条订单记录,20 分钟搞定。换成 MapReduce 的话,至少 2 小时。

角色二:流处理与实时数据接入

数据交换不只有批处理。很多场景需要秒级延迟——比如用户行为日志、交易流水、IoT 传感器数据。

Spark 的 Structured Streaming 提供了「批流一体」的能力。什么意思?同一套 DataFrame API,改一行代码就能从批处理变成流处理。

关键点:Structured Streaming 本质上是「微批处理」,把流切成一个个小批次。延迟在 100ms-10s 之间,适合大部分实时数据交换场景。如果你需要毫秒级延迟,那得用 Flink;但如果你能接受秒级,Spark 的生态整合成本更低。

我曾经帮一个电商团队做实时订单同步。他们原来用 Flink 读 Kafka,写到 HBase,再通过 HBase 的 Coprocessor 同步到数据湖。链路长、问题多。后来改成 Spark Structured Streaming,直接从 Kafka 读到 Delta Lake,代码量减少 60%,延迟从 5 秒降到 2 秒。

// 从 Kafka 读取实时订单流
val streamDF = spark
  .readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host:9092")
  .option("subscribe", "orders_topic")
  .load()

// 解析 JSON 并写入 Delta Lake
val parsedDF = streamDF
  .selectExpr("CAST(value AS STRING) as json")
  .select(from_json($"json", schema).as("data"))
  .select("data.*")

parsedDF.writeStream
  .format("delta")
  .option("checkpointLocation", "/checkpoint/orders")
  .table("lakehouse.orders_realtime")

注意:流写入 Delta Lake 时,checkpoint 目录一定要放在可靠的存储上(比如 HDFS 或 S3)。我曾经因为 checkpoint 放在本地盘,节点挂了之后丢数据,排查了一整天才找到原因。

角色三:交互式查询与数据服务

数据交换的最后一公里,是把数据「交出去」。分析师要查、BI 工具要连、下游系统要拉。

Spark 提供了 Spark SQL 和 Thrift JDBC/ODBC 服务,可以直接对外提供 SQL 查询能力。配合 Delta Lake 的优化(比如数据跳过、Z-Order 索引),查询性能可以媲美传统数仓。

场景 传统方案 Spark 方案 优势
临时 SQL 查询 Presto / Impala Spark SQL + Thrift Server 同一引擎,无需额外集群
BI 报表对接 MySQL / Greenplum Spark + JDBC 驱动 直接查数据湖,无需导入导出
数据导出 Sqoop / DataX Spark 原生写 JDBC / 文件 并行度高,支持断点续传

举个例子,分析师想查「昨天销售额 TOP 10 的商品」,直接在 Spark SQL 里跑:

SELECT product_id, SUM(amount) as total_sales
FROM lakehouse.orders
WHERE dt = '2024-01-15'
GROUP BY product_id
ORDER BY total_sales DESC
LIMIT 10

这条 SQL 在 Spark 3.x + Delta Lake 上,10 亿条数据量级,秒级返回。嗯,这里要注意——前提是做了分区裁剪和 Z-Order 优化,否则全表扫描还是会慢。

Spark 与表格式的深度集成

Spark 在 Lakehouse 里能成为统一引擎,还有一个关键原因:它对 Delta Lake、Apache Iceberg、Apache Hudi 这些表格式的原生支持。

这些表格式提供了 ACID 事务、时间旅行、Schema 演进等能力,而 Spark 是它们的第一等公民。比如 Delta Lake 的很多特性,就是直接嵌入 Spark 的 Catalyst 优化器里的。

我的建议:如果你从零开始搭 Lakehouse,优先选 Delta Lake + Spark 的组合。Delta Lake 是 Databricks 主导的,和 Spark 的集成最深。Iceberg 也不错,但有些高级特性(比如动态分区覆盖)需要额外配置。

避坑指南:Spark 做数据交换的常见问题

说了这么多好处,也得聊聊坑。我踩过的,你尽量别踩。

  1. 小文件问题:Spark 写数据默认每个分区一个文件,如果分区数多、数据量小,会生成大量小文件。Delta Lake 的 OPTIMIZE 命令可以合并,但最好在写入时控制分区数。
  2. 数据倾斜:Join 或 GroupBy 时,某个 key 的数据量特别大,会导致单个 task 跑很久。我习惯用 spark.sql.adaptive.skewJoin.enabled=true 开启自适应倾斜处理。
  3. 内存溢出:Spark 是内存计算,但内存不是无限的。遇到 OOM,先检查数据是否倾斜,再考虑调大 spark.executor.memory。别一上来就加内存,先看代码。
  4. Schema 变更:源表加字段、改类型,Spark 默认会报错。用 Delta Lake 的 mergeSchema 选项可以自动演进,但要小心——类型变更可能导致数据丢失。

我曾经踩过的坑:有一次做数据同步,源端把某个字段从 INT 改成了 STRING,我没开 Schema 演进,任务直接挂了。更惨的是,那天是月底结算日……从那以后,我所有生产任务的 Schema 变更都加了告警和人工审批。

小结

Spark 在 Lakehouse 数据交换中的角色,说白了就是「一个引擎,搞定所有」。批处理、流处理、查询分析、机器学习,全用 Spark。配合 Delta Lake 这类表格式,数据交换的链路变得简单、统一、可维护。

当然,Spark 不是万能的。超低延迟场景(毫秒级)还是得用 Flink,复杂机器学习训练还是得用 TensorFlow/PyTorch。但在 90% 的数据交换场景里,Spark 够用、好用、值得投入。