pyspark底層淺析

losingle發表於2019-02-16

pyspark底層淺析

pyspark簡介

pyspark是Spark官方提供的API介面,同時pyspark也是Spark中的一個程式。

在terminal中輸入pyspark指令,可以開啟python的shell,同時其中預設初始化了SparkConf和SparkContext.

在編寫Spark應用的.py檔案時,可以通過import pyspark引入該模組,並通過SparkConf對Spark的啟動引數進行設定。不過,如果你僅完成了Spark的安裝,直接用python指令執行py檔案並不能檢索到pyspark模組。你可以通過pip等包管理工具安裝該模組,也可以直接使用pyspark(新版本已不支援)或spark-submit直接提交.py檔案的作業。

pyspark program

這裡指的是spark中的bin/pyspark,github地址

實際上pyspark只不過解析了命令列中的引數,並進行了python方面的設定,然後呼叫spark-submit

exec "${SPARK_HOME}"/bin/spark-submit pyspark-shell-main --name "PySparkShell" "$@"

在較新一些的版本如Spark2.2中,已經不支援用pyspark執行py指令碼檔案,一切spark作業都應該使用spark-submit提交。

pyspark module

Spark是用scala編寫的框架,不過考慮到主要是機器學習的應用場景,Spark官方提供了可以用python的API。但是,一方面,python的API是不全的,即不是所有的scala的函式都可以用pyspark呼叫到,雖然新的API也在隨著版本迭代不斷開放;另一方面,pyspark模組,對於很多複雜演算法,是通過反射機制呼叫的Spark中JVM里正在執行的scala編寫的類、方法。所以,如果你將頻繁應用spark於業務或研究,建議學習直接使用scala語言編寫程式,而不是python。

這篇部落格並不會講述如何去使用pyspark來編寫python的spark應用。各類API以及模組如何使用,你完全可以前往官方文件檢視。這裡的連結是最新版pyspark的文件,如果你的機器上的spark不是最新版,請去找對應版本的pyspark文件。因為正如我上面所說,不同版本的pyspark逐步開放了新的API並有對舊API進行改進,你在最新版本看到的類、函式,不一定能在舊版本使用。這裡一提,對於大部分機器學習演算法,你都會看到ml模組與mllib模組都提供了介面,它們的區別在於ml模組接受DataFrame格式的資料而mllib模組接受RDD格式的資料。

關於pyspark底層,這裡主要探索兩個地方。一個是其初始化時的工作,一個是其對JVM中scala程式碼的呼叫

SparkContext

SparkContext類在pyspark/context.py中,在python程式碼裡通過初試化該類的例項來完成Spark的啟動與初始化。這個類的__init__方法中執行了下面幾行程式碼

        self._callsite = first_spark_call() or CallSite(None, None, None)
        SparkContext._ensure_initialized(self, gateway=gateway)
        try:
            self._do_init(master, appName, sparkHome, pyFiles, environment, batchSize, serializer,
                          conf, jsc, profiler_cls)
        except:
            # If an error occurs, clean up in order to allow future SparkContext creation:
            self.stop()
            raise

first_spark_call和CallSite方法都是用來獲取JAVA虛擬機器中的堆疊,它們在pyspark/traceback_util.py中。

之後呼叫了類函式_ensure_initialized函式,對Spark的Java的gate_way和jvm進行設定。
最後呼叫了類中的_do_init_函式,從函式就可以看出是對內部類成員SparkConf的例項_conf函式進行設定,判斷各引數值是否為None,非空的話就進行設定,並讀取一些本地的python環境引數,啟動Spark。

呼叫JVM類與方法

以mllib庫為例,主要邏輯都在pyspark/mllib/common.py中。你去檢視mllib模組中機器學習演算法的類與函式,你會發現基本都是使用self.call或者callMLlibFunc,將函式名與引數傳入。

各類模型的Model類都繼承自common.JavaModelWrapper,這個類程式碼很短:

class JavaModelWrapper(object):
    """
    Wrapper for the model in JVM
    """
    def __init__(self, java_model):
        self._sc = SparkContext._active_spark_context
        self._java_model = java_model

    def __del__(self):
        self._sc._gateway.detach(self._java_model)

    def call(self, name, *a):
        """Call method of java_model"""
        return callJavaFunc(self._sc, getattr(self._java_model, name), *a)

_java_model是來自Java或Scala的類的例項,在呼叫對應的訓練演算法時由對應的scala程式碼在末尾將這些類初始化並返回,其關鍵的類方法call,同callMLLibFunc方法一樣,都是呼叫了callJavaFunc的方法。對於呼叫某一類的方法,是運用python的getattr函式,將類例項與方法名傳入,使用反射機制獲取函式;而對於呼叫一些不屬於類的方法,即使用callMLLibFunc時,是傳入的PythonMLLibAPI類的例項以及方法名,來獲取函式:

def callMLlibFunc(name, *args):
    """ Call API in PythonMLLibAPI """
    sc = SparkContext.getOrCreate()
    api = getattr(sc._jvm.PythonMLLibAPI(), name)
    return callJavaFunc(sc, api, *args)

最終callJavaFunc做的也很簡單,將python的引數*a,使用_py2java方法轉換為java的資料型別,並執行函式,再將結果使用_java2py方法轉換為python的資料型別返回:

def callJavaFunc(sc, func, *args):
    """ Call Java Function """
    args = [_py2java(sc, a) for a in args]
    return _java2py(sc, func(*args))

這裡的_java2py,對很多資料格式的支援不是很好,所以當你嘗試用底層的call方法呼叫一些pyspark尚未支援但scala中已經有的函式時,可能在scala部分可以執行,但是python的返回結果卻不盡如人意。

ml模組的呼叫機制與mllib的機制有些許的不同,但本質上都還是去呼叫在Spark的JVM中scala程式碼的class。

總結

本篇部落格其實說的非常簡單,pyspark即使是不涉及具體演算法的部分,也還有很多內容尚未討論。這裡僅是對pyspark產生一個初步的認識,同時簡單分析了一下底層對scala的呼叫過程。
你興許會有這樣的疑問–“去看這些原始碼有什麼用呢?好像就算知道這些,實際使用時不還是用一下API就好了嗎?”。
實際上,看原始碼首先的就是滿足一下好奇心,對Spark有一個更充分的瞭解;其次關於具體用途,我舉個例子,很多情況你使用的叢集可能不是最新版本的,因為複雜的配置導致一般而言也不可能有一個新版本就更新一次,這時你想用新版本的API怎麼辦?看了這篇部落格想必你也會有一些“大膽的想法”。後一篇部落格會舉例說明我在實際工作中相關的一個問題,以及如何利用這些原始碼去解決的。

相關文章