歡迎轉載,轉載請註明出處,徽滬一郎。html
之因此對spark shell的內部實現產生興趣所有緣於好奇代碼的編譯加載過程,scala是須要編譯才能執行的語言,但提供的scala repl能夠實現代碼的實時交互式執行,這是爲何呢?java
既然scala已經提供了repl,爲何spark還要本身單獨搞一套spark repl,這其中的原因到底何在?node
顯然,這些都是問題,要解開這些謎團,只有再次開啓一段源碼分析之旅了。linux
上圖顯示了java源文件從編譯到加載執行的全局視圖,整個過程當中最主要的步驟是git
這一部分的內容,解釋的很是詳細的某過於《深刻理解jvm》和撒迦的JVM分享,這裏就不班門弄斧了。程序員
那麼講上述這些內容的目的又何在呢,咱們知道scala也是須要編譯執行的,那麼編譯的結果是什麼樣呢,要符合什麼標準?在哪裏執行。github
答案比較明顯,scala源文件也須要編譯成java bytecodes,和java的編譯結果必須符合同一份標準,生成的bytecode都是由jvm的執行引擎轉換成爲機器碼以後調度執行。shell
也就是說盡管scala和java源文件的編譯器不一樣,但它們生成的結果必須符合同一標準,不然jvm沒法正確理解,執行也就無從談起。至於scala的編譯器是如何實現的,文中後續章節會涉及。apache
」CPU是很傻的,加電後,它就會一直不斷的讀取指令,執行指令,不能停的哦。「 若是有了這個意識,看源碼的時候你就會有無窮的疑惑,無數想不通的地方,這也能讓你不斷的進步。vim
再繼續講scala源文件的編譯細節以前,咱們仍是來溫習一下基礎的內容,即一個EFL可執行文件是如何加載到內存真正運行起來的。(本篇博客的內容相對比較底層,很費腦子的,:)
Linux平臺上基本採用ELF做爲可執行文件的格式,java可執行文件自己也是ELF格式的,使用file指令來做檢驗。
file /opt/java/bin/java
下面是輸出的結果,從結果中能夠證明java也是ELF格式。
/opt/java/bin/java: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, BuildID[sha1]=bd74b7294ebbdd93e9ef3b729e5aab228a3f681b, stripped
ELF文件的執行過程大體以下
在文件$KERNEL_HOME/fs/binfmt_elf.c中,init_elf_binfmt函數就實現了註冊任務
static int __init init_elf_binfmt(void) { register_binfmt(&elf_format); return 0; }
來看一看elf_format的定義是什麼
static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, };
execve是一個系統調用,內核中對應的函數是do_execve,具體代碼再也不列出。
do_execve->do_execve_common->search_binary_hander
注意search_binary_handler會找到上一步中註冊的binary_handler即elf_format,找到了對應的handler以後,關鍵的一步就是load_binary了。動態連接過程調用的是load_shlib,這一部分的內容細細展開的話,夠寫幾本書了。
search_binary_handler的部分代碼
retry: read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); bprm->recursion_depth++; retval = fmt->load_binary(bprm); bprm->recursion_depth--; if (retval >= 0 || retval != -ENOEXEC || bprm->mm == NULL || bprm->file == NULL) { put_binfmt(fmt); return retval; } read_lock(&binfmt_lock); put_binfmt(fmt); } read_unlock(&binfmt_lock);
要想對這一部份內容有個比較清楚的瞭解,建議看一下臺灣黃敬羣先生的《深刻淺出Helloworld》和國內出版的《程序員的自我修養》。
源碼走讀其實只是個形式,重要的是能理清楚其執行流程,以到達指令級的理解爲最佳。
在各位java達人面前,我就不顯示本身java水平有多爛了。只是將兩幅最基本的圖搬出來,展現一下java類的加載過程,以及classloader的層次關係。記住這些東東會爲咱們在後頭討論scala repl奠基良好基礎。
Java體系中,另外一個重要的基石就是類的序列化和反序列化。這裏要注意的就是當有繼承體系時,類的序列化和反序列化順序,以及類中有靜態成員變量的時候,如何處理序列化。諸如此類的文章,一搜一大把,我再多加解釋實在是多此一舉,列出來只是說明其重要性罷了。
前面進行了這麼多的鋪墊以後,我想能夠進入正題了。即spark-shell的執行調用路徑到底怎樣。
首次使用Spark通常都是從執行spark-shell開始的,當在鍵盤上敲入spark-shell並回車時,後面究竟發生了哪些事情呢?
export SPARK_SUBMIT_OPTS $FWDIR /bin/spark - submit spark -shell "$@" --class org.apache.spark.repl.Main
能夠看出spark-shell實際上是對spark-submit的一層封裝,但事情到這尚未結束,畢竟尚未找到調用java的地方,繼續往下搜索看看spark-submit腳本的內容。
exec $SPARK_HOME /bin/spark -class org. apache .spark. deploy . SparkSubmit "${ ORIG_ARGS [@]}"
離目標愈來愈近了,spark-class中會調用到java程序,與java相關部分的代碼摘錄以下
# Find the java binary if [ -n "${ JAVA_HOME }" ]; then RUNNER ="${ JAVA_HOME }/ bin/java" else if [ `command -v java ` ]; then RUNNER ="java" else echo " JAVA_HOME is not set" >&2 exit 1 fi fi exec " $RUNNER " -cp " $CLASSPATH " $JAVA_OPTS "$@"
SparkSubmit當中定義了Main函數,在它的處理中會將spark repl運行起來,spark repl可以接收用戶的輸入,經過編譯與運行,返回結果給用戶。這就是爲何spark具備交互處理能力的緣由所在。調用順序以下
修改spark-class,使得JAVA_OPTS看起來以下圖所示
JMX_OPTS="-Dcom.sun.management.jmxremote.port=8300 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1" # Set JAVA_OPTS to be able to load native libraries and to set heap size JAVA_OPTS="-XX:MaxPermSize=128m $OUR_JAVA_OPTS $JMX_OPTS" JAVA_OPTS="$JAVA_OPTS -Xms$OUR_JAVA_MEM -Xmx$OUR_JAVA_MEM"
修改完上述腳本以後先啓動spark-shell,而後再啓動jvisualvm
bin/spark-shell jvisualvm
在Java VisualVM中選擇進程org.apache.spark.deploy.SparkSubmit,若是已經爲jvisualvm安裝了插件Threads Inspector,其界面將會與下圖很相似
在右側選擇「線程」這一tab頁,選擇線程main,而後能夠看到該線程的thread dump信息
既然scala已經提供了repl, spark仍是要本身去實現一個repl,你不覺着事有可疑麼?我谷歌了好長時間,終於找到了大神的討論帖子,不容易啊,原文摘錄以下。
Thanks for looping me in! Just FYI, I would also be okay if instead of making the wrapper code pluggable, the REPL just changed to one based on classes, as in Prashant's example, rather than singleton objects.
To give you background on this, the problem with the "object" wrappers is that initialization code goes into a static initializer that will have to run on all worker nodes, making the REPL unusable with distributed applications. As an example, consider this:// file.txt is a local file on just the masterval data = scala.io.Source.fromFile("file.txt").mkString// now we use the derived string, "data", in a closure that runs on the clusterspark.textFile.map(line => doStuff(line, data))The current Scala REPL creates an object Line1 whose static initializer sets data with the code above, then does import Line1.data in the closure, which will cause the static initializer to run *again* on the remote node and fail. This issue definitely affects Spark, but it could also affect other interesting projects that could be built on Scala's REPL, so it may be an interesting thing to consider supporting in the standard interpreter.Matei
上述內容估計第一次看了以後,除了一頭霧水仍是一頭霧水。翻譯成爲白話就是利用scala原生的repl,是使用object來封裝輸入的代碼的,這有什麼不妥,「序列化和反序列化」的問題啊。反序列化的過程當中,對象的構造函數會被再次調用,而這並非咱們所指望的。咱們但願生成class而不是object,若是你不知道object和class的區別,不要緊,看一下scala的簡明手冊,立刻就明白了。
最重要的一點:Scala Repl默認輸入的代碼都是在本地執行,故使用objectbasedwraper是沒有問題的。但在spark環境下,輸入的內容有可能須要在遠程執行,這樣objectbasedwrapper的源碼生成方式經序列化反序列化會有相應的反作用,致使出錯不可用。
討論詳情,請參考該Link https://groups.google.com/forum/#!msg/scala-internals/h27CFLoJXjE/JoobM6NiUMQJ
再囉嗦一次,scala是須要編譯執行的,而repl給咱們的錯覺是scala是解釋執行的。那咱們在repl中輸入的語句是如何被真正執行的呢?
簡要的步驟是這樣的
那麼怎麼證實我說的是對的呢?很簡單,作個實驗,利用下述語句了啓動scala repl
scala -Dscala.repl.debug=true
若是咱們輸入這樣一條語句 val c = 10,由interpreter生成的scala源碼會以下所列
object $read extends scala.AnyRef { def () = { super.; () }; object $iw extends scala.AnyRef { def () = { super.; () }; object $iw extends scala.AnyRef { def () = { super.; () }; val c = 10 } } }
注意囉,是object哦,不是class。
那咱們再看看spark repl生成的scala源碼是什麼樣子的?
啓動spark-shell以前,修改一下spark-class,在JAVA_OPTS中加入以下內容
-Dscala.repl.debug=true
啓動spark-shell,輸入val b = 10,生成的scala源碼以下所示
class $read extends AnyRef with Serializable { def (): $line10.$read = { $read.super.(); () }; class $iwC extends AnyRef with Serializable { def (): $read.this.$iwC = { $iwC.super.(); () }; class $iwC extends AnyRef with Serializable { def (): $iwC = { $iwC.super.(); () }; import org.apache.spark.SparkContext._; class $iwC extends AnyRef with Serializable { def (): $iwC = { $iwC.super.(); () }; class $iwC extends AnyRef with Serializable { def (): $iwC = { $iwC.super.(); () }; private[this] val b: Int = 100; def b: Int = $iwC.this.b }; private[this] val $iw: $iwC = new $iwC.this.$iwC(); def $iw: $iwC = $iwC.this.$iw }; private[this] val $iw: $iwC = new $iwC.this.$iwC(); def $iw: $iwC = $iwC.this.$iw }; private[this] val $iw: $iwC = new $iwC.this.$iwC(); def $iw: $iwC = $iwC.this.$iw }; private[this] val $iw: $read.this.$iwC = new $read.this.$iwC(); def $iw: $read.this.$iwC = $read.this.$iw }; object $read extends scala.AnyRef with Serializable { def (): $line10.$read.type = { $read.super.(); () }; private[this] val INSTANCE: $line10.$read = new $read(); def INSTANCE: $line10.$read = $read.this.INSTANCE; private def readResolve(): Object = $line10.this.$read } }
注意到與scala repl中的差別了麼,此處是class而非object
是什麼致使有上述的差別的呢?咱們能夠下載scala的源碼,對是scala自己的源碼在github上能夠找到。interpreter中代碼生成部分的處理邏輯主要是在IMain.scala,在spark中是SparkIMain.scala。
比較兩個文件的異同。
gvimdiff IMain.scala SparkIMain.scala
gvimdiff是個好工具,兩個文件的差別一目瞭然,emacs和vim總要有同樣玩的轉才行啊。來個屏幕截圖吧,比較炫吧。
注:spark開發團隊彷佛給scala的開發小組提了一個case,在最新的scala中彷佛已經支持classbasedwrapper,能夠經過現應的選項來設置來選擇classbasedwraper和objectbasedwrapper.
下述代碼見最新版scala,scala-2.12.x中的IMain.scala
private lazy val ObjectSourceCode: Wrapper = if (settings.Yreplclassbased) new ClassBasedWrapper else new ObjectBasedWrapper
scala實現了本身的編譯器,處理邏輯的代碼實現見scala源碼中的src/compiler目錄下的源文件。有關其處理步驟再也不贅述,請參考ref3,ref4中的描述。
有一點想要做個小小提醒的時,當你看到SparkIMain.scala中有new Run的語句殊不知道這個Run在哪的時候,兄弟跟你講在scala中的Global.scala裏能夠找到, :)
編譯和加載是一個很是有意思的話題,便可以說是很基礎也能夠說很冷門,有無動力就這部分進行深究,就看我的的興趣了。