pregel 與 spark graphX 的 pregel api

原文鏈接:https://blog.csdn.net/u013468917/article/details/51199808

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/u013468917/article/details/51199808
簡介
在Hadoop興起之後,google又發佈了三篇研究論文,分別闡述了了Caffeine、Pregel、Dremel三種技術,這三種技術也被成爲google的新“三駕馬車”,其中的Pregel是google提出的用於大規模分佈式圖計算框架。主要用於圖遍歷(BFS)、最短路徑(SSSP)、PageRank計算等等計算。
在Pregel計算模式中,輸入是一個有向圖,該有向圖的每一個頂點都有一個相應的獨一無二的頂點id (vertex identifier)。每一個頂點都有一些屬性,這些屬性可以被修改,其初始值由用戶定義。每一條有向邊都和其源頂點關聯,並且也擁有一些用戶定義的屬性和值,並同時還記錄了其目的頂點的ID。 
一個典型的Pregel計算過程如下:讀取輸入,初始化該圖,當圖被初始化好後,運行一系列的supersteps,每一次superstep都在全局的角度上獨立運行,直到整個計算結束,輸出結果。
在pregel中頂點有兩種狀態:活躍狀態(active)和不活躍狀態(halt)。如果某一個頂點接收到了消息並且需要執行計算那麼它就會將自己設置爲活躍狀態。如果沒有接收到消息或者接收到消息,但是發現自己不需要進行計算,那麼就會將自己設置爲不活躍狀態。這種機制的描述如下圖:

計算過程
Pregel中的計算分爲一個個“superstep”,這些”superstep”中執行流程如下:
1、 首先輸入圖數據,並進行初始化。
2、 將每個節點均設置爲活躍狀態。每個節點根據預先定義好的sendmessage函數,以及方向(邊的正向、反向或者雙向)向周圍的節點發送信息。
3、 每個節點接收信息如果發現需要計算則根據預先定義好的計算函數對接收到的信息進行處理,這個過程可能會更新自己的信息。如果接收到消息但是不需要計算則將自己狀態設置爲不活躍。
4、 每個活躍節點按照sendmessage函數向周圍節點發送消息。
5、 下一個superstep開始,像步驟3一樣繼續計算,直到所有節點都變成不活躍狀態,整個計算過程結束。
下面以一個具體例子來說明這個過程:假設一個圖中有4個節點,從左到右依次爲第1/2/3/4個節點。圈中的數字爲節點的屬性值,實線代表節點之間的邊,虛線是不同超步之間的信息發送,帶陰影的圈是不活躍的節點。我們的目的是讓圖中所有節點的屬性值都變成最大的那個屬性值。


superstep 0:首先所有節點設置爲活躍,並且沿正向邊向相鄰節點發送自身的屬性值。
Superstep 1:所有節點接收到信息,節點1和節點4發現自己接受到的值比自己的大,所以更新自己的節點(這個過程可以看做是計算),並保持活躍。節點2和3沒有接收到比自己大的值,所以不計算、不更新。活躍節點繼續向相鄰節點發送當前自己的屬性值。
Superstep 2:節點3接受信息並計算,其它節點沒接收到信息或者接收到但是不計算,所以接下來只有節點3活躍併發送消息。
Superstep 3:節點2和4接受到消息但是不計算所以不活躍,所有節點均不活躍,所以計算結束。
在pregel計算框架中有兩個核心的函數:sendmessage函數和F(Vertex)節點計算函數。


Spark graphX的pregel API
Spark在其graphX組件中提供了pregel API,讓我們可以用pregel的計算框架來處理spark上的圖數據。以下操作均在spark-shell上進行,我們建立一個圖,然後通過一個求單源最短路徑的例子解釋pregel的操作。
準備工作
操作之前我們需要導入一些可能用到的包:
Import org.apache.spark._
Import org.apache.spark.graphx._
Import org.apache.spark.rdd.RDD

再根據hdfs上的web-Google.txt文件生成圖,這個文件可以在https://snap.stanford.edu/data/web-Google.html下載。
val graph = GraphLoader.edgeListFile(sc,"/Spark/web-Google.txt")

初次用edgelistfile建立圖時,所有vertices、edges、triplets的屬性值由於我沒有指定所以默認值均爲整數 1.
計算
首先設定源點,這裏設置源點爲0:
val sourceId: VertexId = 0


然後對圖進行初始化:

val initialGraph = graph.mapVertices((id, _) => if (id == sourceId) 0.0 else Double.PositiveInfinity)

這段代碼的意思是對所有的非源頂點,將頂點的屬性值設置爲無窮,因爲我們打算將所有頂點的屬性值用於保存源點到該點之間的最短路徑。在正式開始計算之前將源點到自己的路徑長度設爲0,到其它點的路徑長度設爲無窮大,如果遇到更短的路徑替換當前的長度即可。如果源點到該點不可達,那麼路徑長度自然爲無窮大了。
接下來開始計算最短路徑:
val sssp = initialGraph.pregel(Double.PositiveInfinity)(
(id, dist, newDist) => math.min(dist, newDist), // Vertex Program
triplet => { // Send Message
if (triplet.srcAttr + triplet.attr < triplet.dstAttr) {
Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
} else {
Iterator.empty
}
},
(a,b) => math.min(a,b) // Merge Message
)


我們打印一下sssp中的一些值看一下:

我們可以看到0點到354796的最短路徑爲11,到291526不可達。

過程詳解
接下來詳解這個過程:
在調用pregel方法時,initialGraph會被隱式轉換成GraphOps類,這個類中pregel方法的源碼如下:
def pregel[A: ClassTag](
initialMsg: A,
maxIterations: Int = Int.MaxValue,
activeDirection: EdgeDirection = EdgeDirection.Either)(
vprog: (VertexId, VD, A) => VD,
sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],
mergeMsg: (A, A) => A)
: Graph[VD, ED] = {
Pregel(graph, initialMsg, maxIterations, activeDirection)(vprog, sendMsg, mergeMsg)
}

