74
Spark Internals Lijie Xu : เҼยน Bhuridech Sudsee : แปล

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

Embed Size (px)

Citation preview

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

 

Spark   Internals  

   

Lijie   Xu   :   เ�ยน Bhuridech   Sudsee   :      แปล 

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

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 / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d 39 minutes ago Aorjoa 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 7581 Unwatch Star Fork

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

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 andexecutors and Summary on SparkExecutor Driver's ResouceManagement (in Chinese)

There's not yet a conclusion on thissubject since its implementation isstill changing, a link to the blog isadded

Thanks to the following for finding errors:

Weibo Id Chapter Error/Issue Revision status

@Joshuawangzj OverviewWhen multiple applications arerunning, multiple Backendprocess will be created

Corrected, but need to be confirmed. Noidea on how to control the number ofBackend processes

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

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() hasremoved the mapValues()operation, there's noMapValuesRDD generated

Fixed groupByKey() related diagrams andtext

@ JobLogicalPlanN:N relation in FullDepedencyN:N is a NarrowDependency

Modified the description ofNarrowDependency into 3 different caseswith detaild explaination, clearer than the2 cases explaination before

@zzl0Fisrt fourchapters

Lots of typos such as"groupByKey has generated the3 following RDDs" should be 2.Check pull request

All fixed

@TEL

Cache andBroadcastchapter

Lots of typos All fixed

@cloud-fan JobLogicalPlanSome arrows in the Cogroup()diagram should be colored red

All fixed

@CrazyJvm Shuffle details

Starting from Spark 1.1, thedefault value forspark.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, implementationand discussion of big data systems.

Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help

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

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 / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d 40 minutes ago Aorjoa fixed some typo and polish some word

175 lines (129 sloc) 26.4 KB

ภาพรวมของ Apache Spark

เริ่มแรกเราจะให้ความสนใจไปที่ระบบดีพลอยของ Spark คำถามก็คือ : ถ้าดีพลอยเสร็จเรียบร้อยแล้วระบบของแต่ละโหนดในคลัสเตอร์ทำงานอะไรบ้าง?

Deployment Diagram

จากแผนภาพการดีพลอย :

Raw Blame History

0 7581 Unwatch Star Fork

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

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}

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

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 cacheprintln(">>>>>>") println(pairs1.count)println("<<<<<<") println(pairs1.groupByKey(numReducers).count)

sc.stop() }}

หลังจากที่อ่านโค้ดแล้วจะพบว่าโค้ดนี้มีแนวความคิดในการแปลงข้อมูลดังนี้

แอพพลิเคชันนี้ไม่ได้ซับซ้อนอะไรมาก เราจะประเมินขนาดของข้อมูลและผลลัพธ์ซึ่งแอพพลิเคชันก็จะมีการทำงานตามขั้นตอนดังนี้

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

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)

วาดเป็นแผนภาพได้ตามนี้:

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

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 จะ

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

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 ก็จะดึงและสับเปลี่ยนข้อมูลที่ต้องการจะประมวลผล ในขณะที่อยู่ขั้นตอนของการดึงข้อมูลและ

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

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

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

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 / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d 42 minutes ago Aorjoa 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 7581 Unwatch Star Fork

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

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,

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

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 ลูกจะขึ้นอยู่กับพาร์ทิชันของแม่ไม่กี่ตัว เช่น พาร์ทิชันของลูกขึ้นต่อ ทั่วทั้ง พาร์ทิชันของพ่อแม่ (fulldependency)

ShuffleDependency (หรือ Wide dependency, กล่าวถึงในเปเปอร์ของ Matei)

พาร์ทิชันลูกหลายส่วนขึ้นกับพาร์ทิชันพ่อแม่ เช่นในกรณีที่แต่ละพาร์ทิชันของลูกขึ้นกับ บางส่วน ขอวพาร์ทิชันพ่อแม่ (partialdependency)

ยกตัวอย่าง map จะทำให้เกิด Narrow dependency ขณะที่ join จะทำให้เกิด Wide dependency (เว้นแต่ว่าในกรณีของพ่อแม่ที่เอามา join กันทั้งคู่เป็น Hash-partitioned)

ในอีกนัยหนึ่งแต่ละพาร์ทิชันของลูกสามารถขึ้นต่อพาร์ทิชันพ่อแม่ตัวเดียว หรือขึ้นต่อบางพาร์ทิชันของพ่อแม่เท่านั้น

ข้อควรรู้:

สำหรับ NarrowDependency จะรู้ว่าพาร์ทิชันลูกต้องการพาร์ทิชันพ่อแม่หนึ่งหรือหลายตัวมันขึ้นอยู่กับฟังก์ชัน getParents(partition i) ใน RDD ตัวลูก (รายละเอียดเดี๋ยวจะตามมาทีหลัง)ShuffleDependency คล้ายกัย Shuffle dependency ใน MapReduce [ผู้แปล:น่าจะหมายถึงเปเปอร์ของ Google] ตัว Mapper

จะทำพาร์ทิชันเอาท์พุท, จากนั้นแต่ละ Reducer จะดึงพาร์ทิชันที่มันจำเป็นต้องใช้จากพาร์ทิชันที่เป็นเอาท์พุทจาก Mapper ผ่านทางhttp.fetch)

ความขึ้นต่อกันทั้งสองแสดงได้ตามแผนภาพข้างล่าง.

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

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 ของสองพาร์ทิชันแต่นั้นก็ไม่ได้หมายถึงเรคอร์ดจะถูกประมวลผลแบบหนึ่งต่อหนึ่ง

ความแตกต่างระหว่างสองรูปแบบของสองฝั่งนี้จะเหมือนโค้ดที่แสดงสองชุดข้างล่าง

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

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()

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

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]

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

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)

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

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)

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

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 .

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

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)

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

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)

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

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]))] จากนั้นหาผลคูณ

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

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)

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

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)

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

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

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

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 ของสมาชิกตัวแรกในพาร์ทิชันคือ (newRandom(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).

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

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 ภายหลังเป็น partialdependency. 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

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

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 / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d 44 minutes ago Aorjoa 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 7581 Unwatch Star Fork

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

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 ก็ไม่จำเป็นที่จะต้องถูกเก็บไว้

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

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 เหมือนในแผนภาพนี้

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

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 ข้างในมันค่อนข้างซับซ้อน:

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

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() จะประมวลผลเพื่อให้

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

10/30/2559 BE, 1,21 PMSparkInternals/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 }

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

10/30/2559 BE, 1,21 PMSparkInternals/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

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

10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals

Page 8 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md

Stage สุดท้ายของ Job จะสร้างผลลัพธ์ของแต่ละ Job ออกมา ยกตัวอย่างเช่นสำหรับ GroupByTest ที่เราเคยคุยกันในบทแรกนั้นจะเห็นว่ามี Job อยู่ 2 Job และมีผลลัพธ์อยู่ 2 เซ็ตผลลัพธ์ เมื่อ Job ถูกส่งเข้าสู่ DAGScheduler และนำไปวิเคราะห์ตามแผนการในการแบ่งStage แล้วส่ง Stage อันดับแรกเข้าไปซึ่งเป็น Stage ที่ไม่มีพ่อแม่ เพื่อประมวลผล ซึ่งในกระบวนการนี้จำนวน Task จะถูกกำหนดและ Stage

จะถูกประมวลผลหลังจากที่ Stage พ่อแม่ของมันถูกประมวลผลเสร็จไปแล้ว

รายละเอียดของการส่ง Job

มาสรุปการวิเคราะห์ย่อๆสำหรับโค้ดที่ใช้ในการสร้าง Job และการส่ง Job เข้าไปทำงาน เราจะกลับมาคุยเรื่องนี้ในบทเรื่องสถาปัตยกรรม

1. rdd.action() เรียกใช้งาน DAGScheduler.runJob(rdd, processPartition, resultHandler) เพื่อสร้าง Job

2. runJob() รับจำนวนพาร์ทิชันและประเภทของ RDD สุดท้ายโดยเรียกเมธอต rdd.getPartitions() จากนั้นจะจัดสรรให้ Array[Result](partitions.size) เอาไว้สำหรับเก็บผลลัพธ์ตามจำนวนของพาร์ทิชัน

3. สุดท้ายแล้ว runJob(rdd, cleanedFunc, partitions, allowLocal, resultHandler) ใน DAGScheduler จะถูกเรียกเพื่อส่ง Job, cleanedFunc เป็นลักษณะ Closure-cleaned ของฟังก์ชัน processPartition . ในกรณีนี้ฟังก์ชันสามารถที่จะSerialized ได้และสามารถส่งไปยังโหนด Worker ตัวอื่นได้ด้วย

4. DAGScheduler มีเมธอต runJob() ที่เรียกใช้ submitJob(rdd, func, partitions, allowLocal, resultHandler) เพื่อส่ง Job ไปทำการประมวลผล

5. submitJob() รับค่า jobId , หลังจากนั้นจะห่อหรือ Warp ด้วยฟังก์ชันอีกทีหนึ่งแล้วถึงจะส่งข้อความ JobSubmitted ไปหา DAGSchedulerEventProcessActor . เมื่อ DAGSchedulerEventProcessActor ได้รับข้อความแล้ว Actor ก็จะเรียกใช้ dagScheduler.handleJobSubmitted() เพื่อจัดการกับ Job ที่ถูกส่งเข้ามาแล้ว นี่เป็นตัวอย่างของ Event-driven programming

แบบหนึ่ง6. handleJobSubmitted() อันดับแรกเลยจะเรียก finalStage = newStage() เพื่อสร้าง Stage แล้วจากนั้นก็จะ

submitStage(finalStage) . ถ้า finalStage มีพ่อแม่ ตัว Stage พ่อแม่จะต้องถูกประมวลผลก่อนเป็นอันดับแรกในกรณีนี้ finalStage นั้นจริงๆแล้วถูกส่งโดย submitWaitingStages() .

newStage() แบ่ง RDD Chain ใน Stage อย่างไร ?

เมธอต newStage() จะเรียกใช้ getParentStages() ของ RDD ตัวสุดท้ายเมื่อมีการสร้าง Stage ขึ้นมาใหม่ ( newStage(...) )

getParentStages() จะเริ่มไล่จาก RDD ตัวสุดท้ายจากนั้นตรวจสอบย้อนกลับโดยใช้ Logical plan และมันจะเพิ่ม RDD ลงในStage ปัจจุบันถ้าหาก Stage นั้นเป็น NarrowDependency จนกระทั่งมันเจอว่ามี ShuffleDependency ระหว่าง RDD มันจะให้RDD ตัวมันเองอยู่ฝั่งทางขวา (RDD หลังจากกระบวนการ Shuffle) จากนั้นก็จบ Stage ปัจจุบัน ทำแบบนี้ไล่ไปเรื่อยๆโดยเริ่มจาก RDD

ที่อยู่ทางซ้ายมือของ RDD ที่มีการ Shuffle เพื่อสร้าง Stage อื่นขึ้นมาใหม่ (ดูรูปการแบ่ง Stage อาจจะเข้าใจมากขึ้น)

เมื่อ ShuffleMapStage ถูกสร้างขึ้น RDD ตัวสุดท้ายของมันก็จะถูกลงทะเบียนหรือ Register

MapOutputTrackerMaster.registerShuffle(shuffleDep.shuffleId, rdd.partitions.size) . นี่เป็นสิ่งที่สำคัญเนื่องจากว่ากระบวนการ Shuffle จำเป็นต้องรู้ว่าเอาท์พุทจาก MapOuputTrackerMaster

ตอนนี้มาดูว่า submitStage(stage) ส่ง Stage และ Task ได้อย่างไร:

1. getMissingParentStages(stage) จะถูกเรียกเพื่อกำหนด missingParentStages ของ Stage ปัจจุบัน ถ้า Stage พ่อแม่ทุกตัวของมันถูกประมวลผลไปแล้วตัว missingParentStages จะมีค่าว่างเปล่า

2. ถ้า missingParentStages ไม่ว่างเปล่าจะทำการวนส่ง Stage ที่หายไปเหล่านั้นซ้ำและ Stage ปะจุบันจะถูกแทรกเข้าไปใน waitingStages และเมื่อ Stage พ่อแม่ทำงานเรียบร้อยแล้ว Stage ที่อยู่ใน waitingStages จะถูกทำงาน

3. ถ้า missingParentStages ว่างเปล่าและเรารู้ว่า Stage สามารถถูกประมวลผลในขณะนี้ แล้ว submitMissingTasks(stage,jobId) จะถูกเรียกให้สร้างและส่ง Task จริงๆ และถ้า Stage เป็น ShuffleMapStage แล้วเราจะจัดสรร ShuffleMapTask จำนวนมากเท่ากับจำนวนของพาร์ทิชันใน RDD ตัวสุดท้าย ในกรณีที่เป็น ResultStage , ResultTask จะถูกจัดสรรแทน. Task ใน Stage

จะฟอร์ม TaskSet ขึ้นมา จากนั้นขั้นตอนสุดท้าย taskScheduler.submitTasks(taskSet) จำถูกเรียกและส่งเซ็ตของ Task

ทั้งหมดไป4. ชนิดของ taskScheduler คือ TaskSchedulerImpl . ใน submitTasks() แต่ละ taskSet จะได้รับการ Wrap ในตัวแปร

manager ซึ่งเป็นตัวแปรของชนิด TaskSetManager แล้วจึงส่งมันไปทำ schedulableBuilder.addTaskSetManager(manager) . schedulableBuilder สามารถเป็น FIFOSchedulableBuilder หรือ FairSchedulableBuilder , ขึ้นอยู่กับว่าการตั้งค่ากำหนดไว้ว่าอย่างไร จากนั้นขั้นตอนสุดท้ายของ submitTasks() คือแจ้ง backend.reviveOffers() เพื่อให้ Task ทำงาน. ชนิดของ Backend คือ SchedulerBackend . ถ้าแอพพลิเคชันทำงานอยู่บนคลัสเตอร์มันจะเป็น Backend แบบ SparkDeploySchedulerBackend แทน

5. SparkDeploySchedulerBackend เป็น Sub-Class ของ CoarseGrainedSchedulerBackend , backend.reviveOffers()

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

10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals

Page 9 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md

จริงๆจะส่งข้อความ ReviveOffers ไปหา DriverActor . SparkDeploySchedulerBackend จะเปิด DriverActor ในขั้นตอนเริ่มทำงาน. เมื่อ DriverActor ได้รับข้อความ ReviveOffers แล้วมันจะเรียก launchTasks(scheduler.resourceOffers(Seq(new WorkerOffer(executorId, executorHost(executorId),freeCores(executorId))))) เพื่อเปิดให้ Task ทำงาน จากนั้น scheduler.resourceOffers() ได้รับ TaskSetManager ที่เรียงลำดับแล้วจากตัวจัดการงาน FIFO หรือ Fair และจะรวบรวมข้อมูลอื่นๆที่เกี่ยวกับ Task จาก TaskSchedulerImpl.resourceOffer() . ซึ่งข้อมูลเหล่านั้นจัดเก็บอยู่ใน TaskDescription ในขั้นตอนนี้ตำแหน่งที่ตั้งของข้อมูลหรือ Data locality ก็จะถูกพิจารณาด้วย

