先验知识
前面文章 Spark源码分析之ApplicationMaster运行流程 我们介绍了Java|Scala程序在AM端的运行流程,本文介绍Python程序在AM端的运行流程,首先Client看提交命令:
spark-submit --master yarn \
--deploy-mode cluster \
--conf spark.pyspark.python=/root/miniconda2/bin/python \
--py-files 7fresh-sco-fw_demo.zip \
pi.py 100
注:上面--py-files
参数只是为了演示依赖包的运行流程,pi.py 没有调用其内部任何py文件。
提交给集群的上下文参数:
由上图可以看出,启动AM的运行命令是:
{{JAVA_HOME}}/bin/java -server -Xmx1024m -Djava.io.tmpdir={{PWD}}/tmp -Dspark.yarn.app.container.log.dir=<LOG_DIR> org.apache.spark.deploy.yarn.ApplicationMaster --class 'org.apache.spark.deploy.PythonRunner' --primary-py-file pi.py --arg '100' --properties-file {{PWD}}/__spark_conf__/__spark_conf__.properties 1> <LOG_DIR>/stdout 2> <LOG_DIR>/stderr
AM入口类是org.apache.spark.deploy.yarn.ApplicationMaster
,定义的的入口类--class
是org.apache.spark.deploy.PythonRunner
。
PythonRunner启动Python过程
在 Spark源码分析之ApplicationMaster运行流程 中我们介绍了,AM启动后最终会在 org.apache.spark.deploy.yarn.ApplicationMaster 类的startUserApplication()
函数中反射实例化--class
类用于初始化SparkContext,针对PySpark程序的运行,在此函数首先判断如果是py,则会重新组织参数,运行提交命令中的org.apache.spark.deploy.PythonRunner
类(注:如果用户依赖的第三方|自定义的Python依赖包,则通过launch_container.sh
设置PYTHONPATH的方式传递,因此第二个参数是空,如上文提交任务生成的格式,例如:PythonRunner pi.py 空 100
):
我们继续看 org.apache.spark.deploy.PythonRunner 类的main函数,代码分析见注释如下图:
基于Py4J的通信模型
在 一文弄懂PySpark原理与实践 一文中我们知道,为了不破坏 Spark 已有的运行时架构,Spark 在外围包装一层 Python API,借助 Py4j实现 Python 和 Java 的交互,进而实现通过 Python 编写 Spark 应用程序。其原理图下图:
Py4J提供了一套文本协议用来在tcp socket间传递命令,主要作用在Driver端(如上图红圈范围),而在executor端则是通过启动的pyspark.daemon
后通过socket直接通信的。由上节我们知道PythonRunner
在创建Python子进程时会把Py4J监听的端口写入到子进程的环境变量中,这样Python就知道从哪个端口访问JVM了,当然Python在创建JavaGateway时,也可以同时创建一个CallbackClient,实现JVM调用Python过程。默认情况下,PySpark Job是不会启动回调服务的,所有的交互都是 Python -> JVM
模式,但在SparkStreaming中才会用到JVM -> Python
的过程(本文不再重点讲解)。我们首先看一下Driver与Python的整体的运行流程图:
我们接上节,PythonRunner
启动子Python进程运行 python pi.py 100
后,开始初始化SparkContext,如运行pi.py
代码:
spark = SparkSession\
.builder\
.appName("PythonPi")\
.getOrCreate()
首先会依次调用session.py 和 context.py 进行初始化SparkContext,其代码调用链为 SparkSession.Builder.getOrCreate() -> SparkContext.getOrCreate() -> SparkContext.__init__()
,我们直接看SparkContext.__init__()
函数:
首先我们看SparkContext._ensure_initialized()
函数。
如上图,新启动一个Gateway赋值给 _gateway
变量(JavaGateway对象)和 _jvm
变量(JVMView对象),这样就可以通过这个_jvm
变量来访问jvm中的Java对象和方法。下面我们分析 java_gateway.py 中 launch_gateway()
函数,代码如下:
def launch_gateway(conf=None, popen_kwargs=None):
if "PYSPARK_GATEWAY_PORT" in os.environ:
gateway_port = int(os.environ["PYSPARK_GATEWAY_PORT"])
gateway_secret = os.environ["PYSPARK_GATEWAY_SECRET"]
# Process already exists
proc = None
...
# Connect to the gateway (or client server to pin the thread between JVM and Python)
if os.environ.get("PYSPARK_PIN_THREAD", "false").lower() == "true":
gateway = ClientServer(
java_parameters=JavaParameters(
port=gateway_port,
auth_token=gateway_secret,
auto_convert=True),
python_parameters=PythonParameters(
port=0,
eager_load=False))
else:
gateway = JavaGateway(
gateway_parameters=GatewayParameters(
port=gateway_port,
auth_token=gateway_secret,
auto_convert=True))
# Import the classes used by PySpark
java_import(gateway.jvm, "org.apache.spark.SparkConf")
java_import(gateway.jvm, "org.apache.spark.api.java.*")
java_import(gateway.jvm, "org.apache.spark.api.python.*")
java_import(gateway.jvm, "org.apache.spark.ml.python.*")
java_import(gateway.jvm, "org.apache.spark.mllib.api.python.*")
# TODO(davies): move into sql
java_import(gateway.jvm, "org.apache.spark.sql.*")
java_import(gateway.jvm, "org.apache.spark.sql.api.python.*")
java_import(gateway.jvm, "org.apache.spark.sql.hive.*")
java_import(gateway.jvm, "scala.Tuple2")
return gateway
首先从环境变量中拿到环境变量PYSPARK_GATEWAY_PORT,这个就是我们在PythonRunner中设置的环境变量,然后启动一个JavaGateway同GatewayServer进行通讯,最后把Python Api中需要的Java|Scala的类引入引进来,完成了上面的工作后我们就可以真正的初始化SparkContext了
我们回到SparkContext.__init__()
,分析如何_jsc
变量的初始化(其中_jsc
就是JVM中的SparkContext对象在Python中的影子)?
def _do_init(self, master, appName, sparkHome, pyFiles, environment, batchSize, serializer,
conf, jsc, profiler_cls):
...
# Create the Java SparkContext through Py4J
self._jsc = jsc or self._initialize_context(self._conf._jconf)
# Reset the SparkConf to the one actually used by the SparkContext in JVM.
self._conf = SparkConf(_jconf=self._jsc.sc().conf())
...
def _initialize_context(self, jconf):
"""
Initialize SparkContext in function to allow subclass specific initialization
"""
return self._jvm.JavaSparkContext(jconf)
完成了SparkContext的初始化后,就能在业务代码实现自己的逻辑了,如示例pi.py
中使用: