1. Spark-Shell与Spark-Submit的本质差异
第一次接触Spark时,很多人会困惑:为什么要有spark-shell和spark-submit两种工具?这就像学做饭时,有人给你一把瑞士军刀(多功能但适合小规模操作)和一套专业厨具(专为大批量烹饪设计)。两者看似都能完成烹饪,但适用场景完全不同。
交互式探索 vs 批量执行是核心区别。spark-shell是一个REPL环境(Read-Eval-Print Loop),就像Python的交互式解释器。我经常用它快速验证数据转换逻辑,比如测试一个复杂的JSON解析函数:
// 在spark-shell中实时验证数据转换 val sampleDF = spark.read.json("hdfs://data/sample.json") sampleDF.selectExpr("explode(nestedArray) as items").show()而spark-submit更像是生产车间的启动按钮。当你把调试好的代码打包成JAR后,用它把作业推送到集群。比如这个生产环境提交命令:
spark-submit --class com.etl.Main \ --master yarn \ --executor-memory 8G \ production-etl.jar底层架构差异也很关键。spark-shell在启动时就会创建SparkContext(现在推荐用SparkSession),而spark-submit需要你在代码中显式初始化。这就像手动挡和自动挡的区别——前者自动帮你挂挡,后者需要更精确的控制。
2. 典型使用场景对比
2.1 学习测试场景
刚开始用Spark时,我90%的时间都在spark-shell里。它的即时反馈特性太有用了,特别是调试DataFrame操作时:
// 逐步构建复杂查询 val df1 = spark.table("users").filter($"age" > 20) val df2 = df1.groupBy("city").agg(count("*").alias("cnt")) df2.show()但要注意资源管理的坑。有一次我在测试集群误用了local[*],导致所有核心被占满,差点影响线上作业。现在我的原则是:
- 本地测试用
local[2]限制资源 - 连接测试集群时明确指定
--executor-memory 2G等参数
2.2 生产部署场景
真实项目中,spark-submit的参数调优是门学问。这是我总结的配置模板:
spark-submit \ --master yarn \ --deploy-mode cluster \ --executor-memory 16G \ --executor-cores 4 \ --num-executors 20 \ --conf spark.sql.shuffle.partitions=200 \ main.jar几个关键经验:
--deploy-mode cluster比client模式更稳定,尤其当Driver需要大量计算时- executor内存建议设16G-32G,太小会导致频繁GC,太大会引发OOM
- 根据数据量动态调整
spark.sql.shuffle.partitions,我一般设为executor数量的2-3倍
3. 核心参数详解
3.1 集群资源参数
资源分配直接影响作业性能。这个表格是我整理的参数对照:
| 参数 | 本地模式示例 | YARN模式示例 | 作用说明 |
|---|---|---|---|
| --master | local[4] | yarn | 指定集群管理器类型 |
| --executor-memory | 不适用 | 8G | 每个Executor的堆内存 |
| --executor-cores | 不适用 | 4 | 每个Executor的CPU核数 |
| --num-executors | 不适用 | 20 | Executor实例数量 |
| --driver-memory | 2G | 4G | Driver进程内存 |
常见踩坑点:
- YARN模式下
--executor-cores超过5容易导致HDFS吞吐下降 --driver-memory设置过小会导致堆外内存溢出(特别是用Spark ML时)
3.2 部署模式选择
--deploy-mode的两种选择:
client模式(默认):
spark-submit \ --master spark://master:7077 \ --deploy-mode client \ app.jar- Driver运行在提交节点
- 适合需要实时查看日志的调试场景
- 提交节点宕机会导致作业失败
cluster模式:
spark-submit \ --master yarn \ --deploy-mode cluster \ app.jar- Driver运行在集群的某个Worker上
- 生产环境推荐使用
- 日志需要通过yarn logs查看
4. 代码迁移实战
4.1 从Shell到Submit的转换
把spark-shell中的代码迁移到正式项目,需要三步:
- 封装主类:
object ProductionJob { def main(args: Array[String]): Unit = { val spark = SparkSession.builder() .appName("DataPipeline") .getOrCreate() // 移植spark-shell中的代码 val df = spark.read.parquet(args(0)) df.write.saveAsTable("result") } }- 处理依赖:
- 用sbt/assembly插件打包所有依赖
- 通过
--jars传递额外JAR包:
spark-submit --jars mysql-connector-java.jar main.jar- 参数动态化:
// 改用命令行参数 val inputPath = args(0) val outputTable = args(1)4.2 调试技巧
迁移后常见问题排查:
- ClassNotFound:检查
--jars是否包含所有依赖 - 序列化错误:确保所有闭包变量可序列化
- 资源不足:观察Spark UI中的Executor日志
我的调试命令组合:
# 获取详细日志 spark-submit --verbose ... # 获取YARN应用ID yarn application -list # 查看完整日志 yarn logs -applicationId <appId>5. 高级配置技巧
5.1 动态资源分配
对于负载波动大的作业,可以启用动态分配:
spark-submit \ --conf spark.dynamicAllocation.enabled=true \ --conf spark.shuffle.service.enabled=true \ --conf spark.dynamicAllocation.minExecutors=5 \ --conf spark.dynamicAllocation.maxExecutors=50 \ main.jar5.2 数据本地化优化
提高HDFS读取性能:
spark-submit \ --conf spark.locality.wait=30s \ --conf spark.hadoop.dfs.replication=2 \ main.jar5.3 监控集成
对接Prometheus监控:
spark-submit \ --conf spark.metrics.conf=metrics.properties \ --conf spark.ui.prometheus.enabled=true \ main.jar这些实战经验帮助我在多个大型数据项目中稳定运行Spark作业。记住:shell是探索的沙盒,submit才是生产的利器,用好两者的切换能极大提升开发效率。