本文翻譯自
Apache Spark: core concepts, architecture and internals
本文覆蓋了Apache Spark的RDD、DAG、執(zhí)行工作流、tasks的stages的形成、shuffle的實(shí)現(xiàn)等核心概念,還描述了Spark的架構(gòu)和它的主要模塊Spark Driver。
介紹
Spark是分布式數(shù)據(jù)處理的通用框架,提供了用于大規(guī)模操作數(shù)據(jù)的功能API,內(nèi)存數(shù)據(jù)緩存和可重用計(jì)算。它將一組coarse-grained轉(zhuǎn)換作用于分區(qū)數(shù)據(jù),如果失敗則利用數(shù)據(jù)集的血統(tǒng)來(lái)重新計(jì)算tasks。值得一提的是,Spark支持大多數(shù)數(shù)據(jù)格式,與各種存儲(chǔ)系統(tǒng)集成,可以在Mesos或YARN上執(zhí)行。
功能強(qiáng)大且簡(jiǎn)潔的API與豐富的庫(kù)相結(jié)合,可以更輕松地大規(guī)模執(zhí)行數(shù)據(jù)操作。例如,以Parquet格式執(zhí)行Cassandra列族的備份和恢復(fù):
def backup(path: String, config: Config) {
sc.cassandraTable(config.keyspace, config.table)
.map(_.toEvent).toDF()
.write.parquet(path)
}
def restore(path: String, config: Config) {
sqlContext.read.parquet(path)
.map(_.toEvent)
.saveToCassandra(config.keyspace, config.table)
}
或運(yùn)行差異分析,比較不同數(shù)據(jù)存儲(chǔ)中的數(shù)據(jù):
sqlContext.sql {
"""
SELECT count()
FROM cassandra_event_rollups
JOIN mongo_event_rollups
ON cassandra_event_rollups.uuid = cassandra_event_rollups.uuid
WHERE cassandra_event_rollups.value != cassandra_event_rollups.value
""".stripMargin
}
概述
Spark圍繞彈性分布式數(shù)據(jù)集(RDD)和有向無(wú)環(huán)圖(DAG)的概念構(gòu)建,DAG表示它們之間的轉(zhuǎn)換和依賴。
Spark Application(通常也稱為Driver Program或者Application Master)在高級(jí)別由SparkContext和用戶代碼組成,用戶代碼與它交互創(chuàng)建RDD并執(zhí)行一系列轉(zhuǎn)換以實(shí)現(xiàn)最終結(jié)果。然后將這些RDD的轉(zhuǎn)換變?yōu)镈AG并提交給Scheduler以在一組工作節(jié)點(diǎn)上執(zhí)行。
RDD:Resilient Distributed Dataset
RDD可以被認(rèn)為是具有故障恢復(fù)可能性的不可變并行數(shù)據(jù)結(jié)構(gòu)。它提供了各種對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換和實(shí)體化(materializations ),以及控制元素的緩存和分區(qū)以優(yōu)化數(shù)據(jù)放置的API。RDD既可以從外部存儲(chǔ)創(chuàng)建,也可以從另一個(gè)RDD創(chuàng)建,并存儲(chǔ)有關(guān)其父節(jié)點(diǎn)的信息,以優(yōu)化執(zhí)行(通過(guò)流水線操作),并在發(fā)生故障時(shí)重新計(jì)算分區(qū)。
從開(kāi)發(fā)者的角度來(lái)看,RDD表示分布式的不可變數(shù)據(jù)(分區(qū)數(shù)據(jù)+iterator),且存在惰性機(jī)制(transformations)。RDD接口定義了五個(gè)主要的屬性:
//a list of partitions (e.g. splits in Hadoop)
def getPartitions: Array[Partition]
//a list of dependencies on other RDDs
def getDependencies: Seq[Dependency[_]]
//a function for computing each split
def compute(split: Partition, context: TaskContext): Iterator[T]
//(optional) a list of preferred locations to compute each split on
def getPreferredLocations(split: Partition): Seq[String] = Nil
//(optional) a partitioner for key-value RDDs
val partitioner: Option[Partitioner] = None
這是一個(gè)調(diào)用sparkContext.textFile("hdfs://...")
方法創(chuàng)建RDD的例子:
首先加載HDFS blocks到內(nèi)存,然后使用map()函數(shù)過(guò)濾keys,創(chuàng)建兩個(gè)RDDS:
- HadoopRDD
- getPartitions = HDFS blocks
- getDependencies = None
- compute = load block in memory
- getPrefferedLocations = HDFS block locations
- partitioner = None
- MapPartitionsRDD
- getPartitions = same as parent
- getDependencies = parent RDD
- compute = compute parent and apply map()
- getPrefferedLocations = same as parent
- partitioner = None
RDD操作
RDD的操作分為以下幾種:
- Transformations
- 將用戶函數(shù)應(yīng)用于分區(qū)中的每個(gè)元素(或整個(gè)分區(qū))
- 將聚合函數(shù)應(yīng)用于整個(gè)數(shù)據(jù)集(groupBy,sortBy)
- 引入RDD之間的依賴關(guān)系以形成DAG
- 提供重新分區(qū)的功能(repartition,partitionBy)
- Actions
- 觸發(fā)job執(zhí)行
- 用于實(shí)現(xiàn)計(jì)算結(jié)果
- Extra: persistence
- 顯式地將RDD存儲(chǔ)在內(nèi)存,磁盤(pán)或堆外(off-heap)(cache, persist)
- 檢查點(diǎn)(checkpoint),截?cái)郣DD的血統(tǒng)
下面是一些代碼示例,其中匯總了來(lái)自Cassandra的lambda樣式的數(shù)據(jù),將先前匯總的數(shù)據(jù)與原始存儲(chǔ)中的數(shù)據(jù)相結(jié)合,并演示了RDD上可用的一些轉(zhuǎn)換和操作
//aggregate events after specific date for given campaign
val events =
sc.cassandraTable("demo", "event")
.map(_.toEvent)
.filter { e =>
e.campaignId == campaignId && e.time.isAfter(watermark)
}
.keyBy(_.eventType)
.reduceByKey(_ + _)
.cache()
//aggregate campaigns by type
val campaigns =
sc.cassandraTable("demo", "campaign")
.map(_.toCampaign)
.filter { c =>
c.id == campaignId && c.time.isBefore(watermark)
}
.keyBy(_.eventType)
.reduceByKey(_ + _)
.cache()
//joined rollups and raw events
val joinedTotals = campaigns.join(events)
.map { case (key, (campaign, event)) =>
CampaignTotals(campaign, event)
}
.collect()
//count totals separately
val eventTotals =
events.map{ case (t, e) => s"$t -> ${e.value}" }
.collect()
val campaignTotals =
campaigns.map{ case (t, e) => s"$t -> ${e.value}" }
.collect()
執(zhí)行工作流概述
執(zhí)行工作流:將包含RDD轉(zhuǎn)換的用戶代碼變成有向無(wú)環(huán)圖,然后由DAGScheduler劃分stages。stagese組合了不需要shuffling/repartitioning數(shù)據(jù)的任務(wù)。tasks運(yùn)行在workers上,然后將結(jié)果返回客戶端。
DAG
這是上面示例代碼的DAG。因此,基本上任何數(shù)據(jù)處理工作流都可以定義為讀取數(shù)據(jù)源,應(yīng)用一組轉(zhuǎn)換并以不同方式實(shí)現(xiàn)結(jié)果。
轉(zhuǎn)換在RDD之間創(chuàng)建依賴關(guān)系,在這里我們可以看到它們的不同類型。
依賴關(guān)系通常分為“窄”和“寬”:
- 具有“窄”依賴的RDD操作,如map()和filter()
Spark中有兩種類型的tasks:ShuffleMapTask
將其輸入分區(qū),ResultTask將其輸出發(fā)送給driver。
兩種類型的stages:ShuffleMapStage
和ResultStage
。
Shuffle
在shuffle期間,ShuffleMapTask
將blocks寫(xiě)入本地文件,然后接下來(lái)的stages中的tasks通過(guò)網(wǎng)絡(luò)抓取這些blocks。
-
Shuffle Write
- 在分區(qū)之間重新分配數(shù)據(jù)并將文件寫(xiě)入磁盤(pán)
- 每個(gè)hash shuffle task為每個(gè)“reduce” task創(chuàng)建一個(gè)文件(total = MxR)
- sort shuffle task創(chuàng)建一個(gè)文件,其中區(qū)域分配給reducer
- sort shuffle使用內(nèi)存排序和溢出到磁盤(pán)以獲得最終結(jié)果
-
Shuffle Read
- 獲取文件并應(yīng)用reduce()邏輯
- 如果需要數(shù)據(jù)有序,則對(duì)于任何類型的shuffle,它在“reducer”側(cè)排序
在Spark Sort Shuffle是自1.2以來(lái)的默認(rèn)值,但Hash Shuffle也可用。
Sort Shuffle
- 傳入記錄根據(jù)其目標(biāo)分區(qū)ID在內(nèi)存中累加和排序
- 如果溢出,已排序的記錄將寫(xiě)入一個(gè)或多個(gè)文件,然后合并
- 索引文件存儲(chǔ)數(shù)據(jù)文件中數(shù)據(jù)塊的偏移量
- 在某些條件下,可以在不進(jìn)行反序列化的情況下進(jìn)行排序 SPARK-7081
Spark組件
Spark有三個(gè)主要的組件:
- Spark Driver
- 用來(lái)執(zhí)行用戶應(yīng)用程序的單獨(dú)進(jìn)程
- 創(chuàng)建SparkContext以計(jì)劃jobs執(zhí)行并與集群管理器協(xié)商
- Executors
- 運(yùn)行driver安排的tasks
- 將計(jì)算結(jié)果存儲(chǔ)在內(nèi)存,磁盤(pán)或堆外
- 與存儲(chǔ)系統(tǒng)交互
- Cluster Manager
- Mesos
- YARN
- Spark Standalone
相關(guān)閱讀:
[1] Apache Spark: core concepts, architecture and internals
[2] SparkInternals 對(duì)Spark的深刻解讀,非常好的內(nèi)容!
[3] Spark shuffle introduction