1. 程式人生 > >spark學習十六 spark為什麼要實現自己的RPEL

spark學習十六 spark為什麼要實現自己的RPEL

本文中涉及linux作業系統的底層一些知識,有興趣的可以繼續深挖

全域性檢視

 

上圖顯示了java原始檔從編譯到載入執行的全域性檢視,整個過程中最主要的步驟是

  1. 編譯成過程,由編譯器對java原始檔進行編譯整理,生成java bytecodes
  2. 類的載入和初始化,主要由classloader參與
  3. 執行引擎 將位元組碼翻譯成機器碼,然後排程執行

這一部分的內容,解釋的非常詳細的某過於《深入理解jvm》和撒迦的JVM分享,這裡就不班門弄斧了。

那麼講上述這些內容的目的又何在呢,我們知道scala也是需要編譯執行的,那麼編譯的結果是什麼樣呢,要符合什麼標準?在哪裡執行。

答案比較明顯,scala原始檔也需要編譯成java bytecodes,和java的編譯結果必須符合同一份標準,生成的bytecode都是由jvm的執行引擎轉換成為機器碼之後排程執行。

也就是說盡管scala和java原始檔的編譯器不同,但它們生成的結果必須符合同一標準,否則jvm無法正確理解,執行也就無從談起。至於scala的編譯器是如何實現的,文中後續章節會涉及。

ELF可執行檔案的載入和執行

”CPU是很傻的,加電後,它就會一直不斷的讀取指令,執行指令,不能停的哦。“ 如果有了這個意識,看原始碼的時候你就會有無窮的疑惑,無數想不通的地方,這也能讓你不斷的進步。

再繼續講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檔案的執行過程大致如下

  1. fork建立一個程序
  2. 呼叫execve來執行ELF
  3. ELF的載入過程中會有動態載入和連結發生
  4. 全域性變數的初始化,這一部分和glibc相關
  5. 執行main函式
我講述的過程非常籠統,要想更清楚的瞭解細節,請參閱《深入理解Linux核心》中的程式的執行一章,或是《深入Linux核心架構》中的啟動新程式一節。 現在開啟核心中相應的原始碼,看看execve函式是如何找到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,
};

第二步:搜尋處理控制代碼,fs/exec.c

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》和國內出版的《程式設計師的自我修養》

之所以講ELF的載入和執行,是因為要打通java原始檔的編譯執行過程的話,必然會步步深入到此,其實到這還不夠,再往下走就是CPU指令,只有到達CPU指令才算真正到底。這個時候就需要去讀intel ia-64 software programmer guide了。

 原始碼走讀其實只是個形式,重要的是能理清楚其執行流程,以到達指令級的理解為最佳。

Java類的載入和執行

在各位java達人面前,我就不顯示自己java水平有多爛了。只是將兩幅最基本的圖搬出來,展示一下java類的載入過程,以及classloader的層次關係。記住這些東東會為我們在後頭討論scala repl奠定良好基礎。

序列化和反序列化

Java體系中,另一個重要的基石就是類的序列化和反序列化。這裡要注意的就是當有繼承體系時,類的序列化和反序列化順序,以及類中有靜態成員變數的時候,如何處理序列化。諸如此類的文章,一搜一大把,我再多加解釋實在是畫蛇添足,列出來只是說明其重要性罷了。

spark-shell的執行路徑

前面進行了這麼多的鋪墊之後,我想可以進入正題了。即spark-shell的執行呼叫路徑到底怎樣。

首次使用Spark一般都是從執行spark-shell開始的,當在鍵盤上敲入spark-shell並回車時,後面究竟發生了哪些事情呢?

export SPARK_SUBMIT_OPTS
$FWDIR /bin/spark - submit spark -shell "[email protected]" --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 "[email protected]"

SparkSubmit當中定義了Main函式,在它的處理中會將spark repl執行起來,spark repl能夠接收使用者的輸入,通過編譯與執行,返回結果給使用者。這就是為什麼spark具有互動處理能力的原因所在。呼叫順序如下

  1. SparkSubmit
  2. repl.Main
  3. SparkILoop

利用jvisualvm驗證

修改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資訊

spark repl vs. scala repl