6. launchTasks() อยู่ใน DriverActor จะ Serialize แต่ละ Task ถ้าขนาดของ Serialize ไม่เกินลิมิตของ akkaFrameSize จานั้นTask จะถูกส่งครั้งสุดท้ายไปยัง Executor เพื่อประมวลผล: executorActor(task.executorId) ! LaunchTask(newSerializableBuffer(serializedTask)) .

การพูดคุยจนกระทั่งถึงตอนนี้เราคุยกันถึง:

โปรแกรมไดร์เวอร์ Trigger Job ได้อย่างไร?

จะสร้าง Physical plan จาก Logical plan ได้อย่างไร?

อะไรคือการ Pipelining ใน Spark และจำนำมันไปใช้ได้อย่างไร?

โค้ดจริงๆที่สร้าง Job และส่ง Job เข้าสู่ระบบ

อย่างไรก็ตามก็ยังมีหัวข้อที่ไม่ได้ลงรายละเอียดคือ:

กระบวนการ Shuffle

การประมวลผลของ Task และตำแหน่งที่มันถูกประมวลผล

ในหัวข้อถัดไปเราจะถกเถียงกันถึงการะบวนการ Shuffle ใน Spark

ในความเห็นของผู้แต่งแล้วการแปลงจาก Logical plan ไปยัง Physical plan นั้นเป็นผลงานชิ้นเอกอย่างแท้จริง สิ่งที่เป็นนามธรร เช่น การขึ้นต่อกัน, Stage และ Task ทั้งหมดนี้ถูกกำหนดไว้อย่างดีสำหรับลอจิคของขั้นตอนวิธีก็ชัดเจนมาก

โค้ดจากตัวอย่างของ Job

package internals

import org.apache.spark.SparkContextimport org.apache.spark.SparkContext._import org.apache.spark.HashPartitioner

object complexJob { def main(args: Array[String]) {

val sc = new SparkContext("local", "ComplexJob test")

val data1 = Array[(Int, Char)]( (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'), (3, 'f'), (2, 'g'), (1, 'h')) val rangePairs1 = sc.parallelize(data1, 3)

val hashPairs1 = rangePairs1.partitionBy(new HashPartitioner(3))

val data2 = Array[(Int, String)]((1, "A"), (2, "B"), (3, "C"), (4, "D"))

val pairs2 = sc.parallelize(data2, 2) val rangePairs2 = pairs2.map(x => (x._1, x._2.charAt(0)))

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

10/30/2559 BE, 1,21 PMSparkInternals/3-JobPhysicalPlan.md at thai · Aorjoa/SparkInternals

Page 10 of 10https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/3-JobPhysicalPlan.md

val data3 = Array[(Int, Char)]((1, 'X'), (2, 'Y')) val rangePairs3 = sc.parallelize(data3, 2)

val rangePairs = rangePairs2.union(rangePairs3)

val result = hashPairs1.join(rangePairs)

result.foreachWith(i => i)((x, i) => println("[result " + i + "] " + x))

println(result.toDebugString) }}

Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 1 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

This repository Pull requests Issues Gist

SparkInternals / markdown / thai / 4-shuffleDetails.md

Search

Aorjoa / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d an hour ago Aorjoa fixed some typo and polish some word

190 lines (106 sloc) 49.6 KB

กระบวนการ Shuffleก่อนหน้านี้เราได้พูดคุยถึง Physical plan และการประมวลผลของ Spark ในรายละเอียดมาแล้วแต่มีอย่างหนึ่งที่เรายังไม่ได้แตะเลยก็คือข้อมูลผ่าน ShuffleDependency เพื่อไป Stage อื่นได้อย่างไร

เปรียบเทียบ Shuffle ระหว่าง Hadoop and Spark

มีทั้งข้อที่เหมือนกันและแตกต่างกันในกระบวนการ Shuffle ของซอฟต์แวร์ทั้งสองตัวนี้คือ Hadoop และ Spark

จากมุมมองระดับสูงแล้วทั้งสองเหมือนกัน ทั้งคู่มีการพาร์ทิชัน Mapper เอาท์พุท (หรือ ShuffleMapTask ใน Spark) และส่งแต่ละพาร์ทิชันที่ตรงตาม Reducer ของมันไปให้ (ใน Spark มันสามารถเป็น ShuffleMapTask ใน Stage ถัดไปหรือเป็น ResultTask ) ตัวReducer จะบัฟเฟอร์ข้อมูลไว้ในหน่วยความจำ, Shuffle และรวบรวมข้อมูลแล้วนำไปทำฟังก์ชัน reduce() ครั้งหนึ่งเมื่อข้อมูลถูกรวบรวมแล้ว

จากมุมมองระดับต่ำมีความแตกต่างกันค่อนข้างน้อย การ Shuffle ใน Hadoop เป็นลักษณะเรียงตามลำดับหรือ Sort-based เนื่องจากเรคอร์ดมีความจำเป็นที่จะต้องถูกเรียงลำดับก่อนที่จะทำงานฟังก์ชัน combine() และ reduce() การเรียงลำดับสามารถทำได้โดยการใช้ขั้นตอนวิธีจากภายนอก ดังงนั้นจึงทำให้ combine() และ reduce() สามารถจัดการกับปัญหาที่มีเซ็ตข้อมูลขนาดใหญ่มากได้ ในขณะนี้Spark กำหนดค่าเริ่มต้นของกระบวนการ Shuffle เป็นแบบใช้ค่า Hash หรือ Hash-based ซึ่งปกติก็จะใช้ HashMap ในการรวบรวมและShuffle ข้อมูลและจะไม่มีการเรียงลำดับ แต่ถ้าหากผู้ใช้ต้องการเรียงลำดับก็สามารถเรียกใช้ฟังก์ชัน sortByKey() เอาเองได้ ใน Spark1.1 เราสามารถกำหนดการตั้งค่าได้ผ่าน spark.shuffle.manager แล้วตั้งค่าเป็น sort เพื่อเปิดใช้การเรียงตามลำดับในกระบวนการShuffle แต่ใน Spark 1.2 ค่าเริ่มต้นของกระบวนการ Shuffle กำหนดเป็น Sort-based

การนำไปใช้อย่างฉลาดมีความแตกต่างกัน อย่างที่เรารู้กันว่ากลไกการทำงานแต่ละขึ้นตอนของ Hadoop นั้นชัดเจน เรามีการไหลของงาน: map() , spill , merge , shuffle , sort และ reduce() แต่ละขั้นตอนของการรับผิดชอบได้ถูกกำหนดไว้ล่วงหน้าแล้วและมันก็เหมาะสมกับการโปรแกรมแบบเป็นลำดับ อย่างไรก็ดีใน Spark มันไม่ได้มีการกำหนดกลไกที่ชัดเจนและคงที่ไว้ แทนที่จะทำแบบเดียวกับHadoop ตัว Spark มี Stage และซีรีย์ของการแปลงข้อมูลดังนั้นการดำเนินการเช่น spill , merge และ aggregate จำเป็นที่จะต้องรวมอยู่ในกลไกการแปลง (Transformations)

ถ้าเราตั้งชื่อกระบวนการทางฝั่ง Mapper ของการพาร์ทิชันและเก็บข้อมูลว่า Shuffle write และฝั่ง Reducer ที่อ่านข้อมูลและรวบรวมข้อมูลว่า Shuffle read ปัญหาที่จะตามมาก็คือ ทำอย่างไรเราถึงจะรวมลอจิกของ Shuffle write และ Shuffle read ใน Logical หรือPhysical ของ Spark? ทำอย่างไรถึงจะทำให้ Shuffle write และ Shuffle read มีประสิทธิภาพ

Shuffle Write

Shuffle write เป็น Task ที่ค่อนข้างง่ายถ้าไม่ต้องเรียงเอาท์พุทตามลำดับก่อนมันจะแบ่งพาร์ทิชันข้อมูลแล้ว Persist ข้อมูลไว้ได้เลย การPersist ข้อมูลมีข้อดีอยู่ 2 อย่างคือลดความดันของ Heap (ผู้แปล: ลดการที่ข้อมูลปริมาณมากถูกเก็บไว้ที่หน่วยความจำแบบ Heap) และส่งเสริมกระบวนการทนต่อความล้มเหลวหรือ Fault-tolerance

กระบวนการนำไปใช้ก็ง่ายมาก: เพิ่มลอจิกของ Shuffle write ไปที่ท้ายสุดของกระบวนการ ShuffleMapStage (ในกรณีที่เป็น

Raw Blame History

0 7581 Unwatch Star Fork

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 2 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

ShuffleMapTask ) แต่ละเอาท์พุทของเรคอร์ดใน RDD ตัวสุดท้ายในแต่ละ Stage จะแบ่งพาร์ทิชันและ Persist ข้อมูลดังที่แสดงในแผนภาพนี้

จากแผนภาพจะพบว่ามี 4 ShuffleMapTask ที่จะถูกประมวลผลในเครื่อง Worker เครื่องเดียวซึ่งมี 2 Core ผลลัพธ์ของ Task (เรคอร์ดของ RDD ตัวสุดท้ายใน Stage) จะถูกเขียนลงดิสก์ (เราจะเรียกขั้นตอนนี้ว่า Persist ข้อมูล) แต่ละ Task จะมีบัฟฟเฟอร์ R ซึ่งจะมีขนาดเท่ากับจำนวนของ Reducer (จำนวนของ Task ที่จะอยู่ใน Stage ถัดไป) บัฟเฟอร์ใน Spark จะถูกเรียกว่า Bucket ขนาด 32KB (100KB ในSpark 1.1) และสามารถตั้งค่าได้ผ่านตัวตัวตั้งค่า spark.shuffle.file.buffer.kb

ในความเป็นจริงแล้ว Bucket เป็นแนวคิดที่ Spark ใช้แสดงแทนตำแหน่งและพาร์ทิชันของเอาท์พุทของกระบวนการ ShuffleMapTask ในส่วนนี้มันง่ายมากถ้า Bucker จะอ้างถึงบัฟเฟอร์ในหน่วยความจำ

ShuffleMapTask ใช้กลไกของเทคนิคการ Pipeline เพื่อประมวลผลผลลัพธ์ของเรคอร์ดใน RDD ตัวสุดท้าย แต่ละเรคอร์ดจะถูกส่งไปยังBucket ที่รับผิดชอบพาร์ทิชันของมันโดยตรง ซึ่งสามารถกำหนดได้โดย partitioner.partition(record.getKey()) เนื้อหาที่อยู่ในBucket จะถูกเขียนลงไฟล์บนดิสก์อย่างต่อเนื่องซึ่งไฟล์เหล่านี้จะเรียนว่า ShuffleBlockFile หรือย่อๆว่า FileSegment พวก Reducerจะดึงข้อมูลจาก FileSegment เหล่านี้ในช่วงของ Shuffle read

การนำไปใช้งานแบบที่กล่าวมานั้นง่ายมากแต่ก็พบปัญหาบางอย่างเช่น:

1. เราจำเป็นต้องสร้าง FileSegment ออกมามากมาย แต่ละ ShuffleMapTask จะสร้าง R (จำนวนเท่ากับ Reducer) FileSegment , ดังนั้น M ShuffleMapTask จะให้ M*R ไฟล์ สำหรับเซ็ตข้อมูลขนาดใหญ่เราอาจจะได้ M และ R ขนาดใหญ่ด้วยทำให้ไฟล์ข้อมูลระหว่างทางหรือ Intermediate นั้นมีจำนวนมหาศาล

2. บัฟเฟอร์อาจจะใช้พื้นที่มหาศาล บนโหนด Worker เราสามารถมี M * R Bucket สำหรับแต่ละ Core ที่ Spark สามารถใช้งานได้.Spark จะใช้พื้นที่ของบัฟเฟอร์เหล่านั้นซ้ำหลังจากการ ShuffleMapTask แต่ทว่ายังต้องคง R * Core Bucket ไว้ในหน่วยความจำถ้าโหนดมั CPU 8 Core กำลังประมวลผล 1000-reducer Job อยู่ Bucket จะใช้หน่วยความจำสูงถึง 256MB ( R * core * 32KB )

ในปัจจุบันนี้เรายังไม่มีวิธีการที่เหมาะสมในการจัดการกับปัญหาที่สอง ซึ่งเราจำเป็นต้องเขียนบัฟเฟอร์อยู่และถ้ามันมีขนาดเล็กมากจะส่งผลกระทบกับความเร็วของ IO ของระบบ แต่สำหรับปัญหาแรกนั้นเราสามารถแก้ไขได้ด้วยการรวบรวมไฟล์ซึ่งถูกนำไปใช้ใน Spark แล้ว หากสนใจสามารถดูรายละเอียดได้ดังแผนภาพ

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 3 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

จากแผนภาพด้านบนจะเห็นได้อย่างชัดเจนว่า ShuffleMapTask ที่ตามติดกันมาและทำงานอยู่บน Core เดียวกันสามารถใช้ไฟล์ Shuffleร่วมกันได้ แต่ละ Task จะเขียนข้อมูลเอาท์พุทต่อจากเดิม ShuffleBlock i' จะต่อหลังจากเอาท์พุทของ Task ก่อนหน้าคือ ShuffleBlock i (ทีแรกเกิด i ตอนหลังเพิ่ม i' เข้ามาตรงส่วนท้าย) ตัว ShuffleBlock จะเรียกว่า FileSegment ในการทำแบบนี้Reducer ใน Stage ถัดไปสามารถดึงไฟล์ทั้งไฟล์ได้แล้วทำให้เราสามารถลดจำนวนไฟล์ที่โหนด Worker ต้องการให้เหลือ Core * R ได้การรวมไฟล์นี้ถูกกำหนดค่าด้วยการตั้งค่า spark.shuffle.consolidateFiles ให้มีค่าความจริงเป็น True

Shuffle Read

เราจะเริ่มกันที่การตรวจสอบ Physical plan ของ reduceBykey ซึ่งมี ShuffleDependency :

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 4 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

อย่างที่สังหรณ์ใจเราจำเป็นที่จะต้องดึงข้อมูลของ MapPartitionRDD เพื่อที่จะสามารถรู้ค่าของ ShuffleRDD นำมาซึ่งคำถาม:

มันจะดึงเมื่อไหร่? จะดึงทุกครั้งที่มีการ ShuffleMapTask ดึงครั้งเดียวเมื่อ ShuffleMapTask ทุกตัวเสร็จแล้ว?

การดึงและกระบวนการของเรคอร์ดเกิดขึ้นในเวลาเดียวกันหรือว่าดึงก่อนแล้วค่อยเข้ากระบวนการ?

ดึงมาแล้วจะเก็บไว้ที่ไหน?

ทำอย่างไร Task ที่อยู่ใน Stage ถัดไปถึงจะรู้ว่าตำแหน่งของข้อมูลที่ดึงมาอยู่ตรงไหน?

ทางออกที่ Spark ใช้:

เมื่อไหร่ถึงจะดึงข้อมูล? หลังจากที่ทุก ShuffleMapTask เสร็จแล้วถึงจะดึง อย่างที่เราทราบกันดีว่า Stage จะประมวลผลก็ต่อเมื่อStage พ่อแม่ของมันประมวลผลเสร็จแล้วเท่านั้น ดังนั้นมันจะเริ่มดึงข้อมูลก็ต่อเมื่อ ShuffleMapTask ใน Stage ก่อนหน้าทำงานเสร็จแล้ว ส่วน FileSegment ที่ดึงมาแล้วก้จะถูกบัฟเฟอร์ไว้หน่วยความจำ ดังนั้นเราจึงไม่สามารถดึงข้อมูลได้มากจนกว่าเนื้อหาในบัฟเฟอร์จะถูกเขียนลงบนดิสก์ Spark จะลิมิตขนาดของบัฟเฟอร์โดยใช้ spark.reducer.maxMbInFlight ซึ่งเราจะเรียกตัวนี้ว่า softBuffer ซึ่งขนาดของบัฟเฟอร์มีค่าเริ่มต้นเป็น 48MB และ softBuffer มักจะประกอบด้วยการดึงหลาย FileSegment แต่ในบางครั้งแค่ Segment เดียวก็เต็มบัฟเฟอร์แล้ว

การดึงและกระบวนการของเรคอร์ดเกิดขึ้นในเวลาเดียวกันหรือว่าดึงก่อนแล้วค่อยเข้ากระบวนการ การดึงและกระบวนการประมวลผลเรคอร์ดเกิดขึ้นในเวลาเดียวกัน ใน MapReduce ขั้นตอนที่ Stage เป็น Shuffle จะดึงข้อมูลและนำลอจิก combine() ไปทำกับเรคอร์ดในเวลาเดียวกัน อย่างไรก็ดีใน MapReduce ข้อมูลอินพุทของ Reducer นั้นต้องการเรียงตามลำดับดังนั้น reduce() จึงต้องทำงานหลังจากที่มีกระบวนการ Shuffle-sort แล้ว แต่่เนื่องจาก Soark ไม่ต้องการการเรียงตามลำดับก่อนถึงจะให้เป็นข้อมูลอินพุทของReducer จึงไม่จำเป็นต้องรอให้ได้รับข้อมูลทั้งหมดก่อนถึงจะเริ่มดำเนินการ แล้วใน Spark เราใช้งาน Shuffle และกระบวนการได้อย่างไร ในความเป็นจริงแล้ว Spark จะใช้ประโยชน์จากโครงสร้างข้อมูลเช่น HashMap เพื่อทำ Job นั้นๆ แต่ละคู่ <Key, Value> จากกระบวนการ Shuffle จะถูกแทรกเข้าไปใน HashMap ถ้า Key มีอยู่แล้วใน Collection จะเอา Value มารวมกัน โดยจะรวมกันผ่านการใช้ฟังก์ชัน func(hashMap.get(Key), Value) ในตัวอย่างโปรแกรม WordCount จากแผนภาพด้านบน func จะเป็น hashMap.get(Key) + Value และผลลัพธ์ของมันจะกลับไปอัพเดทใน HashMap ตัว func นี่เองที่ทำหน้าที่เหมือนกับ reduce() ใน Hadoop แต่พวกมันก็มีข้อแตกต่างกันในรายละเอียด ซึ่งจะแสดงในโค้ดดังนี้

// MapReducereduce(K key, Iterable<V> values) { result = process(key, values)

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 5 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

return result}

