Spark Standalone模式增加Worker后执行时间近乎恒定的原因探究
问题背景
我了解到使用Spark时建议按每个核心配置一个Worker,但实际测试时,当设置的Worker数量远超过电脑核心数(我的Mac是2核i5),性能并未出现下降。想请教该现象的原因。
测试代码
from time import time from pyspark import SparkContext for j in range(1, 10): sc = SparkContext(master="local[%d]" % (j)) t0 = time() for i in range(10): sc.parallelize([1, 2] * 1000000).reduce(lambda x, y: x + y) print("%2d executors, time=%4.3f" % (j, time() - t0)) sc.stop() # 运行结果: # 1 executors time=6.112 # 2 executors time=5.202 # 3 executors time=4.695 # 4 executors time=5.090 # 5 executors time=5.262 # 6 executors time=5.156 # 7 executors time=5.274 # 8 executors time=5.376 # 9 executors time=5.124
硬件配置
- MacBook Pro
- Intel Core i5,2.3GHz
- 总物理核心数:2个
- L2缓存:256 KB
- L3缓存:4 MB
- 内存:16 GB
这是个很有意思的观察!结合你的硬件、测试代码和运行结果,咱们可以从三个核心角度解释这个现象:
1. 超线程带来的逻辑核心扩展
你的Intel Core i5支持超线程技术——每个物理核心可以模拟出2个逻辑核心,所以系统实际能同时调度的并行线程数是4个(2物理核心 × 2逻辑核心)。这就解释了为什么设置1-4个executors时,任务时间逐步下降:此时线程数逐步填满所有逻辑核心,计算效率不断提升。当线程数超过4个后,操作系统会通过时间片轮转调度这些线程共享逻辑核心,但因为线程切换的开销(同一进程内的线程切换成本远低于进程切换)极小,所以不会导致性能明显下降。
2. Local模式下Executor是线程而非独立进程
在Spark的local[N]模式中,N代表的是执行线程数,而非独立的Worker进程。这些线程共享同一个JVM(PySpark中则是共享Python进程的线程池),线程间的上下文切换不需要切换地址空间,开销非常低。即使线程数超过逻辑核心数,操作系统调度器也能高效地在这些线程间切换,不会带来显著的性能损耗。
3. 轻量级测试任务的特性
你的测试任务是简单的reduce加法操作,属于轻量级计算任务,且数据完全在本地内存中(通过parallelize生成RDD,无网络IO开销)。这类任务的CPU计算占比相对较低,线程在执行过程中可能存在微小的空闲间隙(比如数据序列化/反序列化的短暂等待),额外的线程可以利用这些间隙执行计算,因此不会明显拖慢整体任务时间。
从你的运行结果也能验证这一点:4个executors之后,任务时间稳定在5.1-5.3秒左右,没有出现预期中的性能下降,只是不再有性能提升——这说明超过逻辑核心数后,线程调度的开销被任务本身的特性抵消了。
内容的提问来源于stack exchange,提问作者Arnold Taremwa