既然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 master val data = scala.io.Source.fromFile("file.txt").mkString // now we use the derived string, "data", in a closure that runs on the cluster spark.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,class相當於一個可變物件,而object是例項化的不可變的物件

最重要的一點:Scala Repl預設輸入的程式碼都是在本地執行,故使用objectbasedwraper是沒有問題的。但在spark環境下,輸入的內容有可能需要在遠端執行,這樣objectbasedwrapper的原始碼生成方式經序列化反序列化會有相應的副作用,導致出錯不可用。

scala repl執行過程

再囉嗦一次,scala是需要編譯執行的,而repl給我們的錯覺是scala是解釋執行的。那我們在repl中輸入的語句是如何被真正執行的呢?

簡要的步驟是這樣的

  1. 在repl中輸入的每一行語句,都會被封裝為一個object, 這一工作主要由interpreter完成
  2. 對該object進行編譯
  3. 由classloader載入編譯後的java bytecode
  4. 執行引擎負責真正執行載入入記憶體的bytecode

interpreter in 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

interpreter in spark repl

那我們再看看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

IMain.scala vs. SparkIMain.scala

是什麼導致有上述的差異的呢?我們可以下載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

compiler

scala實現了自己的編譯器,處理邏輯的程式碼實現見scala原始碼中的src/compiler目錄下的原始檔。有關其處理步驟不再贅述,請參考ref3,ref4中的描述。

有一點想要作個小小提醒的時,當你看到SparkIMain.scala中有new Run的語句卻不知道這個Run在哪的時候,兄弟跟你講在scala中的Global.scala裡可以找到

相關推薦

spark學習 spark為什麼實現自己RPEL

本文中涉及linux作業系統的底層一些知識,有興趣的可以繼續深挖 全域性檢視   上圖顯示了java原始檔從編譯到載入執行的全域性檢視,整個過程中最主要的步驟是 編譯成過程,由編譯器對java原始檔進行編譯整理,生成java bytecodes類的載入和初始化,主要

.Spark SQL之讀取複雜的json資料

第一步.準備json資料 test.json {"name":"liguohui","nums":[1,2,3,4,5]} {"name":"zhangsan","nums":[6,7,8,9,10]} test2.json {"name":"Yin","ad

QT學習之路(QWebView實現簡易瀏覽器)

QtWebkit 模組介紹   QtWebkit 模組提供了一個在Qt中使用web browser的engine,這使得我們在QT的應用程式中使用全球資訊網上的內容變得很容易,而且對其網頁內容的控制也可以通過native controls 實現  。   QtWebkit具

spark 原始碼分析之 -- Spark記憶體儲存剖析

上篇spark 原始碼分析之十五 -- Spark記憶體管理剖析 講解了Spark的記憶體管理機制,主要是MemoryManager的內容。跟Spark的記憶體管理機制最密切相關的就是記憶體儲存,本篇文章主要介紹Spark記憶體儲存。 總述 跟記憶體儲存的相關類的關係如下:  

Spring學習()----- Spring AOP實例(Pointcut(切點),Advisor)

dao new ide on() inter exc def row 再次 在上一個Spring AOP通知的例子,一個類的整個方法被自動攔截。但在大多數情況下,可能只需要一種方式來攔截一個或兩個方法,這就是為什麽引入‘切入點‘的原因。它允許你通過它的方法名來攔截方法。另

spring boot 學習(二)攔截器實現IP黑名單

ppi html .html 日期類 dpa asp tails 我們 req 攔截器實現IP黑名單 前言 最近一直在搞 Hexo+GithubPage 搭建個人博客,所以沒怎麽進行 SpringBoot 的學習。所以今天就將上次的”?秒防刷新”進行了一番修改。上次是采用註

spring boot 學習(一)使用@Async實現異步調用

fontsize south 操作 dom img water 截圖 ota app 使用@Async實現異步調用 什麽是”異步調用”與”同步調用” “同步調用”就是程序按照一定的順序依次執行,,每一行程序代碼必須等上一行代碼執行完畢才能執行;”異步調用”則是只要上一行代碼

對抗神經網路學習)——BEGAN實現不同人臉的生成(tensorflow實現)

