Successfully reported this slideshow.
Your SlideShare is downloading. ×

หนังสือภาษาไทย Spark Internal

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
 
Spark   Internals 
 
 
 
 
Lijie   Xu   :   เ ยน 
Bhuridech   Sudsee   :      แปล 
10/30/2559 BE, 1,17 PMSparkInternals/0-Introduction.md at thai · Aorjoa/SparkInternals
Page 1 of 3https://github.com/Aorjo...
10/30/2559 BE, 1,17 PMSparkInternals/0-Introduction.md at thai · Aorjoa/SparkInternals
Page 2 of 3https://github.com/Aorjo...
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Advertisement

Check these out next

1 of 74 Ad

หนังสือภาษาไทย Spark Internal

Download to read offline

หนังสือภาษาไทยของ Spark Internal โดย Lijie Xu แปลโดยภูริเดช สุดสี อาจจะงงๆหน่อยนะครับบางส่วนผมก็ไม่เข้าใจลึกซึ้ง

หนังสือภาษาไทยของ Spark Internal โดย Lijie Xu แปลโดยภูริเดช สุดสี อาจจะงงๆหน่อยนะครับบางส่วนผมก็ไม่เข้าใจลึกซึ้ง

Advertisement
Advertisement

More Related Content

Slideshows for you (20)

Viewers also liked (20)

Advertisement

More from Bhuridech Sudsee (20)

