🧪 Lab 0 จาก 6 · Environment Setup

ติดตั้ง Spark ด้วย Docker

จุดเริ่มต้นของภาคปฏิบัติทั้งหมด — ตั้งค่าสภาพแวดล้อม Spark + Jupyter บนเครื่องตัวเองด้วย Docker ใช้เวลาไม่เกิน 15 นาที ไม่ต้องติดตั้ง Java/Scala/Spark เองทีละตัวให้ยุ่งยาก

🎯
เป้าหมายของ Lab นี้: มี Spark cluster (master + worker) และ Jupyter Notebook ที่เชื่อมต่อกันรันอยู่บนเครื่องตัวเอง พร้อมทดสอบรันโค้ด PySpark ตัวแรกสำเร็จ

1สิ่งที่ต้องมีก่อนเริ่ม

ข้อกำหนดรายละเอียด
Docker Desktopติดตั้งจาก docker.com (รองรับ Windows/Mac/Linux) — ตรวจสอบว่าติดตั้งแล้วด้วย docker --version
Docker Composeมาพร้อม Docker Desktop เวอร์ชันใหม่แล้ว (เรียกผ่าน docker compose ไม่มีขีดกลาง)
RAM ว่างอย่างน้อย 4GBจัดสรรให้ Docker ผ่าน Settings → Resources
พื้นที่ดิสก์ว่าง ~3GBสำหรับ image ของ Spark และ Jupyter

2สร้างโครงสร้างโปรเจกต์

ขั้นตอนที่ 1
สร้างโฟลเดอร์โปรเจกต์และไฟล์ที่จำเป็น
mkdir bigdata-lab
cd bigdata-lab
mkdir data notebooks
touch docker-compose.yml

3เขียน docker-compose.yml

ขั้นตอนที่ 2
ใช้ image ทางการของ Bitnami ที่ตั้งค่า Spark cluster (master + worker) ให้พร้อมใช้งานได้ทันที บวก Jupyter ที่มี PySpark ติดตั้งไว้แล้ว
version: "3.8"

services:
  spark-master:
    image: bitnami/spark:3.5
    environment:
      - SPARK_MODE=master
      - SPARK_RPC_AUTHENTICATION_ENABLED=no
      - SPARK_RPC_ENCRYPTION_ENABLED=no
    ports:
      - "8080:8080"   # Spark master Web UI
      - "7077:7077"   # Spark master port

  spark-worker:
    image: bitnami/spark:3.5
    environment:
      - SPARK_MODE=worker
      - SPARK_MASTER_URL=spark://spark-master:7077
      - SPARK_WORKER_MEMORY=2G
      - SPARK_WORKER_CORES=2
    depends_on:
      - spark-master

  jupyter:
    image: jupyter/pyspark-notebook:spark-3.5.0
    ports:
      - "8888:8888"   # Jupyter Notebook
      - "4040:4040"   # Spark application UI (เปิดตอนมี job รันอยู่)
    volumes:
      - ./notebooks:/home/jovyan/work
      - ./data:/home/jovyan/data
    environment:
      - JUPYTER_ENABLE_LAB=yes
    depends_on:
      - spark-master

4รัน Cluster

ขั้นตอนที่ 3
docker compose up -d
รอสัก 30 วินาทีให้ container ทั้งหมด pull image และ start ให้เรียบร้อย ตรวจสอบสถานะด้วย:
docker compose ps
# ควรเห็น 3 container: spark-master, spark-worker, jupyter ที่ status = running
Checkpoint 1: ตรวจสอบ Spark Master Web UI
เปิดเบราว์เซอร์ไปที่ http://localhost:8080 — ควรเห็นหน้า Spark Master พร้อม Worker 1 ตัวที่ status ALIVE แสดงอยู่ในตาราง "Workers"
Checkpoint 2: หา Jupyter access token
docker compose logs jupyter | grep token
คัดลอก URL ที่มี ?token=... ต่อท้าย แล้วเปิดในเบราว์เซอร์ หรือเข้าที่ http://localhost:8888 แล้ววาง token ตอนถูกถาม

5ทดสอบรัน PySpark ตัวแรก