// Sparkreduce(K key, Iterable<V> values) { result = null for (V value : values) result = func(result, value) return result}

ใน Hadoop MapReduce เราสามารถกำหนดโครงสร้างข้อมูลใดๆตามที่เราต้องการได้ในฟังก์ชัน process ซึ่งมันเป็นแค่ฟังก์ชันที่รับ Iterable เป็นพารามิเตอร์ เราสามารถที่จะเลือกแคช values สำหรับใช้ประมวลผลต่อไปในอนาคตได้ และใน Spark มีเทคนิคคล้าย foldLeft ที่ถูกใช้กับ func เช่นใน Hadoop มันสามารถหาค่าเฉลี่ยได้ง่ายมากจากสมการ sum(values) / values.length แต่ไม่ใช่กับ Spark เราจะมาพูดถึงเรื่องนี้กันอีกครั้งภายหลัง

ดึงมาแล้วจะเก็บไว้ที่ไหน? FileSegment ที่ดึงมาแล้วจะถูกบัฟเฟอร์ไว้ใน softBuffer หลังจากนั้นข้อมูลจะถูกประมวลผลและเขียนลงไปในตำแหน่งที่ได้กำหนดการตั้งค่าไว้แล้ว ถ้า spark.shuffle.spill เป็น False แล้วตำแหน่งที่จะเขียนเก็บไว้จะอยู่ในหน่วยความจำเท่านั้น โครงสร้างข้อมูลแบบพิเศษคือ AppendOnlyMap จะถูกใช้เก็บข้อมูลของกระบวนการนี้เอาไว้ในหน่วยความจำ ไม่งั้นมันจะเขียนข้อมูลของกระบวนการลงทั้งในดิสก์และหน่วยความจำโดยใช้ ExternalAppendOnlyMap โครงสร้างข้อมูลนี้สามารถล้นออกไปเรียง Key/Value ตามลำดับบนดิสก์ได้ในกรณีที่หน่วยความจำมีที่ว่างไม่พอ ปัญหาสำคัญคือเมื่อเราใช้ทั้งหน่วยความจำและดิสก์ทำ

อย่างไรเราถึงจะทำให้มันสมดุลกันได้ ใน Hadoop จะกำหนดค่าเริ่มต้น 70% ของหน่วยความจำจะถูกจองไว้สำหรับใช้กับข้อมูลShuffle เมื่อ 66% ของพื้นที่หน่วยความจำส่วนนี้ถูกใช้ไปแล้ว Hadoop จะเริ่มกระบวนการ Merge-combine-spill ในส่วนของ Sparkจะมีกลยุทธ์ที่คล้ายๆกันซึ่งเราก็จะคุยเรื่องนี่้ในบทถัดไปทำอย่างไร Task ที่อยู่ใน Stage ถัดไปถึงจะรู้ว่าตำแหน่งของข้อมูลที่ดึงมาอยู่ตรงไหน? นึกย้อนกลับไปถึงบทล่าสุดที่เราผ่านมาซึ่งมีขั้นตอนที่สำคัญมากคือ ShuffleMapStage ซึ่งจะลงทะเบียน RDD ตัวสุดท้ายโดยการเรียกใช้ MapOutputTrackerMaster.registerShuffle(shuffleId, rdd.partitions.size) ดังนั้นระหว่างกระบวนการ Shuffle นี้Reducer จะได้รับตำแหน่งของข้อมูลโดยเรียกถาม MapOutputTrackerMaster ในโปรแกรมไดรว์เวอร์ และเมื่อ ShuffleMapTask ดำเนินการเรียบร้อยแล้วมันจะรายงานตำแหน่งของไฟล์ที่เป็น FileSegment ไปยัง MapOutputTrackerMaster

ตอนนี้เราจะมาถกเถียงกันในประเด็นหลักของไอเดียที่ซ่อนอยู่เบื้องหลังการทำงานของ Shuffle write และ Shuffle read รวมถึงการนำไปใช้งานในบางรายละเอียด

Shuffle Read of Typical Transformations

reduceByKey(func)

เราเคยคุยกันคร่าวๆแล้วเกี่ยวกับกระบวนการดึงและ Reduce ของ reduceByKey() แต่โปรดทราบว่าสำหรับ RDD ใดๆแล้วไม่จำเป็นว่าทั้งหมดของข้อมูลจะต้องอยู่บนหน่วยความจำในตอนที่เรากำหนดค่า การประมวลผลจะทำบนเรคอร์ดเป็นหลัก เรคอร์ดที่ประมวลผลเสร็จแล้วจะถูกปฏิเสธถ้าเป็นไปได้ ในมุมมองจากระดับของเรคอร์ด reduce() จะถูกแสดงไว้ดังแผนภาพด้านล่าง:

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 6 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

เราจะเห็นว่าเรคอร์ดที่ถูกดึงมาได้รวมกันโดยใช้ HashMap และเมื่อทุกเรคอร์ดถูกรวมเข้าด้วยกันครั้งหนึ่งแล้วเราจะได้ผลลัพธ์ออกมา ตัว func ต้องการสับเปลี่ยน

การดำเนินการ mapPartitionsWithContext ใช้สำหรับการแปลงจาก ShuffleRDD ไปเป็น MapPartitionRDD

เพื่อลดภาระของการจราขรบนเครือข่ายระหว่างโหนด เราสามารถใช้ combine() ในฝั่ง Map ได้ใน Hadoop ในส่วนของ Spark มันก็สะดวกสบายเช่นกัน ทั้งหมดที่เราต้องทำคือนำ mapPartitionsWithContext ไปใช้กับ ShuffleMapStage เช่น ใน reduceByKey การแปลงจาก ParallelCollectionRDD ไป MapPartitionsRDD มีค่าเทียบเท่ากับการ Combine ในฝั่ง Map

ข้อเปรียบเทียบระหว่าง map()->reduce() ใน Hadoop และ reduceByKey ใน Spark

ฝั่ง Map : ในส่วนนี้จะไม่มีความแตกต่างกัน สำหรับลอจิก combine() Hadoop ต้องการเรียงตามลำดับก่อนที่จะ combine() .Spark ใช้ conbine() ในรูปแบบของการใช้ Hash map

ฝั่ง Reduce : กระบวนการ Shuffle ใน Hadoop จะดึงข้อมูลจนกระทั่งถึงจำนวนหนึ่งจากนั้นจะทำ combine() แล้วจะรวมการเรียงลำดับของข้อมูลเพื่อป้อนให้ฟังก์ชัน reduce() ใน Spark การดึงข้อมูลและ Reduce เกิดขึ้นในเวลาเดียวกัน (ใน Hash map) ดังนั้นฟังก์ชัน Reduce จะต้องการการสับเปลี่ยน

ข้อเปรียบเทียบในแง่ของการใช้งานหน่วยความจำ

ฝั่ง Map : Hadoop ต้องใช้บัฟเฟอร์แบบวงกลมเพื่อถือและเรียงลำดับของข้อมูลเอาท์พุทจาก map() แต่ส่วนของ combine() ไม่ต้องการพื้นที่หน่วยความจำเพิ่มเติม Spark ต้องการใช้ Hash map เพื่อทำ combine() และการเก็บข้อมูลเรอคอร์ดเหล่านั้นลงดิสก์ต้องการใช้บัฟเฟอร์ (Bucket)

ฝั่ง Reduce: Hadoop ต้องใช้เนื้อที่ของหน่วยความจำบางส่วนเพื่อนที่จะเก็บข้อมูลที่ Shuffle แล้วเอาไว้. combine() และ reduce() ไม่จำเป็นต้องใช้เนื้อที่ของหน่วยความจำเพิ่มเติมเนื่องจากอินพุทเหล่านี้ถูกเรียงตามลำดับไว้เรียบร้อยแล้วดังนั้นจึงสามารถจะจัดกลุ่มและรวบรวมได้เลย ใน Spark softBuffer จำเป็นกับการดึงข้อมูล และ Hash map ถูกใช้สำหรับเก็บข้อมูลผลลัพธ์ของการ combine() และ reduce() เอาไว้ถ้ามีแค่การใช้งานหน่วยความจำในกระบวนการประมวลผลข้อมูล อย่างไรก็ตามส่วนของข้อมูลสามารถเก็บบนดิสก์ได้ถ้ามีการตั้งค่าไว้เป็นแบบใช้งานทั้งหน่วยความจำและดิสก์

groupByKey(numPartitions)

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 7 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

กระบวนการที่คล้ายกันกับ reduceByKey() ตัว func จะเป็น result = result ++ result.value นั่นคือแต่ละ Key จะจัดกลุ่มของValue รวมเอาไว้ด้วยกันโดยไม่มีการรวบรวมกันอีกภายหลัง

distinct(numPartitions)

คล้ายกับการทำงานของ reduceByKey() ตัว func คือ result = result == null ? record.value : result นั่นหมายความว่าจะตรวจสอบดูเรคอร์ดใน HashMap ก่อนว่ามีหรือเปล่า ถ้ามีอยู่แล้วก็จะปฏิเสธเรคอร์ดนั้น ถ้ายังไม่มีอยู่ก็จะเพิ่มเข้าไปใน Map. ซึ่งฝั่งที่ทำการ Map จะทำงานเหมือนกับ reduceByKey() คือมีการ combine() ที่ฝั่ง Map นั่นเอง

cogroup(otherRDD, numPartitions)

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 8 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

สามารถเป็นได้ทั้ง 0, 1 หรือหลาย ShuffleDependency สำหรับส่วนของ CoGroupedRDD แต่ในกระบวนการ Shuffle เราไม้ได้สร้างHash map สำหรับ Shuffle dependency แต่ละตัวแต่จะใช่ Hash map แค่ตัวเดียวกับ Shuffle dependency ทุกตัว ซึ่งแตกต่างการ reduceByKey ที่ Hash map จำถูกสร้างในเมธอต compute() ของ RDD มากกว่า mapPartitionWithContext()

Task ของการประมวลผลของ RDD จะจัดสรรให้มี Array[ArrayBuffer] ซึ่ง Array ตัวนี้จะมีจำนวนของ ArrayBuffer ที่ว่างเปล่าเท่ากับจำนวนของ RDD อินพุท ยกตัวอย่างของแผนภาพด้านบนเรามี ArrayBffer อยู่ 2 ตัวในแต่ละ Task ซึ่งเท่ากับจำนวน RDD อินพุทที่เข้ามา เมื่อคู่ Key/Value มาจาก RDD a มันจะเพิ่มเข้าไปใน ArrayBuffer ตัวแรกถ้าคู่ Key/Value มาจาก RDD b มันจะเพิ่มเข้าไปใน ArrayBuffer ตัวที่สองจากนั้นจะเรียก mapValues() ให้ทำการแปลงจาก Values .ห้เป็นชนิดที่ถูกต้อง: (ArrayBuffer,ArrayBuffer) => (Iterable[V], Iterable[W]) .

intersection(otherRDD) and join(otherRDD, numPartitions)

การดำเนินการของสองตัวนี้ใช้ cogroup ดังนั้นแล้วกระบวนการ Shuffle มันก็จะเป็นแบบ cogroup ด้วย

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 9 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

sortByKey(ascending, numPartition)

กระบวนการประมวลผลลอจิกของ sortByKey() แตกต่างกับ reduceByKey() เพียงเล็กน้อยคือตัวนี้มันไม่ได้ใช้ HashMap เพื่อจัดการกับเรคอร์ดข้อมูลที่ถูกดึงมา แต่ทุกคู่ Key/Value จะเป็นพาร์ทิชันแบบ Range partition เรคอร์ดที่อยู่ในพาร์ทิชันเดียวกันจะอยู่ในลักษณะเรียงลำดับตาม Key เรียบร้อยแล้ว

coalesce(numPartitions, shuffle = true)

coalesce() จะสร้าง ShuffleDependency ก็จริงแต่ว่ามันไม่ได้จำเป็นว่าเราจะต้องรวมเรคอร์ดที่ดึงมาไว้ด้วยกันดังนั้น Hash map ก็ไม่มีความจำเป็น

HashMap ใน Shuffle Read

ดังที่เราได้เห็นมาว่า Hash map เป็นโครงสร้างข้อมูลที่มีการใช้บ่อยในกระบวนการ Shuffle ของ Spark ซึ่งตัว Spark เองก็มี Hash mapอยู่ 2 เวอร์ชั่นที่มีลักษณะเฉพาะ: AppendOnlyMap เป็น Hash map ที่อยู่ในหน่วยความจำ และอีกเวอร์ชันเป็นเวอร์ชันที่อยู่ได้ทั้งในหน่วยความจำและดิสก์คือ ExternalAppendOnlyMap เดี๋ยวเราจะมาดูว่าทั้งสอง Hash map นี้มีความแตกต่างกันยังไง

AppendOnlyMap

ในเอกสารของ Spark อธิบายว่า AppendOnlyMap เป็น "ตาราง Hash แบบเปิดง่ายๆที่ถูกปรับแต่งให้มีลักษณะเพิ่มเข้าไปได้เท่านั้น, Keyไม่สามารถถูกลบออกได้แต่ Value ของแต่ละ Key สามารถเปลี่ยนแปลงได้" วิธีการนำไปใช้ของมันก็ง่ายมาก: จัดสรร Array ของ Object ขนาดใหญ่ หากดูตามแผนภาพด้านล่างจะเห็นว่า Key จะถูกเก็บอยู่ในส่วนสีน้ำเงินและ Value จะถูกเก็บในส่วนสีขาว

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 10 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

เมื่อมีการ put(K,V) เกิดขึ้นเราจะหาช่องของ Array ได้โดย hash(K) ถ้าตำแหน่งช่องที่ได้มามีข้อมูลอยู่แล้วจะใช้วิธี Quandraticprobing เพื่อหาช่องวางไหม่ (ดูคำอธิบายในย่อหน้าถัดไป) ยกตัวอย่างในแผนภาพด้านบน K6 การ Probing ครั้งที่สามจึงจะพบช่อองว่างซึ่งเป็นช่องที่หลังจาก K4 จากนั้น Value จะถูกแทรกเพิ่มหลังจากที่ Key แทรกเข้าไปแล้ว เมื่อ get(K6) เราก็จะใช้เทคนิคเดียวกันนี้เข้าถึงแล้วดึง V6 ซึ่งเป็น Value ในช่องถัดจาก Key ออกมาจากนั้นคำนวณค่า Value ใหม่แล้วก็เขียนกลับไปในตำแหน่งเดิมของ V6

