spark論文簡介

《Spark: Cluster Computing with Working Sets》讀書報告


介紹

  大數據和人工智能的誕生給在集羣計算機上進行並行計算提出了需求。
  Apache Spark 是專爲大規模數據處理而設計的快速通用的計算引擎。Spark是UC Berkeley AMP lab (加州大學伯克利分校的AMP實驗室)所設計的,類似Hadoop MapReduce的通用並行框架。Spark保持了MapReduce的可擴展性和容錯性,但不同於MapReduce適合用於非循環數據流的是,spark比較適合處理複用的數據,像現在的機器學習算法基本上對數據都要進行迭代運算,一個數據集的數據要處理多遍。Spark主要抽象了RDD這種彈性分佈式數據集,使得處理能力比hadoop好很多。
  每個task中間輸出結果可以保存在內存中,從而不再需要讀寫HDFS,因此Spark能更好地適用於數據挖掘與機器學習等需要迭代的MapReduce的算法。
  Spark 是一種與 Hadoop 相似的開源集羣計算環境,但是兩者之間還存在一些不同之處,Spark 啓用了內存分佈數據集,除了能夠提供交互式查詢外,它還可以優化迭代工作負載。
  Spark 是在 Scala 語言中實現的,它將 Scala 用作其應用程序框架。與 Hadoop 不同,Spark 和 Scala 能夠緊密集成,其中的 Scala 可以像操作本地集合對象一樣輕鬆地操作分佈式數據集。
  儘管創建 Spark 是爲了支持分佈式數據集上的迭代作業,但是實際上它是對 Hadoop 的補充,可以在 Hadoop 文件系統中並行運行。通過名爲 Mesos 的第三方集羣框架可以支持此行爲。----百度百科