ขั้นตอนที่ 4
สร้าง notebook ใหม่ใน Jupyter (Python 3 kernel) แล้วรันโค้ดนี้เพื่อเชื่อมต่อกับ Spark cluster ที่เพิ่งตั้งค่าไว้
from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("Lab0-FirstConnection") \
    .master("spark://spark-master:7077") \
    .getOrCreate()

print("Spark version:", spark.version)
print("Master:", spark.sparkContext.master)

# ทดสอบสร้าง DataFrame ง่าย ๆ
data = [("Alice", 25), ("Bob", 30), ("Charlie", 35)]
df = spark.createDataFrame(data, ["name", "age"])
df.show()

spark.stop()
ผลลัพธ์ที่ควรเห็น
Spark version: 3.5.0
Master: spark://spark-master:7077
+-------+---+
|   name|age|
+-------+---+
|  Alice| 25|
|    Bob| 30|
|Charlie| 35|
+-------+---+
ถ้าเห็นตารางนี้ แปลว่า Jupyter เชื่อมต่อกับ Spark cluster สำเร็จแล้ว — พร้อมไปต่อ Lab 1

6คำสั่งที่ใช้บ่อยและการแก้ปัญหา

คำสั่งใช้ทำอะไร
docker compose logs -f jupyterดู log แบบ real-time ของ container jupyter (มีประโยชน์เวลา debug)
docker compose downหยุดและลบ container ทั้งหมด (ข้อมูลใน ./notebooks และ ./data ยังอยู่เพราะ mount เป็น volume)
docker compose restart spark-workerรีสตาร์ท worker ตัวเดียว โดยไม่กระทบ master/jupyter
docker statsดูการใช้ CPU/memory ของแต่ละ container แบบ real-time
ปัญหาที่พบบ่อย
"Port already in use": พอร์ต 8080/8888/7077 ถูกใช้อยู่แล้ว — แก้โดยเปลี่ยนพอร์ตฝั่งซ้ายใน docker-compose.yml เช่น "8081:8080"
Jupyter เชื่อมต่อ Spark master ไม่ได้: ตรวจสอบว่าใช้ชื่อ spark-master ไม่ใช่ localhost ใน .master(...) เพราะ container คุยกันผ่านชื่อ service ใน docker network ไม่ใช่ localhost
Worker ไม่ขึ้นสถานะ ALIVE: รอสักครู่ให้ worker เชื่อมต่อ master เสร็จสมบูรณ์ก่อน (มักใช้เวลา 10-20 วินาทีหลัง docker compose up)

7ก่อนรัน: เข้าใจว่าส่วนประกอบใดทำหน้าที่อะไร

Docker Compose ในแล็บนี้ไม่ได้สร้าง “Spark หนึ่งกล่อง” แต่สร้างหลาย Service ที่อยู่ใน Network เดียวกัน ถ้าแยกบทบาทไม่ออก เวลาเกิดปัญหาเราจะ Restart ทุกอย่างโดยไม่รู้ว่าจุดเสียอยู่ตรงไหน

ส่วนประกอบหน้าที่สิ่งที่ไม่ควรเข้าใจผิด
Spark Masterรับ Worker และจัดสรร Application ไปยัง Clusterไม่ใช่ Driver ของทุกงานและไม่เก็บข้อมูลทั้งหมด
Spark Workerเสนอ CPU/Memory และเปิด ExecutorWorker ไม่ใช่ Executor หนึ่งต่อหนึ่งเสมอ
Driverสร้าง SparkSession วาง DAG และประสาน Jobใน Notebook Driver มักอยู่ฝั่ง Jupyter Container
Executorรัน Task และเก็บ Cache/Shuffle ของ Applicationปิด Application แล้ว Executor ของงานนั้นหาย
Jupyterสภาพแวดล้อมเขียนและรัน PySparkหน้า Notebook เปิดได้ไม่ได้แปลว่าเชื่อม Master สำเร็จ
อุปมาเรื่องโรงงาน
Master เหมือนฝ่ายรับงาน Worker เหมือนอาคารที่แจ้งว่ามีพื้นที่และคน Driver เหมือนผู้จัดการโครงการที่แตกงาน ส่วน Executor คือทีมที่ถูกตั้งขึ้นมาทำโครงการนั้น Jupyter เป็นโต๊ะที่เราใช้สั่งงาน การเห็นโต๊ะไม่ได้พิสูจน์ว่าโรงงานรับคำสั่งแล้ว