(Quandratic probing เป็นวิธีการหาช่องว่างของตาราง Hash ในกรณีที่ไม่สามารถหาช่องว่างจาก hash(K) โดยตรงได้จะเอา hash(K)บวกเลขกําลังสองของจํานวนครั้งที่เกิดซ้ํา เช่น hash(K) + 1*1 ยังไม่ว่างก็ไปหา hash(K) + 2*2 ถ้ายังไม่ว่างอีก hash(K) + 3*3

การวนซ้ำบน AppendOnlyMap จะเป็นแค่การแสกน Array

ถ้า 70% ของ Array ถูกจัดสรรให้ใช้ไปแล้วมันจะมีการขยายเพิ่มเป็น 2 เท่าทำให้ Key จะถูกคำนวณ Hash ใหม่และตำแหน่งก็จะเปลี่ยนแปลงไป

AppendOnlyMap มีเมธอต destructiveSortedIterator(): Iterator[(K, V)] ซึ่งคืนค่าคู่ Key/Value ที่เรียงตามลำดับแล้ว ในขั้นตอนการทำงานของมันจะเริ่มจากการที่กระชับคู่ Key/Value ไปให้อยู่ในลักษณะ Array ที่ค่าคู่ Key/Value อยู่ในช่องเดียวกัน (แผนภาพด้านบนมันอยู่คนละช่อง) แล้วจากนั้นใช้ Array.sort() ซึ่งเป็นการเรียกให้เกิดการเรียงตามลำดับของข้อมูลใน Array แต่การดำเนินการนี้จะทำลายโครงสร้างของข้อมูล

ExternalAppendOnlyMap

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 11 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

หากจะเปรียบเทียบกับ AppendOnlyMap การนำ ExternalAppendOnlyMap ไปใช้ดูจะซับซ้อนกว่า เพราะมันมีแนวคิดคล้ายๆกับกระบวนการ shuffle-merge-combine-sort ใน Hadoop

ExternalAppendOnlyMap จะใช้ AppendOnlyMap คู่ Key/Value ที่เข้ามาจะถูกเพิ่มเข้าไปใน AppendOnlyMap เมื่อ AppendOnlyMap มีขนาดเกือบเท่าของตัวมันเราจะตรวจสอบว่ามีเนื้อที่ว่างบนหน่วยความจำเหลืออยู่ไหม? ถ้ายังเหลือ AppendOnlyMap ก็จะเพิ่มขนาดเป็นสองเท่า ถ้าไม่พอมันจะเอาคู่ Key/Value ทั้งหมดของตัวมันไปเรียงตามลำดับจากนั้นก็จะเอาไปเขียนบนดิสก์ โดยใช้ destructiveSortedIterator() ในแผนภาพจะเห็นว่า Map มีการล้นหรือ Spill อยู่ 4 ครั้งซึ่งแต่ละครั้งที่ Spill แต่ละครั้งก็จะมีไฟล์ของ spillMap เกิดขึ้นมาใหม่ทุกครั้งและตัว AppendOnlyMap จะถูกสร้างขึ้นมาเพื่อรอรับคู่ Key/Value. ใน ExternalAppendOnlyMap เมื่อคู่ Key/Value ถูกใส่เพิ่มเข้ามาแล้วมันจะเกิดการรวมกันเฉพาะส่วนที่อยู่บนหน่วยความจำ ( AppendOnlyMap ) ดังนั้นหมายความว่าถ้าเราอยากได้ผลลัพธ์สุดท้าย Global merge-aggregate จะถูกเรียกใช้บนทุกๆ Spill และ AppendOnlyMap ในหน่วยความจำ

Global merge-aggregate ทำงานดังต่อไปนี้ เริ่มแรกส่วนที่อยู่ในหน่วยความจำ ( AppendOnlyMap ) จะถูกเรียงตามลำดับเป็น sortedMap จากนั้น DestructiveSortedIterator (สำหรับ sortedMap ) หรือ DiskMapIterator (สำหรับ spillMap ที่อยู่บนดิสก์) จะถูกใช้เพื่ออ่านส่วนของคู่ Key/Value แต่ละส่วนเข้าสู่ StreamBuffer จากนั้น StreamBuffer จะเพิ่มเข้าไปใน mergeHeap ใน

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

10/30/2559 BE, 1,22 PMSparkInternals/4-shuffleDetails.md at thai · Aorjoa/SparkInternals

Page 12 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/4-shuffleDetails.md

แต่ละ StreamBuffer ทุกเรคอร์ดจะมี hash(key) เดียวกัน สมมติว่าในตัวอย่างเรามี hash(K1) == hash(K2) == hash(K3) <hash(K4) < hash(K5) เราจะเห็นว่ามี 3 เรคอร์ดแรกของ Map ที่ Spill แรกมี hash(key) เดียวกันจึงอ่านเข้าสู่ StreamBuffer ตัวเดียวกัน ขั้นตอนการรวมกันของมันก็ไม่ยาก: เอา StreamBuffer ที่มีค่า hash(key) จากนั้นก็เก็บเข้าใน ArrayBuffer[StreamBuffer] ( mergedBuffer ) สำหรับผลการรวม StreamBuffer ตัวแรกที่ถูกเพิ่มเข้าไปเรียกว่า minBuffer ซึ่ง Key ของมันจะเรียกว่า minKey การรวมหรือ Merge หนึ่งครั้งจะรวบรวมทุกๆคู่ Key/Value ที่มี Key เป็น minKey ใน mergedBuffer จากนั้นก็ให้ผลลัพธ์ออกมา เมื่อการดำเนินการ Merge ใน mergedBuffer เสร็จแล้วคู่ Key/Value ที่เหลืออยู่จะคืนค่ากลับไปยัง mergeHeap และทำ StreamBuffer ให้ว่าง จากนั้นจะอ่านเข้ามาแทนใหม่จากในหน่วยความจำหรือ Spill ที่อยู่บนดิสก์

ยังมีอีก 3 ประเด็นที่จะต้องพูดคุยกัน:

การตรวจสอบหน่วยความจำว่าว่างหรือเปล่านั้นใน Hadoop จะกำหนดไว้ที่ 70% ของหน่วยความจำของ Reducer สำหรับ Shuffle-sort และก็คล้ายๆกันกับใน Spark จะตั้งค่า spark.shuffle.memoryFraction * spark.shuffle.safetyFraction (ค่าเริ่มต้น 0.3 * 0.8) สำหรับ ExternalAppendOnlyMap ซึ่งดูเหมือนว่า Spark สงวนหน่วยความจำเอาไว้ และยิ่งไปกว่านั้นคือ 24% ของหน่วยความจำจะถูกใช้งานร่วมกันในทุก Reducer ที่อยู่ใน Executor เดียวกัน ตัว Executor เองก็มีการถือครอง ShuffleMemoryMap: HashMap[threadId, occupiedMemory] เอาไว้อยู่เพื่อตรวจสอบการใช้งานหน่วยความจำของ ExternalAppendOnlyMap ในแต่ละ Reducer ก่อนที่ AppendOnlyMap จะขยายขนาดขึ้นจะต้องตรวจสอบดูก่อนว่าขนาดหลังจากที่ขยายแล้วเป็นเท่าไหร่โดยใช้ข้อมูลจาก ShuffleMemoryrMap ซึ่งต้องมีที่ว่างมากพอถึงจะขยายได้ ดังนั้นโปรดทราบว่า 1000 เรเคอร์ดแรกมันจะไม่มีการกระตุ้นให้มีการตรวจสอบ Spill

AppendOnlyMap เป็นขนาดโดยประมาณ เพราะถ้าหากเราต้องการทราบค่าที่แน่นอนของ AppendOnlyMap เราก็ต้องคำนวณหาขนาดในทุกๆตัวที่มีการอ้างถึงในขณะที่มีการขยายตัวมันไปด้วยแต่มันใช้เวลามาก Spark จึงเลือกใช้วิธีประมาณค่าซึ่งความซับซ้อนของขั้นตอนวิธีเป็น O(1) ในความหลักของมันคืออยากรู้ว่าขนาดของ Map เปลี่ยนไปอย่างไรหลังจากการเพิ่มเข้าและรวบรวมกันของเรคอร์ดจำนวนหนึ่งเพื่อประมาณการขนาดของทั้งโครงสร้าง รายละเอียดอยู่ใน SizeTrackingAppendOnlyMap และ SizeEstimator

กระบวนการ Spill จำเหมือนกับ Shuffle write คือ Spark จะสร้างบัฟเฟอร์เมื่อมีการ Spill เรเคอร์ดไปยังดิสก์ ขนาดของมันคือค่าที่ตั้งค่าใน spark.shuffle.file.buffer.kb โดยค่าเริ่มต้นคือ 32KB เนื่องจาก Serializer ก็ได้จัดสรรบัฟเฟอร์สำหรับทำ Job ไว้ด้วยดังนั้นปัญหาก็จะเกิดขึ้นเมื่อเราลอง Spill เรคอร์ดจำนวนมากมหาศาลในเวลาเดียวกัน ทำให้ Spark จำกัดจำนวนเรคอร์ดที่สามารถSpill ได้ในเวลาเดียวกันนี้ในตัวตั้งค่า spark.shuffle.spill.batchSize ซึ่งขนาดเริ่มต้นเป็น 10000 ตัว

การพูดคุย

อย่างที่เราเห็นในบทนี้คือ Spark มีแนวทางจัดการปัญหาที่ยืดหยุ่นมากในกระบวนการ Shuffle เมื่อเทียบกับที่ Hadoop ใช้คือการกำหนดตายตัวลงไปเลยว่าต้อง shuffle-combine-merge-reduce ใน Spark เป็นไปได้ที่จะผสมผสานกันระหว่างกลยุทธ์ที่หลากหลายในกระบวนการ Shuffle โดยใช้โครงสร้างข้อมูลที่แตกต่างกันไปเพื่อที่จะให้กระบวนการ Shuffle ที่เหมาะสมบนพื้นฐานของการแปลงข้อมูล

ดังนั้นเราจึงได้มีการพูดคุยกันถึงกระบวนการ Shuffle ใน Spark ที่ปราศจากการเรียงลำดับพร้อมกับทำอย่างไรกระบวนการนึ้ถึงจะควบรวมกับ Chain การประมวลผลของ RDD จริงๆ อีกทั้งเราคุยกันถึงเรื่องเกี่ยวกับปัญหาของหน่วยความจำและดิสก์ รวมถึงเปรียบเทียบในบางแง่มุมกับ Hadoop ในบทถัดไปเราจะอธิบายถึงกระบวนการการที่ Job ถูกประมวลผลจากแง่มุมของการสื่อสารกันระหว่างโปรเซส Inter-process communication. ปัญหาของตำแหน่งข้อมูลก็ได้กล่าวถึงในบทนี้ด้วยเช่นกัน

เพิ่มเติมในบทนี้คือมีบล๊อคที่น่าสนใจมากๆ (เขียนในภาษาจีน) โดย Jerry Shao, Deep Dive into Spark's shuffle implementation.

Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 1 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

This repository Pull requests Issues Gist

SparkInternals / markdown / thai / 5-Architecture.md

Search

Aorjoa / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d an hour ago Aorjoa fixed some typo and polish some word

271 lines (198 sloc) 26.9 KB

สถาปัตยกรรมเราเคยคุยกันเรื่อง Spark Job กันมาแล้วในบทที่ 3 ในบทนี้เราจะคุยกันเกี่ยวกับเรื่องของ สถาปัตยกรรมและ Master, Worker, Driver,Executor ประสานงานกันอย่างไรจนกระทั้งทำงานเสร็จเรียบร้อย

จะดูแผนภาพโดยไม่ดูโค้ดเลยก็ได้ไม่ต้องซีเรียส

Deployment diagram

จากแผนภาพการดีพลอยในบทที่เป็นภาพรวม overview

Raw Blame History

0 7581 Unwatch Star Fork

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 2 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

ต่อไปเราจะคุยกันถึงบางรายละเอียดเกี่ยวกับมัน

การส่ง Job

แผนภาพด้านล่างจะอธิบายถึงว่าโปรแกรมไดรว์เวอร์ (บนโหนด Master) สร้าง Job และส่ง Job ไปยังโหนด Worker ได้อย่างไร?

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 3 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

ฝั่งไดรว์เวอร์จะมีพฤติกรรมการทำงานเหมือนกับโค้ดด้านล่างนี้

finalRDD.action()=> sc.runJob()

// generate job, stages and tasks=> dagScheduler.runJob()=> dagScheduler.submitJob()=> dagSchedulerEventProcessActor ! JobSubmitted=> dagSchedulerEventProcessActor.JobSubmitted()=> dagScheduler.handleJobSubmitted()=> finalStage = newStage()=> mapOutputTracker.registerShuffle(shuffleId, rdd.partitions.size)=> dagScheduler.submitStage()=> missingStages = dagScheduler.getMissingParentStages()=> dagScheduler.subMissingTasks(readyStage)

// add tasks to the taskScheduler=> taskScheduler.submitTasks(new TaskSet(tasks))=> fifoSchedulableBuilder.addTaskSetManager(taskSet)

// send tasks=> sparkDeploySchedulerBackend.reviveOffers()=> driverActor ! ReviveOffers=> sparkDeploySchedulerBackend.makeOffers()=> sparkDeploySchedulerBackend.launchTasks()=> foreach task CoarseGrainedExecutorBackend(executorId) ! LaunchTask(serializedTask)

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 4 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

คำอธิบาย:

เมื่อโค้ดด้านบนต้องการทราบค่า (มี Action) โปรแกรมไดรว์เวอร์จะมีการสื่อสารระหว่างกันเกิดขึ้นหลายตัวเป็นขบวน เช่น การประมวลผลJob, Threads, Actors เป็นต้น

val sc = new SparkContext(sparkConf)

บรรทัดนี้เป็นการกำหนดหน้าที่ของไดรว์เวอร์

Job logical plan

transformation() ในโปรแกรมไดรว์เวอร์จะสร้าง Chain การคำนวณ (ซีรีย์ของ RDD) ในแต่ละ RDD:

ฟังก์ชัน compute() กำหนดการดำเนินการคำนวณของเรคอร์ดสำหรับพาร์ทิชันของมันฟังก์ชัน getDependencies() กำหนดเกี่ยวกับความสัมพันธ์ของการขึ้นต่อกันทั่วทั้งพาร์ทิชันของ RDD

Job physical plan

แต่ละ action() จะกระตุ้นให้เกิด Job:

ในระหว่างที่ dagScheduler.runJob() Stage จะถูกแยกและกำหนด (แยก Stage ตาม Shuffle ที่ได้อธิบายไปในบทก่อนหน้านี้แล้ว)

ในระหว่างที่ submitStage() , ResultTasks และ ShuffleMapTasks จำเป็นต้องใช้ใน Stage ที่ถูกสร้างขึ้นมา จากนั้นจะถูกห่อไว้ใน TaskSet และส่งไปยัง TaskScheduler ถ้า TaskSet สามารถประมวลผลได้ Task จะถูกส่งไป sparkDeploySchedulerBackend ซึ่งจะกระจาย Task ออกไปทำงาน

การกระจาย Task เพื่อประมวลผลหลังจากที่ sparkDeploySchedulerBackend ได้รับ TaskSet ตัว Driver Actor จะส่ง Task ที่ถูก Serialize แล้วส่งไป CoarseGrainedExecutorBackend Actor บนโหนด Worker

การรับ Job

หลังจากที่ได้รับ Task แล้วโหนด Worker จะทำงานดังนี้:

coarseGrainedExecutorBackend ! LaunchTask(serializedTask)=> executor.launchTask()=> executor.threadPool.execute(new TaskRunner(taskId, serializedTask))

*Executor จะห่อแต่ละ Task เข้าไปใน taskRunner และเลือก Thread ที่ว่างเพื่อให้ Task ทำงาน ตัวโปรเซสของ CoarseGrainedExecutorBackend เป็นได้แค่หนึ่ง Executor *

การประมวลผล Task

แผนภาพด้านล่างแสดงถึงการประมวลผลของ Task เมื่อ Task ถูกรับโดยโหนด Worker และไดรว์เวอร์ประมวลผล Task จนกระทั่งได้ผลลัพธ์ออกมาได้อย่างไร

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 5 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

หลังจากที่ได้รับ Task ที่ถูก Serialize มาแล้ว Executor ก็จะทำการ Deserialize เพื่อแปลงกลับให้เป็น Task ปกติ และหลังจากนั้นจำสั่งให้Task ทำงานเพื่อให้ได้ directResult ซึ่งจะสามารถส่งกลับไปที่ตัว Driver ได้ น่าสังเกตว่าข้อมูลที่ถูกห่อส่งมาจาก Actor ไม่สามารถมีขนาดใหญ่มากได้:

ถ้าผลลัพธ์มีขนาดใหญ่มาก (เช่น หนึ่งค่าใน groupByKey ) มันจะถูก Persist ในหน่วยความจำและฮาร์ดดิสก์และถูกจัดการโดย blockManager ตัวไดรว์เวอร์จะได้เฉพาะข้อมูล indirectResult ซึ่งมีข้อมูลตำแหน่งของแหล่งเก็บข้อมูลอยู่ด้วย และเมื่อมีความจำเป็นต้องใช้ตัวไดรว์เวอร์ก็จะดึงผ่าน HTTP ไปถ้าผลลัพธ์ไม่ได้ใหญ่มาก (น้อยกว่า spark.akka.frameSize = 10MB มันจะถูกส่งโดยตรงไปที่ไดรว์เวอร์

รายละเอียดบางอย่างเพิ่มเติมสำหรับ blockManager :

เมื่อ directResult > akka.frameSize ตัว memoryStorage ของ blockManager จะสร้าง LinkedHashMap เพื่อเก็บข้อมูลที่มีขนาดน้อยกว่า Runtime.getRuntime.maxMemory * spark.storage.memoryFraction (ค่าเริ่มต้น 0.6) เอาไว้ในหน่วยความจำ แต่ถ้า LinkedHashMap ไม่เหลือพื้นที่ว่างพอสำหรับข้อมูลที่เข้ามาแล้ว ข้อมูลเหล่านั้นจะถูกส่งต่อไปยัง diskStore เพื่อเก็บข้อมูลลงในฮาร์ดดิสก์(ถ้า storageLevel ระบุ "disk" ไว้ด้วย)

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 6 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

In TaskRunner.run()// deserialize task, run it and then send the result to => coarseGrainedExecutorBackend.statusUpdate()=> task = ser.deserialize(serializedTask)=> value = task.run(taskId)=> directResult = new DirectTaskResult(ser.serialize(value))=> if( directResult.size() > akkaFrameSize() ) indirectResult = blockManager.putBytes(taskId, directResult, MEMORY+DISK+SER) else return directResult=> coarseGrainedExecutorBackend.statusUpdate(result)=> driver ! StatusUpdate(executorId, taskId, result)

ผลลัพธ์ที่ได้มาจากการทำงานของ ShuffleMapTask และ ResultTask นั้นแตกต่างกัน

ShuffleMapTask จะสร้าง MapStatus ซึ่งประกอบไปด้ว 2 ส่วนคือ:

BlockManagerId ของ BlockManager ของ Task: (executorId + host, port, nettyPort

ขนาดของแต่ละเอาท์พุทของ Task ( FileSegment )

ResultTask จะสร้างผลลัพธ์ของการประมวลผลโดยการเจาะจงฟังก์ชันในแต่ละพาร์ทิชัน เช่น ฟังก์ชันของ count() เป็นฟังก์ชันง่ายๆเพื่อนับค่าจำนวนของเรคอร์ดในพาร์ทิชันหนึ่งๆ เนื่องจากว่า ShuffleMapTask ต้องการใช้ FileSegment สำหรับเขียนข้อมูลลงดิสก์ แลเยมีความต้องการใช้ OutputStream ซึ่งเป็นตัวเขียนข้อมูลออก ตัวเขียนข้อมูลเหล่านี้ถูกสร้างและจัดการโดย blockManager ของ shuffleBlockManager

In task.run(taskId)// if the task is ShuffleMapTask=> shuffleMapTask.runTask(context)=> shuffleWriterGroup = shuffleBlockManager.forMapTask(shuffleId, partitionId, numOutputSplits)=> shuffleWriterGroup.writers(bucketId).write(rdd.iterator(split, context))=> return MapStatus(blockManager.blockManagerId, Array[compressedSize(fileSegment)])

//If the task is ResultTask=> return func(context, rdd.iterator(split, context))

ซีรีย์ของการดำเนินการข้างบนจะทำงานหลังจากที่ไดรว์เวอร์ได้รับผลลัพธของ Task มาแล้ว

TaskScheduler จะได้รับแจ้งว่า Task นั้นเสร็จเรียบร้อยแล้วผลลัพธ์ของมันจะถูกประมวลผล:

ถ้ามันเป็น indirectResult , BlockManager.getRemotedBytes() จะถูกร้องขอเพื่อดึงข้อมูลจากผลลัพธ์จริงๆถ้ามันเป็น ResultTask , ResultHandler() จะร้องขอฝั่งไดรว์เวอร์ให้เกิดการคำนวณบนผลลัพธ์ (เช่น count() จะใช้ sum ดำเนินการกับทุกๆ ResultTask )

ถ้ามันเป็น MapStatus ของ ShuffleMapTask แล้ว MapStatus จำสามารถเพิ่มเข้าใน MapStatuses ของ MapOutputTrackerMaster ซึ่งทำให้ง่ายกว่าในการเรียกข้อมูลในขณะที่ Reduce shuffle

ถ้า Task ที่รับมาบนไดรว์เวอร์เป็น Task สุดท้ายของ Stage แล้ว Stage ต่อไปจะถูกส่งไปทำงาน แต่ถ้า Stage นั้นเป็น Stage สุดท้ายแล้ว dagScheduler จะแจ้งว่า Job ประมวลผลเสร็จแล้ว

After driver receives StatusUpdate(result)=> taskScheduler.statusUpdate(taskId, state, result.value)=> taskResultGetter.enqueueSuccessfulTask(taskSet, tid, result)=> if result is IndirectResult serializedTaskResult = blockManager.getRemoteBytes(IndirectResult.blockId)=> scheduler.handleSuccessfulTask(taskSetManager, tid, result)=> taskSetManager.handleSuccessfulTask(tid, taskResult)=> dagScheduler.taskEnded(result.value, result.accumUpdates)=> dagSchedulerEventProcessActor ! CompletionEvent(result, accumUpdates)=> dagScheduler.handleTaskCompletion(completion)=> Accumulators.add(event.accumUpdates)

// If the finished task is ResultTask

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 7 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

=> if (job.numFinished == job.numPartitions) listenerBus.post(SparkListenerJobEnd(job.jobId, JobSucceeded))=> job.listener.taskSucceeded(outputId, result)=> jobWaiter.taskSucceeded(index, result)=> resultHandler(index, result)

// If the finished task is ShuffleMapTask=> stage.addOutputLoc(smt.partitionId, status)=> if (all tasks in current stage have finished) mapOutputTrackerMaster.registerMapOutputs(shuffleId, Array[MapStatus]) mapStatuses.put(shuffleId, Array[MapStatus]() ++ statuses)=> submitStage(stage)

Shuffle read

ในย่อหน้าก่อนหน้านี้เราได้คุยกันถึงการทำ Task ว่าถูกประมวลผลและมีกระบวนการที่จะได้ผลลัพธ์มาอย่างไร ในตอนนี้เราจะคุยกันเรื่องว่าทำอย่างไร Reducer (Task ที่ต้องการ Shuffle) จึงจะได้รับข้อมูลอินพุท ส่วนของ Shuffle read ในท้ายบทนี้ก็ได้มีการคุยถึงกระบวนการของReducer ที่ทำกับข้อมูลอินพุทมาบ้างแล้ว

ทำอย่างไร Reducer ถึงจะรู้ว่าข้อมูลที่ต้องไปดึงอยู่ตรงไหน?

Reducer ต้องการทราบว่าโหนดในที่ FileSegment ถูกสร้างโดย ShuffleMapTask ของ Stage พ่อแม่ ประเภทของข้อมูลที่จะส่งไปไดรว์เวอร์คือ mapOutputTrackerMaster เมื่อ ShuffleMapTasl ทำงานเสร็จข้อมูลจะถูกเก็บใน mapStatuses:HashMp[stageId,Array[MapStatus]] หากให้ stageId เราก็จะได้ Array[MapStatus] ออกมาซึ่งในนั้นมีข้อมูลที่เกี่ยวกับ FileSegment ทีี่สร้างจาก ShuffleMapTask บรรจุอยู่ Array(taskId) จะมีข้อมูลตำแหน่ง ( blockManagerId ) และขนาดของแต่ละ FileSegment เก็บอยู่

เมื่อ Reducer ต้องการดึงข้อมูลอินพุท มันจะเริ่มจากการร้องขอ blockStoreShuffleFetcher เพื่อขอข้อมูลตำแหน่งของ

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 8 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

FileSegment ต่อมา blockStoreShuffleFetcher จะเรียก MapOutputTrackerWorker บนโหนด Worker เพื่อทำงาน ตัว MapOutputTrackerWorker ใช้ mapOutputTrackerMasterActorRef เพื่อสื่อสารกับ mapOutputTrackerMasterActor ตามลำดับเพื่อรับ MapStatus กระบวนการ blockStoreShuffleFetcher จะประมวลผล MapStatus แล้วจะพบว่าที่ Reducer ต้องไปดึงข้อมูลของ FileSegment จากนั้นจะเก็บข้อมูลนี้ไว้ใน blocksByAddress . blockStoreShuffleFetcher จะเป็นตัวบอกให้ basicBlockFetcherIterator เป็นตัวดึงข้อมูล FileSegment

rdd.iterator()=> rdd(e.g., ShuffledRDD/CoGroupedRDD).compute()=> SparkEnv.get.shuffleFetcher.fetch(shuffledId, split.index, context, ser)=> blockStoreShuffleFetcher.fetch(shuffleId, reduceId, context, serializer)=> statuses = MapOutputTrackerWorker.getServerStatuses(shuffleId, reduceId)

=> blocksByAddress: Seq[(BlockManagerId, Seq[(BlockId, Long)])] = compute(statuses)=> basicBlockFetcherIterator = blockManager.getMultiple(blocksByAddress, serializer)=> itr = basicBlockFetcherIterator.flatMap(unpackBlock)

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 9 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

หลังจากที่ basicBlockFecherIterator ได้รับ Task ของการเรียกดูข้อมูลมันจะสร้าง fetchRequest แต่ละ Request จะประกอบไปด้วย Task ที่จะดึงข้อมูล FileSegment จากหลายๆโหนด ตามที่แผนภาพด้านบนแสดง เราทราบว่า reducer-2 ต้องการดึง FileSegment (ย่อ: FS, แสดงด้วยสีขาว) จากโหนด Worker 3 โหนดการเข้าถึงข้อมูลระดับโกลบอลสามารถเข้าถึงและดึงข้อมูลได้ด้วย blockByAddress : 4 บล๊อคมาจาก node 1 , 3 บล๊อคมาจาก node 2 และ 4 บล๊อคมาจาก node 3

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 10 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

เพื่อที่จะเพิ่มความเร็วการดึงข้อมูลเราสามารถแบ่ง Task ( fetchRequest ) แบบโกลบอลให้เป็น Task แบบย่อยๆ ทำให้แต่ละ Taskสามารถมีหลายๆ Thread เพื่อดึงข้อมูลได้ ซึ่ง Spark กำหนดค่าเริ่มต้นไว้ที่ Thread แบบขนาน 5 ตัวสำหรับแต่ละ Reducer (เท่ากับHadoop) เนื่องจากการดึงข้อมูลมาจะถูกบัฟเฟอร์ไว้ในหน่วยความจำดังนั้นในการดึงข้อมูลหนึ่งครั้งไม่สามารถมีขนาดได้สูงนัก (ไม่มากกว่า spark.reducer.maxMbInFlight 48MB ) โปรดทราบว่า 48MB เป็นค่าที่ใช้ร่วมกันระหว่าง 5 Thread ดังนั้น Task ย่อยจะมีขนาดไม่เกิน 48MB / 5 = 9.6MB จากแผนภาพ node 1 เรามี size(FS0-2) + size(FS1-2) < 9.6MB, แต ่size(FS0-2) +size(FS1-2) + size(FS2-2) > 9.6MB ดังนั้นเราต้องแยกกันระหว่าง t1-r2 และ t2-r2 เพราพขนาดเกินจะได้ผลลัพธ์คือ 2 fetchRequest ที่ดึงข้อมูลมาจาก node 1 จะมี fetchRequest ที่ขนาดใหญ่กว่า 9.6MB ได้ไหม? คำตอบคือได้ ถ้ามี FileSegment ที่มีขนาดใหญ่มากมันก็ยังต้องดึงด้วย Request เพียงตัวเดียว นอกจากนี้ถ้า Reducer ต้องการ FileSegment บางตัวที่มีอยู่แล้วในโหนดโลคอลมันก็จะอ่านที่โลคอลออกมา หลังจากจบ Shuffle read แล้วมันจะดึง FileSegment มา Deserialize แล้วส่งการวนซ้ำของเรคอร์ดไป RDD.compute()

In basicBlockFetcherIterator:

// generate the fetch requests=> basicBlockFetcherIterator.initialize()=> remoteRequests = splitLocalRemoteBlocks()=> fetchRequests ++= Utils.randomize(remoteRequests)

// fetch remote blocks=> sendRequest(fetchRequests.dequeue()) until Size(fetchRequests) > maxBytesInFlight=> blockManager.connectionManager.sendMessageReliably(cmId, blockMessageArray.toBufferMessage)=> fetchResults.put(new FetchResult(blockId, sizeMap(blockId)))=> dataDeserialize(blockId, blockMessage.getData, serializer)

// fetch local blocks=> getLocalBlocks() => fetchResults.put(new FetchResult(id, 0, () => iter))

รายละเอียดบางส่วน:

Reducer ส่ง fetchRequest ไปยังโหนดที่ต้องการได้อย่างไร? โหนดปลายทางประมวลผล fetchRequest ได้อย่างไร? อ่านและส่งกลับ FileSegment ไปยัง Reducer

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 11 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

เมื่อ RDD.iterator() เจอ ShuffleDependency , BasicBlockFetcherIterator จะถูกเรียกใช้เพื่อดึงข้อมูล FileSegment โดย BasicBlockFetcherIterator จะใช้ connectionManager ของ blockManager เพื่อส่ง fetchRequest ไปยัง connectionManager บนโหนดอื่นๆ NIO ใช้สำหรับการติดต่อสื่อสารระหว่าง connnectionManager บนโหนดอื่น ยกตัวอย่างโหนดWorker node 2 จะรับข้อความแล้วส่งต่อข้อความไปยัง blockManager ถัดมาก็ใช้ diskStore อ่าน FileSegment ตามที่ระบุคำร้องขอไว้ใน fetchRequest จากนั้นก็ส่งกลับผ่าน connectionManager และถ้าหากว่า FileConsolidation ถูกกำหนดไว้ diskStore จะต้องการตำแหน่งของ blockId ที่ได้รับจาก shuffleBolockManager ถ้า FileSegment มีขนาดไม่เกิน spark.storage.memoryMapThreshold = 8KB แล้ว diskStore จะวาง FileSegment ไว้ในหน่วยความจำในขณะที่กำลังอ่านข้อมูลอยู่ ไม่อย่างนั้นแล้วเมธอตใน FileChannel ของ RandomAccessFile ซึ่งจะ Mapping หน่วยความจำไว้ทำให้สามารถอ่าน FileSegment ขนาดใหญ่เข้ามาในหน่วยความจำได้

และเมื่อไหร่ที่ BasicBlockFetcherIterator ได้รับ Serialize ของ FileSegment จากโหนดอื่นแล้วมันจะทำการ Deserialize และส่งไปใน fetchResults.Queue มีข้อควรทราบอย่างหนึ่งก็คือ fetchResults.Queue คล้ายกัน softBuffer ในรายละเอียดของบทที่เป็น Shuffle ถ้า FileSegment ต้องการโดย BasicBlockFetcherIterator บนโหนดนั้นมันจะสามารถหาได้จาก diskStore ในโหนดนั้นและวางใน fetchResult , สุดท้ายแล้ว Reducer จะอ่านเรคอร์ดจาก FileSegment และประมวลผลมัน

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

10/30/2559 BE, 1,23 PMSparkInternals/5-Architecture.md at thai · Aorjoa/SparkInternals

Page 12 of 12https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/5-Architecture.md

After the blockManager receives the fetch request

=> connectionManager.receiveMessage(bufferMessage)=> handleMessage(connectionManagerId, message, connection)

// invoke blockManagerWorker to read the block (FileSegment)=> blockManagerWorker.onBlockMessageReceive()=> blockManagerWorker.processBlockMessage(blockMessage)=> buffer = blockManager.getLocalBytes(blockId)=> buffer = diskStore.getBytes(blockId)=> fileSegment = diskManager.getBlockLocation(blockId)=> shuffleManager.getBlockLocation()=> if(fileSegment < minMemoryMapBytes) buffer = ByteBuffer.allocate(fileSegment) else channel.map(MapMode.READ_ONLY, segment.offset, segment.length)

Reducer ทุกตัวจะมี BasicBlockFetcherIterator และ BasicBlockFetcherIterator แต่ละตัวจะสามารถถือข้อมูล fetchResults ได้ 48MB ในทางทฤษฏี และในขณะเดียวกัน FileSegment ใน fetchResults บางตัวอาจจะทำให้เต็ม 48MB ได้เลย

BasicBlockFetcherIterator.next()=> result = results.task()=> while (!fetchRequests.isEmpty && (bytesInFlight == 0 || bytesInFlight + fetchRequests.front.size <= maxBytesInFlight)) { sendRequest(fetchRequests.dequeue()) }=> result.deserialize()

การพูดคุยในเรื่องของการออกแบบสถาปัตยกรรม การใช้งาน และโมดูลเป็นส่ิงที่แยกจากกันเป็นอิสระได้อย่างดี BlockManager ถูกออกแบบมาอย่างดี แต่มันดูเหมือนจะถูกออกแบบมาสำหรับจัดการของหลายสิ่ง (บล๊อคข้อมูล, หน่วยความจำ, ดิสก์ และการติดต่อสื่อสารกันระหว่างเครือข่าย)

ในบทนี้คุยกันเรื่องว่าโมดูลในระบบของ Spark แต่ละส่วนติดต่อประสานงานกันอย่างไรเพื่อให้งานเสร็จ (Production, Submision,Execution, Result collection Result computation และ Shuffle) โค้ดจำนวนมากถูกวางไว้และแผนภาพจะนวนมากที่ถูกวาดขึ้น ซึ่งรายละเอียดจะแสดงในโค้ดถ้าหากต้องการดู

รายละเอียดของ BlockManager สามารถอ่านเพิ่มเติมได้จากบล๊อคภาษาจีนที่ blog

Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help

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

10/30/2559 BE, 1,24 PMSparkInternals/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 / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d an hour ago Aorjoa 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, การเรียกดูข้อมูลแบบมีปฏิสัมพันธ์ เป็นต้น ซึ่งแตกต่างกับ HadoopMapReduce 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 7581 Unwatch Star Fork

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 2 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

Physical plan

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 3 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

Q: RDD ประเภทไหนที่เราต้องแคชเอาไว้ ?

พวก RDD ที่มีการประมวลผลซ้ำๆ และก็ไม่ใหญ่มาก

Q: จะแคช RDD ได้อย่างไร ?

แค่สั่ง rdd.cache() ในโปรแกรมไดรว์เวอร์ เมื่อ rdd ที่เข้าถึงได้จากผู้ใช้งานแล้ว เช่น RDD ที่ถูกสร้างโดย transformation() จะสามารถแคชจากผู้ใช้ได้ แต่บาง RDD ที่สร้างโดย Spark ผู้ใช้ไม่มาสารถเข้าถึงได้จึงไม่สามารถแคชโดยผู้ใช้ได้ เช่น ShuffledRDD , MapPartitionsRDD ขณะที่ทำงาน reduceByKey() เป็นต้น

Q: Spark แคช RDD ได้อย่างไร ?

เราสามารถลองเดาดูอย่างที่เราสังหรณ์ใจว่าเมื่อ Task ได้รับเรคอร์ดแรกของ RDD แล้วมันจะทดสอบว่า RDD สามารถแคชไว้ได้หรือเปล่าถ้าสามารถทำได้เรคอร์ดและเรคอร์ดที่ตามมาจะถูกส่งไปยัง memoryStore ของ blockManager และถ้า memoryStore ไม่สามารถเก็บทุกเรคอร์ดไว้นหน่วยความจำได้ diskStore จะถูกใช้แทน

การนำไปใช้นั้นคล้ายกับสิ่งเท่าเราเดาไว้ แต่มีบางส่วนที่แตกต่าง Spark จะทดสอบว่า RDD สามารถแคชได้หรือเปล่าแค่ก่อนที่จะทำการประมวลผลพาร์ทิชันแรก และถ้า RDD สามารถแคชได้ พาร์ทิชันจะถูกประมวลผลแล้วแคชไว้ในหน่วยความจำ ซึ่ง cache ใช้หน่วยความจำเท่านั้น หากต้องการจะเขียนลงดิสก์จะเรียกใช้ checkpoint

หลังจากที่เรียกใช้งาน rdd.cache() แล้ว rdd จะกลายเป็น persistRDD ซึ่งมี storageLevel เป็น MEMORY_ONLY ตัว persistRDD จะบอก driver ว่ามันต้องการที่จะ Persist

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 4 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

แผนภาพด้านดนสามารถแสดงได้ในโค้ดนี้:

rdd.iterator()=> SparkEnv.get.cacheManager.getOrCompute(thisRDD, split, context, storageLevel)=> key = RDDBlockId(rdd.id, split.index)=> blockManager.get(key)=> computedValues = rdd.computeOrReadCheckpoint(split, context) if (isCheckpointed) firstParent[T].iterator(split, context) else compute(split, context)=> elements = new ArrayBuffer[Any]=> elements ++= computedValues=> updatedBlocks = blockManager.put(key, elements, tellMaster = true)

เมื่อ rdd.iterator() ถูกเรียกใช้เพื่อประมวลผลในบางพาร์ทิชันของ rdd แล้ว blockId จะถูกใช้เพื่อกำหนดว่าพาร์ทิชันไหนจะถูกแคช เมื่อ blockId มีชนิดเป็น RDDBlockId ซึ่งจะแตกต่างกับชนิดของข้อมูลอื่นที่อยู่ใน memoryStore เช่น result ของ Task จากนั้นพาร์ทิชันใน blockManager จะถูกเช็คว่ามีการ Checkpoint แล้ว ถ้าเป็นเช่นนั้นแล้วเราก็จะสามารถพูดได้ว่า Task ถูกทำงานเรียบร้อยแล้วไม่ได้ต้องการทำการประมวลผลบนพาร์ทิชันนี้อีก elements ที่มีชนิด ArrayBuffer จะหยิบทุกเรคอร์ดของพาร์ทิชันมาจากCheckpoint ถ้าไม่เป็นเช่นนั้นแล้วาร์ทิชันจะถูกประมวลผลก่อน แล้วทุกเรคอร์ดของมันจะถูกเก็บลงใน elements สุดท้ายแล้ว elements

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 5 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

จะถูกส่งไปให้ blockManager เพื่อทำการแคช

blockManager จะเก็บ elements (partition) ลงใน LinkedHashMap[BlockId, Entry] ที่อยู่ใน memoryStore ถ้าขนาดของพาร์ทิชันใหญ่กว่าขนาดของ memoryStore จะจุได้ (60% ของขนาด Heap) จะคืนค่าว่าไม่สามารถที่จะถือข้อมูลนี้ไว้ได้ ถ้าขนาดไม่เกินมันจะทิ้งบางพาร์ทิชันของ RDD ที่เคยแคชไว้แล้วเพื่อที่จะทำให้มีที่ว่างพอสำหรับพาร์ทิชันใหม่ที่จะเข้ามา และถ้าพื้นที่มีมากพอพาร์ทิชันที่เข้ามาใหม่จะถูกเก็บลลงใน LinkedHashMap แต่ถ้ายังไม่พออีกระบบจะส่งกลับไปบอกว่าพื้นที่ว่างไม่พออีกครั้ง ข้อควรรู้สำหรับพาร์ทิชันเดิมที่ขึ้นกับ RDD ของพาร์ทิชันใหม่จะไม่ถูกทิ้ง ในอุดมคติแล้ว "first cached, first dropped"

Q: จะอ่าน RDD ที่แคชไว้ยังไง ?

เมื่อ RDD ที่ถูกแคชไว้แล้วต้องการที่จะประมวลผลใหม่อีกรอบ (ใน Job ถัดไป), Task จะอ่าน blockManager โดยตรงจาก memoryStore , เฉพาะตอนที่อยู่ระหว่างการประมวลผลของบางพาร์ทิชันของ RDD (โดยการเรียก rdd.iterator() ) blockManager จะถูกเรียกถามว่ามีแคชของพาร์ทิชันหรือยัง ถ้ามีแล้วและอยู่ในโหนดโลคอลของมันเอง blockManager.getLocal() จะถูกเรียกเพื่ออ่านข้อมูลจาก memoryStore แต่ถ้าพาร์ทิชันถูกแคชบนโหนดอื่น blockManager.getRemote() จะถูกเรียก ดังแสดงด้านล่าง:

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 6 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

ตำแหน่งของเหล่งเก็บข้อมูลพาร์ทิชันชันที่ถูกแคช: ตัว blockManager ของโหนดซึ่งพาร์ทชันถูกแคชเก็บไว้อยู่จะแจ้งไปยัง blockManagerMasterActor บนโหนด Master วาสพาร์ทิชันถูกแคชอยู่ซึ่งข้อมูลถูกเก็บอยู่ในรูปของ blockLocations: HashMap ของ blockMangerMasterActor เมื่อ Task ต้องการใช้ RDD ที่แคชไว้มันจะส่ง blockManagerMaster.getLocations(blockId) เป็นคำร้องไปยังไดรว์เวอร์เพื่อจะขอตำแหน่งของพาร์ทิชัน จากนั้นไดรว์เวอร์จะมองหาใน blockLocations เพื่อส่งข้อมูลตำแหน่งกลับไป

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 7 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

การพาร์ทิชันที่ถูกแคชไว้บนโหนดอื่น: เมื่อ Task ได้รัยข้อมูลตำแหน่งของพาร์ทิชันที่ถูกแคชไว้แล้วว่าอยู่ตำแหน่งใดจากนั้นจะส่ง getBlock(blockId) เพื่อร้องขอไปยังโหนดปลายทางผ่าน connectionManager โหนดปลายทางก็จะรับและส่งกลับพาร์ทิชันที่ถูกแคชไว้แล้วจาก memoryStore ของ blockManager บนตัวมันเอง

Checkpoint

Q: RDD ประเภทไหนที่ต้องการใช้ Checkpoint ?

การประมวลผล Task ใช้เวลานาน

Chain ของการประมวลผลเป็นสายยาว

ขึ้นต่อหลาย RDD

ในความเป็นจริงแล้วการบันทึกข้อมูลเอาท์พุทจาก ShuffleMapTask บนโหนดโลคอลก็เป็นการ checkpoint แต่นั่นเป็นการใช้สำหรับข้อมูลที่เป็นข้อมูลเอาท์พุทของพาร์ทิชัน

Q: เมื่อไหร่ที่จะ Checkpoint ?

อย่างที่ได้พูดถึงข้างบนว่าทุกครั้งที่พาร์ทิชันที่ถูกประมวลผลแล้วต้องการที่จะแคชมันจะแคชลงไปในหน่วยความจำ แต่สำหรับ checkpoint() มันไม่ได้เป็นอย่างนั้น เพราะ Checkpoint ใช้วิธีรอจนกระทั่ง Job นั้นทำงานเสร็จก่อนถึงจะสร้าง Job ใหม่เพื่อมาCheckpoint RDD ที่ต้องการ Checkpoint จะมีการประมวลผลของงานใหม่อีกครั้ง ดังนั้นจึงแนะนำให้สั่ง rdd.cache() เพื่อแคชข้อมูลเอาไว้ก่อนที่จะสั่ง rdd.checkpoint() ในกรณีนี้งานที่ Job ที่สองจะไม่ประมวลผลซ้ำแต่จะหยิบจากที่เคยแคชไว้มาใช้ ซึ่งในความจริงSpark มีเมธอต rdd.persist(StorageLevel.DISK_ONLY) ให้ใช้เป็นลักษณะของการแคชลงไปบนดิสก์ (แทนหน่วยความจำ) แต่ชนิดของ persist() และ checkpoint() มีความแตกต่างกัน เราจะคุยเรื่องนี้กันทีหลัง

Q: นำ Checkpoint ไปใช้ได้อย่างไร ?

นี่คือขั้นตอนการนำไปใช้

RDD จะเป็น: [ เริ่มกำหนดค่า --> ทำเครื่องหมายว่าจะ Checkpoint --> ทำการ Checkpoint --> Checkpoint เสร็จ ]. ในขั้นตอนสุดท้ายRDD ก็จะถูก Checkpoint แล้ว

เริ่มกำหนดค่า

ในฝั่งของไดรว์เวอร์หลังจากที่ rdd.checkpoint() ถูกเรียกแล้ว RDD จะถูกจัดการโดย RDDCheckpointData ผู้ใช้สามารถตั้งค่าแหล่งเก็บข้อมูลชี้ไปที่ตำแหน่งที่ต้องให้เก็บไว้ได้ เช่น บน HDFS

ทำเครื่องหมายว่าจะ Checkpoint

หลังจากที่เริ่มกำหนดค่า RDDCheckpointData จะทำเครื่องหมาย RDD เป็น MarkedForCheckpoint

ทำการ Checkpoint

เมื่อ Job ประมวลผลเสร็จแล้ว finalRdd.doCheckpoint() จะถูกเรียกใช้ finalRdd จำสแกน Chain ของการประมวลผลย้อนกลับไปและเมื่อพบ RDD ที่ต้องการ Checkpoint แล้ว RDD จะถูกทำเครื่องหมาย CheckpointingInProgress จากนั้นจะตั้งค่าไฟล์ (สำหรับเขียนไปยัง HDFS) เช่น core-site.xml จะถูก Broadcast ไปยัง blockManager ของโหนด Worker อื่นๆ จากนั้น Job จะถูกเรียกเพื่อทำCheckpoint ให้สำเร็จ

rdd.context.runJob(rdd, CheckpointRDD.writeToFile(path.toString, broadcastedConf))

Checkpoint เสร็จ

หลังจากที่ Job ทำงาน Checkpoint เสร็จแล้วมันจะลบความขึ้นต่อกันของ RDD และตั้งค่า RDD ไปยัง Checkpoint จากนั้น เพื่มการขึ้นต่อกันเสริมเข้าไปและตั้งค่าให้ RDD พ่อแม่มันเป็น CheckpointRDD ตัว CheckpointRDD จะถูกใช้ในอนาคตเพื่อที่จะอ่านไฟล์ Checkpointบนระบบไฟล์แล้วสร้างพาร์ทิชันของ RDD

อะไรคือสิ่งที่น่าสนใจ:

RDD สองตัวถูก Checkpoint บนโปรแกรมไดรว์เวอร์ แต่มีแค่ result (ในโค้ดด่านล่าง) เท่านั้นที่ Checkpoint ได้สำเร็จ ไม่แน่ใจว่าเป็นBug หรือเพราะว่า RDD มันตามน้ำหรือจงใจให้เกิด Checkpoint กันแน่

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 8 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

val data1 = Array[(Int, Char)]((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'), (3, 'f'), (2, 'g'), (1, 'h'))val pairs1 = sc.parallelize(data1, 3)

val data2 = Array[(Int, Char)]((1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'))val pairs2 = sc.parallelize(data2, 2)

pairs2.checkpoint

val result = pairs1.join(pairs2)result.checkpoint

Q: จะอ่าน RDD ที่ถูก Checkpoint ไว้อย่างไร ?

runJob() จะเรียกใช้ finalRDD.partitions() เพื่อกำหนดว่าจะมี Task เกิดขึ้นเท่าไหร่. rdd.partitions() จะตรวจสอบว่าถ้าRDD ถูก Checkpoint ผ่าน RDDCheckpointData ซึ่งจัดการ RDD ที่ถูก Checkpoint แล้ว, ถ้าใช่จะคืนค่าพาร์ทิชันของ RDD( Array[Partition] ). เมื่อ rdd.iterator() ถูกเรียกใช้เพื่อประมวลผลพาร์ทิชันของ RDD, computeOrReadCheckpoint(split:Partition) ก็จะถูกเรียกด้วยเพื่อตรวจสอบว่า RDD ถูก Checkpoint แล้ว ถ้าใช่ iterator() ของ RDD พ่อแม่จะถูกเรียก (รูจักกันในชื่อ CheckpointRDD.iterator() จะถูกเรียก) CheckpointRdd จะอ่านไฟล์บนระบบไฟล์เพื่อที่จะสร้างพาร์ทิชันของ RDD นั่นเป็นเคล็ดลับที่ว่าทำไม CheckpointRDD พ่อแม่จึงถูกเพิ่มเข้าไปใน RDD ที่ถูก Checkpoint ไว้แล้ว

Q: ข้อแตกต่างระหว่าง cache และ checkpoint ?

นี่คือคำตอบที่มาจาก Tathagata Das:

มันมีความแตกต่างกันอย่างมากระหว่าง cache และ checkpoint เนื่องจากแคชนั้นจะสร้าง RDD และเก็บไว้ในหน่วยความจำ (และ/หรือดิสก์) แต่ Lineage (Chain ของการกระมวลผล) ของ RDD (มันคือลำดับของการดำเนินการบน RDD) จะถูกจำไว้ ดังนั้นถ้าโหนดล้มเหลวไปและทำให้บางส่วนของแคชหายไปมันสามารถที่จะคำนวณใหม่ได้ แต่อย่างไรก็ดี Checkpoint จะบันทึกข้อมูลของ RDD ลงเป็นไฟล์ในHDFS และจะลืม Lineage อย่างสมบูรณ์ ซึ่งอนุญาตให้ Lineage ซึ่งมีสายยาวถูกตัดและข้อมูลจะถูกบันทึกไว้ใน HDFS ซึ่งมีกลไกการทำสำเนาข้อมูลเพื่อป้องกันการล้มเหลวตามธรรมชาติของมันอยู่แล้ว

นอกจากนี้ rdd.persist(StorageLevel.DISK_ONLY) ก็มีความแตกต่างจาก Checkpoint ลองนึกถึงว่าในอดีตเราเคย Persist พาร์ทิชันของ RDD ไปยังดิสก์แต่ว่าพาร์ทิชันของมันถูกจัดการโดย blockManager ซึ่งเมื่อโปรแกรมไดรว์เวอร์ทำงานเสร็จแล้ว มันหมายความว่า CoarseGrainedExecutorBackend ก็จะหยุดการทำงาน blockManager ก็จะหยุดตามไปด้วย ทำให้ RDD ที่แคชไว้บนดิสก์ถูกทิ้งไป(ไฟล์ที่ถูกใช้โดย blockManager จะถูกลบทิ้ง) แต่ Checkpoint สามารถ Persist RDD ไว้บน HDFS หรือโลคอลไดเรกทอรี่ ถ้าหากเราไม่ลบมือเองมันก็จะอยู่ไปในที่เก็บแบบนั้นไปเรื่อยๆ ซึ่งสามารถเรียกใช้โดยโปรแกรมไดรว์เวอร์อื่นถัดไปได้

การพูดคุยเมื่อครั้งที่ Hadoop MapReduce ประมวลผล Job มันจะ Persist ข้อมูล (เขียนลงไปใน HDFS) ตอนท้ายของการประมวลผล Task ทุกๆTask และทุกๆ Job เมื่อมีการประมวลผล Task จะสลับไปมาระหว่างหน่วยความจำและดิสก์. ปัญหาของ Hadoop ก็คือ Task ต้องการที่จำประมวลผลใหม่เมื่อมี Error เกิดขึ้น เช่น Shuffle ที่หยุดเมื่อ Error จะทำให้ข้อมูลที่ถูก Persist ลงบนดิสก์มีแค่ครึ่งเดียวทำให้เมื่อมีการShuffle ใหม่ก็ต้อง Persist ข้อมูลใหม่อีกครั้ง ซึ่ง Spark ได้เรียบในข้อนี้เนื่องจากหากเกิดการผิดพลาดขึ้นจะมีการอ่านข้อมูลจากCheckpoint แต่ก็มีข้อเสียคือ Checkpoint ต้องการการประมวลผล Job ถึงสองครั้ง

Example

package internals

import org.apache.spark.SparkContextimport org.apache.spark.SparkContext._import org.apache.spark.SparkConf

object groupByKeyTest {

def main(args: Array[String]) { val conf = new SparkConf().setAppName("GroupByKey").setMaster("local") val sc = new SparkContext(conf) sc.setCheckpointDir("/Users/xulijie/Documents/data/checkpoint")

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

10/30/2559 BE, 1,24 PMSparkInternals/6-CacheAndCheckpoint.md at thai · Aorjoa/SparkInternals

Page 9 of 9https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/6-CacheAndCheckpoint.md

val data = Array[(Int, Char)]((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'), (3, 'f'), (2, 'g'), (1, 'h') ) val pairs = sc.parallelize(data, 3)

pairs.checkpoint pairs.count

val result = pairs.groupByKey(2)

result.foreachWith(i => i)((x, i) => println("[PartitionIndex " + i + "] " + x))

println(result.toDebugString) }}

Contact GitHub API Training Shop Blog About© 2016 GitHub, Inc. Terms Privacy Security Status Help

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

10/30/2559 BE, 1,27 PMSparkInternals/7-Broadcast.md at thai · Aorjoa/SparkInternals

Page 1 of 4https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/7-Broadcast.md

This repository Pull requests Issues Gist

SparkInternals / markdown / thai / 7-Broadcast.md

Search

Aorjoa / SparkInternalsforked from JerryLead/SparkInternals

Code Pull requests 0 Projects 0 Wiki Pulse Graphs Settings

thai Branch: Find file Copy path

1 contributor

1a5512d an hour ago Aorjoa fixed some typo and polish some word

91 lines (48 sloc) 23.3 KB

Broadcast

เหมือนกับชื่อมันโดยปริยายไปเลย คือ Broadcast ที่หมายถึงการส่งข้อมูลจากโหนดหนึ่งไปยังโหนดอื่นทุกโหนดในคลัสเตอร์ มันมีประโยชน์มากในหลายสถานการณ์ ยกตัวอย่างเรามีตารางหนึ่งในไดรว์เวอร์แล้วโหนดอื่นทุกโหนดต้องการที่จะอ่านค่าจากตารางนั้น ถ้าใช้Broadcast เราจะสามารถส่งตารางไปทุกโหนด เสร็จแล้ว Task ที่ทำงานอยู่บนโหนดนั้นๆ ก็สามารถที่อ่านค่าได้ภายในโหนดโลคอลของมันเอง จริงๆ แลวกลไกนี้มันยากและท้าทายที่จะนำไปใช้อย่างมีความน่าเชื่อถือและมีประสิทธิภาพ ในเอกสารของ Spark บอกว่า:

ตัวแปร Broadcast อนุญาตให้นักพัฒนาโปรแกรมยังคงแคชตัวแปรแบบอ่านอย่างเดียว (Read-only)ไว้ในแต่ละเครื่องมากกว่าที่จะส่งมันไปกับ Task ยกตัวอย่างที่สามารถใช้คุณสมบัตินี้ได้ เช่น การให้ทุกๆโหนดมีสำเนาของเซ็ตของข้อมูลขนาดใหญ่ในขณะที่สามารถจัดการได้อย่างมีประสิทธิภาพ Spark ก็พยายามที่จะกระจายตัวแปร Broardcast อย่างมีประสิทธิภาพโดยใช้ขั้นตอนวิธีการBroadcast ที่มีประสิทธิภาพเพื่อลดค่าใช้จ่ายของการติดต่อสื่อสาร

ทำไมต้อง read-only?

นี่เป็นปัญหาเรื่อง Consistency ถ้าตัวแปร Broadcast สามารถที่จะเปลี่ยนแปลงค่าหรือ Mutated ได้แล้ว ถ้ามีการเปลี่ยนแปลงที่โหนดใดโหนดหนึ่งเราจะต้องอัพเดททุกๆโหนดด้วย และถ้าหลายๆโหรดต้องการอัพเดทสำเนาของตัวแปรที่อยู่กับตัวเองหละเราะจะทำอย่างไรเพื่อที่จะทำให้มันประสานเวลากันและอัพเดทได้ย่างอิสระ? ไหนจะปัญหา Fualt-tolerance ที่จะตามมาอีก เพื่อหลีกเลี่ยงปัญหาเหล่านี้ Spark จะสนับสนุนแค่การใช้ตัวแปร Broadcast แบบอ่านอย่างเดียวเท่านั้น

ทำไม Brodcast ไปที่โหนดแทนที่จะเป็น Task?

เนื่องจากแต่ละ Task ทำงานภายใน Thread และทุกๆ Task ประมวลผลได้แค่กับแอพพลิเคชันของ Spark เดียวกันดังนั้นการทำสำเนาBroadcast ตัวเดียวไว้บนโหนด (Executor) สามารถแบ่งปันกันใช้ได้กับทุก Task

จะใช้ Broadcast ได้อย่างไร?

ตัวอย่างโปรแกรมไดรว์เวอร์:

val data = List(1, 2, 3, 4, 5, 6)val bdata = sc.broadcast(data)

val rdd = sc.parallelize(1 to 6, 2)val observedSizes = rdd.map(_ => bdata.value.size)

ไดรว์เวอร์สามารถใช้ sc.broadcast() เพื่อที่จะประกาศข้อมูลที่จะถูก Broadcast จากตัวอย่างข้างบน bdata คือ Broadcast ตัว rdd.transformation(func) จะใช้ bdata โดยตรงภายในฟังก์ชันเหมือนกับเป็นตัวแปรโลคอลของมันเอง

Broadcast นำไปใช้งานด้อย่างไร?

การดำเนินงานของหลังจากการ Broadcast ไปแล้วน่าสนใจมาก

Raw Blame History

0 7581 Unwatch Star Fork

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

10/30/2559 BE, 1,27 PMSparkInternals/7-Broadcast.md at thai · Aorjoa/SparkInternals

Page 2 of 4https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/7-Broadcast.md

การกระจาย Metadata ของตัวแปร Broadcast

ไดรว์เวอร์จะสร้างโลคอลไดเรกทอรี่เพื่อที่จะเก็ยข้อมูลที่ได้มาจากการ Broadcast และเรียกใช้ HttpServer เพื่อเข้าใช้งานไดเรกทอรี่นี้โดยข้อมูลจะเขียนลงไปในไดเรกเทอรี่นี้จริงๆเมื่อ Broadcast มีการเรียกใช้ ( val bdata = sc.broadcast(data) ) ในขณะเดียวกันข้อมูลก็ถูกเขียนไปที่ไดรว์เวอร์ในส่วนของ blockManager ด้วยโดยกำหนดระดับของ StorageLevel เป็นหน่วยความจำ + ดิสก์. Block

Manager จะจัดสรร blockId (ด้วยชนิด BroadcastBlockId ) สำหรับข้อมูลและเมื่อฟังก์ชันการแปลงใช้ตัวแปร Broadcast ตัว submitTask() ของไดรว์เวอร์จะ Serialize ข้อมูล Metadata ของมัน แล้วจึงส่ง Matadata ที่ Serialize พร้อมกับฟังก์ชันที่ถูก Serialize

ไปทุกโหนด. ระบบ Akka มีการกำหนดขนาดของข้อความให้มีขนาดจำกัด ทำให้เราไม่สามารถที่จะส่งข้อมูลจริงๆไปได้ในการ Broadcast

ทำไมไดรว์เวอร์ต้องมีการเก็บข้อมูลไว้ทั้งในโลคอลไดเรกทอรี่และ Block manager? การที่เก็บข้อมูลไว้ในโลคอลไดเรกทอรี่ใช้สำหรับ HttpServer และการเก็บข้อมูลไว้ใน Block Manager นั้นสะดวกกว่าสำหรับการใช้ข้อมูลภายในโปรแกรมไดรว์เวอร์

แล้วเมื่อไหร่ที่ข้อมูลจริงๆจะถูก Broadcast เมื่อ Executor ได้ Deserialize Task ที่ได้รับมาและมันจะได้ Metadata ของตัวแปร Broadcast

มาด้วยในรูปแบบของวัตถุ Broadcast จากนั้นแล้วจะเรียกเมธอต readObject() ของวัตถุ Metadata (ตัวแปร bdata ) ในเมธอตนี้จะมีการตรวจสอบก่อนเป็นอันดับแรกว่าใน Block manager ของตัวมันเองมีสำเนาอยู่แล้วหรือเปล่า ถ้าไม่มีมันถึงจะดึงมาจากไดรว์เวอร์มาเก็บไว้ที่ Block manager สำหรับการใช้งานที่จะตามมา

Spark มีการดำเนินงานในการดึงข้อมูลอยู่ 2 แบบที่แตกต่างกัน

HttpBroadcast

วิธีการนี้จะดึงข้อมูลผ่านทางโพรโตคอลการเชื่อมต่อ HTTP ระหว่าง Executor และไดรว์เวอร์

ไดรว์เวอร์จะสร้างวัตถุของ HttpBroadcast ขึ้นมาเป็นเพื่อเก็บข้อมูลที่จะ Broadcast ไว้ใน Block manager ของไดรว์เวอร์ ในขณะเดียวกันข้อมูลจะถูกเขียนลงในโลคอลดิสก์ที่เป็นไดเรกทอรี่อย่างที่เคยอธิบายไว้ก่อนหน้านี้แล้ว ยกตัวอย่างชื่อของไดเรกทอรี่ เช่น /var/folders/87/grpn1_fn4xq5wdqmxk31v0l00000gp/T/spark-6233b09c-3c72-4a4d-832b-6c0791d0eb9c/broadcast_0

ไดรว์เวอร์และ Executor จะสร้างวัตถุ broadcastManager ในระยะเริ่มต้น และไดเรกทอรี่จะถูกสร้างโดยการสั่งเมธอต HttpBroadcast.initialize() ซึ่งเมธอตนี้ก็จะสั่งให้ HTTP server ทำงานด้วย

การดึงข้อมูลที่เป็นข้อมูจริงๆนั้นเกิดขึ้นจากการส่งผ่านข้อมูลระหว่างโหนดสองโหนดผ่านการเชื่อมต่อแบบโปรโตคอล HTTP

ปัญหาก็คือ HttpBroadcast มีข้อจำกัดเรื่องคอขวดของเครือข่ายในโหนดที่ทำงานเป็นไดรว์เวอร์เนื่องจากมันต้องส่งข้อมูลไปยังโหนดอื่นทุกๆโหนดในเวลาเดียวกัน

TorrentBroadcast

เพื่อที่จะแก้ปัญหาคอขวดที่เกิดกับระบบเครือข่ายของไดรว์เวอร์ใน HttpBroadcast ดังนั้น Spark จึงได้มีการนำเสนอวิธี Broadcast แบบใหม่ที่ชื่อว่า TorrentBroadcast ซึ่งได้รับแรงบัลดาลใจมาจาก BitTorrent หลักการง่ายของวิธีการนี้ก็คือจะเอาข้อมูลที่ต้องการBroadcast หั่นเป็นบล๊อค และเมื่อ Executor ตัวไหนได้รับข้อมูลบล๊อคนั้นแล้วจะสามารถทำตัวเป็นแหล่งข้อมูลให้คนอื่นต่อได้

ไม่เหมือนกับการโอนถ่ายข้อมูลใน HttpBroadcast ตัว TorrentBroadcast จะใช้ blockManager.getRemote() => NIOConnectionManager เพื่อทำงานและการรับ-ส่งข้อมูลจริงๆจะคล้ายกันอย่างมากกับการแคช RDD ที่เราคุยจะกันในบทสุดท้ายนี้ (ดูแผนภาพใน CacheAndCheckpoint).

รายละเอียดบางอย่างใน TorrentBroadcast :

Driver

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

10/30/2559 BE, 1,27 PMSparkInternals/7-Broadcast.md at thai · Aorjoa/SparkInternals

Page 3 of 4https://github.com/Aorjoa/SparkInternals/blob/thai/markdown/thai/7-Broadcast.md

ไดรว์เวอร์จะ Serialize ข้อมูลให้อยู่ในรูปของ ByteArray และตัดออกจากกันตามขนาดของ BLOCK_SIZE (กำหนดโดย spark.broadcast.blockSize = 4MB ) เป็นบล๊อค หลังจากที่จัด ByteArray แล้วตัวเดิมมันก็ยังจะค้าางอยู่ชั่วคราวดังนั้นเราจะมี 2

สำเนาของข้อมูลอยู่ในหน่วยความจำ

หลังจากที่เราตัดแบ่งแล้วข้อมูลบางส่วนที่เกี่ยวกับบล๊อค (เรียกว่า Metadata) จะถูกเก็บไว้ใน Block manager ของไดรว์เวอร์ที่ระดับการเก็บเป็นหน่วยความจำ + ดิสก์ ซึ่งพอถึงตอนนี้ blockManagerMaster จะแจ้งว่า Metadata ถูกเก็บเรียบร้อยแล้ว ขั้นตอนนี้สำคัญมากเนื่องจาก blockManagerMaster สามารถถูกเข้าถึงได้จากทุกๆ Executor นั่นหมายความว่าบล๊อคของ Metadata จะกลายเป็นข้อมูลโกลบอลของคลัสเตอร์

ไดรว์เวอร์จะจบการทำงานของมันโดยเก็บบล๊อคข้แมูลที่อยู่ภายใช้ Block manager ไว้ในแหล่งเก็บข้อมูลทางกายภาพ

Executor

เมื่อได้รับ Task ที่ Serialize มาแล้ว Executor จะทำการ Deserialize กลับเป็นอันดับแรกซึ่งการ Deserialize ก็รวมไปถึง Metadata ที่Broadcast มาแล้ว ถ้ามีประเภทเป็น TorrentBroadcast แล้วมันจะถูกเรียกเมธอต TorrentBroadcast.readObject() คล้ายกับขั้นตอนที่เคยได้กล่าวถึงในด้านบน จากนั้น Block manager ที่อยู่โลคอลจะตรวจสอบดูก่อนว่ามีบล๊อคข้อมูลไหนที่ถูกดึงมาอยู่ในเครื่องอยู่แล้วถ้าไม่มี Executor จะถามไปที่ blockManagerMaster เพื่อขอ Metadata ของบล๊อคข้อมูลแล้วหลังจากนั้นกระบวน BitTorrent จึงจะถูกเริ่มเพื่อดึงบล๊อคข้อมูล

กระบวนการ BitTorrent: ตัว arrayOfBlocks = new Array[TorrentBlock](totalBlocks) จะถูกจัดสรรบนโลคอลโหนดเพื่อใช้เก็บข้อมูลที่ถูกดึงมา แล้ว TorrentBlock จะห่อบล๊อคข้อมูลไว้. ลำดับของการดึงข้อมูลนั้นจะเป็นแบบสุ่ม ยกตัวอย่าง เช่น ถ้ามี 5 บล๊อคมันอาจจะเป็น 3-1-2-4-5 ก็ได้ แล้วจากนั้น Executor จะเริ่มดึงบล๊อคข้อมูลทีละตัว: blockManager บนโลคอล => connectionManager บนโลคอล => cutor ของเครื่องอื่น => ข้อมูล. การดึงบล๊อคข้อมูลแต่ละครั้งจะถูกเก็บใว้ใต้ Block manager และ blockManagerMaster ของไดรว์เวอร์จะแจ้งว่าบล๊อคข้อมูลถูกดึงสำเร็จแล้ว อย่างที่คิดไว้เลยก็คือขั้นตอนนี้เป็นขั้นตอนที่สำคัญเพราะว่าในตอนนี้ทุกๆโหนดในคลัสเตอร์จะรู้ว่ามีแหล่งข้อมูลที่ใหม่สำหรับบล๊อคข้อมูล ถ้าโหนดอื่นต้องการดึงบล๊อคข้อมูลเดียวกันนี้มันจะสุ่มว่าจะเลือกดึงจากที่ไหน ถ้าบล๊อคข้อมูลที่จะถูกดึงมีจำนวนมากการกระจายด้วยวิธีนี้จะช่วยให้กลไกการ Broadcast เร็วขึ้น ถ้าจะให้เห็นภาพมากขึ้นลองอ่านเรื่องBitTorrent บน wikipedia.

เมื่อบล๊อคข้อมูลทุกบล๊อคถูกดึงมาไว้ที่โหนดโลคอลแล้ว Array[Byte] ที่มีขนาดใหญ่จะถูกจัดสรรเพื่อสร้างข้อมูลที่ Broadcast มาขึ้นมาใหม่จากบล๊อคข้อมูลย่อยๆถูกถูกดึงมา สุดท้ายแล้ว Array นี้ก็จะถูก Deserialize และเก็บอยู่ภายใต้ Block manager ของโหนดโลคอลโปรดทราบว่าเมืื่อเรามีตัวแปร Broadcast ใน Block manager บนโหนดโลคอลแล้วเราสามารถลบบล๊อคของข้อมูลที่ถูกดึงมาได้อย่างปลอดภัย (ซึ่งก็ถูกเก็บอยู่ใน Block manager บนโหนดโลคอลเช่นเดียวกัน)

คำถามอักอย่างหนึ่งก็คือ: แล้วเกี่ยวกับการ Broadcast RDD หล่ะ? จริงๆแล้วไม่มีอะไรแย่ๆเกิดขึ้นหรอก RDD จะถูกทราบค่าในแต่ละExecutor ดังนั้นแต่ละโหนดจะมีสำเนาผลลัพธ์ของมันเอง

การพูดคุยการใช้ตัวแปร Broadcast แบ่งกันข้อมูลเป็นคุณสมบัติที่มีประโยชน์ ใน Hadoop เราจะมี DistributedCache ซึ่งถูกใช้งานในหลายๆสถานการณ์ เช่น พารามิเตอร์ของ -libjars จะถูกส่งไปยังทุกโหนดโดยการใช้ DistributedCache อย่างไรก็ดี Hadoop จะBroadcast ข้อมูลโดยการอัพโหลดไปยัง HDFS ก่อนและไม่มีกลไกในการแบ่งปันข้อมูลระหว่าง Task ในโหนดเดียวกัน ถ้าบางโหนดต้องการประมวลผลโดยใช้ 4 Mapper ใน Job เดียวกันแล้วตัวแปร Broadcast จำต้องถูกเก็บ 4 ครั้งในโหนดนั้น (หนึ่งสำเนาต่อไดเรกทอรีที่ Mapper ทำงาน) ข้อดีของวิธีการนี้คือไม่เกิดคอขวดของระบบเนื่องจาก HDFS นั้นมีการตัดส่วนของข้อมูลออกเป็นบล๊อคและกระจายตัวทั่วทั้งตลัสเตอร์อยู่แล้ว

สำรับ Spark นั้น Broadcast จะใส่ใจเกี่ยวกับการส่งข้อมูลไปทุกโหนดและปล่อยให้ Task ในโหนดเดียวกันนั้นมีการแบ่งปันข้อมูลกัน ในSpark มี Blog manager ที่จะช่วยแก้ไขปัญหาเรื่องการแบ่งปันข้อมูลระว่าง Task ในโหนดเดียวกัน การเก็บข้อมูลไว้ใน Block manager บนโหนดโลคอลโดยการใช้ระดับการเก็บข้อมูลแบบหน่วยความจำ + ดิสก์ จะรับร้องได้ว่าทุก Task บนโหนดสามารถที่จะเข้าถึงหน่วยความจำที่แบ่งปันกันนี้ได้ ซึ่งการทำแบบนี้สามารถช่วยเลี่ยงการเก็บข้อมูลที่มีความซ้ำซ้อน Spark มีการดำเนินการ Broadcast อยู่ 2 วิธีก็คือ HttpBroadcast ซึ่งมีคอขวดอยู่กับโหนดไดรว์เวอร์ และ TorrentBroadcast ซึ่งเป็นการแก้ปัญหาโดยใช้วิธีการของ BitTorrent ที่จะช้าในตอนแรกแต่เมื่อได้มัการดึงข้อมูลไปกระจายตาม Executor ตัวอื่นๆแล้วก็จะเร็วขึ้นและกระบวนการสร้างใหม่ของข้อมูลจากบล๊อคข้อมูลต้องการพื้นที่บนหน่วยความจำเพิ่มมากขึ้น

จริงๆแล้ว Spark มีการทดลองใช้ทางเลือกอื่นคือ TreeBroadcast ในรายละเอียดเชิงเทคนิคดูได้ที่: Performance and Scalability of

Broadcast in Spark.

ในความคิดเห็นของผู้เขียนคุณสมบัติ Broadcast นี้สามารถดำเนินการโดยใช้โปรโตคอลแบบ Multicast ได้ แต่เนื่องจาก Multicast มาจากพื้นฐานของ UDP ดังนั้นเราจึงต้องการกลไกที่มีความน่าเชื่อถือในระดับแอพพลิเคชันเลเยอร์