基本概念(自https://blog.csdn.net/hongmofang10/article/details/84587262):

在這裏插入圖片描述
  假設我們現在有一個集羣,我們設有一個master節點,兩個worker節點,並且驅動程序(本文中使用tasker名稱)運行在master節點上。
在這裏插入圖片描述
  在master節點提交應用以後,在master節點中啓動driver進程,driver進程向集羣管理者申請資源(executor),集羣管理者在不同的worker上啓動了executor進程,相當於分配資源。
在這裏插入圖片描述
  driver進程會將我們編寫的spark應用代碼拆分成多個stage,每個stage執行一部分代碼片段,併爲每個stage創建一批tasks,然後將這些tasks分配到各個executor中執行。
兩種算子
  RDD有兩種算子:
1.Transformation(轉換):屬於延遲Lazy計算,當一個RDD轉換成另一個RDD時並沒有立即進行轉換,僅僅是記住數據集的邏輯操作;
2.Action(執行):觸發Spark作業運行,真正觸發轉換算子的計算;
共享變量
1、廣播變量
  Spark提供的Broadcast Variable,是隻讀的。並且在每個節點上只會有一份副本,而不會爲每個worker都拷貝一份副本。因此其最大作用,就是減少變量到各個節點的網絡傳輸消耗,以及在各個節點上的內存消耗。此外,spark自己內部也使用了高效的廣播算法來減少網絡消耗。
  當用戶創建一個值爲v的廣播變量b,v就會被存入共享文件系統中的文件裏。b的序列化格式就是指向這個文件的路徑。當worker結點查詢b的值時,Spark首先檢查v是否位於本地緩存,否則從文件系統中將其讀入。使用的是新開發的一個更加高效的流廣播系統。
2、累加器變量
  Spark提供的Accumulator,主要用於多個節點對一個變量進行共享性的操作。Accumulator只提供了累加的功能,允許做全局累加操作,accumulator變量廣泛使用在應用中記錄當前的運行指標的情景。但是worker只能對Accumulator進行累加操作,不能讀取它的值。只有Driver程序可以讀取Accumulator的值。
  累加器的實現是使用到了一個不同的序列化手法。在累加器創建時,其就被賦予一個獨一無二的id;當被存儲時,累加器的序列化形式包含其id和對應其類型的“0”值。在工作結點,爲每個使用本線程變量來運行任務的線程創建一份累加器的拷貝,並在任務啓動時將其重置爲“0”。在每個任務都運行之後,工作結點向驅動程序發送一段信息,信息中包含其對於變量累加器所做的更新。當然在任務因失敗而重新執行情況下,驅動確保只一次使用從每個操作的每個分區中傳來的更新以防重複計算。

示例及說明:

  文中給出了幾個代碼示例說明了spark與一般運算平臺的不同。

1、

val file = spark.textFile("hdfs://...")
val errs = file.filter(_.contains("ERROR"))
val ones = errs.map(_ => 1)
val count = ones.reduce(_+_)

  這段代碼統計了hdfs中一個log文檔的出現ERROR的行數。加上val cachedErrs = errs.cache()這句以後,errs這個中間計算節點就可以緩存在內存中,以後再使用就會很快速。rdd流如下
在這裏插入圖片描述
  上邊的操作會生成一系列數據集(RDD),如圖所示,這些數據集會以對象鏈條形式保存以捕獲每一個RDD的邏輯關係和生成流程關係;每個數據集對象均保留有一個指針指向其父輩並存留有其父輩如何轉換生成它的信息。
  在內部,每個RDD對象實現相同的簡易接口,包含三個操作:
getPartitions:返回一組分區id號
getIterator(partition),遍歷一個分區。
getPreferredLocations(partition),用於任務調度以由本地獲取數據。
  當在一個數據集上調用一個並行化操作,Spark爲數據集的每個分區創建對應的task,並將這些tasks送達工作結點。使用名爲延遲調度的技術將每個task送達其最佳位置。一旦在工作結點上運行,每個task調用getIterator開始讀其分區。
不同類型RDD的相異處僅在於其如何實現RDD接口。例如,對於hdfs-textFile,分區(partition)是在HDFS中的區塊id號,其最優位置就是區塊的位置,同時getIterator打開一個流來讀這個塊。
  在MappedDataset中,分區和最優位置與其父輩相同,但是迭代器將map函數用於父輩的元素。最後,在CachedDataset中,getIterator尋找轉換分區的本地緩存副本;同時每個分區最佳位置開始就等同於此分區在其父輩中的最優位置,這個性質會保存直到這個分區被緩存到其他結點,之後會進行該分區位置的更新(否則會一直和其父輩同位置)以使得之前結點可挪作他用。這樣的設計使得錯誤的處理變得容易:若一個結點失效,分區將從其父輩數據集中重新讀取並最終緩存到其他結點。
  最後,分發tasker的時候,scala的閉包(java對象)也需要使用java序列化發送給各個tasker,

2、

// Read points from a text file and cache them
val points = spark.textFile(...)
.map(parsePoint).cache()
// Initialize w to random D-dimensional vector
var w = Vector.random(D)
// Run multiple iterations to update w
for (i <- 1 to ITERATIONS) {
val grad = spark.accumulator(new Vector(D))
for (p <- points) { // Runs in parallel
val s = (1/(1+exp(-p.y*(w dot p.x)))-1)*p.y
grad += s * p.x
}
w -= grad.value
}

  文中給出的邏輯迴歸算法的spark代碼,這段代碼使用了累加器常量grad來做梯度下降。代碼第二個for循環是並行運算的。
代碼第56行原文中有誤,應該改爲
val s=(1/(p.y*(1+exp(-w dot p.x))-1))*p.y
spark邏輯迴歸算法和hadoop比較
在這裏插入圖片描述
  發現對於迭代運算,spark優勢明顯

3、

val Rb = spark.broadcast(R)
for (i <- 1 to ITERATIONS) {
U = spark.parallelize(0 until u)
.map(j => updateUser(j, Rb, M))
.collect()
M = spark.parallelize(0 until m)
.map(j => updateUser(j, Rb, U))
.collect()
}

  相比前兩個,第三個代碼是cpu密集型的。該算法用於預測u個觀衆對m部電影的好感度。分別用兩個k個特徵來表徵觀衆的性格和電影的類型特徵。M是m×km\times k的矩陣,U是k×uk\times u的矩陣,M×U=RM\times U=R,R代表各觀衆-各電影的合適程度,已知歷史R矩陣,計算得到U和M矩陣之後就可以用某行U矩陣乘某列M矩陣獲得某觀衆對某電影的好感度。
  算法如下:1、隨機初始化M;2、固定M,優化U減小與R的誤差;3、固定U,優化M減小與R的誤差;4、循環2、3
##spark整合編譯器
  如何將Spark整合進Scala的編譯器中的呢
  Scala是通過將使用者碼進去的每一行編譯成一個類來運行,這個類包含單對象,此對象中又含有本行裏的變量或者函數,最終在其結構中跑本行的代碼。如:

var x = 5;
println(x)
// 編譯器定義一個類,假設是Line1,其中包含變量x。
// 將第二行編譯成 println(Line1.getInstance().x)
// 這些類被加載進JVM以完成對於每一行的運行。

基於Scala的以上代碼編譯運行方式,爲了使得其編譯器可用於Spark,做了兩處改變:

  • 使得編譯器將其定義的那些類輸出到共享文件系統中,從而在工作結點裏使用正常的Java類加載器就能加載這些類。
  • 改變生成的代碼,從而每行的單對象可以直接引用之前行的單個類,而非使用靜態方法 getInstance()。這使得無論何時閉包被序列化傳送到worker結點中時,可以捕獲其引用的單個類的狀態。如果不這樣做,那麼對於單對象的更新(如前設置x = 7)將不會被廣播到工作結點中。

感想

  spark是一個使用了hadoop,建立在scala框架上的集羣並行運算計算引擎。通過閱讀這篇論文,對spark的框架和實現、使用有了大致的瞭解,以後還需要在使用中學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章