8ตรวจระบบแบบเป็นขั้น ไม่เดาจากหน้าจอเดียว

  1. ใช้ docker compose ps ตรวจว่า Container อยู่สถานะ Running
  2. เปิด Master UI ตรวจว่า Worker ลงทะเบียนและทรัพยากรตรงค่า Compose
  3. จาก Notebook ตรวจ spark.sparkContext.master ว่าชี้ spark://spark-master:7077
  4. รัน Action จริง เช่น count() แล้วดู Application ใน UI
  5. ดู Executors/Stages เพื่อยืนยันว่า Task ถูกส่งไป Worker ไม่ใช่ Local Mode
print("Spark version:", spark.version)
print("Master:", spark.sparkContext.master)
print("Application ID:", spark.sparkContext.applicationId)
print("Default parallelism:", spark.sparkContext.defaultParallelism)

rdd = spark.sparkContext.parallelize(range(1_000_000), 8)
print("Partitions:", rdd.getNumPartitions())
print("Sum:", rdd.sum())
Local[*] เป็นกับดักของแล็บ Cluster
ถ้า Notebook สร้าง SparkSession ด้วย master("local[*]") งานรันได้สวย แต่ใช้ CPU ภายใน Jupyter Container ไม่แตะ Worker เลย การรันผ่านจึงไม่เท่ากับตั้ง Cluster ถูก ต้องตรวจค่า Master และ UI ทุกครั้ง

9ทดลอง Scale Worker แล้ววัด ไม่ใช่เพียงดูว่ามีเพิ่ม

เพิ่ม Worker แล้ว Application ไม่จำเป็นต้องเร็วขึ้น ถ้า RDD มีสอง Partition ต่อให้มีแปด Core ก็มี Task พร้อมกันเพียงสองตัว การทดลองต้องควบคุม Partition, Data Size และวัดหลายรอบ

import time

def benchmark(partitions, n=20_000_000):
    rdd = spark.sparkContext.parallelize(range(n), partitions)
    start = time.perf_counter()
    value = rdd.map(lambda x: (x * 17) % 101).sum()
    return value, time.perf_counter() - start

for p in [1, 2, 4, 8, 16, 32]:
    value, elapsed = benchmark(p)
    print(p, "partitions:", round(elapsed, 3), "seconds")

รอบแรกอาจรวม Startup/JIT/Container Warm-up จึงควรรันซ้ำและรายงาน Median อย่าเคลม Speedup จากการจับเวลาครั้งเดียว งานเล็กเกินไปยังถูก Scheduling Overhead กลบจน Worker มากช้ากว่าเดิมได้

สิ่งที่ตรึงสิ่งที่เปลี่ยนสิ่งที่วัด
Code, Data Size, Image, เครื่อง Hostจำนวน Worker/Core หรือ PartitionMedian Time, Speedup, Efficiency
จำนวน WorkerData SizeWeak Scaling

10Resource Limit และความจริงของ Docker Desktop

ค่า SPARK_WORKER_CORES เป็นทรัพยากรที่ Worker ประกาศ ไม่ได้สร้าง CPU จริง ถ้า Docker Desktop ได้ CPU สี่ Core แล้วกำหนด Worker สองตัว ตัวละสี่ Core ทั้งคู่กำลังแย่ง CPU เดิม Oversubscription อาจทำให้ Context Switching เพิ่ม

Memory ก็เช่นกัน Worker ประกาศได้มากกว่าที่ Container/VM มีจริง แต่เมื่อใช้จริงอาจถูก Kill ควรตรวจ Docker Desktop Resource, Container Limit, Spark Worker Memory และ Executor Memory ให้สอดคล้องกัน

docker stats
docker inspect spark-worker --format '{{json .HostConfig.Memory}}'
docker compose exec spark-worker bash -lc 'nproc; free -h'
ป้ายที่นั่งไม่ได้สร้างเก้าอี้
เขียนป้ายว่า Worker มี 8 Core เหมือนติดป้าย “ห้องนี้รับได้ 80 คน” แต่ในห้องมีเก้าอี้ 20 ตัว Spark เชื่อป้ายตอนจัดงาน ส่วนระบบปฏิบัติการเป็นคนบอกความจริงเมื่อทุกคนเข้ามานั่ง

11Volumes, Network และ Port Mapping

Bind Mount ทำให้ไฟล์ Notebook/Data อยู่บน Host และไม่หายเมื่อ Container ถูกสร้างใหม่ แต่ Path ฝั่ง Container ต้องตรงกับ Code ส่วน Named Volume เหมาะกับข้อมูลที่ Docker จัดการให้ ความต่างนี้สำคัญเวลา Backup และส่งงาน

รูปแบบความหมายตัวอย่าง
./data:/home/jovyan/dataBind Mount จากโฟลเดอร์โปรเจกต์เห็นไฟล์จาก Windows/Host ได้
spark-data:/dataNamed VolumeDocker ดูแลตำแหน่งจริง
8080:8080Host Port → Container PortBrowser Host เปิด Master UI

localhost ใน Container หมายถึง Container นั้นเอง Service จึงคุยกันผ่านชื่อ spark-master บน Compose Network ส่วน Browser บน Host ใช้ localhost:8080 นี่คือคนละมุมมองของ Network

12Failure Drill: ทำให้พังอย่างมีแผน

การตั้งระบบสำเร็จยังไม่พอ เราต้องรู้ว่าเมื่อ Worker หาย Spark แสดงอะไร เริ่ม Job ที่ใช้เวลานาน แล้วหยุด Worker ระหว่างงาน สังเกต UI, Log และผลลัพธ์

# ใน Notebook: งานที่นานพอให้ทดลองหยุด Worker
slow = spark.sparkContext.parallelize(range(50_000_000), 64) \
    .map(lambda x: ((x * 31) % 1009) ** 2)
print(slow.sum())

# อีก Terminal
docker compose stop spark-worker
docker compose logs --tail=100 spark-master
docker compose start spark-worker

ผลขึ้นกับจังหวะและ Configuration งานอาจ Retry, ช้าลง หรือ Fail เป้าหมายไม่ใช่รับประกันว่าจะสำเร็จทุกครั้ง แต่ฝึกอ่านหลักฐานและอธิบาย Recovery Path

13Checklist ส่งแล็บและสรุปขั้นตอนถัดไป

สรุปและขั้นตอนถัดไป: ตอนนี้เรายังไม่ได้เรียน RDD อย่างจริงจัง แต่สร้างสนามทดลองที่มองเห็น Driver, Executor, Worker และ Task แล้ว Lab 1 จะใช้สนามนี้พิสูจน์ Lazy Evaluation, Partition, Shuffle และ Cache โดยต้องอ่าน UI ควบคู่กับผลลัพธ์ ไม่ใช่เชื่อโค้ดเพียงด้านเดียว

แบบฝึกหัดก่อนไป Lab 1
  1. เปิด Spark Master Web UI (localhost:8080) แล้วลองเพิ่ม worker ตัวที่สองใน docker-compose.yml (ก็อปปี้ block spark-worker เปลี่ยนชื่อ service) แล้ว docker compose up -d ใหม่ — ตรวจสอบว่าเห็น 2 worker ใน Web UI
  2. ลองปรับ SPARK_WORKER_MEMORY และ SPARK_WORKER_CORES แล้วสังเกตว่าตัวเลขใน Web UI เปลี่ยนตามไหม
  3. ทดลองรันโค้ด PySpark checkpoint ด้านบนซ้ำ แต่เปลี่ยนข้อมูลใน data เป็นข้อมูลของตัวเอง (เช่น รายชื่อเพื่อน 5 คนพร้อมอายุ)

20วิธีทำแล็บให้ได้มากกว่าคำว่า “รันผ่าน”

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

