10/30/2559 BE, 1,17PMSparkInternals/0-Introduction.md at thai · Aorjoa/SparkInternals
Page 2 of 3https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/0-Introduction.md
ภาษาจีน
ผู้เขียนไม่สามารถเขียนเอกสารได้เสร็จในขณะนี้ ครั้งล่าสุดที่ได้เขียนคือราว 3 ปีที่แล้วตอนที่กำลังเรียนคอร์ส Andrew Ng's ML อยู่ ซึ่งตอน
นั้นมีแรงบัลดาลใจที่จะทำ ตอนที่เขียนเอกสารนี้ผู้เขียนใช้เวลาเขียนเอกสารขึ้นมา 20 กว่าวันใช้ช่วงซัมเมอร์ เวลาส่วนใหญ่ที่ใช้ไปใช้กับการ
ดีบั๊ก, วาดแผนภาพ, และจัดวางไอเดียให้ถูกที่ถูกทาง. ผู้เขียนหวังเป็นอย่างยิ่งว่าเอกสารนี้จะช่วยผู้อ่านได้
เนื้อหา
เราจะเริ่มกันที่การสร้าง Spark Job และคุยกันถึงเรื่องว่ามันทำงานยังไง จากนั้นจึงจะอธิบายระบบที่เกี่ยวข้องและฟีเจอร์ของระบบที่ทำให้งาน
เราสามารถประมวลผลออกมาได้
1. Overview ภาพรวมของ Apache Spark
2. Job logical plan แผนเชิงตรรกะ : Logical plan (data dependency graph)
3. Job physical plan แผนเชิงกายภาย : Physical plan
4. Shuffle details กระบวนการสับเปลี่ยน (Shuffle)
5. Architecture กระบวนการประสานงานของโมดูลในระบบขณะประมวลผล
6. Cache and Checkpoint Cache และ Checkpoint
7. Broadcast ฟีเจอร์ Broadcast
8. Job Scheduling TODO
9. Fault-tolerance TODO
เอกสารนี้เขียนด้วยภาษา Markdown, สำหรับเวอร์ชัน PDF ภาษาจีนสามารถดาวน์โหลด ที่นี่.
ถ้าคุณใช้ Max OS X, เราขอแนะนำ MacDown แล้วใช้ธีมของ github จะทำให้อ่านได้สะดวก
ตัวอย่าง
บางตัวอย่างที่ผู้เขียนสร้างข้นเพื่อทดสอบระบบขณะที่เขียนจะอยู่ที่ SparkLearning/src/internals.
Acknowledgement
Note : ส่วนของกิตติกรรมประกาศจะไม่แปลครับ
I appreciate the help from the following in providing solutions and ideas for some detailed issues:
@Andrew-Xia Participated in the discussion of BlockManager's implemetation's impact on broadcast(rdd).
@CrazyJVM Participated in the discussion of BlockManager's implementation.
@ Participated in the discussion of BlockManager's implementation.
Thanks to the following for complementing the document:
Weibo Id Chapter Content Revision status
@OopsOutOfMemory Overview
Relation between workers and
executors and Summary on Spark
Executor Driver's Resouce
Management (in Chinese)
There's not yet a conclusion on this
subject since its implementation is
still changing, a link to the blog is
added
Thanks to the following for finding errors:
Weibo Id Chapter Error/Issue Revision status
@Joshuawangzj Overview
When multiple applications are
running, multiple Backend
process will be created
Corrected, but need to be confirmed. No
idea on how to control the number of
Backend processes
10/30/2559 BE, 1,18PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals
Page 3 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md
import org.apache.spark.SparkContext._
/**
* Usage: GroupByTest [numMappers] [numKVPairs] [valSize] [numReducers]
*/
object GroupByTest {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("GroupBy Test")
var numMappers = 100
var numKVPairs = 10000
var valSize = 1000
var numReducers = 36
val sc = new SparkContext(sparkConf)
val pairs1 = sc.parallelize(0 until numMappers, numMappers).flatMap { p =>
val ranGen = new Random
var arr1 = new Array[(Int, Array[Byte])](numKVPairs)
for (i <- 0 until numKVPairs) {
val byteArr = new Array[Byte](valSize)
ranGen.nextBytes(byteArr)
arr1(i) = (ranGen.nextInt(Int.MaxValue), byteArr)
}
arr1
}.cache
// Enforce that everything has been calculated and in cache
println(">>>>>>")
println(pairs1.count)
println("<<<<<<")
println(pairs1.groupByKey(numReducers).count)
sc.stop()
}
}
หลังจากที่อ่านโค้ดแล้วจะพบว่าโค้ดนี้มีแนวความคิดในการแปลงข้อมูลดังนี้
แอพพลิเคชันนี้ไม่ได้ซับซ้อนอะไรมาก เราจะประเมินขนาดของข้อมูลและผลลัพธ์ซึ่งแอพพลิเคชันก็จะมีการทำงานตามขั้นตอนดังนี้
8.
10/30/2559 BE, 1,18PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals
Page 4 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md
1. สร้าง SparkConf เพื่อกำหนดการตั้งค่าของ Spark (ตัวอย่างกำหนดชื่อของแอพพลิเคชัน)
2. สร้างและกำหนดค่า numMappers=100, numKVPairs=10,000, valSize=1000, numReducers= 36
3. สร้าง SparkContext โดยใช้การตั้งค่าจาก SparkConf ขั้นตอนนี้สำคัญมากเพราะ SparkContext จะมี Object และ Actor ซึ่งจำเป็น
สำหรับการสร้างไดรว์เวอร์
4. สำหรับ Mapper แต่ละตัว arr1: Array[(Int, Byte[])] Array ชื่อ arr1 จะถูกสร้างขึ้นจำนวน numKVPairs ตัว, ภายใน Array
แต่ละตัวมีคู่ Key/Value ซึ่ง Key เป็นตัวเลข (ขนาด 4 ไบต์) และ Value เป็นไบต์ขนาดเท่ากับ valSize อยู่ ทำให้เราประเมินได้ว่า ขนาด
ของ arr1 = numKVPairs * (4 + valSize) = 10MB , ดังนั้นจะได้ว่า ขนาดของ pairs1 = numMappers * ขนาดของ arr1
1000MB ตัวนี้ใช้เป็นการประเมินการใช้พื้นที่จัดเก็บข้อมูลได้
5. Mapper แต่ละตัวจะถูกสั่งให้ Cache ตัว arr1 (ผ่านทาง pairs1) เพื่อเก็บมันไว้ในหน่วยความจำเผื่อกรณีที่มีการเรียกใช้ (ยังไม่ได้ทำ
นะแต่สั่งใว้)
6. จากนั้นจะมีการกระทำ count() เพื่อคำนวณหาขนาดของ arr1 สำหรับ Mapper ทุกๆตัวซึ่งผลลัพธ์จะเท่ากับ numMappers *
numKVPairs = 1,000,000 การกระทำในขั้นตอนนี้ทำให้เกิดการ Cahce ที่ตัว arr1 เกิดขึ้นจริงๆ (จากคำสั่ง pairs1.count()1
ตัวนี้จะมีสมาชิก 1,000,000 ตัว)
7. groupByKey ถูกสั่งให้ทำงานบน pairs1 ซึ่งเคยถูก Cache เก็บไว้แล้ว โดยจำนวนของ Reducer (หรือจำนวนพาร์ทิชัน) ถูกกำหนด
ในตัวแปร numReducers ในทางทฤษฏีแล้วถ้าค่า Hash ของ Key มีการกระจายอย่างเท่าเทียมกันแล้วจะได้ numMappers *
numKVPairs / numReducer 27,777 ก็คือแต่ละตัวของ Reducer จะมีคู่ (Int, Array[Byte]) 27,777 คู่อยู่ในแต่ละ Reducer ซึ่ง
จะมีขนาดเท่ากับ ขนาดของ pairs1 / numReducer = 27MB
8. Reducer จะรวบรวมเรคคอร์ดหรือคู่ Key/Value ที่มีค่า Key เหมือนกันเข้าไว้ด้วยกันกลายเป็น Key เดียวแล้วก็ List ของ Byte (Int,
List(Byte[], Byte[], ..., Byte[]))
9. ในขั้นตอนสุดท้ายการกระทำ count() จะนับหาผลรวมของจำนวนที่เกิดจากแต่ละ Reducer ซึ่งผ่านขั้นตอนการ groupByKey มาแล้ว
(ทำให้ค่าอาจจะไม่ได้ 1,000,000 พอดีเป๊ะเพราะว่ามันถูกจับกลุ่มค่าที่ Key เหมือนกันซึ่งเป็นการสุ่มค่ามาและค่าที่ได้จากการสุ่มอาจจะ
ตรงกันพอดีเข้าไว้ด้วยกันแล้ว) สุดท้ายแล้วการกระทำ count() จะรวมผลลัพธ์ที่ได้จากแต่ละ Reducer เข้าไว้ด้วยกันอีกทีเมื่อทำงาน
เสร็จแล้วก็จะได้จำนวนของ Key ที่ไม่ซ้ำกันใน paris1
แผนเชิงตรรกะ Logical Plan
ในความเป็นจริงแล้วกระบวนการประมวลผลของแอพพลิเคชันของ Spark นั้นซับซ้อนกว่าที่แผนภาพด้านบนอธิบายไว้ ถ้าจะว่าง่ายๆ แผนเชิง
ตรรกะ Logical Plan (หรือ data dependency graph - DAG) จะถูกสร้างแล้วแผนเชิงกายภาพ Physical Plan ก็จะถูกผลิตขึ้น (English : a
logical plan (or data dependency graph) will be created, then a physical plan (in the form of a DAG) will be generated
เป็นการเล่นคำประมาณว่า Logical plan มันถูกสร้างขึ้นมาจากของที่ยังไม่มีอะไร Physical plan ในรูปของ DAG จาก Logical pan นั่นแหละ
จะถูกผลิตขึ้น) หลังจากขั้นตอนการวางแผนทั้งหลายแหล่นี้แล้ว Task จะถูกผลิตแล้วก็ทำงาน เดี๋ยวเราลองมาดู Logical plan ของ
แอพพลิเคชันดีกว่า
ตัวนี้เรียกใช้ฟังก์ชัน RDD.toDebugString แล้วมันจะคืนค่า Logical Plan กลับมา:
MapPartitionsRDD[3] at groupByKey at GroupByTest.scala:51 (36 partitions)
ShuffledRDD[2] at groupByKey at GroupByTest.scala:51 (36 partitions)
FlatMappedRDD[1] at flatMap at GroupByTest.scala:38 (100 partitions)
ParallelCollectionRDD[0] at parallelize at GroupByTest.scala:38 (100 partitions)
วาดเป็นแผนภาพได้ตามนี้:
10/30/2559 BE, 1,19PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals
Page 14 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md
Cartesian() จะคืนค่าเป็นผลคูณคาร์ทีเซียนระหว่าง 2 RDD ผลลัพธ์ของ RDD จะมีพาร์ทิชันจำนวน = จํานวนพาร์ทิชันของ (RDD a) x
จํานวนพาร์ทิชันของ (RDD b) ต้องให้ความสนใจกับการขึ้นต่อกันด้วย แต่ละพาร์ทิชันใน CartesianRDD ขึ้นต่อกันกับ ทั่วทั้ง ของ 2 RDD
พ่อแม่ พวกมันล้วนเป็น NarrowDependency ทั้งสิ้น
CartesianRDD.getDependencies() จะคืน rdds: Array(RDD a, RDD b) . พาร์ทิชันตัวที่ i ของ CartesianRDD จะขึ้นกับ:
a.partitions(i / จํานวนพาร์ทิชันของA)
b.partitions(i % จํานวนพาร์ทิชันของ B)
10) coalesce(numPartitions, shuffle = false)
26.
10/30/2559 BE, 1,19PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals
Page 15 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md
10/30/2559 BE, 1,21PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals
Page 6 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md
ผลลัพธ์สุดท้ายออกมา กระบวนการเหล่านี้แสดงได้ตามแผนภาพด้านล่าง:
กระบวนการประมวลผลนี้ไม่สามารถใช้กับ Physical plan ของ Spark ได้ตรงๆเพราะ Physical plan ของ Hadoop MapReduce นั้นมันเป็น
กลไลง่ายๆและกำหนดไว้ตายตัวโดยที่ไม่มีการ Pipeline ด้วย
ไอเดียหลักของการทำ Pipeline ก็คือ ข้อมูลจะถูกประมวลผลจริงๆ ก็ต่อเมื่อมันต้องการจะใช้จริงๆในกระบวนการไหลของข้อมูล เราจะเริ่ม
จากผลลัพธ์สุดท้าย (ดังนั้นจึงทราบอย่างชัดเจนว่าการคำนวณไหนบ้างที่ต้องการจริงๆ) จากนั้นตรวจสอบย้อนกลับตาม Chain ของ RDD เพื่อ
หาว่า RDD และพาร์ทิชันไหนที่เราต้องการรู้ค่าเพื่อใช้ในการคำนวณจนได้ผลลัพธ์สุดท้ายออกมา กรณีที่เจอคือส่วนใหญ่เราจะตรวจสอบย้อน
กลับไปจนถึงพาร์ทิชันบางตัวของ RDD ที่อยู่ฝั่งซ้ายมือสุดและพวกบางพาร์ทิชันที่อยู่ซ้ายมือสุดนั่นแหละที่เราต้องการที่จะรู้ค่าเป็นอันดับแรก
สำหรับ Stage ที่ไม่มีพ่อแม่ ตัวมันจะเป็น RDD ที่อยู่ซ้ายสุดโดยตัวมันเองซึ่งเราสามารถรู้ค่าได้โดยตรงอยู่แล้ว (มันไม่มีการขึ้นต่อกัน) และ
แต่ละเรคอร์ดที่รู้ค่าแล้วสามารถ Stream ต่อไปยังกระบวนการคำนวณที่ตามมาภายหลังมัน (Pipelining) Chain การคำนวณอนุมาณได้ว่า
มาจากขั้นตอนสุดท้าย (เพราะไล่จาก RDD ตัวสุดท้ายมา RDD ฝั่งซ้ายมือสุด) แต่กลไกการประมวลผลจริง Stream ของเรคอร์ดนั้นดำเนินไป
ข้างหน้า (ทำจริงจากซ้ายไปขวา แต่ตอนเช็คไล่จากขวาไปซ้าย) เรคอร์ดหนึ่งๆจถูกทำทั้ง Chain การประมวลผลก่อนที่จะเริ่มประมวลผลเร
คอร์ดตัวอื่นต่อไป
สำหรับ Stage ที่มีพ่อแม่นั้นเราต้องประมวลผลที่ Stage พ่อแม่ของมันก่อนแล้วจึงดึงข้อมูลผ่านทาง Shuffle จากนั้นเมื่อพ่อแม่ประมวลผล
เสร็จหนึ่งครั้งแล้วมันจะกลายเป็นกรณีที่เหมือนกัน Stage ที่ไม่มีพ่อแม่ (จะได้ไม่ต้องคำนวณซ้ำๆ)
ในโค้ดแต่ละ RDD จะมีเมธอต getDependency() ประกาศการขึ้นต่อกันของข้อมูลของมัน, compute() เป็นเมธอตที่ดูแลการรับเร
คอร์ดมาจาก Upstream (จาก RDD พ่อแม่หรือแหล่งเก็บข้อมูล) และนำลอจิกไปใช้กับเรคอร์ดนั้นๆ เราจะเห็นบ่อยมากในโคดที่เกี่ยว
กับ RDD (ผู้แปล: ตัวนี้เคยเจอในโค้ดของ Spark ในส่วนที่เกี่ยวกับ RDD) firstParent[T].iterator(split,
context).map(f) ตัว firstParent บอกว่าเป็น RDD ตัวแรกที่ RDD มันขึ้นอยู่ด้วย, iterator() แสดงว่าเรคอร์ดจะถูกใช้
แบบหนึ่งต่อหนึ่ง, และ map(f) เรคอร์ดแต่ละตัวจะถูกนำประมวลผลตามลอจิกการคำนวณที่ถูกกำหนดไว้. สุดท้ายแล้วเมธอต
compute() ก็จะคืนค่าที่เป็น Iterator เพื่อใช้สำหรับการประมวลผลถัดไป
สรุปย่อๆดังนี้ : *การคำนวณทั่วทั้ง Chain จะถูกสร้างโดยการตรวจสอบย้อนกลับจาก RDD ตัวสุดท้าย แต่ละ ShuffleDependency จะเป็น
ตัวแยก Stage แต่ละส่วนออกจากกัน และในแต่ละ Stage นั้น RDD แต่ละตัวจะมีเมธอต compute() ที่จะเรียกการประมวลผลบน RDD พ่อ
แม่ parentRDD.itererator() แล้วรับ Stream เรคอร์ดจาก Upstream *
โปรดทราบว่าเมธอต compute() จะถูกจองไว้สำหรับการคำนวณลิจิกที่สร้างเอาท์พุทออกจากโดยใช้ RDD พ่อแม่ของมันเท่านั้น. RDD
จริงๆของพ่อแม่ที่ RDD มันขึ้นต่อจะถูกประกาศในเมธอต getDependency() และพาร์ทิชันจริงๆที่มันขึ้นต่อจะถูกประกาศไว้ในเมธอต
dependency.getParents()
ลองดูผลคูณคาร์ทีเซียน CartesianRDD ในตัวอย่างนี้
// RDD x is the cartesian product of RDD a and RDD b
// RDD x = (RDD a).cartesian(RDD b)
// Defines how many partitions RDD x should have, what are the types for each partition
override def getPartitions: Array[Partition] = {
// create the cross product split
val array = new Array[Partition](rdd1.partitions.size * rdd2.partitions.size)
for (s1 <- rdd1.partitions; s2 <- rdd2.partitions) {
val idx = s1.index * numPartitionsInRdd2 + s2.index
array(idx) = new CartesianPartition(idx, rdd1, rdd2, s1.index, s2.index)
}
array
}
35.
10/30/2559 BE, 1,21PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals
Page 7 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md
// Defines the computation logic for each partition of RDD x (the result RDD)
override def compute(split: Partition, context: TaskContext) = {
val currSplit = split.asInstanceOf[CartesianPartition]
// s1 shows that a partition in RDD x depends on one partition in RDD a
// s2 shows that a partition in RDD x depends on one partition in RDD b
for (x <- rdd1.iterator(currSplit.s1, context);
y <- rdd2.iterator(currSplit.s2, context)) yield (x, y)
}
// Defines which are the dependent partitions and RDDs for partition i in RDD x
//
// RDD x depends on RDD a and RDD b, both in `NarrowDependency`
// For the first dependency, partition i in RDD x depends on the partition with
// index `i / numPartitionsInRDD2` in RDD a
// For the second dependency, partition i in RDD x depends on the partition with
// index `i % numPartitionsInRDD2` in RDD b
override def getDependencies: Seq[Dependency[_]] = List(
new NarrowDependency(rdd1) {
def getParents(id: Int): Seq[Int] = List(id / numPartitionsInRdd2)
},
new NarrowDependency(rdd2) {
def getParents(id: Int): Seq[Int] = List(id % numPartitionsInRdd2)
}
)
การสร้าง Job
จนถึงตอนนี้เรามีความรู้เรื่อง Logical plan และ Plysical plan แล้ว ทำอย่างไรและเมื่อไหร่ที่ Job จะถูกสร้าง? และจริงๆแล้ว Job มันคือ
อะไรกันแน่
ตารางด้านล่างแล้วถึงประเภทของ action() และคอลัมภ์ถัดมาคือเมธอต processPartition() มันใช้กำหนดว่าการประมวลผลกับเร
คอร์ดในแต่ละพาร์ทิชันจะทำอย่างไรเพื่อให้ได้ผลลัพธ์ คอลัมภ์ที่สามคือเมธอต resultHandler() จะเป็นตัวกำหนดว่าจะประมวลผลกับ
ผลลัพธ์บางส่วนที่ได้มาจากแต่ละพาร์ทิชันอย่างไรเพื่อให้ได้ผลลัพธ์สุดท้าย
Action finalRDD(records) => result compute(results)
reduce(func)
(record1, record2) => result, (result, record
i) => result
(result1, result 2) => result, (result, result i)
=> result
collect() Array[records] => result Array[result]
count() count(records) => result sum(result)
foreach(f) f(records) => result Array[result]
take(n) record (i<=n) => result Array[result]
first() record 1 => result Array[result]
takeSample() selected records => result Array[result]
takeOrdered(n,
[ordering])
TopN(records) => result TopN(results)
saveAsHadoopFile(path) records => write(records) null
countByKey() (K, V) => Map(K, count(K)) (Map, Map) => Map(K, count(K))
แต่ละครั้งที่มีการเรียก action() ในโปรแกรมไดรว์เวอร์ Job จะถูกสร้างขึ้น ยกตัวอย่าง เช่น foreach() การกระทำนี้จะเรียกใช้
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(f))) เพื่อส่ง Job ไปยัง DAGScheduler และถ้ามีการเรียก
action() อื่นๆอีกในโปรแกรมไดรว์เวอร์ระบบก็จะสร้า Job ใหม่และส่งเข้าไปใหม่อีกครั้ง ดังนั้นเราจึงจะมี Job ได้หลายตัวตาม
action() ที่เราเรียกใช้ในโปรแกรมไดรว์เวอร์ นี่คือเหตุผลที่ว่าทำไมไดรว์เวอร์ของ Spark ถูกเรียกว่าแอพพลิเคชันมากกว่าเรียกว่า Job
10/30/2559 BE, 1,24PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals
Page 1 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md
This repository Pull requests Issues Gist
SparkInternals / markdown / thai / 6-CacheAndCheckpoint.md
Search
Aorjoa / SparkInternals
forked from JerryLead/SparkInternals
Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings
thaiBranch: Find file Copy path
1 contributor
1a5512d an hour agoAorjoa fixed some typo and polish some word
170 lines (104 sloc) 23.9 KB
cache และ checkpoint
cache (หรือ persist ) เป็นฟีเจอร์ที่สำคัญของ Spark ซึ่งไม่มีใน Hadoop ฟีเจอร์นี้ทำให้ Spark มีความเร็วมากกว่าเพราะสามารถใช้
เช็ตข้อมูลเดินซ้ำได้ เช่น Iterative algorithm ใน Machine Learning, การเรียกดูข้อมูลแบบมีปฏิสัมพันธ์ เป็นต้น ซึ่งแตกต่างกับ Hadoop
MapReduce Job เนื่องจาก Sark มี Logical/Physical pan ที่สามารถขยายงานออกกว้างดังนั้น Chain ของการคำนวณจึงจึงยาวมากและ
ใช้เวลาในการประมวลผล RDD นานมาก และหากเกิดข้อผิดพลาดในขณะที่ Task กำลังประมวลผลอยู่นั้นการประมวลผลทั้ง Chain ก็จะต้อง
เริ่มใหม่ ซึ่งส่วนนี้ถูกพิจารณาว่ามีข้อเสีย ดังนั้นจึงมี checkpoint กับ RDD เพื่อที่จะสามารถประมวลผลเริ่มต้นจาก checkpoint ได้
โดยที่ไม่ต้องเริ่มประมวลผลทั้ง Chain ใหม่
cache()
ลองกลับไปดูการทำ GroupByTest ในบทที่เป็นภาพรวม เราจะเห็นว่า FlatMappedRDD จะถูกแคชไว้ดังนั้น Job 1 (Job ถัดมา) สามารถ
เริ่มได้ที่ FlatMappedRDD นี้ได้เลย เนื่องจาก cache() ทำให้สามารถแบ่งปันข้อมูลกันใช้ระหว่าง Job ในแอพพลิเคชันเดียวกัน
Logical plan
Raw Blame History
0 7581Unwatch Star Fork
64.
10/30/2559 BE, 1,24PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals
Page 2 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md
Physical plan