這個方法採用的是典型的柯里化定義方式,第一個括號中的參數序列分別爲initialMsg、maxIterations、activeDirection。第一個參數initialMsg表示第一次迭代時即superstep 0,每個節點接收到的消息。maxIterations表示迭代的最大次數,activeDirection表示消息發送的方向,該值爲EdgeDirection類型,這是一個枚舉類型,有三個可能值:EdgeDirection.In/ EdgeDirection.Out/ EdgeDirection.Either.可以看到,第二和第三個參數都有默認值。
第二個括號中參數序列爲三個函數,分別爲vprog、sendMsg和mergeMsg。
vprog是節點上的用戶定義的計算函數,運行在單個節點之上,在superstep 0,這個函數會在每個節點上以初始的initialMsg爲參數運行並生成新的節點值。在隨後的超步中只有當節點收到信息,該函數纔會運行。
sendMsg在當前超步中收到信息的節點用於向相鄰節點發送消息,這個消息用於下一個超步的計算。
mergeMsg用於聚合發送到同一節點的消息,這個函數的參數爲兩個A類型的消息,返回值爲一個A類型的消息。
最後調用Pregel對象的apply方法返回一個graph對象。
Apply方法的源碼如下,我們可以看到graph和計算的參數都被傳過來了:
def apply[VD: ClassTag, ED: ClassTag, A: ClassTag]
(graph: Graph[VD, ED],
initialMsg: A,
maxIterations: Int = Int.MaxValue,
activeDirection: EdgeDirection = EdgeDirection.Either)
(vprog: (VertexId, VD, A) => VD,
sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],
mergeMsg: (A, A) => A)
: Graph[VD, ED] =
{
//要求最大迭代數大於0,不然報錯。
require(maxIterations > 0, s"Maximum number of iterations must be greater than 0," +
s" but got ${maxIterations}")
//第一次迭代,對每個節點用vprog函數計算。
var g = graph.mapVertices((vid, vdata) => vprog(vid, vdata, initialMsg)).cache()
// 根據發送、聚合信息的函數計算下次迭代用的信息。
var messages = GraphXUtils.mapReduceTriplets(g, sendMsg, mergeMsg)
//數一下還有多少節點活躍
var activeMessages = messages.count()
// 下面進入循環迭代
var prevG: Graph[VD, ED] = null
var i = 0
while (activeMessages > 0 && i < maxIterations) {
// 接受消息並更新節點信息
prevG = g
g = g.joinVertices(messages)(vprog).cache()
 
val oldMessages = messages
// Send new messages, skipping edges where neither side received a message. We must cache
// messages so it can be materialized on the next line, allowing us to uncache the previous
/*iteration這裏用mapReduceTriplets實現消息的發送和聚合。mapReduceTriplets的*參數中有一個map方法和一個reduce方法,這裏的*sendMsg就是map方法,*mergeMsg就是reduce方法
*/
messages = GraphXUtils.mapReduceTriplets(
g, sendMsg, mergeMsg, Some((oldMessages, activeDirection))).cache()
// The call to count() materializes `messages` and the vertices of `g`. This hides oldMessages
// (depended on by the vertices of g) and the vertices of prevG (depended on by oldMessages
// and the vertices of g).
activeMessages = messages.count()
 
logInfo("Pregel finished iteration " + i)
 
// Unpersist the RDDs hidden by newly-materialized RDDs
oldMessages.unpersist(blocking = false)
prevG.unpersistVertices(blocking = false)
prevG.edges.unpersist(blocking = false)
// count the iteration
i += 1
}
messages.unpersist(blocking = false)
g
} // end of apply


接下來再看一下我們剛開始的求單源最短路徑的算法:
首先將所有除了源頂點的其它頂點的屬性值設置爲無窮大,源頂點的屬性值設置爲0.
Superstep 0:然後對所有頂點用initialmsg進行初始化,實際上這次初始化並沒有改變什麼。
Superstep 1 :對於每個triplet:計算triplet.srcAttr + triplet.attr 和 triplet.dstAttr比較,以第一次爲例:假設有一條邊從0到a,這時就滿足triplet.srcAttr + triplet.attr < triplet.dstAttr,這個triplet.attr的值實際上爲1(沒有自己指定,默認值都是1),而0的attr值我們早已初始化爲0,0+1<無窮,所以發出的消息就是(a,1)這個在每個triplet中是從src發放dst的。如果某個邊是從3到5,那麼triplet.srcAttr + triplet.attr < triplet.dstAttr就不成立,因爲無窮大加1等於無窮大,這時消息就是空的。Superstep 1就是這樣,這一步執行完後圖中所有的與0直接相連的點的attr都成了1而且成爲獲躍節點,其它點的attr不變同時變成不活躍節點。活結點根據triplet.srcAttr + triplet.attr < triplet.dstAttr繼續發消息,mergeMsg函數會對發送到同一節點的多個消息進行聚合,聚合的結果就是最小的那個值。
Superstep 2:所有收到消息的節點比較自己的attr和發過來的attr,將較小的值作爲自己的attr。然後自己成爲活節點繼續向周圍的節點發送attr+1這個消息,然後再聚合。
直到沒有節點的attr被更新,不再滿足activeMessages > 0 && i < maxIterations (活躍節點數爲大於0且沒有達到最大允許迭代次數)。這時就得到節點0到其它節點的最短路徑了。這個路徑值保存在其它節點的attr中。
————————————————
版權聲明:本文爲CSDN博主「古月慕南」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u013468917/article/details/51199808

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