เปรียบเหมือนการทดลองในครัว
ถ้าเราเปลี่ยนทั้งเตา ภาชนะ ปริมาณวัตถุดิบ และเวลาปรุงพร้อมกัน แม้อาหารอร่อยขึ้น เราก็ไม่รู้ว่าอะไรเป็นสาเหตุ การทดลองระบบก็เหมือนกัน ควรเปลี่ยนทีละปัจจัย และจดสิ่งที่คงเดิมไว้ให้ชัด
  1. คาดก่อนรัน: เขียนหนึ่งหรือสองประโยคว่าคิดว่าจะเกิดอะไรและเพราะอะไร
  2. กำหนดสิ่งที่คุม: ใช้ข้อมูล โค้ด และทรัพยากรเดิม แล้วเปลี่ยนเฉพาะตัวแปรที่กำลังศึกษา
  3. เก็บหลักฐาน: บันทึกเวลา แผนการทำงาน Metric, Log หรือภาพ Spark UI ไม่อาศัยความรู้สึกว่า “เร็วขึ้น”
  4. อธิบายกลไก: เชื่อมผลกับ Partition, Shuffle, State, Memory, Network หรือ Scheduling
  5. บอกข้อจำกัด: ผลจาก Laptop และข้อมูลจำลองอาจไม่เหมือน Production ให้ระบุสิ่งที่ยังสรุปไม่ได้

21สมุดบันทึกการทดลอง

รายการสิ่งที่ควรบันทึกเหตุผล
คำถามครั้งนี้ต้องการรู้อะไรเพียงเรื่องเดียวกันการทดลองหลุดประเด็น
สมมติฐานผลที่คาดและกลไกที่คิดว่าเป็นสาเหตุทำให้ตรวจความเข้าใจได้
ตัวแปรสิ่งที่เปลี่ยน สิ่งที่คุม และค่าตั้งต้นทำให้เปรียบเทียบอย่างเป็นธรรม
หลักฐานเวลา Metric, Plan, Log, UI และ Sample outputทำให้คนอื่นตรวจซ้ำได้
ข้อสรุปหลักฐานสนับสนุนหรือหักล้างสมมติฐานอย่างไรแยกข้อเท็จจริงออกจากความเห็น
คำถามใหม่ถ้ามีเวลาเพิ่ม จะเปลี่ยนอะไรต่อเชื่อมไปสู่การทดลองรอบถัดไป

อย่าจดเฉพาะผลลัพธ์สุดท้าย ควรจด Environment ด้วย เช่น Spark version, Python version, จำนวน Core, Memory, จำนวน Partition, Seed และขนาดข้อมูล เปรียบเหมือนใบเสร็จและสูตรอาหารที่ทำให้คนอื่นย้อนทำจานเดิมได้ หากไม่มีข้อมูลเหล่านี้ ผลที่ดูน่าเชื่อถืออาจทำซ้ำไม่ได้

22ชุดการทดลองหลัก: สภาพแวดล้อม Spark บน Docker

การทดลอง 1: ตรวจเส้นทางของงาน

ลงมือทำ: ส่งงานจาก Notebook ไปยัง Master แล้วดูว่า Worker และ Executor รับงานอย่างไร ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: ภาพหน้า Spark UI ที่ชี้ Driver, Executor และจำนวน Task พร้อมคำอธิบายด้วยคำของตนเอง แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

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

การทดลอง 2: เปรียบเทียบ local กับ cluster

ลงมือทำ: รันงานเดียวกันด้วย local[*] และ spark://spark-master:7077 โดยคุมข้อมูลและโค้ดให้เหมือนกัน ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: ตารางเวลาและคำอธิบายว่าทำไม local[*] ไม่ได้พิสูจน์ว่า Cluster ทำงาน แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

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

การทดลอง 3: เพิ่ม Worker

ลงมือทำ: เริ่มจากหนึ่ง Worker แล้วเพิ่มเป็นสอง เปรียบเทียบงานที่ใหญ่พอและงานที่เล็กมาก ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: กราฟเวลาและข้อสังเกตเรื่อง Overhead แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

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

การทดลอง 4: จำกัดทรัพยากร

ลงมือทำ: จำกัด CPU และ Memory ของ Container แล้วรันงานเดิม ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: หลักฐาน Spill, GC หรือ Task ที่ช้าลงจาก Spark UI แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

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

การทดลอง 5: ทดลองความเสียหาย

ลงมือทำ: หยุด Worker ระหว่างงานที่ทำซ้ำได้ แล้วติดตามการจัด Task ใหม่ ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: Timeline ก่อนเสีย ระหว่างกู้ และหลังงานจบ แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

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