หนังสือภาษาไทย Spark Internal

  1. 1.   Spark   Internals          Lijie   Xu   :   เ ยน  Bhuridech   Sudsee   :      แปล 
  2. 2. 10/30/2559 BE, 1,17 PMSparkInternals/0-Introduction.md at thai · Aorjoa/SparkInternals Page 1 of 3https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/0-Introduction.md This repository Pull requests Issues Gist SparkInternals / markdown / thai / 0-Introduction.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 39 minutes agoAorjoa fixed some typo and polish some word 85 lines (55 sloc) 11.4 KB ภายใน Spark (Spark Internals) Apache Spark รุ่น: 1.0.2, เอกสาร รุ่น: 1.0.2.0 ผู้เขียน Weibo Id Name @JerryLead Lijie Xu แปลและเรียบเรียง Twitter Name @AorJoa Bhuridech Sudsee เกริ่นนำ เอกสารนี้เป็นการพูดคุยแลกเปลี่ยนเกี่ยวการการออกแบบและใช้งานซอฟต์แวร์ Apache Spark ซึ่งจะโฟกัสไปที่เรื่องของหลักการออกแบบ, กลไกการทำงาน, สถาปัตยกรรมของโค้ด และรวมไปถึงการปรับแต่งประสิทธิภาพ นอกจากประเด็นเหล่านี้ก็จะมีการเปรียบเทียบในบางแง่มุม กับ Hadoop MapReduce ในส่วนของการออกแบบและการนำไปใช้งาน อย่างหนึ่งที่ผู้เขียนต้องการให้ทราบคือเอกสารนึ้ไม่ได้ต้องการใช้ โค้ดเป็นส่วนนำไปสู่การอธิบายจึงจะไม่มีการอธิบายส่วนต่างๆของโค้ด แต่จะเน้นให้เข้าใจระบบโดยรวมที่ Spark ทำงานในลักษณะของการ ทำงานเป็นระบบ (อธิบายส่วนโน้นส่วนนี้ว่าทำงานประสานงานกันยังไง) ลักษณะวิธีการส่งงานที่เรียกว่า Spark Job จนกระทั่งถึงการทำงาน จนงานเสร็จสิ้น มีรูปแบบวิธีการหลายอย่างที่จะอธิบายระบบของคอมพิวเตอร์ แต่ผู้เขียนเลือกที่จะใช้ problem-driven หรือวิธีการขับเคลื่อนด้วยปัญหา ขั้น ตอนแรกของคือการนำเสนอปัญหาที่เกิดขึ้นจากนั้นก็วิเคราะห์ข้อมูลทีละขั้นตอน แล้วจึงจะใช้ตัวอย่างที่มีทั่วๆไปของ Spark เพื่อเล่าถึงโมดูล ของระบบและความต้องการของระบบเพื่อที่จะใช้สร้างและประมวลผล และเพื่อให้เห็นภาพรวมๆของระบบก็จะมีการเลือกส่วนเพื่ออธิบายราย ละเอียดของการออกแบบและนำไปใช้งานสำหรับบางโมดูลของระบบ ซึ่งผู้เขียนก็เชื่อว่าวิธีนี้จะดีกว่าการที่มาไล่กระบวนการของระบบทีละ ส่วนตั้งแต่ต้น จุดมุ่งหมายของเอกสารชุดนี้คือพวกที่มีความรู้หรือ Geek ที่อยากเข้าใจการทำงานเชิงลึกของ Apache Spark และเฟรมเวิร์คของระบบ ประมวลผลแบบกระจาย (Distributed computing) ตัวอื่นๆ ผู้เขียนพยายามที่จะอัพเดทเอกสารตามรุ่นของ Spark ที่เปลี่ยนอย่างรวดเร็ว เนื่องจากชุมชนนักพัฒนาที่แอคทิฟมากๆ ผู้เขียนเลือกที่จะใช้เลข รุ่นหลักของ Spark มาใช้กับเลขที่รุ่นของเอกสาร (ตัวอย่างใช้ Apache Spark 1.0.2 เลยใช้เลขรุ่นของเอกสารเป็น 1.0.2.0) สำหรับข้อถกเถียงทางวิชาการ สามารถติดตามได้ที่เปเปอร์ดุษฏีนิพนธ์ของ Matei และเปเปอร์อื่นๆ หรือว่าจะติดตามผู้เขียนก็ไปได้ที่ บล๊อค Raw Blame History 0 7581Unwatch Star Fork
  3. 3. 10/30/2559 BE, 1,17 PMSparkInternals/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
  4. 4. 10/30/2559 BE, 1,17 PMSparkInternals/0-Introduction.md at thai · Aorjoa/SparkInternals Page 3 of 3https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/0-Introduction.md @_cs_cm Overview Latest groupByKey() has removed the mapValues() operation, there's no MapValuesRDD generated Fixed groupByKey() related diagrams and text @ JobLogicalPlan N:N relation in FullDepedency N:N is a NarrowDependency Modified the description of NarrowDependency into 3 different cases with detaild explaination, clearer than the 2 cases explaination before @zzl0 Fisrt four chapters Lots of typos such as "groupByKey has generated the 3 following RDDs" should be 2. Check pull request All fixed @ TEL Cache and Broadcast chapter Lots of typos All fixed @cloud-fan JobLogicalPlan Some arrows in the Cogroup() diagram should be colored red All fixed @CrazyJvm Shuffle details Starting from Spark 1.1, the default value for spark.shuffle.file.buffer.kb is 32k, not 100k All fixed Special thanks to @ Andy for his great support. Special thanks to the rockers (including researchers, developers and users) who participate in the design, implementation and discussion of big data systems. Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help
  5. 5. 10/30/2559 BE, 1,18 PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals Page 1 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md This repository Pull requests Issues Gist SparkInternals / markdown / thai / 1-Overview.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 40 minutes agoAorjoa fixed some typo and polish some word 175 lines (129 sloc) 26.4 KB ภาพรวมของ Apache Spark เริ่มแรกเราจะให้ความสนใจไปที่ระบบดีพลอยของ Spark คำถามก็คือ : ถ้าดีพลอยเสร็จเรียบร้อยแล้วระบบของแต่ละโหนดในคลัสเตอร์ ทำงานอะไรบ้าง? Deployment Diagram จากแผนภาพการดีพลอย : Raw Blame History 0 7581Unwatch Star Fork
  6. 6. 10/30/2559 BE, 1,18 PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals Page 2 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md - โหนด Master และโหนด Worker ในคลัสเตอร์ มีหน้าที่เหมือนกับโหนด Master และ Slave ของ Hadoop - โหนด Master จะมีโปรเซส Master ที่ทำงานอยู่เบื้องหลังเพื่อที่จะจัดการโหนด Worker ทุกตัว - โหนด Worker จะมีโปรเซส Worker ทำงานอยู่เบื้องหลังซึ่งรับผิดชอบการติดต่อกับโหนด Master และจัดการกับ Executer ภายในตัว โหนดของมันเอง - Driver ในเอกสารที่เป็นทางการอธิบายว่า "The process running the main() function of the application and creating the SparkContext" ไดรว์เวอร์คือโปรเซสที่กำลังทำงานฟังก์ชั่น main() ซึ่งเป็นฟังก์ชันที่เอาไว้เริ่มต้นการทำงานของแอพพลิเคชันของเราและ สร้าง SparkContext ซึ่งจะเป็นสภาพแวดล้อมที่แอพพลิเคชันจะใช้ทำงานร่วมกัน และแอพพลิเคชันก็คือโปรแกรมของผู้ใช้ที่ต้องให้ประมวล ผล บางทีเราจะเรียกว่าโปรแกรมไดรว์เวอร์ (Driver program) เช่น WordCount.scala เป็นต้น หากโปรแกรมไดรเวอร์กำลังทำงานอยู่บน โหนด Master ยกตัวอย่าง ./bin/run-example SparkPi 10 จากโค้ดด้านบนแอพพลิเคชัน SparkPi สามารถเป็นโปรแกรมไดรว์เวอร์สำหรับโหนด Master ได้ ในกรณีของ YARN (ตัวจัดการคลัสเตอร์ ตัวหนึ่ง) ไดรว์เวอร์อาจจะถูกสั่งให้ทำงานที่โหนด Worker ได้ ซึ่งถ้าดูตามแผนภาพด้านบนมันเอาไปไว้ที่โหนด Worker 2 และถ้าโปรแกรม ไดรเวอร์ถูกสร้างภายในเครื่องเรา เช่น การใช้ Eclipse หรือ IntelliJ บนเครื่องของเราเองตัวโปรแกรมไดรว์เวอร์ก็จะอยู่ในเครื่องเรา พูด ง่ายๆคือไดรว์เวอร์มันเปลี่ยนที่อยู่ได้ val sc = new SparkContext("spark://master:7077", "AppName") แม้เราจะชี้ตัว SparkContext ไปที่โหนด Master แล้วก็ตามแต่ถ้าโปรแกรมทำงานบนเครื่องเราตัวไดรว์เวอร์ก้ยังจะอยู่บนเครื่องเรา อย่างไร ก็ดีวิธีนี้ไม่แนะนำให้ทำถ้าหากเน็ตเวิร์คอยู่คนละวงกับ Worker เนื่องจากจะทำใหการสื่อสารระหว่าง Driver กับ Executor ช้าลงอย่างมาก มี ข้อควรรู้บางอย่างดังนี้ เราสามารถมี ExecutorBackend ได้ตั้งแต่ 1 ตัวหรือหลายตัวในแต่ละโหนด Worker และตัว ExecutorBackend หนึ่งตัวจะมี Executor หนึ่งตัว แต่ละ Executor จะดูแล Thread pool และ Task ซึ่งเป็นงานย่อยๆ โดยที่แต่ละ Task จะทำงานบน Thread ตัวเดียว แต่ละแอพพลิเคชันมีไดรว์เวอร์ได้แค่ตัวเดียวแต่สามารถมี Executor ได้หลายตัว, และ Task ทุกตัวที่อยู่ใน Executor เดียวกันจะเป็น ของแอพพลิเคชันตัวเดียวกัน ในโหมด Standalone, ExecutorBackend เป็นอินสแตนท์ของ CoarseGrainedExecutorBackend คลัสเตอร์ของผู้เขียนมีแค่ CoarseGrainedExecutorBackend ตัวเดียวบนแต่ละโหนด Worker ผู้เขียนคิดว่าหากมีหลาย แอพพลิเคชันรันอยู่มันก็จะมีหลาย CoarseGrainedExecutorBackend แต่ไม่ฟันธงนะ อ่านเพิ่มในบล๊อค (ภาษาจีน) Summary on Spark Executor Driver Resource Scheduling เขียนโดย @OopsOutOfMemory ถ้าอยากรู้เพิ่มเติมเกี่ยวกับ Worker และ Executor โหนด Worker จะควบคุม CoarseGrainedExecutorBackend ผ่านทาง ExecutorRunner หลังจากดูแผนภาพการดีพลอยแล้วเราจะมาทดลองสร้างตัวอย่างของ Spark job เพื่อดูว่า Spark job มันถูกสร้างและประมวลผลยังไง ตัวอย่างของ Spark Job ตัวอย่างนี้เป็นตัวอย่างการใช้งานแอพพลิเคชันที่ชื่อ GroupByTest ภายใต้แพ็กเกจที่ติดมากับ Spark ซึ่งเราจะสมมุติว่าแอพพลิเคชันนี้ ทำงานอยู่บนโหนด Master โดยมีคำสั่งดังนี้ /* Usage: GroupByTest [numMappers] [numKVPairs] [valSize] [numReducers] */ bin/run-example GroupByTest 100 10000 1000 36 โค้ดที่อยู่ในแอพพลิเคชันมีดังนี้ package org.apache.spark.examples import java.util.Random import org.apache.spark.{SparkConf, SparkContext}
  7. 7. 10/30/2559 BE, 1,18 PMSparkInternals/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. 8. 10/30/2559 BE, 1,18 PMSparkInternals/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) วาดเป็นแผนภาพได้ตามนี้:
  9. 9. 10/30/2559 BE, 1,18 PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals Page 5 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md ข้อควรทราบ data in the partition เป็นส่วนที่เอาไว้แสดงค่าว่าสุดท้ายแล้วแต่ละพาร์ทิชันมีค่ามีค่ายังไง แต่มันไม่ได้ หมายความว่าข้อมูลทุกตัวจะต้องอยู่ในหน่วยความจำในเวลาเดียวกัน ดังนั้นเราขอสรุปดังนี้: ผู้ใช้จะกำหนดค่าเริ่มต้นให้ข้อมูลมีค่าเป็น Array จาก 0 ถึง 99 จากคำสั่ง 0 until numMappers จะได้จำนวน 100 ตัว parallelize() จะสร้าง ParallelCollectionRDD แต่ละพาร์ทิชันก็จะมีจำนวนเต็ม i FlatMappedRDD จะถูกผลิตโดยการเรียกใช้ flatMap ซึ่งเป็นเมธอตการแปลงบน ParallelCollectionRDD จากขั้นตอนก่อนหน้า ซึ่งจะ ให้ FlatMappedRDD ออกมาในลักษณะ Array[(Int, Array[Byte])] หลังจากการกระทำ count() ระบบก็จะทำการนับสมาชิกที่อยู่ในแต่ละพาร์ทิชันของใครของมัน เมื่อนับเสร็จแล้วผลลัพธ์ก็จะถูกส่งกลับไป รวมที่ไดรว์เวอร์เพื่อที่จะได้ผลลัพธ์สุดท้ายออกมา เนื่องจาก FlatMappedRDD ถูกเรียกคำสั่ง Cache เพื่อแคชข้อมูลไว้ในหน่วยความจำ จึงใช้สีเหลืองให้รู้ว่ามีความแตกต่างกันอยู่นะ groupByKey() จะผลิต 2 RDD (ShuffledRDD และ MapPartitionsRDD) เราจะคุยเรื่องนี้กันในบทถัดไป บ่อยครั้งที่เราจะเห็น ShuffleRDD เกิดขึ้นเพราะงานต้องการการสับเปลี่ยน ลักษณะความสัมพันธ์ของตัว ShuffleRDD กับ RDD ที่ให้ กำเนิดมันจะเป็นลักษณะเดียวกันกับ เอาท์พุทของ Mapper ที่สัมพันธ์กับ Input ของ Reducer ใน Hadoop MapPartitionRDD เก็บผลลัพธ์ของ groupByKey() เอาไว้ ค่า Value ของ MapPartitionRDD ( Array[Byte] ) จำถูกแปลงเป็น Iterable ตัวการกระทำ count() ก็จะทำเหมือนกับที่อธิบายไว้ด้านบน เราจะเห็นได้ว่าแผนเชิงตรรกะอธิบายการไหลของข้อมูลในแอพพลิเคชัน: การแปลง (Transformation) จะถูกนำไปใช้กับข้อมูล, RDD ระหว่างทาง (Intermediate RDD) และความขึ้นต่อกันของพวก RDD เหล่านั้น แผนเชิงกายภาพ Physical Plan ในจุดนี้เราจะชี้ให้เห็นว่าแผนเชิงตรรกะ Logical plan นั้นเกี่ยวข้องกับการขึ้นต่อกันของข้อมูลแต่มันไม่ใช่งานจริงหรือ Task ที่เกิดการ ประมวลผลในระบบ ซึ่งจุดนี้ก็เป็นอีกหนึ่งจุดหลักที่ทำให้ Spark ต่างกับ Hadoop, ใน Hadoop ผู้ใช้จะจัดการกับงานที่กระทำในระดับ กายภาพ (Physical task) โดยตรง: Mapper tasks จะถูกนำไปใช้สำหรับดำเนินการ (Operations) บนพาร์ทิชัน จากนั้น Reduce task จะ
  10. 10. 10/30/2559 BE, 1,18 PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals Page 6 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md ทำหน้าที่รวบรวมกลับมา แต่ทั้งนี้เนื่องจากว่าการทำ MapReduce บน Hadoop นั้นเป็นลักษณะที่กำหนดการไหลของข้อมูลไว้ล่วงหน้าแล้วผู้ ใช้แค่เติมส่วนของฟังก์ชัน map() และ reduce() ในขณะที่ Spark นั้นค่อนข้างยืดหยุ่นและซับซ้อนมากกว่า ดังนั้นมันจึงยากที่จะรวม แนวคิดเรื่องความขึ้นต่อกันของข้อมูลและงานทางกายภาพเข้าไว้ด้วยกัน เหตุผลก็คือ Spark แยกการไหลของข้อมูลและงานที่จะถูกประมวล ผลจริง, และอัลกอริทึมของการแปลงจาก Logical plan ไป Physical plan ซึ่งเราจะคุยเรื่องนี้ันต่อในบทถัดๆไป ยกตัวอย่างเราสามารถเขียนแผนเชิงกายภาพของ DAG ดังนี้: เราจะเห็นว่าแอพพลิเคชัน GroupByTest สามารถสร้างงาน 2 งาน งานแรกจะถูกกระตุ้นให้ทำงานโดยคำสั่ง pairs1.count() มาดูราย ละเอียดของ Job นี้กัน: Job จะมีเพียง Stage เดียว (เดี๋ยวเราก็จะคุยเรื่องของ Stage กันทีหลัง) Stage 0 มี 100 ResultTask แต่ละ Task จะประมวลผล flatMap ซึ่งจะสร้าง FlatMappedRDD แล้วจะทำ count() เพื่อนับจำนวนสมาชิกในแต่ละพาร์ทิชัน ยก ตัวอย่างในพาร์ทิชันที่ 99 มันมีแค่ 9 เรคอร์ด เนื่องจาก pairs1 ถูกสั่งให้แคชดังนั้น Tasks จะถูกแคชในรูปแบบพาร์ทิชันของ FlatMappedRDD ภายในหน่วยความจำของตัว Executor หลังจากที่ Task ถูกทำงานแล้วไดรว์เวอร์จะเก็บผลลัพธ์มันกลับมาเพื่อบวกหาผลลัพธ์สุดท้าย Job 0 ประมวลผลเสร็จเรียบร้อย ส่วน Job ที่สองจะถูกกระตุ้นให้ทำงานโดยการกระทำ pairs1.groupByKey(numReducers).count : มี 2 Stage ใน Job Stage 0 จะมี 100 ShuffleMapTask แต่ละ Task จะอ่านส่วนของ paris1 จากแคชแล้วพาร์ทิชันมันแล้วก็เขียนผลลัพธ์ของพาร์ทิชัน ไปยังโลคอลดิสก์ ยกตัวอย่าง Task ที่มีเรคอร์ดลักษณะคีย์เดียวกันเช่น Key 1 จาก Value เป็น Byte ก็จะกลายเป็นตระกร้าของ Key 1 เช่น (1, Array(...)) จากนั้นก็ค่อยเก็บลงดิสก์ ซึ่งขั้นตอนนี้ก็จะคล้ายกับการพาร์ทิชันของ Mapper ใน Hadoop Stage 1 มี 36 ResultTask แต่ละ Task ก็จะดึงและสับเปลี่ยนข้อมูลที่ต้องการจะประมวลผล ในขณะที่อยู่ขั้นตอนของการดึงข้อมูลและ
  11. 11. 10/30/2559 BE, 1,18 PMSparkInternals/1-Overview.md at thai · Aorjoa/SparkInternals Page 7 of 7https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/1-Overview.md ทำงานคำสั่ง mapPartitions() ก็จะเกิดการรวบรวมข้อมูลไปด้วย ถัดจากนั้น count() จะถูกเรียกใช้เพื่อให้ได้ผลลัพธ์ ตัวอย่างเช่น สำหรับ ResultTask ที่รับผิดชอบกระกร้าของ 3 ก็จะดึงข้อมูลทุกตัวที่มี Key 3 จาก Worker เข้ามารวมไว้ด้วยกันจากนั้นก็จะรวบรวม ภายในโหนดของตัวเอง หลังจากที่ Task ถูกประมวลผลไปแล้วตัวไดรว์เวอร์ก็จะรวบรวมผลลัพธ์กลับมาแล้วหาผลรวมที่ได้จาก Task ทั้งหมด Job 1 เสร็จเรียบร้อย เราจะเห็นว่า Physical plan มันไม่ง่าย ยิ่ง Spark สามารถมี Job ได้หลาย Job แถมแต่ละ Job ยังมี Stage ได้หลาย Stage pังไม่พอแต่ละ Stage ยังมีได่้หลาย Tasks หลังจากนี้เราจะคุยกันว่าทำไมต้องกำหนด Job และต้องมี Stage กับ Task เข้ามาให้วุ่นวายอีก การพูดคุย โอเค ตอนนี้เรามีความรู้เบื้อตั้งเกี่ยวกับ Job ของ Spark ทั้งการสร้่างและการทำงานแล้ว ต่อไปเราจะมีการพูดคุยถึงเรื่องการแคชของ Spark ด้วย ในหัวข้อต่อไปจะคุยกันถึงรายละเอียดในเรื่อง Job ดังนี้: 1. การสร้าง Logical plan 2. การสร้าง Physical plan 3. การส่ง Job และ Scheduling 4. การสร้าง การทำงานและการจัดการกับผลลัพธ์ของ Task 5. การเรียงสับเปลี่ยนของ Spark 6. กลไกของแคช 7. กลไก Broadcast Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help
  12. 12. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 1 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md This repository Pull requests Issues Gist SparkInternals / markdown / thai / 2-JobLogicalPlan.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 42 minutes agoAorjoa fixed some typo and polish some word 290 lines (182 sloc) 42.5 KB Job Logical Plan ตัวย่างการสร้าง Logical plan แผนภาพด้านบนอธิบายให้เห็นว่าขั้นตอนของแผนมีอยู่ 4 ขั้นตอนเพื่อให้ได้ผลลัพธ์สุดท้ายออกมา 1. เริ่มต้นสร้าง RDD จากแหล่งไหนก็ได้ (in-memory data, ไฟล์บนเครื่อง, HDFS, HBase, etc). (ข้อควรทราบ parallelize() มี ความหมายเดียวกับ createRDD() ที่เคยกล่าวถึงในบนที่แล้ว) 2. ซีรีย์ของ transformation operations หรือการกระทำการแปลงบน RDD แสดงโดย transformation() แต่ละ transformation() จะสร้าง RDD[T] ตั้งแต่ 1 ตัวขึ้นไป โดยที่ T สามารถเป็นตัวแปรประเภทไหนของ Scala ก็ได้ (ถ้าเขียนใน Scala) ข้อควรทราบ สำหรับคู่ Key/Value ลักษณะ RDD[(K, V)] นั้นจะจัดการง่ายกว่าถ้า K เป็นตัวแปรประเภทพื้นฐาน เช่น Int , Double , String เป็นต้น แต่มันไม่สามารถทำได้ถ้ามันเป็นตัวแปรชนิด Collection เช่น Array หรือ List เพราะ กำหนดการพาร์ทิชันได้ยากในขั้นตอนการสร้างพาร์ทิชันฟังก์ชันของตัวแปรที่เป็นพวก Collection 3. Action operation แสดงโดย action() จะเรียกใช้ที่ RDD ตัวสุดท้าย จากนั้นในแต่ละพาร์ทิชันก็จะสร้างผลลัพธ์ออกมา 4. ผลลัพธ์เหล่านี้จะถูกส่งไปที่ไดรว์เวอร์จากนั้น f(List[Result]) จะคำนวณผลลัพธ์สุดท้ายที่จะถูกส่งกลับไปบอกไคลเอนท์ ตัวอย่าง เช่น count() จะเกิด 2 ขั้นตอนคำ action() และ sum() Raw Blame History 0 7581Unwatch Star Fork
  13. 13. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 2 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md RDD สามารถที่จะแคชและเก็บไว้ในหน่วยความจำหรือดิสก์โดยเรียกใช้ cache() , persist() หรือ checkpoint() จำนวนพาร์ ทิชันโดยปกติถูกกำหนดโดยผู้ใช้ ความสัมพธ์ของการพาร์ทิชันระหว่าง 2 RDD ไม่สามารถเป็นแบบ 1 to 1 ได้ และในตัวอย่างด้านบน เราจะเห็นว่าไม่ใช้แค่ความสัมพันธ์แบบ 1 to 1 แต่เป็น Many to Many ด้วย แผนเชิงตรรกะ Logical Plan ตอนที่เขียนโค้ดของ Spark คุณก็จะต้องมีแผนภาพการขึ้นต่อกันอยู่ในหัวแล้ว (เหมือนตัวอย่างที่อยู่ข้างบน) แต่ไอ้แผนที่วางไว้มันจะเป็นจริงก็ ต่อเมื่อ RDD ถูกทำงานจริง (มีคำสั่งmี่เป็นการกระทำ Action) เพื่อที่จะให้เข้าใจชัดเจนยิ่งขึ้นเราจะคุยกันในเรื่องของ จะสร้าง RDD ได้ยังไง ? RDD แบบไหนถึงจะประมวลผล ? จะสร้างความสัมพันธ์ของการขึ้นต่อกันของ RDD ได้อย่างไร ? 1. จะสร้าง RDD ได้ยังไง ? RDD แบบไหนถึงจะประมวลผล ? คำสั่งพวก transformation() โดยปกติจะคืนค่าเป็น RDD แต่เนื่องจาก transformation() นั้นมีการแปลงที่ซับซ้อนทำให้มี sub- transformation() หรือการแปลงย่อยๆเกิดขึ้นนั่นทำให้เกิด RDD หลายตัวขึ้นได้จึงเป็นเหตุผลที่ว่าทำไม RDD ถึงมีเยอะขึ้นได้ ซึ่ง ในความเป็นจริงแล้วมันเยอะมากกว่าที่เราคิดซะอีก Logical plan นั้นเป็นสิ่งจำเป็นสำหรับ Computing chain ทุกๆ RDD จะมี compute() ซึ่งจะรับเรคอร์ดข้อมูลจาก RDD ก่อนหน้าหรือ จากแหล่งข้อมูลมา จากนั้นจะแปลงตาม transformation() ที่กำหนดแล้วให้ผลลัพธ์ส่งออกมาเป็นเรคอร์ดที่ถูกประมวลผลแล้ว คำถามต่อมาคือแล้ว RDD อะไรที่ถูกประมวลผล? คำตอบก็ขึ้นอยู่กับว่าประมวลผลด้วยตรรกะอะไร transformation() และเป็น RDD อะไรที่ รับไปประมวลผลได้ เราสามารถเรียนรู้เกี่ยวกับความหมาบของแต่ละ transformation() ได้บนเว็บของ Spark ส่วนรายละเอียดที่แสดงในตารางด้านล่างนี้ ยกมาเพื่อเพิ่มรายละเอียด เมื่อ iterator(split) หมายถึง สำหรับทุกๆเรคอร์ดในพาร์ทิชัน ช่องที่ว่างในตารางเป็นเพราะความซับซ้อน ของ transformation() ทำให้ได้ RDD หลายตัวออกมา เดี๋ยวจะได้แสดงต่อไปเร็วๆนี้ Transformation Generated RDDs Compute() map(func) MappedRDD iterator(split).map(f) filter(func) FilteredRDD iterator(split).filter(f) flatMap(func) FlatMappedRDD iterator(split).flatMap(f) mapPartitions(func) MapPartitionsRDD f(iterator(split)) mapPartitionsWithIndex(func) MapPartitionsRDD f(split.index, iterator(split)) sample(withReplacement, fraction, seed) PartitionwiseSampledRDD PoissonSampler.sample(iterator(split)) BernoulliSampler.sample(iterator(split)) pipe(command, [envVars]) PipedRDD union(otherDataset) intersection(otherDataset) distinct([numTasks])) groupByKey([numTasks]) reduceByKey(func, [numTasks]) sortByKey([ascending], [numTasks]) join(otherDataset, [numTasks]) cogroup(otherDataset,
  14. 14. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 3 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md [numTasks]) cartesian(otherDataset) coalesce(numPartitions) repartition(numPartitions) 2. จะสร้างความสัมพันธ์ของการขึ้นต่อกันของ RDD ได้อย่างไร ? เราอยากจะชี้ให้เห็นถึงสิ่งต่อไปนี้: การขึ้นต่อกันของ RDD, RDD x สามารถขึ้นต่อ RDD พ่อแม่ได้ 1 หรือหลายตัว ? มีพาร์ทิชันอยู่เท่าไหร่ใน RDD x ? อะไรที่เป็นความสัมพันธ์ระหว่างพาร์ทิชันของ RDD x กับพวกพ่อแม่ของมัน? 1 พาร์ทิชันขึ้นกับ 1 หรือหลายพาร์ทิชันกันแน่ ? คำถามแรกนั้นจิ๊บจ้อยมาก ยกตัวอย่างเช่น x = rdda.transformation(rddb) , e.g., val x = a.join(b) หมายความว่า RDD x ขึ้นต่อ RDD a และ RDD b (แหงหล่ะเพราะ x เกิดจากการรวมกันของ a และ b นิ) สำหรับคำถามที่สอง อย่างที่ได้บอกไว้ก่อนหน้านี้แล้วว่าจำนวนของพาร์ทิชันนั้นถูกกำหนดโดยผู้ใช้ ค่าเริ่มต้นของมันก็คือมันจะเอาจำนวนพาร์ ทิชันที่มากที่สุดของพ่อแม่มันมา) max(numPartitions[parent RDD 1], ..., numPartitions[parent RDD n]) คำถามสุดท้ายซับซ้อนขึ้นมาหน่อย เราต้องรู้ความหมายของการแปลง transformation() ซะก่อน เนื่องจาก transformation() แต่ละตัวมีการขึ้นต่อกันที่แตกต่างกันออกไป ยกตัวอย่าง map() เป็น 1[1 ในขณะที่ groupByKey() ก่อให้เกิด ShuffledRDD ซึ่งในแต่ละพาร์ทิชันก็จะขึ้นต่อทุกพาร์ทิชันที่เป็นพ่อแม่ของมัน นอกจากนี้บาง transformation() ยังซับซ้อนขึ้นไปกว่านี้ อีก ใน Spark จะมีการขึ้นต่อกันอยู่ 2 แบบ ซึ่งกำหนดในรูปของพาร์ทิชันของพ่อแม่: NarrowDependency (OneToOneDependency, RangeDependency) แต่ละพาร์ทิชันของ RDD ลูกจะขึ้นอยู่กับพาร์ทิชันของแม่ไม่กี่ตัว เช่น พาร์ทิชันของลูกขึ้นต่อ ทั่วทั้ง พาร์ทิชันของพ่อแม่ (full dependency) ShuffleDependency (หรือ Wide dependency, กล่าวถึงในเปเปอร์ของ Matei) พาร์ทิชันลูกหลายส่วนขึ้นกับพาร์ทิชันพ่อแม่ เช่นในกรณีที่แต่ละพาร์ทิชันของลูกขึ้นกับ บางส่วน ขอวพาร์ทิชันพ่อแม่ (partial dependency) ยกตัวอย่าง map จะทำให้เกิด Narrow dependency ขณะที่ join จะทำให้เกิด Wide dependency (เว้นแต่ว่าในกรณีของพ่อแม่ที่เอา มา join กันทั้งคู่เป็น Hash-partitioned) ในอีกนัยหนึ่งแต่ละพาร์ทิชันของลูกสามารถขึ้นต่อพาร์ทิชันพ่อแม่ตัวเดียว หรือขึ้นต่อบางพาร์ทิชันของพ่อแม่เท่านั้น ข้อควรรู้: สำหรับ NarrowDependency จะรู้ว่าพาร์ทิชันลูกต้องการพาร์ทิชันพ่อแม่หนึ่งหรือหลายตัวมันขึ้นอยู่กับฟังก์ชัน getParents(partition i) ใน RDD ตัวลูก (รายละเอียดเดี๋ยวจะตามมาทีหลัง) ShuffleDependency คล้ายกัย Shuffle dependency ใน MapReduce [ผู้แปล:น่าจะหมายถึงเปเปอร์ของ Google] ตัว Mapper จะทำพาร์ทิชันเอาท์พุท, จากนั้นแต่ละ Reducer จะดึงพาร์ทิชันที่มันจำเป็นต้องใช้จากพาร์ทิชันที่เป็นเอาท์พุทจาก Mapper ผ่านทาง http.fetch) ความขึ้นต่อกันทั้งสองแสดงได้ตามแผนภาพข้างล่าง.
  15. 15. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 4 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md ตามที่ให้คำจำกัดความไปแล้ว เราจะเห็นว่าสองภาพที่อยู่แถวบนเป็น NarrowDependency และภาพสุดท้ายจะเป็น ShuffleDependency . แล้วภาพล่างซ้ายหล่ะ? กรณีนี้เป็นกรณีที่เกิดขึ้นได้น้อยระหว่างสอง RDD มันคือ NarrowDependency (N:N) Logical plan ของมันจะ คล้ายกับ ShuffleDependency แต่มันเป็น Full dependency มันสามารถสร้างขึ้นได้ด้วยเทคนิคบางอย่างแต่เราจะไม่คุยกันเรื่องนี้เพราะ NarrowDependency เข้มงวดมากเรื่องความหมายของมันคือ แต่ละพาร์ทิชันของพ่อแม่ถูกใช้โดยพาร์ทิชันของ RDD ลูกได้อย่างมากพาร์ ทิชันเดียว บางแบบของการขึ้นต่อกันของ RDD จะถูกเอามาคุยกันเร็วๆนี้ โดยสรุปคร่าวๆ พาร์ทิชันขึ้นต่อกันตามรายการด้านล่างนี้ NarrowDependency (ลูกศรสีดำ) RangeDependency -> เฉพาะกับ UnionRDD OneToOneDependency (1[1) -> พวก map, filter NarrowDependency (N[1) -> พวก join co-partitioned NarrowDependency (N:N) -> เคสหายาก ShuffleDependency (ลูกศรสีแดง) โปรดทราบว่าในส่วนที่เหลือของบทนี้ NarrowDependency จะถูกแทนด้วยลูกศรสีดำและ ShuffleDependency จะแทนด้วยลูกษรสีแดง NarrowDependency และ ShuffleDependency จำเป็นสำหรับ Physical plan ซึ่งเราจะคุยกันในบทถัดไป เราจะประมวลผลเรคอร์ดของ RDD x ได้ยังไง กรณี OneToOneDependency จะถูกแสดงในภาพด้านล่าง ถึงแม้ว่ามันจะเป็นความสัมพันธ์แบบ 1 ต่อ 1 ของสองพาร์ทิชันแต่นั้นก็ไม่ได้หมาย ถึงเรคอร์ดจะถูกประมวลผลแบบหนึ่งต่อหนึ่ง ความแตกต่างระหว่างสองรูปแบบของสองฝั่งนี้จะเหมือนโค้ดที่แสดงสองชุดข้างล่าง
  16. 16. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 5 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md code1 of iter.f() เป็นลักษณะของการวนเรคอร์ดทำฟังก์ชัน f int[] array = {1, 2, 3, 4, 5} for(int i = 0; i < array.length; i++) f(array[i]) code2 of f(iter) เป็นลักษณะการส่งข้อมูลทั้งหมดไปให้ฟังก์ชัน f ทำงานเลย int[] array = {1, 2, 3, 4, 5} f(array) 3. ภาพอธิบายประเภทการขึ้นต่อกันของการคำนวณ 1) union(otherRDD) union() เป็นการรวมกัยง่ายๆ ระหว่างสอง RDD เข้าไว้ด้วยกันโดยไม่เปลี่ยนพาร์ทิชันของข้อมูล RangeDependency (1[1) ยังคงรักษา ขอบของ RDD ดั้งเดิมไว้เพื่อที่จะยังคงความง่ายในการเข้าถึงพาร์ทิชันจาก RDD ที่ถูกสร้างจากฟังก์ชัน union()
  17. 17. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 6 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md 2) groupByKey(numPartitions) [เปลี่ยนใน Spark 1.3] เราเคยคุยกันถึงการขึ้นต่อกันของ groupByKey มาก่อนหน้านี้แล้ว ตอนนี้เราจะมาทำให้มันชัดเจนขึ้น groupByKey() จะรวมเรคอร์ดที่มีค่า Key ตรงกันเข้าไว้ด้วยกันโดยใช้วิธีสับเปลี่ยนหรือ Shuffle ฟังก์ชัน compute() ใน ShuffledRDD จะดึงข้อมูลที่จำเป็นสำหรับพาร์ทิชันของมัน จากนั้นทำคำสั่ง mapPartition() (เหมือน OneToOneDependency ), MapPartitionsRDD จะถูกผลิตจาก aggregate() สุดท้ายแล้วชนิดข้อมูลของ Value คือ ArrayBuffer จะถูก Cast เป็น Iterable groupByKey() จะไม่มีการ Combine เพื่อรวมผลลัพธ์ในฝั่งของ Map เพราะการรวมกันมาจากฝั่งนั้นไม้่ได้ทำให้จำนวนข้อมูลที่ Shuffle ลดลงแถมยังต้องการให้ฝั่ง Map เพิ่มข้อมูลลงใน Hash table ทำให้เกิด Object ที่เก่าแล้วเกิดขึ้นในระบบมากขึ้น ArrayBuffer จะถูกใช้เป็นหลัก ส่วน CompactBuffer มันเป็นบัฟเฟอร์แบบเพิ่มได้อย่างเดียวซึ่งคล้ายกับ ArrayBuffer แต่จะใช้ หน่วยความจำได้มีประสิทธิภาพมากกว่าสำหรับบัฟเฟอร์ขนาดเล็ก (ตในส่วนนี้โค้ดมันอธีบายว่า ArrayBuffer ต้องสร้าง Object ของ Array ซึ่งเสียโอเวอร์เฮดราว 80-100 ไบต์ 3) reduceyByKey(func, numPartitions) [เปลี่ยนแปลงในรุ่น 1.3]
  18. 18. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 7 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md reduceByKey() คล้ายกับ MapReduce เนื่องจากการไหลของข้อมูลมันเป็นไปในรูปแบบเดียวกัน redcuceByKey อนุญาตให้ฝั่งที่ทำ หน้าที่ Map ควบรวม (Combine) ค่า Key เข้าด้วยกันเป็นค่าเริ่มต้นของคำสั่ง และคำสั่งนี้กำเนินการโดย mapPartitions ก่อนจะสับ เปลี่ยนและให้ผลลัพธ์มาในรูปของ MapPartitionsRDD หลังจากที่สับเปลี่ยนหรือ Shuffle แล้วฟังก์ชัน aggregate + mapPartitions จะถูกนำไปใช้กับ ShuffledRDD อีกครั้งแล้วเราก็จะได้ MapPartitionsRDD 4) distinct(numPartitions)
  19. 19. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 8 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md distinct() สร้างขึ้นมาเพื่อที่จะลดความซ้ำซ้อนกันของเรคอร์ดใน RDD เนื่องจากเรคอร์ดที่ซ้ำกันสามารถเกิดได้ในพาร์ทิชันที่ต่างกัน กลไกการ Shuffle จำเป็นต้องใช้ในการลดความซ้ำซ้อนนี้โดยการใช้ฟังก์ชัน aggregate() อย่างไรก็ตามกลไก Shuffle ต้องการ RDD ในลักษณะ RDD[(K, V)] ซึ่งถ้าเรคอร์ดมีแค่ค่า Key เช่น RDD[Int] ก็จะต้องทำให้มันอยู่ในรูปของ <K, null> โดยการ map() ( MappedRDD ) หลังจากนั้น reduceByKey() จะถูกใช้ในบาง Shuffle (mapSideCombine->reduce->MapPartitionsRDD) ท้ายสุด แล้วจะมีแค่ค่า Key ทีถูกยิบไปจากคุ่ โดยใช้ map() ( MappedRDD ). ReduceByKey() RDDs จะใช้สีน้ำเงิน (ดูรูปจะเข้าใจมากขึ้น) 5) cogroup(otherRDD, numPartitions)
  20. 20. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 9 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md มันแตกต่างจาก groupByKey() , ตัว cogroup() นี้จะรวม RDD ตั้งแต่ 2 หรือมากกว่าเข้ามาไว้ด้วยกัน อะไรคือความสัมพันธ์ระหว่าง GoGroupedRDD และ (RDD a, RDD b)? ShuffleDependency หรือ OneToOneDependency จำนวนของพาร์ทิชัน จำนวนของพาร์ทิชันใน CoGroupedRDD จะถูกำหนดโดยผูใช้ซึ่งมันจะไม่เกี่ยวกับ RDD a และ RDD b เลย อย่างไรก็ดีถ้าจำนวนพาร์ ทิชันของ CoGroupedRDD แตกต่างกับตัว RDD a/b มันก็จะไม่เป็น OneToOneDependency ชนิดของตังแบ่งพาร์ทิชัน ตังแบ่งพาร์ทิชันหรือ partitioner จะถูกกำหนดโดยผู้ใช้ (ถ้าผู้ใช้ไม่ตั้งค่าจะมีค่าเริ่มต้นคือ HashPartitioner ) สำหรับ cogroup() แล้วมันเอาไว้พิจารณาว่าจะวางผลลัพธ์ของ cogroup ไว้ตรงไหน ถึงแม้ว่า RDD a/b และ CoGroupedRDD จะมีจำนวน ของพาร์ทิชันเท่ากัน ในขณะที่ตัวแบ่งพาร์ทิชันต่างกัน มันก็ไม่สามารถเป็น OneToOneDependency ได้. ดูได้จากภรูปข้างบนจะเห็นว่า RDD a มีตัวแบ่งพาร์ทิชันเป็นชนิด RangePartitioner , ส่วน RDD b มีตัวแบ่งพาร์ทิชันเป็นชนิด HashPartitioner , และ CoGroupedRDD มีตัวแบ่งพาร์ทิชันเป็นชนิด RangePartitioner โดยที่จำนวนพาร์ทิชันมันเท่ากับจำนวนพาร์ทิชันของ RDD a .
  21. 21. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 10 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md หากสังเกตจะพบได้อย่างชัดเจนว่าเรคอร์ดในแต่ละพาร์ทิชันของ RDD a สามารถส่งไปที่พาร์ทิชันของ CoGroupedRDD ได้เลย แต่ สำหรับ RDD b จำถูกแบ่งออกจากกัน (เช่นกรณีพาร์ทิชันแรกของ RDD b ถูกแบ่งออกจากกัน) แล้วสับเปลี่ยนไปไว้ในพาร์ทิชันที่ถูก ต้องของ CoGroupedRDD โดยสรุปแล้ว OneToOneDependency จะเกิดขึ้นก็ต่อเมื่อชนิดของตัวแบ่งพาร์ทิชันและจำนวนพาร์ทิชันของ 2 RDD และ CoGroupedRDD เท่ากัน ไม่อย่างนั้นแล้วมันจะต้องเกิดกระบวนการ ShuffleDependency ขึ้น สำหรับรายละเอียดเชิงลึกหาอ่านได้ที่โค้ดในส่วนของ CoGroupedRDD.getDependencies() Spark มีวิธีจัดการกับความจริงเกี่ยวกับ CoGroupedRDD ที่พาร์ทิชันมีการขึ้นต่อกันบนหลายพาร์ทิชันของพ่อแม่ได้อย่างไร อันดับแรกเลย CoGroupedRDD จะวาง RDD ที่จำเป็นให้อยู่ในรูปของ rdds: Array[RDD] จากนั้น, Foreach rdd = rdds(i): if CoGroupedRDD and rdds(i) are OneToOneDependency Dependecy[i] = new OneToOneDependency(rdd) else Dependecy[i] = new ShuffleDependency(rdd) สุดท้ายแล้วจำคืน deps: Array[Dependency] ออกมา ซึ่งเป็น Array ของการขึ้นต่อกัน Dependency ที่เกี่ยวข้องกับแต่และ RDD พ่อ แม่ Dependency.getParents(partition id) คืนค่า partitions: List[Int] ออกมาซึ่งคือพาร์ทิชันที่จำเป็นเพื่อสร้างพาร์ทิชันไอดีนี้ ( partition id ) ใน Dependency ที่กำหนดให้ getPartitions() เอาไว้บอกว่ามีพาร์ทิชันใน RDD อลู่เท่าไหร่และบอกว่าแต่ละพาร์ทิชัน serialized ไว้อย่างไร 6) intersection(otherRDD)
  22. 22. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 11 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md intersection() ตั้งใจให้กระจายสมาชิกทุกตัวของ RDD a และ b . ตัว RDD[T] จะถูก Map ให้อยู่ในรูป RDD[(T, null)] (T เป็น ชนิดของตัวแปร ไม่สามารถเป็น Collection เช่นพวก Array, List ได้) จากนั้น a.cogroup(b) (แสดงด้วยสำน้ำเงิน). filter() เอา เฉพาะ [iter(groupA()), iter(groupB())] ไม่มีตัวไหนเป็นค่าว่าง ( FilteredRDD ) สุดท้ายแล้วมีแค่ keys() ที่จะถูกเก็บไว้ ( MappedRDD ) 7)join(otherRDD, numPartitions)
  23. 23. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 12 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md join() รับ RDD[(K, V)] มา 2 ตัว, คล้ายๆกับการ join ใน SQL. และคล้ายกับคำสั่ง intersection() , มันใช้ cogroup() ก่อนจากนั้นให้ผลลัพธ์เป็น MappedValuesRDD ชนิดของพวกมันคือ RDD[(K, (Iterable[V1], Iterable[V2]))] จากนั้นหาผลคูณ
  24. 24. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 13 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md คาร์ทีเซียน Cartesian product ระหว่างสอง Iterable , ขั้นตอนสุดท้ายเรียกใช้ฟังก์ชัน flatMap() . นี่เป็นตัอย่างสองตัวอย่างของ join กรณีแรก, RDD 1 กับ RDD 2 ใช้ RangePartitioner ส่วน CoGroupedRDD ใช้ HashPartitioner ซึ่งแตกต่างกับ RDD 1/2 ดังนั้นมันจึงมีการเรียกใช้ ShuffleDependency . ในกรณีที่สอง, RDD 1 ใช้ตัวแบ่งพาร์ ทิชันบน Key ชนิด HashPartitioner จากนั้นได้รับ 3 พาร์ทิชันซึ่งเหมือนกับใน CoGroupedRDD แป๊ะเลย ดังนั้นมันเลยเป็น OneToOneDependency นอกจากนั้นแล้วถ้า RDD 2 ก็ถูกแบ่งโดนตัวแบ่งแบบเดียวกันคือ HashPartitioner(3) แล้วจะไม่เกิด ShuffleDependency ขึ้น ดังนั้นการ join ประเภทนี้สามารถเรียก hashjoin() 8) sortByKey(ascending, numPartitions) sortByKey() จะเรียงลำดับเรคอร์ดของ RDD[(K, V)] โดยใช้ค่า Key จากน้อยไปมาก ascending ถูกกำหนดใช้โดยตัวแปร Boolean เป็นจริง ถ้ามากไปน้อยเป็นเท็จ. ผลลัพธ์จากขั้นนี้จะเป็น ShuffledRDD ซึ่งมีตัวแบ่งชนิด rangePartitioner ตัวแบ่งชนิดของ พาร์ทิชันจะเป็นตัวกำหนดขอบเขตของแต่ละพาร์ทิชัน เช่น พาร์ทิชันแรกจะมีแค่เรคอร์ด Key เป็น char A to char B และพาร์ทิชันที่สองมี เฉพาะ char C ถึง char D ในแต่ละพาร์ทิชันเรคอร์ดจะเรียงลำดับตาม Key สุดท้ายแล้วจะได้เรคร์ดมาในรูปของ MapPartitionsRDD ตามลำดับ sortByKey() ใช้ Array ในการเก็บเรคอร์ดของแต่ละพาร์ทิชันจากนั้นค่อยเรียงลำดับ 9) cartesian(otherRDD)
  25. 25. 10/30/2559 BE, 1,19 PMSparkInternals/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. 26. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 15 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md
  27. 27. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 16 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md coalesce() สามารถใช้ปรับปรุงการแบ่งพาร์ทิชัน อย่างเช่น ลดจำนวนของพาร์ทิชันจาก 5 เป็น 3 หรือเพิ่มจำนวนจาก 5 เป็น 10 แต่ต้อง ขอแจ้งให้ทราบว่าถ้า shuffle = false เราไม่สามารถที่จะเพิ่มจำนวนของพาร์ทิชันได้ เนื่องจากมันจะถูกบังคับให้ Shuffle ซึ่งเราไม่ อยากให้มันเกิดขึ้นเพราะมันไม่มีความหมายอะไรเลยกับงาน เพื่อทำความเข้าใจ coalesce() เราจำเป็นต้องรู้จักกับ ความสัมพันธ์ระหว่างพาร์ทิชันของ CoalescedRDD กับพาร์ทิชันพ่อแม่ coalesce(shuffle = false) ในกรณีที่ Shuffle ถูกปิดใช้งาน สิ่งที่เราต้องทำก็แค่จัดกลุ่มบางพาร์ทิชันของพ่อแม่ และในความ เป็นจริงแล้วมันมีตัวแปรหลายตัวที่จะถูกนำมาพิจารณา เช่น จำนวนเรคอร์ดในพาร์ทิชัน, Locality และบาลานซ์ เป็นต้น ซึ่ง Spark ค่อน ข้างจะมีขั้นตอนวิธีที่ซับซ้อนในการทำ (เดี๋ยวเราจะคุยกันถึงเรื่องนี้) ยกตัวอย่าง a.coalesce(3, shuffle = false) โดยทั่วไป แล้วจะเป็น NarrowDependency ของ N[1. coalesce(shuffle = true) ถ้าหากเปิดใช้งาน Shuffle ฟังก์ชัน coalesce จะแบ่งทุกๆเรคอร์ดของ RDD ออกจากกันแบบง่าย เป็น N ส่วนซึ่งสามารถใช้ทริกคล้ายๆกับ Round-robin ทำได้: สำหรับแต่ละพาร์ทิชันทุกๆเรคอร์ดที่อยู่ในนั้นจะได้รับ Key ซึ่งจะเป็นเลขที่เพิ่มขึ้นในแต่ละเรคอร์ด (จำนวนที่นับเพิ่มเรื่อย) hash(key) ทำให้เกิดรูปแบบเดียวกันคือกระจายตัวอยู่ทุกๆพาร์ทิชันอย่างสม่ำเสมอ ในตัวอย่างที่สอง สมาชิกทุกๆตัวใน RDD a จะถูกรวมโดยค่า Key ที่เพิ่มขึ้นเรื่อยๆ ค่า Key ของสมาชิกตัวแรกในพาร์ทิชันคือ (new Random(index)).nextInt(numPartitions) เมื่อ index คืออินเด็กซ์ของพาร์ทิชันและ numPartitions คือจำนวนของพาร์ทิชั นใน CoalescedRDD ค่าคีย์ต่อมาจะเพิ่มขึ้นทีละ 1 หลังจาก Shuffle แล้วเรคอร์ดใน ShffledRDD จะมีรูปแบบการจะจายเหมือนกัน ความสัมพันธ์ระหว่าง ShuffledRDD และ CoalescedRDD จะถูกกำหนดโดยความซับข้อนของขั้นตอนวิธี ในตอนสุดท้าย Key เหล่า นั้นจะถูกลบออก ( MappedRDD ). 11) repartition(numPartitions) มีความหมายเท่ากับ coalesce(numPartitions, shuffle = true) Primitive transformation() combineByKey() ก่อนหน้านี้เราได้เห็น Logical plan ซึ่งบางตัวมีลักษณะคล้ายกันมาก เหตุผลก็คือขึ้นอยู่กับการนำไปใช้งานตามความเหมาะสมฃ อย่างที่เรารู้ RDD ที่อยู่ฝั่งซ้ายมือของ ShuffleDependency จะเป็น RDD[(K, V)] , ในขณะที่ทางฝั่งขวามือทุกเรคอร์ดที่มี Key เดียวกัน จะถูกรวมเข้าด้วยกัน จากนั้นจะดำเนินการอย่างไรต่อก็ขึ้นอยู่กับว่าผู้ใช้สั่งอะไรโดยที่มันก็จะทำกับเรคอร์ดที่ถูกรวบรวมไว้แล้วนั่นแหละ ในความเป็นจริงแล้ว transformation() หลายตัว เช่น groupByKey() , reduceBykey() , ยกเว้น aggregate() ขณะที่มีการ คำนวณเชิงตรรกะ ดูเหมือนกับว่า aggregate() และ compute() มันทำงานในเวลาเดียวกัน Spark มี combineByKey() เพื่อใช้การ ดำเนินการ aggregate() + compute() และนี่ก็คือก็คำนิยามของ combineByKey() def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C, partitioner: Partitioner, mapSideCombine: Boolean = true, serializer: Serializer = null): RDD[(K, C)] มี 3 พารามิเตอร์สำคัญที่เราต้องพูดถึงก็คือ: createCombiner , ซึ่งจะเปลี่ยนจาก V ไปเป็น C (เช่น การสร้าง List ที่มีสมาชิกแค่ตัวเดียว) mergeValue , เพื่อรวม V เข้าใน C (เช่น การเพิ่มข้อมูลเข้าไปที่ท้าย List) mergeCombiners , เพื่อจะรวมรวม C เป็นค่าเดียว รายละเอียด: เมื่อมีบางค่า Key/Value เป็นเรคอร์ด (K, V) ถูกสั่งให้ทำ combineByKey() , createCombiner จะเอาเรคอร์ดแรกออกมเพื่อเริ่มต้น ตัวรวบรวม Combiner ชนิด C (เช่น C = V).
  28. 28. 10/30/2559 BE, 1,19 PMSparkInternals/2-JobLogicalPlan.md at thai · Aorjoa/SparkInternals Page 17 of 17https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/2-JobLogicalPlan.md จากนั้น, mergeValue จะทำงานกับทุกๆเรคอร์ดที่เข้ามา mergeValue(combiner, record.value) จำถูกทำงานเพื่ออัพเดท Combiner ดูตัวอย่างการ sum เพื่อให้เข้าใจขึ้น combiner = combiner + recoder.value สุดท้ายแล้วทุกเรคอร์ดจะถูกรวมเข้า กับ Combiner ถ้ามีเซ็ตของเรคอร์ดอื่นเข้ามาซึ่งมีค่า Key เดียวกับค่าที่กล่าวไปด้านบน combineByKey() จะสร้าง combiner' ตัวอื่น ในขั้นตอน สุดท้ายจะได้ผลลัพธ์สุดท้ายที่มีค่าเท่ากับ mergeCombiners(combiner, combiner') . การพูดคุย ก่อนหน้านี้เราได้พูดคุยกันถึงการสร้าง Job ที่เป็น Logical plan, ความซับซ้อนของการขึ้นต่อกันและการคำนวณเบื้องหลัง Spark transformation() จะรู้ว่าต้องสร้าง RDD อะไรออกมา บาง transformation() ก็ถูกใช้ซ้ำโดยบางคำสั่งเช่น cogroup การขึ้นต่อกันของ RDD จะขึ้นอยู่กับว่า transformation() เกิดขึ้นได้อย่างไรที่ให้ให้เกิด RDD เช่น CoGroupdRDD ขึ้นอยู่กับทุกๆ RDD ที่ใช้ใน cogroup() ความสัมพันธ์ระหว่างพาร์ทิชันของ RDD กับ NarrowDependency ซึ่งเดิมทีนั้นเป็น full dependency ภายหลังเป็น partial dependency. NarrowDependency สามารถแทนได้ด้วยหลายกรณี แต่การขึ้นต่อกันจะเป็นแบบ NarrowDependency ก็ต่อเมื่อจำนวน ของพาร์ทิชันและตัวแบ่งพาร์ทิชันมีชนิดชนิดเดยวกัน ขนาดเท่ากัน ถ้าพูดในแง่การไหลของข้อมูล MapReduce เทียบเท่ากัย map() + reduceByKey() ในทางเทคนิค, ตัว reduce ของ MapReduce จะ มีประสิทธิภาพสูงกว่า reduceByKey() ของเหล่านี้จะถูกพูดถึงในหัวข้อ Shuffle details. Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help
  29. 29. 10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals Page 1 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md This repository Pull requests Issues Gist SparkInternals / markdown / thai / 3-JobPhysicalPlan.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 44 minutes agoAorjoa fixed some typo and polish some word 250 lines (169 sloc) 38.9 KB Physical Plan เราเคยสรุปสั้นๆ เกี่ยวกับกลไกของระบบคล้ายกับ DAG ใน Physical plan ซี่งรวมไปถึง Stage และ Task ด้วย ในบทนี้เราจะคุยกันถึงว่า ทำ อย่างไร Physical plan (Stage และ Task) จะถูกสร้างโดยให้ Logical plan ของแอพลิเคชันใน Spark ความซับซ้อนของ Logical Plan โค้ดของแอพพลิเคชันนี้จะแนบไว้ท้ายบท Raw Blame History 0 7581Unwatch Star Fork
  30. 30. 10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals Page 2 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md ทำอย่างไรถึงจะกำหนด Stage และ Task ได้อย่างเหมาะสม เช่น ความซับซ้อนของกราฟการขึ้นต่อกันของข้อมูล? ไอเดียอย่างง่ายๆที่เกี่ยว กับความเกี่ยวข้องระหว่าง RDD หนึ่งกับอีก RDD หนึ่งที่เกิดขึ้นก่อนหน้ามันจะอยู่ในรูปแบบของ Stage ซึ่งเป็นการอธิบายโดยใช้ทิศทางหัว ลูกศร อย่างในแผนภาพด้านบนก็จะกลายมาเป็น Task. ในกรณีที่ RDD 2 ตัวรวมเข้ามาเป็นตัวเดียวกันนั้นเราสามารถสร้าง Stage โดยใช้ 3 RDD ซึ่งวิธีนี้ใช้ได้ก็จริงแต่ไม่ค่อยมีประสิทธิภาพ มันบอบบางและอาจจะก่อให้เกิดปัญหา Intermediate data จำนวนมากซึ่งต้องการที่เก็บ สำหรับ Physical task แล้วผลลัพธ์ของตัวมันจะถูกเก็บทั้งในโลคอลดิสก์ หรือในหน่วยความจำ หรือเก็บไว้ทั้งสองที่ สำหรับ Task ที่ถูกสร้าง เพื่อแทนหัวลูกศรแต่ละตัวในกราฟการขึ้นต่อกันของข้อมูลระบบจะต้องเก็บข้อมูลของ RDD ทุกตัวไว้ ซึ่งทำให้สิ้นเปลืองทรัพยากรมาก ถ้าเราตรวจสอบ Logical plan อย่างใกล้ชิดเราก็จะพบว่าในแต่ละ RDD นั้นพาร์ทิชันของมันจะไม่ขึ้นต่อกันกับตัวอื่น สิ่งที่ต้องการจะบอกก็คือ ใน RDD แต่ละตัวข้อมูลที่อยู่ในพาร์ทิชันจะไม่ยุ่งเกี่ยวกันเลย จากข้อสังเกตนี้จึงรวบรวมแนวความคิดเกี่ยวกับการรวมทั้งแผนภาพเข้ามาเป็น Stage เดียวและให้ Physical task เพื่อทำงานแค่ตัวเดียวสำหรับแต่ละพาร์ทิชันใน RDD สุดท้าย ( FlatMappedValuesRDD ) แผนภาพข้าง ล่างนี้จะทำให้เห็นแนวความคิดนี้ได้มากขึ้น ลูกศรเส้นหนาทั้งหมดที่อยู่ในแผนภาพจะเป็นของ Task1 ซึ่งมันจะสร้างให้ผลลัพธ์ของพาร์ทิชันแรกของ RDD ตัวสุดท้ายของ Job นั้น โปรด ทราบว่าเพื่อที่จะคำนวณ CoGroupedRDD เราจะต้องรู้ค่าของพาร์ทิชันทุกตัวของ RDD ที่เกิดก่อนหน้ามันเนื่องจากมันเป็นลักษณะของ ShuffleDependency ดังนั้นการคำนวณที่เกิดขึ้นใน Task1 เราถือโอกาสที่จะคำนวณ CoGroupedRDD ในพาร์ทิชันที่สองและสามสำหรับ Task2 และ Task3 ตามลำดับ และผลลัพธ์จาก Task2 และ Task3 แทนด้วยลูกศรบเส้นบางและลูกศรเส้นประในแผนภาพ อย่างไรก็ดีแนวความคิดนี้มีปัญหาอยู่สองอย่างคือ: Task แรกจะมีขนาดใหญ่มากเพราะเกิดจาก ShuffleDependency เราจำเป็นต้องรู้ค่าของทุกพาร์ทิชันของ RDD ที่เกิดก่อนหน้า ต้องใช้ขั้นตอนวิธีที่ฉลาดในการกำหนดว่าพาร์ทิชันไหนที่จะถูกแคช แต่มีจุดหนึ่งที่เป็นข้อดีที่น่าสนใจของไอเดียนี้ก็คือ Pipeline ของข้อมูลซึ่งข้อมูลจะถูกประมวลผลจริงก็ต่อเมื่อมันมีความจำเป็นที่จะใช้จริงๆ ยกตัวอย่างใน Task แรก เราจะตรวจสอบย้อนกลัยจาก RDD ตัวสุดท้าย ( FlatMappedValuesRDD ) เพื่อดูว่า RDD ตัวไหนและพาร์ทิชันตัว ไหนที่จำเป็นต้องรู้ค่าบ้าง แล้วถ้าระหว่าง RDD เป็นความสัมพันธ์แบบ NarrowDependency ตัว Intermediate data ก็ไม่จำเป็นที่จะต้องถูก เก็บไว้
  31. 31. 10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals Page 3 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md Pipeline มันจะเข้าใจชัดเจนขี้นถ้าเราพิจารณาในมุมมองระดับเรคอร์ด แผนภาพต่อไปนี้จะนำเสนอรูปแบบการประเมินค่าสำหรับ RDD ที่เป็น NarrowDependency รูปแบบแรก (Pipeline pattern) เทียบเท่ากับการทำงาน: for (record <- records) { f(g(record)) } พิจารณาเรคอร์ดเป็น Stream เราจะเห็นว่าไม่มี Intermediate result ที่จำเป็นจะต้องเก็บไว้ มีแค่ครั้งเดียวหลังจากที่ f(g(record)) ทำงานเสร็จแล้วผลลัพธ์ของการทำงานถึงจะถูกเก็บและเรคอร์ดสามารถถูก Gabage Collected เก็บกวาดให้ได้ แต่สำหรับบางรูปแบบ เช่น รูปแบบที่สามมันไม่เป็นเช่นนั้น: for (record <- records) { val fResult = f(record) store(fResult) // need to store the intermediate result here } for (record <- fResult) { g(record) ... } ชัดเจนว่าผลลัพธ์ของฟังก์ชัน f จะถูกเก็บไว้ที่ไหนสักที่ก่อน ทีนี้กลับไปดูปัญหาที่เราเจอเกี่ยวกับ Stage และ Task ปัญหาหลักที่พบในไอเดียนี้ก็คือเราไม่สามารถทำ Pipeline แล้วทำให้ข้อมูลไหลต่อกัน ได้จริงๆ ถ้ามันเป็น ShuffleDependency ดังนั้นเราจึงต้องหาวิธีตัดการไหลข้อมูลที่แต่ละ ShuffleDependency ออกจากกัน ซึ่งจะทำให้ Chain หรือสายของ RDD มันหลุดออกจากกัน แล้วต่อกันด้วย NarrowDependency แทนเพราะเรารู้ว่า NarrowDependency สามารถทำ Pipeline ได้ พอคิดได้อย่างนี้เราก็เลยแยก Logical plan ออกเป็น Stage เหมือนในแผนภาพนี้
  32. 32. 10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals Page 4 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md กลยุทธ์ที่ใช้ในการสร้าง Stage คือ *ตรวจสอบย้อนกลับโดยเริ่มจาก RDD ตัวสุดท้าย แล้วเพิ่ม NarrowDependency ใน Stage ปัจจุบัน จากนั้นแยก ShuffleDependency ออกเป็น Stage ใหม่ ซึ่งในแต่ละ Stage จะมีจำนวน Task ที่กำหนดโดยจำนวนพาร์ทิชันของ RDD ตัว สุดท้าย ของ Stage * จากแผนภาพข้างบนเว้นหนาคือ Task และเนื่องจาก Stage จะถูกกำหนกย้อนกลับจาก RDD ตัวสุดท้าย Stage ตัวสุดท้ายเลยเป็น Stage 0 แล้วถึงมี Stage 1 และ Stage 2 ซึ่งเป็นพ่อแม่ของ Stage 0 ตามมา ถ้า Stage ไหนให้ผลลัพธ์สุดท้ายออกมา Task ของมันจะมีชนิดเป็น ResultTask ในกรณีอื่นจะเป็น ShuffleMapTask ชื่อของ ShuffleMapTask ได้มาจากการที่ผลลัพธ์ของมันจำถูกสับเปลี่ยนหรือ Shuffle ก่อนที่จะถูกส่งต่อไปทำงานที่ Stage ต่อไป ซึ่งลักษณะนี้เหมือนกับที่เกิดขึ้นในตัว Mapper ของ Hadoop MapReduce ส่วน ResultTask สามารถมองเป็น Reducer ใน Hadoop ก็ได้ (ถ้ามันรับข้อมูลที่ถูกสับเปลี่ยนจาก Stage ก่อนหน้ามัน) หรืออาจจะดูเหมือน Mapper (ถ้า Stage นั้นไม่มีพ่อแม่) แต่ปัญหาอีกอย่างหนึ่งยังคงอยู่ : NarrowDependency Chain สามารถ Pipeline ได้ แต่ในตัวอย่างแอพพลิเคชันที่เรานำเสนอเราแสดง เฉพาะ OneToOneDependency และ RangeDependency แล้ว NarrowDependency แบบอื่นหล่ะ? เดี๋ยวเรากลับไปดูการทำผลคูณคาร์ทีเซียนที่เคยคุยกันไปแล้วในบทที่แล้ว ซึ่ง NarrowDependency ข้างในมันค่อนข้างซับซ้อน:
  33. 33. 10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals Page 5 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md โดยมี Stage อย่างนี้: ลูกศรเส้นหนาแสดงถึง ResultTask เนื่องจาก Stage จะให้ผลลัพธ์สุดท้ายออกมาโดยตรงในแผนภาพด้านบนเรามี 6 ResultTask แต่ มันแตกต่างกับ OneToOneDependency เพราะ ResultTask ในแต่ละ Job ต้องการรู้ค่า 3 RDD และอ่าน 2 บล๊อคข้อมูลการทำงานทุก อย่างที่ว่ามานี้ทำใน Task ตัวเดียว *จะเห็นว่าเราไม่สนใจเลยว่าจริงๆแล้ว NarrowDependency จะเป็นแบบ 1.1 หรือ N:N, NarrowDependency Chain สามารถเป็น Pipeline ได้เสมอ โดยที่จำนวนของ Task จะเท่ากับจำนวนพาร์ทิชันของ RDD ตัวสุดท้าย * การประมวลผลของ Physical Plan เรามี Stage และ Task ปัญหาต่อไปคือ Task จะถูกประมวลผลสำหรับผลลัพธ์สุดท้ายอย่างไร? กลับไปดูกันที่ Physical plan ของแอพพลิเคชันตัวอย่างในบทนี้แล้วนึกถึง Hadoop MapReduce ซึ่ง Task จะถูกประมวลผลตามลำดับ ฟังก์ชัน map() จะสร้างเอาท์พุทของฟังก์ชัน Map ซึ่งเป็นการรับพาร์ทิชันมาแล้วเขียนลงไปในดิสก์เก็บข้อมูล จากนั้นกระบวนการ shuffle-sort-aggregate จะถูกนำไปใช้เพื่อสร้างอินพุทให้กับฟังกืชัน Reduce จากนั้นฟังก์ชัน reduce() จะประมวลผลเพื่อให้

×