《前后端面试题
》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux... 。

文章目录
- 一、本文面试题目录
-
-
- [91. 如何用Scala/Python编写一个简单的Spark WordCount程序?](#91. 如何用Scala/Python编写一个简单的Spark WordCount程序?)
- [92. 编写Spark程序时,常见的错误有哪些?如何避免?](#92. 编写Spark程序时,常见的错误有哪些?如何避免?)
- [93. 如何处理Spark程序中的`OutOfMemoryError`?](#93. 如何处理Spark程序中的
OutOfMemoryError
?) - [94. Spark任务运行缓慢,可能的原因有哪些?如何排查?](#94. Spark任务运行缓慢,可能的原因有哪些?如何排查?)
- [95. 如何在Spark程序中实现自定义排序?](#95. 如何在Spark程序中实现自定义排序?)
- [96. 如何在Spark SQL中自定义UDF(用户自定义函数)?](#96. 如何在Spark SQL中自定义UDF(用户自定义函数)?)
- [97. Spark程序中,如何获取当前的时间戳并用于数据处理?](#97. Spark程序中,如何获取当前的时间戳并用于数据处理?)
- [98. 如何在Spark Streaming中保证Exactly-Once语义?](#98. 如何在Spark Streaming中保证Exactly-Once语义?)
- [99. 如何将Spark处理的结果写入到HDFS?需要注意什么?](#99. 如何将Spark处理的结果写入到HDFS?需要注意什么?)
- [100. 用Spark处理大规模数据时,如何保证程序的稳定性和可扩展性?](#100. 用Spark处理大规模数据时,如何保证程序的稳定性和可扩展性?)
-
- 二、100道Spark面试题目录列表
一、本文面试题目录
91. 如何用Scala/Python编写一个简单的Spark WordCount程序?
原理说明 :
WordCount是Spark的入门程序,核心逻辑是:读取文本数据→分割单词→计数→输出结果,涉及RDD的flatMap
、map
和reduceByKey
等转换操作。
Scala实现:
scala
import org.apache.spark.sql.SparkSession
object WordCount {
def main(args: Array[String]): Unit = {
// 1. 创建SparkSession
val spark = SparkSession.builder()
.appName("WordCountScala")
.master("local[*]") // 本地模式,生产环境移除
.getOrCreate()
val sc = spark.sparkContext
// 2. 读取数据(本地文件或HDFS路径)
val lines = sc.textFile("input.txt") // 输入文件路径
// 3. 处理数据:分割单词→映射为(单词,1)→聚合计数
val wordCounts = lines
.flatMap(line => line.split(" ")) // 分割单词,扁平化
.map(word => (word, 1)) // 映射为键值对
.reduceByKey(_ + _) // 按单词聚合计数
// 4. 输出结果
wordCounts.collect().foreach(println) // 本地打印
wordCounts.saveAsTextFile("output") // 保存到文件
// 5. 关闭资源
spark.stop()
}
}
Python实现:
python
from pyspark.sql import SparkSession
if __name__ == "__main__":
# 1. 创建SparkSession
spark = SparkSession.builder \
.appName("WordCountPython") \
.master("local[*]") # 本地模式,生产环境移除
.getOrCreate()
sc = spark.sparkContext
# 2. 读取数据
lines = sc.textFile("input.txt")
# 3. 处理数据
word_counts = lines \
.flatMap(lambda line: line.split(" ")) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda x, y: x + y)
# 4. 输出结果
for (word, count) in word_counts.collect():
print(f"{word}: {count}")
word_counts.saveAsTextFile("output")
# 5. 关闭资源
spark.stop()
92. 编写Spark程序时,常见的错误有哪些?如何避免?
常见错误及避免方法:
-
序列化错误(NotSerializableException)
-
原因 :在闭包(如
map
、filter
)中使用不可序列化的对象(如数据库连接、自定义非序列化类)。 -
避免 :
- 确保闭包中使用的对象实现
Serializable
接口。 - 避免在闭包中创建连接,改用连接池或广播变量。
scala// 错误示例:非序列化对象 class NonSerializableClass // 未实现Serializable val obj = new NonSerializableClass rdd.map(_ => obj.method()) // 抛出NotSerializableException // 正确示例:实现序列化 class SerializableClass extends Serializable // 实现接口
- 确保闭包中使用的对象实现
-
-
空指针异常(NullPointerException)
- 原因 :处理数据时未判断
null
值(如RDD中存在null
元素)。 - 避免 :使用
filter
过滤null
值,或通过Option
处理。
scalardd.filter(_ != null).map(...) // 过滤null元素
- 原因 :处理数据时未判断
-
数据倾斜(Data Skew)
- 原因:某几个Key的数据量过大,导致单个Task执行缓慢。
- 避免:使用加盐、广播Join、自定义分区等方法(详见问题60)。
-
Shuffle溢出(Shuffle OOM)
- 原因:Shuffle数据量过大,超过Executor内存限制。
- 避免 :
- 减少Shuffle数据量(如预聚合)。
- 调大
spark.executor.memory
和spark.shuffle.memoryFraction
。
-
资源配置不合理
- 原因:Executor内存/核数不足,或并行度过低。
- 避免 :根据数据量调整
--executor-memory
、--executor-cores
和分区数。
-
未关闭SparkSession
- 原因 :程序结束未调用
spark.stop()
,导致资源泄漏。 - 避免:在程序最后显式关闭SparkSession。
- 原因 :程序结束未调用
93. 如何处理Spark程序中的OutOfMemoryError
?
OutOfMemoryError
(OOM) 通常由内存不足或内存分配不合理导致,处理方法如下:
-
Driver OOM
- 原因 :Driver内存不足(如
collect
大量数据、广播超大变量)。 - 解决 :
- 避免
collect
全量数据,改用take
或分布式计算。 - 调大
spark.driver.memory
(如--driver-memory 4g
)。 - 若广播变量过大,拆分变量或减少广播数据量。
- 避免
- 原因 :Driver内存不足(如
-
Executor OOM
- 原因:Executor内存不足(如数据倾斜、缓存数据过多)。
- 解决 :
- 调大
spark.executor.memory
(如--executor-memory 8g
)。 - 减少单个Executor处理的数据量(增加分区数或Executor数量)。
- 优化缓存策略:使用
MEMORY_AND_DISK_SER
替代MEMORY_ONLY
,或减少缓存数据。 - 处理数据倾斜(如加盐、拆分大Key)。
- 调大
-
Shuffle OOM
- 原因:Shuffle过程中数据缓冲溢出。
- 解决 :
- 调大
spark.shuffle.memoryFraction
(默认0.2,可增至0.4),分配更多内存用于Shuffle。 - 调小
spark.reducer.maxSizeInFlight
(默认48MB),减少Reducer单次拉取的数据量。
- 调大
-
堆外内存OOM
- 原因:堆外内存不足(如Netty网络缓冲)。
- 解决 :调大
spark.executor.memoryOverhead
(默认Executor内存的10%)。
示例配置:
bash
spark-submit \
--driver-memory 4g \
--executor-memory 8g \
--executor-cores 2 \
--conf spark.shuffle.memoryFraction=0.4 \
--conf spark.executor.memoryOverhead=2g \
...
94. Spark任务运行缓慢,可能的原因有哪些?如何排查?
常见原因及排查方法:
-
数据倾斜
- 现象:少数Task执行时间过长(长尾效应),其他Task快速完成。
- 排查 :
- 查看Spark UI的Stages页面,对比各Task的执行时间和数据量。
- 检查
Shuffle Read
分布,若某Task读取数据量远高于平均,说明存在倾斜。
- 解决:加盐、广播Join、拆分大Key(详见问题60)。
-
资源不足
- 现象:Executor频繁GC,或任务等待资源调度。
- 排查 :
- 查看Executors页面:
GC Time
占比高(如>20%)说明内存不足。 - YARN页面显示"资源不足",说明集群资源被占满。
- 查看Executors页面:
- 解决:增加Executor内存/核数,或开启动态资源分配。
-
Shuffle开销过大
- 现象:Shuffle Read/Write数据量巨大,Stage执行缓慢。
- 排查 :
- Spark UI的Stages页面查看
Shuffle Read Size
和Shuffle Write Size
。
- Spark UI的Stages页面查看
- 解决 :使用
reduceByKey
替代groupByKey
,过滤无效数据后再Shuffle。
-
小文件过多
- 现象:读取阶段生成大量Task,调度开销大。
- 排查:查看输入数据的文件数量,若单个文件<100MB,可能是小文件问题。
- 解决:合并小文件(详见问题66)。
-
数据本地性差
- 现象 :多数Task的本地性为
Any
(数据与计算节点分离)。 - 排查 :Spark UI的Stages页面查看
Data Locality
分布。 - 解决 :调整分区策略,或增加
spark.locality.wait
(默认3秒)。
- 现象 :多数Task的本地性为
-
低效算子或代码
- 现象 :使用
collect
、foreach
等算子导致数据集中到Driver。 - 排查:检查代码中是否有全局聚合或循环操作。
- 解决 :改用分布式算子(如
aggregate
、mapPartitions
)。
- 现象 :使用
95. 如何在Spark程序中实现自定义排序?
原理说明 :
Spark中可通过自定义Ordering
(Scala)或keyfunc
(Python)实现自定义排序,适用于按多字段、复杂逻辑排序的场景。
Scala实现:
scala
import org.apache.spark.sql.SparkSession
// 定义样例类
case class Person(name: String, age: Int, salary: Double)
object CustomSort {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("CustomSort")
.master("local[*]")
.getOrCreate()
val sc = spark.sparkContext
val data = Seq(
Person("Alice", 30, 5000.0),
Person("Bob", 25, 6000.0),
Person("Charlie", 30, 5500.0)
)
val rdd = sc.parallelize(data)
// 自定义排序:先按年龄升序,再按薪水降序
implicit val personOrdering: Ordering[Person] = new Ordering[Person] {
override def compare(a: Person, b: Person): Int = {
if (a.age != b.age) {
a.age.compareTo(b.age) // 年龄升序
} else {
-a.salary.compareTo(b.salary) // 薪水降序(加负号)
}
}
}
// 排序并输出
val sortedRDD = rdd.sortBy(identity) // 使用自定义Ordering
sortedRDD.collect().foreach(println)
// 输出:
// Person(Bob,25,6000.0)
// Person(Charlie,30,5500.0)
// Person(Alice,30,5000.0)
spark.stop()
}
}
Python实现:
python
from pyspark.sql import SparkSession
if __name__ == "__main__":
spark = SparkSession.builder \
.appName("CustomSortPython") \
.master("local[*]") \
.getOrCreate()
sc = spark.sparkContext
data = [
("Alice", 30, 5000.0),
("Bob", 25, 6000.0),
("Charlie", 30, 5500.0)
]
rdd = sc.parallelize(data)
# 自定义排序键:(年龄, -薪水),按年龄升序、薪水降序
sorted_rdd = rdd.sortBy(lambda x: (x[1], -x[2]))
for item in sorted_rdd.collect():
print(item)
# 输出:
# ('Bob', 25, 6000.0)
# ('Charlie', 30, 5500.0)
# ('Alice', 30, 5000.0)
spark.stop()
96. 如何在Spark SQL中自定义UDF(用户自定义函数)?
原理说明 :
UDF(User-Defined Function)允许用户扩展Spark SQL的功能,实现自定义数据处理逻辑(如格式转换、复杂计算)。定义UDF需指定输入输出类型,注册后可在SQL或DataFrame中使用。
Scala实现:
scala
import org.apache.spark.sql.{SparkSession, functions}
import org.apache.spark.sql.types.IntegerType
object SparkUDF {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("SparkUDF")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 1. 创建测试数据
val df = Seq(("Alice", 25), ("Bob", 30)).toDF("name", "age")
// 2. 定义UDF:年龄加10
val addTen = functions.udf((age: Int) => age + 10)
// 3. 注册UDF(可选,用于SQL)
spark.udf.register("add_ten", addTen)
// 4. 在DataFrame中使用
df.select($"name", addTen($"age").alias("age_plus_10")).show()
// 5. 在SQL中使用
df.createOrReplaceTempView("people")
spark.sql("SELECT name, add_ten(age) AS age_plus_10 FROM people").show()
spark.stop()
}
}
Python实现:
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import udf
from pyspark.sql.types import IntegerType
if __name__ == "__main__":
spark = SparkSession.builder \
.appName("SparkUDFPython") \
.master("local[*]") \
.getOrCreate()
# 1. 创建测试数据
df = spark.createDataFrame([("Alice", 25), ("Bob", 30)], ["name", "age"])
# 2. 定义UDF:年龄加10
def add_ten(age):
return age + 10
# 3. 注册UDF(指定返回类型)
add_ten_udf = udf(add_ten, IntegerType())
spark.udf.register("add_ten", add_ten_udf)
# 4. 在DataFrame中使用
df.select("name", add_ten_udf("age").alias("age_plus_10")).show()
# 5. 在SQL中使用
df.createOrReplaceTempView("people")
spark.sql("SELECT name, add_ten(age) AS age_plus_10 FROM people").show()
spark.stop()
注意:
- UDF应尽量简单,复杂逻辑可能导致性能下降。
- 避免在UDF中使用
null
值,需显式处理(如返回None
或默认值)。
97. Spark程序中,如何获取当前的时间戳并用于数据处理?
原理说明 :
获取时间戳可用于数据分区(如按时间划分输出目录)、记录处理时间等场景。Spark中可通过java.util.Date
(Scala)或datetime
(Python)获取当前时间,或使用Spark SQL的内置函数。
Scala实现:
scala
import org.apache.spark.sql.{SparkSession, functions}
import java.util.Date
import java.text.SimpleDateFormat
object TimestampExample {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("TimestampExample")
.master("local[*]")
.getOrCreate()
import spark.implicits._
// 1. 获取当前时间戳(毫秒级)
val currentTs = System.currentTimeMillis()
println(s"当前时间戳(毫秒):$currentTs")
// 2. 格式化时间(如yyyy-MM-dd HH:mm:ss)
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val currentTime = sdf.format(new Date())
println(s"当前时间:$currentTime")
// 3. 在DataFrame中添加时间戳列
val df = Seq(("Alice"), ("Bob")).toDF("name")
val dfWithTs = df.withColumn("process_time", functions.current_timestamp())
dfWithTs.show(false)
// 输出:
// +-----+-----------------------+
// |name |process_time |
// +-----+-----------------------+
// |Alice|2023-10-01 15:30:00.123|
// |Bob |2023-10-01 15:30:00.123|
// +-----+-----------------------+
// 4. 按时间分区写入数据
dfWithTs.write
.partitionBy("process_time")
.mode("overwrite")
.csv("output_with_ts")
spark.stop()
}
}
Python实现:
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import current_timestamp
import time
from datetime import datetime
if __name__ == "__main__":
spark = SparkSession.builder \
.appName("TimestampExamplePython") \
.master("local[*]") \
.getOrCreate()
# 1. 获取当前时间戳(秒级/毫秒级)
current_ts_sec = int(time.time()) # 秒级
current_ts_ms = int(time.time() * 1000) # 毫秒级
print(f"当前时间戳(秒):{current_ts_sec}")
print(f"当前时间戳(毫秒):{current_ts_ms}")
# 2. 格式化时间
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"当前时间:{current_time}")
# 3. 在DataFrame中添加时间戳列
df = spark.createDataFrame([("Alice",), ("Bob",)], ["name"])
df_with_ts = df.withColumn("process_time", current_timestamp())
df_with_ts.show(truncate=False)
# 4. 按时间分区写入
df_with_ts.write \
.partitionBy("process_time") \
.mode("overwrite") \
.csv("output_with_ts_py")
spark.stop()
98. 如何在Spark Streaming中保证Exactly-Once语义?
原理说明 :
Exactly-Once语义指数据仅被处理一次,即使发生故障也不会重复或丢失。实现需满足:数据输入可重放 、状态可持久化 、输出幂等性。
实现方式:
-
使用可重放的数据源:
- 如Kafka(保存offset)、HDFS(文件可重读),避免使用不可重放源(如Socket)。
-
启用Checkpoint:
- 持久化StreamingContext状态(如offset、算子逻辑),故障后从Checkpoint恢复。
scalaval checkpointDir = "hdfs:///spark/checkpoint" def createSSC(): StreamingContext = { val ssc = new StreamingContext(sc, Seconds(5)) ssc.checkpoint(checkpointDir) // 设置Checkpoint目录 // 定义DStream逻辑 ssc } val ssc = StreamingContext.getOrCreate(checkpointDir, createSSC)
-
管理输入offset:
- 手动保存Kafka offset到可靠存储(如HBase、MySQL),避免自动提交。
scalastream.foreachRDD { rdd => val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges // 处理数据 // 手动提交offset到外部存储 saveOffsets(offsetRanges) }
-
输出幂等性:
- 确保写入外部系统的操作幂等(重复执行结果一致),如:
- 用唯一ID更新数据(
INSERT OR REPLACE
)。 - 写入支持事务的系统(如Delta Lake、HBase)。
- 用唯一ID更新数据(
- 确保写入外部系统的操作幂等(重复执行结果一致),如:
-
使用Structured Streaming:
- 内置支持Exactly-Once语义,通过Checkpoint和事务输出实现:
scaladf.writeStream .format("kafka") .option("checkpointLocation", "hdfs:///checkpoint") .start()
99. 如何将Spark处理的结果写入到HDFS?需要注意什么?
原理说明 :
Spark通过saveAsTextFile
(RDD)或write
(DataFrame)将结果写入HDFS,需指定HDFS路径(如hdfs://namenode:9000/output
),并注意文件格式、分区和权限。
写入示例:
-
RDD写入HDFS:
scalaval rdd = sc.parallelize(Seq(("Alice", 30), ("Bob", 25))) // 写入为文本文件(每个元素一行) rdd.saveAsTextFile("hdfs:///user/spark/output/rdd_result") // 写入为SequenceFile(键值对格式) rdd.saveAsSequenceFile("hdfs:///user/spark/output/seq_result")
-
DataFrame写入HDFS:
scalaval df = spark.createDataFrame(Seq(("Alice", 30), ("Bob", 25))).toDF("name", "age") // 写入为Parquet(列式存储,推荐) df.write .mode("overwrite") // 写入模式:overwrite/append/ignore/error .parquet("hdfs:///user/spark/output/df_parquet") // 按字段分区写入 df.write .partitionBy("age") .csv("hdfs:///user/spark/output/df_csv_partitioned")
注意事项:
- 写入模式 :根据需求选择
mode
,避免误删数据(如overwrite
会覆盖现有目录)。 - 文件格式:优先使用Parquet/ORC等列式格式(压缩率高、查询快),避免大量小文件。
- 分区策略:按高频过滤字段分区(如日期),但分区数不宜过多(否则元数据开销大)。
- 权限问题 :确保Spark运行用户有HDFS目录的写权限(可通过
hdfs dfs -chmod
设置)。 - 原子性 :使用
DataFrameWriter
的option("path", ...)
配合saveAsTable
,确保写入原子性。 - 小文件合并 :写入后若产生大量小文件,可通过
coalesce
减少分区数(如df.coalesce(10).write...
)。
100. 用Spark处理大规模数据时,如何保证程序的稳定性和可扩展性?
保证稳定性和可扩展性的方法:
-
合理的资源配置
- 按数据量动态调整Executor数量、内存和核数(如开启动态资源分配)。
- 避免单Executor内存过大(如>16GB),减少GC压力。
- 配置:
spark.executor.memory=8g
,spark.executor.cores=2
,spark.dynamicAllocation.enabled=true
。
-
优化数据处理逻辑
- 减少Shuffle操作(如用
reduceByKey
替代groupByKey
)。 - 避免
collect
、broadcast
过大数据,防止OOM。 - 使用
mapPartitions
替代map
,减少对象创建开销。
- 减少Shuffle操作(如用
-
处理数据倾斜
- 提前检测倾斜(通过Spark UI),采用加盐、拆分大Key等方法。
- 对Join操作,小表用广播Join,大表用自定义分区。
-
容错与重试机制
- 启用Checkpoint(适用于Streaming)和日志持久化(History Server)。
- 配置Task重试参数:
spark.task.maxFailures=4
,spark.stage.maxConsecutiveAttempts=3
。
-
合理的存储策略
- 输入数据使用Parquet/ORC格式,减少IO和存储空间。
- 合并小文件,避免读取时生成过多Task。
- 缓存频繁访问的数据,选择合适的持久化级别(如
MEMORY_AND_DISK_SER
)。
-
监控与调优
- 实时监控Spark UI,关注Task执行时间、Shuffle数据量、GC时间。
- 定期分析作业性能,优化慢查询和资源瓶颈。
-
代码健壮性
- 处理
null
值和异常数据(如filter
过滤无效值)。 - 模块化代码,避免硬编码(如通过配置文件管理路径和参数)。
- 编写单元测试,验证核心逻辑(如使用
spark-testing-base
)。
- 处理
-
扩展能力
- 设计支持水平扩展的算法(如分布式训练、分块处理)。
- 对超大规模数据,采用分区表和增量处理(如只处理新增数据)。