23บันไดแก้ปัญหาเมื่อผลไม่เป็นอย่างที่คิด

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

  1. ยืนยันอาการ: คัดลอก Error แรก ไม่ใช่เฉพาะบรรทัดสุดท้าย และบันทึกคำสั่งที่ทำให้เกิดซ้ำ
  2. ลดปัญหา: ใช้ข้อมูลเล็กที่สุดและโค้ดสั้นที่สุดที่ยังทำให้ Error เกิด
  3. ตรวจสัญญาข้อมูล: ดู Schema, Type, Null, Key และ Path ก่อนสงสัย Cluster
  4. ตรวจ Plan และ UI: ดูว่างานเริ่มจริงหรือค้างอยู่ก่อนสร้าง Job; แยกปัญหา Driver, Executor และ Storage
  5. ตรวจทรัพยากร: ดู Memory, Disk, Port, Permission และ Container status
  6. เปลี่ยนทีละจุด: เมื่อแก้แล้ว ให้ย้อนอธิบายว่าการเปลี่ยนนั้นจัดการสาเหตุใด
กติกาสำคัญ
การ Restart อาจทำให้อาการหาย แต่ยังไม่ใช่คำอธิบาย ถ้าต้อง Restart ให้เก็บ Log ก่อนและเขียนไว้ตรง ๆ ว่ายังหาสาเหตุรากไม่พบ ในงานจริง ความซื่อสัตย์ต่อหลักฐานสำคัญกว่ารายงานที่ดูเรียบร้อย

24คำถามชวนคิดหลังทำแล็บ

  1. ผลที่เห็นเกิดจาก Algorithm หรือเกิดจากการจัดวางข้อมูลและข้อจำกัดของเครื่อง?
  2. ถ้าข้อมูลใหญ่ขึ้นหนึ่งร้อยเท่า ขั้นตอนไหนจะพังก่อน และเรามีหลักฐานอะไร?
  3. ถ้าเครื่องหนึ่งหายไประหว่างงาน คำตอบจะยังถูกต้องหรือเพียงแค่งานยังรันต่อได้?
  4. ค่า Default ใดช่วยเราอยู่ และค่าใดอาจกลายเป็นกับดักเมื่อขึ้น Production?
  5. ถ้าต้องอธิบายผลให้เพื่อนที่ไม่ได้เห็นหน้าจอ เรามีหลักฐานครบพอหรือยัง?

25สิ่งที่ต้องส่ง

26เกณฑ์ตรวจงานแบบสั้น

ด้านงานที่ผ่านงานที่เข้าใจจริง
ความถูกต้องรันได้และได้คำตอบตรวจ Schema, Edge case และเทียบกับคำตอบเล็กที่คำนวณเองได้
การทดลองมีผลหนึ่งชุดคุมตัวแปร ทำซ้ำ และอธิบายความแปรปรวน
หลักฐานมี Screenshotเลือก Metric หรือ Plan ที่ตอบคำถามและตีความถูก
ความเข้าใจบอกว่าอะไรเร็วกว่าอธิบายกลไกและเงื่อนไขที่ข้อสรุปอาจกลับด้าน
การสื่อสารมี Codeผู้อื่นทำซ้ำได้และเห็นเส้นทางจากคำถามไปถึงข้อสรุป

27สรุปและขั้นตอนถัดไป

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

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

28สถานการณ์จำลอง: จากข้อมูลก้อนเล็กไปสู่งานที่เริ่มมีปัญหา

ให้เริ่มด้วยข้อมูลก้อนเล็กที่ตรวจคำตอบด้วยมือได้ จากนั้นขยายเป็นสามระดับ โดยไม่เปลี่ยนความหมายของโจทย์ ระดับแรกใช้ตรวจความถูกต้อง ระดับที่สองใช้เปิดให้เห็น Parallelism และระดับที่สามใช้เปิดให้เห็นคอขวด วิธีนี้ช่วยแยกคำถามสองข้อที่มักปนกัน คือ “คำตอบถูกหรือไม่” กับ “ระบบรองรับขนาดงานได้หรือไม่” ระบบที่เร็วแต่ตอบผิดไม่มีประโยชน์ และระบบที่ตอบถูกเฉพาะข้อมูลสิบแถวก็ยังไม่ใช่คำตอบของงานขนาดใหญ่