一、背景 BEGAN,即邊界平衡GAN(Boundary Equilibrium GAN),是DavidBerthelot等人[1]於2017年03月提出的一種方法。傳統的GAN是利用判別器去評估生成器生成的圖片和真實圖片的資料分佈是否一致,而BEGAN則代替了這種概率估計的方法,作者認為只要分

Java程式設計師從笨鳥到菜鳥(五) java 實現簡訊驗證碼

方式一: 1、註冊 2、檢視 API 介面 3、獲取簡訊金鑰 4、工具類: SendMsgUtil.java程式碼 package util; import org.apache.commons.httpclient.Header; import org.

Spark學習筆記(一)----spark運算元操作

1.前言   最近在幫公司瞭解大資料方面的技術,涉及到spark的相關內容,所以想寫個筆記記錄一下。目前用到的時spark2.1.0的版本,僅供學習參考。 2.正文   2.1spark官網運算元的分類   spark官網上面有對於運算元的描述,但是spark對於運算元的分類粒度較粗,大致為transform

孤荷凌寒自學python第四天開始建構自己用起來更順手一點的Python模組與類嘗試第一天

 孤荷凌寒自學python第四十六天開始建構自己用起來更順手一點的Python模組與類,嘗試第一天   (完整學習過程螢幕記錄視訊地址在文末,手寫筆記在文末) 按上一天的規劃,這是根據過去我自學其它程式語言的經驗,我覺得對Python的膚淺的基礎的知識學習完成之後,一定也要開始

<C++學習>C++語句(未完待續)

 摘要: 本篇部落格僅作為筆記,如有侵權,請聯絡,立即刪除(網上找部落格學習,然後手記筆記,因紙質筆記不便儲存,所以儲存到網路筆記) 一、簡單語句 二、語句作用域   以上兩點都特別簡單,任何書籍都有描寫,並且在實踐中很容易掌握。 三、條件語句   if語句和switch語句

推薦系統遇上深度學習()--詳解推薦系統中的常用評測指標

最近閱讀論文的過程中,發現推薦系統中的評價指標真的是五花八門,今天我們就來系統的總結一下,這些指標有的適用於二分類問題,有的適用於對推薦列表topk的評價。1、精確率、召回率、F1值我們首先來看一下混淆矩陣,對於二分類問題,真實的樣本標籤有兩類,我們學習器預測的類別有兩類,那麼根據二者的類別組合可以劃分為四組

ActionScript 3.0 學習() away3D學習1

            下面幾期會發布away3d相關的一些基本內容,away3d的類庫是開源的可以在網上下載。本節介紹基本的用away3d繪製一個小球,程式碼如下:   package { import flash

MySQL學習()

MySQL高階部分 觸發器 觸發器是一類特殊的事務,可以監視某種資料操作(insert/update/delete),並觸發相關的操作(insert/update/delete) 觸發器建立語法之4要素 1 監視地點table 2 監視事件insert/update/delete 3 觸發時間after/be

Spark學習筆記4——spark執行機制

Spark架構及執行機制 Spark執行架構包括叢集資源管理器(Cluster Manager)、執行作業任務的工作節點(Worker Node)、每個應用的任務控制節點(Driver)和每個工作節點上負責具體任務的執行程序(Executor)。其中,叢集資源管理器可以是S

spark學習-SparkSQL--10-spark的一些異常

Caused by: java.io.IOException: com.google.protobuf.ServiceException: java.lang.NoClassDefFoundError: com/yammer/metrics/core/Gauge

分散式服務架構學習(一):實現自己的RPC框架(採用Java Socket)

RPC實現原理圖: 1、Service API對應服務介面。 HelloService.java程式碼如下: package com.shan.rpc.service; public interface HelloService { public String

Spark學習筆記之-Spark遠端除錯

Spark遠端除錯 本例子介紹簡單介紹spark一種遠端除錯方法,使用的IDE是IntelliJ IDEA。 1、瞭解jvm一些引數屬性 -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,addres

java學習 、內部類

定義:內部類是指在一個外部類的內部再定義一個類。 內部類作為外部類的成員,並且依附於外部類而存在。 內部類可為靜態,可用protected和private修飾,而外部類只能使用public和預設的包訪問許可權。 內部類有4中形式:靜態內部類、成員內部類、區域性內部類、匿名內