ระดับจุดประสงค์สิ่งที่ควรเห็นสิ่งที่ยังสรุปไม่ได้
เล็กมากตรวจคำตอบด้วยมือและทดสอบ Edge caseOutput ทุกแถวและลำดับการแปลงข้อมูลประสิทธิภาพและการกระจายงาน
พอดีเครื่องดูจำนวน Task, Partition และการใช้ Coreงานหลาย Task ทำพร้อมกันและมี Metric ให้อ่านพฤติกรรมเมื่อ Memory ไม่พอ
เริ่มเกินหน่วยความจำดู Spill, Shuffle, GC และการอ่านเขียน Diskคอขวดปรากฏชัดใน UIพฤติกรรมของคลัสเตอร์ Production ขนาดใหญ่
ข้อมูลเอียงดูผลของ Key หรือกลุ่มที่ใหญ่ผิดปกติTask บางตัวช้ากว่าเพื่อนอย่างชัดเจนว่าวิธีแก้หนึ่งแบบเหมาะกับข้อมูลทุกชุด
Metaphor: ซ้อมขนย้ายบ้าน
กล่องเล็กหนึ่งใบช่วยตรวจว่าเราติดป้ายที่อยู่ถูก แต่ไม่ช่วยบอกว่ารถบรรทุกพอหรือไม่ ส่วนการขนของทั้งบ้านตั้งแต่ครั้งแรกทำให้เราแยกไม่ออกว่าช้าเพราะป้ายผิด ทางแคบ หรือรถไม่พอ การเพิ่มขนาดทีละระดับทำให้เห็นสาเหตุชัดขึ้น

29อ่าน Spark UI ให้เป็นเรื่องราว ไม่ใช่เพียงหน้าจอสีสวย

เริ่มจาก Job ว่าถูกสร้างเพราะ Action ใด แล้วลงไปที่ Stage เพื่อดูว่า Shuffle แบ่งเส้นทางตรงไหน จากนั้นจึงดู Task ว่ากระจายตัวสม่ำเสมอหรือมีตัวใดลากยาว สุดท้ายดู Executor ว่างานไปอยู่เครื่องใด ใช้ Memory เท่าไร และมี Failure หรือ Retry หรือไม่ ลำดับนี้เหมือนอ่านแผนที่จากประเทศ จังหวัด ถนน แล้วจึงถึงบ้าน หากกระโดดไปดู Task หนึ่งตัวทันที เราอาจเห็นอาการแต่ไม่เห็นบริบท

หน้าหรือ Metricคำถามที่ตอบได้สัญญาณที่ควรสงสัย
JobsAction ใดเริ่มการคำนวณ และจบหรือยังมี Job มากกว่าที่คาดจากการเรียก Action ซ้ำ
Stagesงานถูกตัดตรงไหนและ Stage ใดใช้เวลามากStage หลัง Shuffle ใช้เวลาครองงานทั้งหมด
Tasksงานย่อยกระจายสม่ำเสมอหรือไม่Max สูงกว่า Median มาก บ่งชี้ Skew หรือเครื่องช้า
Executorsใช้ Core/Memory/Storage อย่างไรExecutor หาย, Task Failed, GC สูง หรือ Disk Spill มาก
SQL PlanOptimizer เลือก Scan, Join และ Exchange แบบใดอ่านคอลัมน์เกินจำเป็น มี Exchange ซ้ำ หรือ Join ผิดคาด
StorageCache อะไร อยู่ระดับใด และกินพื้นที่เท่าไรCache ไว้แต่ไม่ถูกใช้ หรือผลักข้อมูลสำคัญออกจาก Memory

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

30แยกความถูกต้อง ความเร็ว และความทนทานออกจากกัน

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

  1. ความถูกต้อง: สร้างข้อมูลจิ๋วที่รู้คำตอบล่วงหน้า ใส่ Null, Duplicate, Key ที่ไม่มีคู่ และค่าขอบเขต แล้วตรวจผลทีละกรณี
  2. ความเร็ว: วัดหลายรอบ แยก Warm-up ออกจากรอบจริง ระบุขนาดข้อมูลและทรัพยากร และรายงานทั้งค่ากลางกับช่วง ไม่เลือกเฉพาะรอบที่ดีที่สุด
  3. ความทนทาน: ทำให้ส่วนหนึ่งล้มอย่างตั้งใจ ตรวจว่างาน Retry, Resume หรือคำนวณใหม่จาก Lineage และตรวจว่าคำตอบหลังการกู้ไม่ซ้ำหรือขาด
  4. ต้นทุน: เวลาเร็วขึ้นอาจแลกกับ Memory, Storage, Network หรือความซับซ้อนในการดูแล ให้บันทึกสิ่งที่จ่ายเพิ่มเสมอ
แบบตรวจสั้นก่อนเชื่อผล Benchmark
ข้อมูลเท่ากันหรือไม่? Output เท่ากันหรือไม่? Cache ของแต่ละรอบเท่ากันหรือไม่? มีงานอื่นแย่งเครื่องหรือไม่? จำนวน Partition และ Core เท่ากันหรือไม่? ถ้าตอบข้อใดไม่ได้ ให้เรียกผลนั้นว่า “ข้อสังเกตเบื้องต้น” ก่อน ยังไม่ควรเรียกว่า “ข้อสรุป”

31จาก Error Message ไปสู่คำอธิบาย

ให้เลือก Error จริงหนึ่งครั้งจากแล็บแล้วเขียน Postmortem สั้น ๆ โดยไม่กล่าวโทษผู้ใช้หรือเครื่องมือ เริ่มจากผลกระทบ ตามด้วยลำดับเหตุการณ์ สาเหตุใกล้ตัว สาเหตุราก วิธีแก้เฉพาะหน้า และวิธีป้องกันซ้ำ ตัวอย่างเช่น “Job ล้มเพราะ Out of Memory” ยังเป็นเพียงอาการ สาเหตุรากอาจเป็นการ collect ข้อมูลทั้งหมดมายัง Driver, Broadcast ตารางใหญ่เกินไป หรือ Partition หนึ่งก้อนใหญ่ผิดปกติ ซึ่งมีวิธีแก้คนละแบบ

ส่วนของ Postmortemตัวอย่างคำถาม
ผลกระทบงานใดหยุด คำตอบใดขาด และเสียเวลาเท่าไร
ลำดับเหตุการณ์คำสั่งใดเกิดก่อน Error; Metric เริ่มผิดปกติเมื่อใด
สาเหตุรากการตัดสินใจหรือข้อสมมติใดทำให้ปัญหาเกิดได้
การกู้ทำอย่างไรให้งานกลับมา และข้อมูลถูกตรวจซ้ำอย่างไร
การป้องกันเพิ่ม Test, Validation, Alert หรือเปลี่ยน Design ตรงไหน

จุดสำคัญคือแยก “แก้ให้งานผ่านครั้งนี้” ออกจาก “แก้ไม่ให้เกิดซ้ำ” การเพิ่ม Memory อาจช่วยงานรอบนี้ แต่ถ้าข้อมูลโตต่อเนื่อง ปัญหาจะกลับมา การเปลี่ยนรูปแบบ Aggregation, ลดข้อมูลก่อน Shuffle หรือเลิกดึงทุกอย่างเข้า Driver อาจแตะสาเหตุได้ตรงกว่า

32แบบฝึกสอนกลับ: อธิบายให้เพื่อนฟังในห้านาที

จับคู่กับเพื่อนแล้วผลัดกันอธิบาย สภาพแวดล้อม Spark บน Docker โดยห้ามเริ่มจากชื่อ Class หรือ API ให้เริ่มจากปัญหาที่ระบบกำลังแก้ ใช้ภาพหนึ่งภาพและตัวอย่างข้อมูลไม่เกินสิบแถว ผู้ฟังมีหน้าที่ถามว่า “รู้ได้อย่างไร” และ “ถ้าข้อมูลใหญ่ขึ้นจะเกิดอะไร” วิธีนี้ช่วยเปิดจุดที่เราจำคำสั่งได้แต่ยังเชื่อมเหตุผลไม่ครบ

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