Big Data Analytics · บทที่ 10 จาก 13

Streaming แบบเรียลไทม์

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

📚
ความตั้งใจของบทนี้: เปลี่ยนภาพ Streaming จาก “Batch ที่รันถี่ขึ้น” ให้เป็นระบบคำนวณที่ต้องจัดการ Event Time, Window, Watermark, State, Delivery Semantics และ Backpressure พร้อมปูแก่นของ Smart Window Counting ว่าเหตุใดบางคำถามจึงไม่ควรเก็บข้อมูลทุกเหตุการณ์ไว้ตอบแบบ exact

Streaming ไม่ได้แปลว่าเร็วอย่างเดียว

คำว่า Real-time มักทำให้เรานึกถึงคำตอบที่ออกทันที แต่ระบบ Streaming ที่ดีต้องตอบคำถามอย่างน้อยสี่ข้อพร้อมกัน

คำถามสิ่งที่ต้องออกแบบถ้าไม่ตอบให้ชัด
เหตุการณ์เกิดขึ้นเมื่อใดEvent Time และการจัดการข้อมูลมาช้าเหตุการณ์เดียวกันอาจถูกนับผิดหน้าต่าง
ระบบเห็นเหตุการณ์เมื่อใดProcessing Time, Trigger และ Latencyเราอาจวัดความเร็วของระบบผิดจากความจริงของธุรกิจ
ต้องจำอดีตมากแค่ไหนWindow, State, Watermark และ State TTLState โตไม่มีขอบเขตจนระบบล้ม
ถ้าเกิดซ้ำหรือระบบล้มจะทำอย่างไรCheckpoint, Replay, Idempotency และ Delivery Semanticsยอดเงินหรือจำนวนเหตุการณ์อาจถูกนับซ้ำ
อุปมาเรื่องด่านเก็บค่าผ่านทาง
รถผ่านด่านเป็น stream ไม่มีคำว่า “รถคันสุดท้ายของโลก” เราจึงต้องถามเป็นช่วง เช่น 5 นาทีล่าสุดมีรถกี่คัน แต่รถบางคันส่งข้อมูลช้าเพราะสัญญาณขาด ระบบต้องตัดสินใจว่าจะรออีกนานเท่าไร จะย้อนแก้ยอดเดิมหรือไม่ และถ้าข้อความถูกส่งซ้ำจะนับรถคันเดิมสองครั้งหรือเปล่า ปัญหาของ Streaming จึงอยู่ที่ความหมายของเวลาและคำตอบ ไม่ใช่เพียงรับข้อมูลให้เร็ว

1Data Stream Management System (DSMS) และ Complex Event Processing (CEP)

ความต่างจากฐานข้อมูลแบบดั้งเดิม
ฐานข้อมูลทั่วไปเก็บข้อมูลนิ่ง ๆ แล้วรับ query เข้ามาถาม — DSMS กลับด้าน: ข้อมูลไหลผ่านตลอดเวลา ส่วน query อยู่นิ่งและรอรับข้อมูลใหม่เข้ามาประมวลผลต่อเนื่อง ข้อมูลถูกสังเกตผ่าน "window" (หน้าต่างเวลาหรือจำนวน record) ไม่ใช่มองทั้งก้อนพร้อมกันแบบ batch
แนวคิดรายละเอียด
DSMS (Data Stream Management System)เน้นตอบคำถามเชิงปริมาณ/สถิติต่อเนื่อง เช่น "ค่าเฉลี่ยของราคาหุ้นใน 5 นาทีล่าสุดคือเท่าไร"
CEP (Complex Event Processing)เน้นตรวจจับ "pattern" ของเหตุการณ์ที่เกิดขึ้นตามลำดับ เช่น "ถ้าราคาหุ้นตกติดกัน 3 ครั้งภายใน 10 นาที ให้แจ้งเตือน" — CEP เพิ่มความสามารถในการ deploy rule ที่อธิบายรูปแบบเหตุการณ์ที่ต้องการค้นหา
Windowing: กลไกที่ทำให้ stream ประมวลผลได้จริง
เพราะ stream ไม่มีจุดสิ้นสุด การประมวลผล aggregate (เช่น sum, average) แบบทั้งหมดตั้งแต่ต้นจนจบเป็นไปไม่ได้ในทางปฏิบัติ — ต้องกำหนด "window" เช่น window ตามเวลา (ทุก 5 นาที) หรือ window ตามจำนวน (ทุก 1000 record) เพื่อจำกัดขอบเขตการคำนวณให้จบได้จริง

Stream เป็นข้อมูลที่ไม่มีขอบเขต แต่คำถามต้องมีขอบเขต

ใน Batch เราอ่านไฟล์ตั้งแต่ต้นจนจบแล้วสรุปผลได้ แต่ Stream ไม่มี EOF ที่มีความหมายทางธุรกิจ การถามว่า “ยอดขายทั้งหมดของ Stream เท่าไร” จึงเป็นคำถามที่ไม่จบ หรือกลายเป็นค่าที่เปลี่ยนตลอดเวลา วิธีทำให้ตอบได้คือกำหนดขอบเขต เช่นยอดขายตั้งแต่เปิดร้าน ยอดขาย 15 นาทีล่าสุด หรือยอดขายต่อ Session ของลูกค้า

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

DSMS, Stream Analytics และ CEP ซ้อนกัน แต่ไม่ใช่เรื่องเดียวกัน

มุมมองโจทย์ตัวอย่างState ที่ต้องจำ
Continuous Query / DSMSค่าเฉลี่ยอุณหภูมิ 10 นาทีล่าสุดต่อโรงงานผลรวม จำนวน และขอบเขตหน้าต่างต่อโรงงาน
Stream Analyticsคำนวณ feature แล้วให้โมเดลประเมินความเสี่ยงทุกเหตุการณ์ค่าล่าสุด ประวัติย่อ หรือโมเดลที่โหลดไว้
CEPพบ Login ผิด 5 ครั้ง ตามด้วย Login สำเร็จจากประเทศใหม่ภายใน 3 นาทีสถานะของ pattern ลำดับเหตุการณ์และ timer
Event-driven Applicationเมื่อชำระเงินสำเร็จ ให้จองสินค้า ออกใบเสร็จ และแจ้งผู้ซื้อสถานะ workflow, retry และ compensation

ระบบหนึ่งอาจมีทั้งสี่แบบ เช่นแพลตฟอร์มธนาคารนับธุรกรรมต่อบัญชี ตรวจ pattern ฉ้อโกง ใช้โมเดลให้คะแนน และสั่งระงับบัตร ความต่างอยู่ที่คำถามและ state ไม่ใช่ชื่อผลิตภัณฑ์ที่เลือกใช้

2เวลาสามแบบที่ต้องแยกให้ออก

ความผิดพลาดที่พบบ่อยที่สุดใน Streaming คือใช้คำว่า “เวลา” เหมือนมีความหมายเดียว ในความจริงเหตุการณ์หนึ่งมีเวลาได้หลายค่า และแต่ละค่าตอบคนละคำถาม

ชนิดของเวลาความหมายตัวอย่าง
Event Timeเวลาที่เหตุการณ์เกิดขึ้นในโลกของต้นทางเครื่องวัดบันทึกว่าอุณหภูมิสูงเมื่อ 10:03
Ingestion Timeเวลาที่แพลตฟอร์มรับเหตุการณ์เข้ามาKafka รับข้อความเมื่อ 10:07 หลังเครือข่ายกลับมา
Processing Timeเวลาที่ operator กำลังประมวลผลเหตุการณ์Spark นำข้อความเข้า micro-batch เมื่อ 10:08
จดหมายสามเวลา
จดหมายมีวันที่ผู้ส่งเขียน วันที่ไปรษณีย์รับ และวันที่ผู้รับเปิดอ่าน Event Time เหมือนวันที่เขียน Processing Time เหมือนวันที่เปิดอ่าน ถ้าเราสรุปยอดคำสั่งซื้อของวันจันทร์ตามเวลาที่เปิดซอง จดหมายที่ส่งวันจันทร์แต่มาถึงวันอังคารจะถูกจัดผิดวัน

ทำไม Processing Time จึงง่ายแต่เปลี่ยนตามสภาพระบบ

การแบ่งหน้าต่างตามเวลาที่ระบบประมวลผลง่าย เพราะนาฬิกาของระบบพร้อมใช้และไม่ต้องจัดการเหตุการณ์มาช้า แต่ถ้าคลัสเตอร์ติด Backlog เหตุการณ์ที่เกิดในนาทีเดียวกันอาจถูกแยกคนละหน้าต่าง เพียงเพราะบางข้อความรอคิว การ Restart หรือเพิ่มทรัพยากรยังอาจทำให้ผลต่างกันทั้งที่ input เดิม

Event Time สะท้อนธุรกิจมากกว่า แต่ต้องยอมรับความยุ่งยาก

Event Time ช่วยให้ยอดของช่วง 10:00–10:05 หมายถึงเหตุการณ์ที่เกิดจริงในช่วงนั้น ไม่ว่าข้อความจะมาถึงเมื่อใด แต่ระบบต้องเก็บ state รอข้อมูลที่ยังมาไม่ถึง และต้องตัดสินใจว่า “ช้าเกินไป” คือกี่นาที ถ้ารอตลอดไปคำตอบสมบูรณ์ขึ้น แต่ state ก็ไม่ถูกล้างและ dashboard ไม่เคยนิ่ง

Out-of-Order ไม่ได้แปลว่าข้อมูลผิด

ข้อความอาจสลับลำดับเพราะ mobile device offline, network retry, partition ต่างกัน หรือ producer หลายตัวมีนาฬิกาไม่ตรงกัน ระบบจึงไม่ควรตีความว่าข้อความมาทีหลังต้องเกิดทีหลังเสมอ ลำดับที่ต้องการอาจเป็นลำดับต่อ device, ต่อ account หรือทั่วทั้งระบบ ซึ่งมีต้นทุนและความเป็นไปได้ต่างกัน

นาฬิกาก็เป็นข้อมูลชนิดหนึ่ง
Event Time ที่แนบมากับข้อความอาจผิดจาก clock skew หรือ timezone conversion การใช้ Event Time จึงต้องกำหนดหน่วย เขตเวลา แหล่งที่มาของ timestamp และวิธีรับมือค่าที่อยู่ไกลเกินจริง ไม่เช่นนั้นเหตุการณ์จาก “อนาคต” เพียงหนึ่งรายการอาจดัน watermark ไปไกลและทำให้ข้อมูลปกติถูกมองว่าสาย

3Window: วิธีแบ่งสิ่งที่ไม่มีวันจบให้ตอบได้

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

Windowลักษณะตัวอย่างที่เหมาะต้นทุนสำคัญ
Tumblingช่วงขนาดเท่ากัน ไม่ทับกันยอดขายทุก 5 นาทีแต่ละ event อยู่หนึ่ง window จัดการง่าย
Sliding / Hoppingช่วงทับกัน เลื่อนทุกระยะที่กำหนดค่าเฉลี่ย 30 นาทีล่าสุด อัปเดตทุก 5 นาทีevent หนึ่งอาจอยู่หลาย window ทำให้ state และงานเพิ่ม
Sessionรวมเหตุการณ์ที่ห่างกันไม่เกิน inactivity gapการใช้งานเว็บหนึ่งรอบของผู้ใช้ขอบเขตเปลี่ยนตามข้อมูลและ session อาจ merge กัน
Count-basedใช้จำนวนรายการล่าสุดแทนเวลาค่าเฉลี่ยจากการวัด 1,000 ครั้งล่าสุดช่วงเวลาจริงสั้นยาวตามอัตราข้อมูล
Globalข้อมูลอยู่ window เดียวแล้วใช้ trigger/กฎอื่นควบคุมCustom state หรือ cumulative resultต้องออกแบบการล้าง state เองอย่างระมัดระวัง

Tumbling Window: ง่ายแต่มีปัญหาที่ขอบ

ถ้าเหตุการณ์สำคัญสองรายการเกิดเวลา 10:04:59 และ 10:05:01 มันจะอยู่คนละหน้าต่าง 5 นาที แม้ห่างกันเพียงสองวินาที สำหรับรายงานตามช่วงเวลานี่อาจถูกต้อง แต่สำหรับตรวจ burst หรือ pattern อาจทำให้พลาด Sliding Window ช่วยลดปัญหาขอบโดยให้หน้าต่างทับกัน แลกกับการคำนวณมากขึ้น

Sliding Window: ขนาดกับระยะเลื่อนเป็นคนละค่า

Window 30 นาทีที่เลื่อนทุก 5 นาที หมายความว่า event หนึ่งอาจมีส่วนใน 6 windows ไม่ใช่เพียงหนึ่ง ยิ่ง window ยาวและ slide สั้น จำนวนกลุ่มที่ต้องอัปเดตยิ่งมาก การตั้งค่าให้ dashboard “สด” ทุกวินาทีอาจสร้างงานซ้ำจำนวนมหาศาลทั้งที่ผู้ใช้ดูผลทุกหนึ่งนาที

Session Window: ให้ข้อมูลบอกขอบเขต

Session ไม่มีเวลาเริ่มจบคงที่ ถ้าผู้ใช้มี event ใหม่ก่อน inactivity gap หมด session จะยาวออกไป เหมือนบทสนทนาที่ถือว่ายังต่อเนื่องตราบใดที่ยังไม่มีช่วงเงียบนานเกินกำหนด เหมาะกับพฤติกรรมผู้ใช้ เครื่องจักร หรือธุรกรรมที่มีช่วงกิจกรรมเป็นชุด

import org.apache.spark.sql.functions._

val events = spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "broker:9092")
  .option("subscribe", "events")
  .load()
  .select(from_json($"value".cast("string"), eventSchema).as("e"))
  .select("e.*")

val counts = events
  .withWatermark("event_time", "10 minutes")
  .groupBy(
    window($"event_time", "30 minutes", "5 minutes"),
    $"event_type"
  )
  .count()

โค้ดข้างต้นมีสามระยะที่ต้องไม่สับสน: window ยาว 30 นาที, เลื่อนทุก 5 นาที และ watermark ยอมรับความล่าช้าตามเกณฑ์ 10 นาที แต่ watermark ไม่ได้แปลว่า “รอทุก event 10 นาทีแล้วค่อยแสดง” มันเป็นกลไกติดตามความคืบหน้าของ Event Time และช่วยกำหนดว่า state เก่าพอจะล้างได้เมื่อใด

4Spark Streaming และ Micro-batch: รากทางประวัติศาสตร์

DStream = Sequence of RDDs
Spark Streaming เดิม (ก่อน Structured Streaming) จำลอง stream ด้วยการ "สับ" ข้อมูลที่ไหลเข้ามาเป็น batch เล็ก ๆ ทุก X วินาที แล้วประมวลผลแต่ละ batch เป็น RDD ธรรมดา (จากบทที่ 9) — เรียกว micro-batch processing
// DStream: ลำดับของ RDD ที่แทน stream ข้อมูล
val tweets = ssc.twitterStream()

// transformation: แปลง DStream หนึ่งเป็นอีกอันหนึ่ง
val hashTags = tweets.flatMap(status => getTags(status))

// windowed operation: นับ hashtag ในช่วง 1 นาทีล่าสุด อัปเดตทุก 5 วินาที
val tagCounts = hashTags.window(Minutes(1), Seconds(5)).countByValue()

// output operation: ส่งผลลัพธ์ไปเก็บที่ HDFS
hashTags.saveAsHadoopFiles("hdfs://...")
Incremental Window: ไม่ต้องคำนวณใหม่ทั้งหมดทุกครั้ง
// วิธีไร้เดียงสา: คำนวณ count ใหม่ทั้ง window ทุกครั้ง (ช้า)
val tagCounts = hashtags.countByValueAndWindow(Minutes(10), Seconds(1))

// วิธีฉลาด: "บวก" ค่าจาก batch ใหม่ และ "ลบ" ค่าจาก batch ที่หลุดจาก window
val tagCounts = hashtags.reduceByKeyAndWindow(_ + _, _ - _, Minutes(10), Seconds(1))
ฟังก์ชัน "inverse reduce" (การลบ) ทำให้ไม่ต้องประมวลผลข้อมูลทั้ง window ซ้ำทุกครั้งที่ window เลื่อน — ประหยัดการคำนวณไปมากเมื่อ window ยาวแต่ batch สั้น

Micro-batch ไม่ใช่ Batch ธรรมดาที่ตั้งเวลาให้ถี่ขึ้น

DStream แบ่ง input เป็น RDD ตามช่วงเวลา แต่ระบบยังต้องดูแล receiver, checkpoint, scheduling และการกู้คืนต่อเนื่อง ความคิดสำคัญคือเอากลไก fault tolerance ของ RDD มาใช้กับข้อมูลไหล วิธีนี้ให้ throughput สูงและรวมกับ Spark ecosystem ได้ดี แต่ latency ถูกผูกกับ batch interval และเวลาที่แต่ละ batch ใช้ประมวลผล

Batch Interval คือสัญญาระหว่าง Latency กับ Overhead

ถ้าตั้ง batch ทุก 100 มิลลิวินาที ระบบต้องสร้างและจัดตารางงานถี่มาก overhead อาจกินเวลาส่วนใหญ่ แต่ถ้าตั้งทุก 1 นาที ผลตอบสนองช้าและ batch หนึ่งอาจใหญ่เกินทรัพยากร หลักพื้นฐานคือเวลาเฉลี่ยและ tail processing time ของ batch ควรต่ำกว่า batch interval อย่างมีระยะเผื่อ มิฉะนั้น batch ใหม่จะเข้ามาเร็วกว่าที่ batch เก่าถูกระบาย เกิด backlog สะสม

บันไดเลื่อนที่คนขึ้นเร็วกว่าคนลง
ถ้าทุก 5 วินาทีมีงานเข้ามา 10 วินาทีของการประมวลผล Queue จะยาวขึ้นเรื่อย ๆ แม้ทุก batch ทำงานสำเร็จ ระบบไม่ได้ “ช้าหน่อย” แต่กำลังล้มแบบช้า ๆ Latency ที่ผู้ใช้เห็นจะโตตาม backlog จนคำว่า Real-time เหลือเพียงชื่อระบบ

Inverse Reduce ใช้ได้เมื่อ operation ย้อนกลับได้

Sliding sum สามารถบวกค่าที่เข้ามาใหม่และลบค่าที่หมดอายุ แต่ไม่ใช่ทุก aggregate มี inverse ที่ง่าย เช่นค่า MAX ถ้าค่าสูงสุดหลุดจาก window เราไม่รู้ค่าสูงสุดตัวใหม่จากค่า MAX เดิมเพียงค่าเดียว ต้องเก็บโครงสร้างเพิ่มหรือคำนวณใหม่บางส่วน ส่วน median และ quantile ยิ่งต้องใช้ state ซับซ้อนหรือ approximation

Aggregateอัปเดตเมื่อข้อมูลเข้าอัปเดตเมื่อข้อมูลออก
COUNT / SUMบวกจำนวนหรือค่าใหม่ลบจำนวนหรือค่าเก่าได้ตรงไปตรงมา
AVERAGEเก็บ sum และ countลบทั้ง sum และ count แล้วคำนวณใหม่
MAX / MINเทียบกับค่าสถานะเดิมถ้าค่าที่ออกเป็นค่าหลัก ต้องรู้ค่าลำดับถัดไป
MEDIANต้องรักษาโครงสร้างลำดับ/summaryการลบทำให้โครงสร้างเปลี่ยน ต้องมี algorithm เฉพาะ

5Watermark และ Late Data: จะรออดีตนานแค่ไหน

เมื่อใช้ Event Time ระบบไม่รู้ว่าข้อมูลของ window หนึ่งมาครบหรือยัง เพราะข้อความที่เก่ากว่าอาจกำลังติดอยู่ในเครือข่าย Watermark เป็นกลไกบอกความคืบหน้าของเวลาเชิงเหตุการณ์ เพื่อช่วยตัดสินใจว่า state ของอดีตช่วงใดเก่าพอจะล้างได้

แนวคิดของ Watermark
ระบบติดตาม Event Time สูงสุดที่สังเกตเห็น แล้วหักด้วยเกณฑ์ความล่าช้าที่กำหนด เช่น 10 นาที ถ้า Event Time สูงสุดที่เห็นคือ 12:14 ค่าขอบเขตโดยแนวคิดอยู่แถว 12:04 State ของ window ที่เก่ากว่าขอบเขตและตรงตามเงื่อนไขของ operator จึงมีโอกาสถูกล้าง ไม่ต้องเก็บอดีตทุกช่วงไว้ตลอดไป

Watermark ไม่ใช่กำแพงตรงเวลาที่รับประกันทุกกรณี

ผู้เรียนมักตีความว่าเหตุการณ์ที่ช้ากว่า 10 นาที “ถูกทิ้งแน่นอน” และเหตุการณ์ที่ช้าไม่เกิน 10 นาที “รับแน่นอน” ในทางปฏิบัติ semantics ขึ้นกับ operator, output mode และความคืบหน้าของ query เอกสาร Spark อธิบาย guarantee ในด้านที่ข้อมูลช้าภายในเกณฑ์จะไม่ถูกทิ้ง ส่วนข้อมูลที่ช้ากว่าเกณฑ์อาจถูกนำมารวม หรืออาจถูกทิ้งเมื่อ state ถูกล้างแล้วก็ได้ เราจึงไม่ควรใช้ watermark เป็นเครื่องตรวจเวลาสายแบบ deterministic โดยไม่ดู semantics ของ operation

เกณฑ์ Watermark ควรมาจาก Distribution ของความล่าช้า

การเลือก 10 นาทีเพราะเป็นเลขกลมไม่ใช่การออกแบบที่ดี เราควรวัด ingestion_time - event_time ตามแหล่งข้อมูล ดู percentile และดูเหตุการณ์ผิดปกติ เช่น mobile offline หลายชั่วโมง จากนั้นค่อยตัดสินใจว่าอยากครอบคลุมข้อมูลกี่เปอร์เซ็นต์ และรับ state/latency ได้เท่าไร

ตั้ง Watermark สั้นตั้ง Watermark ยาว
State เล็กลงและล้างเร็วรองรับข้อมูลมาช้าได้มากขึ้น
ผลลัพธ์นิ่งและปิดหน้าต่างเร็วขึ้นผลอาจถูกปรับย้อนหลังนานขึ้น
เสี่ยงทิ้งข้อมูลมาช้าที่ธุรกิจยังต้องการใช้ memory/storage และเวลา recovery มากขึ้น

Late Data มีทางออกมากกว่าทิ้งหรือรอ

ตัวเลขเร็วกับตัวเลขสุดท้ายอาจเป็นคนละผลิตภัณฑ์
Dashboard การจราจรต้องการภาพเร็ว แม้ขาดรถบางคัน แต่ใบแจ้งหนี้ต้องการยอดที่ตรวจสอบได้ ระบบหนึ่งอาจมี “provisional result” จาก Streaming และ “finalized result” จาก reconciliation ภายหลัง ความถูกต้องไม่ได้มีค่าเดียว แต่ผูกกับเวลาและวัตถุประสงค์

6Stateful Streaming: สิ่งที่ระบบต้องจำระหว่างเหตุการณ์

Operation เช่น select หรือ filter ประมวลผลแต่ละ event ได้โดยไม่จำอดีต เรียกว่า stateless แต่การนับต่อ key, window aggregation, deduplication, stream-stream join, session และ CEP ต้องเก็บ state ข้ามเหตุการณ์ ความยากของ Streaming ส่วนใหญ่จึงซ่อนอยู่ในคำถามว่า “ต้องจำอะไร และลืมเมื่อใด”

OperationState ต่อ Key/Windowเงื่อนไขล้าง
Running countตัวนับต่อ keyTTL, สิ้นอายุบัญชี หรือไม่ล้างถ้าต้องการตลอดชีวิต
Window aggregateผลรวม/จำนวนต่อ key และ windowWatermark ผ่านจุดสิ้นสุดตาม semantics
DeduplicationEvent ID ที่เคยเห็นเมื่อแน่ใจว่า ID เก่าไม่ย้อนกลับมาอีก
Sessionขอบเขตและ aggregate ของ session ที่ยังเปิดพ้น inactivity gap และ watermark
Stream-stream joinแถวจากทั้งสองฝั่งที่รอคู่Time constraint และ watermark บอกว่าไม่มีคู่ใหม่ที่เป็นไปได้

State โตจาก Cardinality × ช่วงเวลาที่เก็บ

ถ้ามีผู้ใช้ 100 ล้านรายและเก็บ state หนึ่งก้อนต่อผู้ใช้ แม้ก้อนละไม่กี่ร้อย byte ก็กลายเป็นหลายสิบ GB ยังไม่รวม index, serialization และ checkpoint ถ้า key ใหม่เพิ่มต่อเนื่องโดยไม่มี TTL state จะโตอย่างไม่มีขอบเขต แม้อัตรา event ต่อวินาทีคงที่

Hot Key ใน Streaming

Key UNKNOWN หรือบัญชียอดนิยมอาจรับ event มากกว่าค่าเฉลี่ยหลายล้านเท่า ทำให้ partition หนึ่งแบก state และงานหนักกว่าส่วนอื่น การเพิ่ม executor ไม่ช่วยถ้า hot key ยังต้องอยู่ task เดียวเพื่อรักษาความถูกต้อง วิธีแก้อาจเป็นแยก key พิเศษ, ทำ local aggregation, salting แล้วรวมสองชั้น หรือเปลี่ยนนิยามผลลัพธ์

Custom Stateful Processing

Built-in window และ aggregation เหมาะกับโจทย์มาตรฐาน แต่บางงานต้องเก็บ state หลายค่า ตั้ง timer หรือจัดการ lifecycle เฉพาะ ใน Spark 4.x มี transformWithState เป็น API รุ่นใหม่สำหรับ arbitrary stateful processing ต่อ key แนวคิดสำคัญกว่าชื่อ API คือ state ต้องมี schema, การหมดอายุ และเส้นทางกู้คืนที่ชัด ไม่ใช่ HashMap ที่ปล่อยให้โตไปเรื่อย ๆ

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

7Smart Window Counting: จำให้น้อย แต่ตอบได้มาก

Incremental Window ยังถือว่าเราจำค่าที่ต้องนำออกจากหน้าต่างได้ แต่ถ้า stream เร็วมากหรือหน้าต่างยาวมาก การเก็บทุกเหตุการณ์อาจแพงเกินไป เราจึงยอมให้คำตอบคลาดเคลื่อนเล็กน้อย แลกกับหน่วยความจำที่ลดลงมาก

แนวคิดของ DGIM หรือ Exponential Histogram
แทนที่จะจำตำแหน่งของเลข 1 ทุกตัว เรารวมเหตุการณ์เก่าเป็นถังขนาด 1, 2, 4, 8, ... เหตุการณ์ใหม่จำละเอียด เหตุการณ์เก่าจำเป็นกลุ่ม คล้ายการดูภาพใกล้ด้วยความละเอียดสูง แต่ย่อรายละเอียดของฉากที่อยู่ไกลออกไป เราจึงประมาณจำนวนเหตุการณ์ในช่วงล่าสุดได้โดยไม่เก็บ stream ทั้งหมด
โจทย์แนวคิดที่ใช้
ผลรวมที่ลบของเก่าได้บวกข้อมูลใหม่และลบข้อมูลที่หลุดจาก window
นับเหตุการณ์ในหน้าต่างใหญ่DGIM หรือ Exponential Histogram เก็บกลุ่มขนาดเพิ่มเป็นเท่าตัว
ถามว่าเคยเห็นหรือยังBloom Filter ตอบแบบประหยัดพื้นที่ โดยยอมให้มี false positive ได้
หาค่าที่พบบ่อยCount-Min Sketch เก็บตัวนับจาก hash หลายชุดแทนการเก็บทุก key
ไม่ต้องจำทุกสูตร
แก่นที่ต้องเข้าใจคือข้อมูลแบบ stream ไม่มีวันจบ เราจึงมีสามทางเลือก: เก็บเฉพาะหน้าต่างล่าสุด เก็บเพียงสถานะที่อัปเดตต่อได้ หรือเก็บคำตอบโดยประมาณ ไม่มีวิธีใดเหมาะกับทุกคำถาม

เริ่มจากโจทย์ที่ง่ายแต่เก็บตรง ๆ ไม่ไหว

สมมุติมี stream ของ bit โดย 1 หมายถึงเกิดเหตุการณ์ที่สนใจ และ 0 หมายถึงไม่เกิด เราต้องตอบว่าใน k ตำแหน่งล่าสุดมีเลข 1 กี่ตัว โดย k ≤ N และ window ใหญ่สุดยาว N วิธีตรงไปตรงมาคือเก็บ bit ทั้ง N ตัวหรือเก็บ timestamp ของทุกเลข 1 แต่ถ้า N ใหญ่มากหรือมี stream หลายล้าน key หน่วยความจำจะกลายเป็นข้อจำกัดหลัก

DGIM เปลี่ยนคำถามจาก “จำทุกเหตุการณ์อย่างไร” เป็น “ต้องจำโครงสร้างอะไรจึงประมาณคำตอบได้” จุดเปลี่ยนนี้เป็นแก่นของ Streaming Algorithm เราไม่ได้ย่อข้อมูลหลังคำนวณเสร็จ แต่สร้าง summary ที่อัปเดตได้ตั้งแต่ข้อมูลไหลเข้ามา

Bucket ใน DGIM เก็บอะไร

Bucket หนึ่งเก็บอย่างน้อยสองอย่าง คือ ขนาด ซึ่งเป็นจำนวนเลข 1 ใน bucket และ timestamp ของเลข 1 ตัวล่าสุดใน bucket ขนาดเป็นกำลังของสอง ได้แก่ 1, 2, 4, 8, ... และรักษากฎว่าแต่ละขนาดมี bucket ได้ไม่เกินจำนวนที่กำหนด ในเวอร์ชันพื้นฐานมักไม่เกินสอง bucket ต่อขนาด

กฎพื้นฐานของ DGIM

  1. ปลายขวาของ bucket คือ timestamp ของเลข 1 ล่าสุดที่อยู่ใน bucket
  2. ขนาด bucket เป็น 1, 2, 4, 8, ... ตามจำนวนเลข 1 ไม่ใช่จำนวนตำแหน่งทั้งหมด
  3. เมื่อมองย้อนอดีต ขนาด bucket จะไม่เล็กลง เหตุการณ์ใกล้ปัจจุบันจึงละเอียดกว่าอดีต
  4. มี bucket ของแต่ละขนาดได้อย่างมากสองก้อน ถ้ามีก้อนที่สามให้รวมสองก้อนที่เก่าที่สุดเป็นขนาดสองเท่า
  5. ลบ bucket ที่อยู่นอก window ใหญ่สุด N ออก

เมื่อเลข 1 ใหม่เข้ามา Algorithm ทำอะไร

  1. สร้าง bucket ขนาด 1 ที่ timestamp ปัจจุบัน
  2. ถ้ามี bucket ขนาด 1 เกินสองก้อน รวมสองก้อนที่เก่าที่สุดเป็น bucket ขนาด 2
  3. ถ้าการรวมทำให้ขนาด 2 เกินสองก้อน รวมสองก้อนเก่าที่สุดเป็นขนาด 4
  4. ทำต่อเหมือนการทดเลข จนทุกขนาดกลับมาตรงกฎ
  5. ทิ้ง bucket ที่หมดอายุจาก window N
เหมือนจัดเหรียญแบบทดฐานสอง
เมื่อมีเหรียญ 1 บาทกองที่สาม เรารวมเหรียญเก่าสองกองเป็นกอง 2 บาท ถ้ากอง 2 บาทมีเกินก็รวมต่อเป็น 4 บาท เหรียญใหม่ยังแยกละเอียด ส่วนเหรียญเก่าถูกมัดเป็นก้อนใหญ่ขึ้นเรื่อย ๆ จึงใช้พื้นที่เพียงตามจำนวนระดับของกำลังสอง ไม่โตตามจำนวนเหตุการณ์ทั้งหมด

ตัวอย่างการอัปเดต Bucket

เวลาBit ใหม่Bucket จากใหม่ → เก่าสิ่งที่เกิดขึ้น
111สร้าง bucket ขนาด 1
211, 1ยังมีขนาด 1 เพียงสองก้อน
301, 1ไม่สร้าง bucket แต่เวลาเดินต่อ
411, 2เกิด bucket ขนาด 1 ก้อนที่สาม จึงรวมสองก้อนเก่าเป็น 2
511, 1, 2เพิ่มขนาด 1 อีกก้อน
611, 2, 2รวมสอง bucket ขนาด 1 ที่เก่ากว่า
711, 1, 2, 2ยังไม่เกินสองก้อนต่อขนาด
811, 2, 4เกิดการรวมต่อเนื่องจาก 1 → 2 → 4

ตารางนี้ย่อเฉพาะขนาดเพื่อเห็นรูปแบบ ใน implementation จริงต้องเก็บ timestamp ของขอบ bucket ด้วย เพราะเลข 0 ทำให้ระยะตำแหน่งไม่เท่ากับจำนวนเลข 1

ตอบ Query ใน k ตำแหน่งล่าสุดอย่างไร

เราไล่ bucket จากใหม่ไปเก่า บวกขนาดเต็มของ bucket ที่อยู่ในช่วง k ทั้งหมด เมื่อถึง bucket เก่าสุดที่คาบขอบ window เราไม่รู้ว่าเลข 1 ใน bucket นั้นอยู่ด้านในกี่ตัว จึงประมาณด้วยครึ่งหนึ่งของ bucket นั้น ส่วน bucket ที่เก่ากว่านั้นไม่นับ

estimate = 0
for bucket from newest to oldest:
    if bucket อยู่ภายใน k ทั้งก้อน:
        estimate += bucket.size
    else if bucket คาบขอบของ k:
        estimate += bucket.size / 2
        break

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

พื้นที่จัดเก็บลดลงอย่างไร

มี bucket size เพียงประมาณ log₂N ระดับ แต่ละระดับมีจำนวน bucket คงที่ และแต่ละ bucket ต้องเก็บ size กับ timestamp ที่ใช้ประมาณ log N bit จึงใช้พื้นที่ระดับ O(log²N) bit ในรูปแบบพื้นฐาน แทน O(N) bit ของการเก็บหน้าต่างตรง ๆ ความต่างมหาศาลเมื่อ N ใหญ่และมีหลาย key

ปรับความแม่นยำได้หรือไม่

ได้ แนวคิดทั่วไปคือยอมให้มี bucket ต่อขนาดมากกว่าสองก้อนก่อนรวม เมื่อ bucket เล็กลงเมื่อเทียบกับผลรวม ความคลาดเคลื่อนสัมพัทธ์จากก้อนที่คาบขอบก็ลดลง แต่จำนวน bucket และหน่วยความจำเพิ่มขึ้น นี่เป็น trade-off ที่ตรงไปตรงมา: อยากให้ความผิดพลาดเล็กลง ต้องจ่าย state มากขึ้น

DGIM ไม่ใช่ Window Aggregation ทั่วไปทุกชนิด

DGIM แบบพื้นฐานออกแบบเพื่อประมาณจำนวนเลข 1 ใน suffix ของ binary stream เราประยุกต์กับ integer ขนาดจำกัดผ่านการแยก bit หรือขยายแนวคิดไปยัง summary แบบอื่นได้ แต่ไม่ควรกล่าวว่า DGIM แก้ average, distinct count หรือ top-k โดยตรงทุกอย่าง Algorithm แต่ละแบบรักษาข้อมูลไม่เหมือนกัน เพราะคำถามที่ต้องตอบต่างกัน

เลือก Summary ให้ตรงคำถาม

คำถามโครงสร้างโดยประมาณความผิดพลาดแบบใดสิ่งที่ไม่ควรคาดหวัง
มีเหตุการณ์ binary กี่ครั้งใน window ล่าสุดDGIM / Exponential Histogramคลาดเคลื่อนที่ bucket คาบขอบไม่คืนรายการ event เดิมทุกตัว
เคยเห็นสมาชิกนี้หรือยังBloom FilterFalse positive แต่ไม่มี false negative ภายใต้สมมติฐานมาตรฐานลบสมาชิกไม่ได้ตรง ๆ ใน Bloom Filter พื้นฐาน
ความถี่ของ key ใด key หนึ่งCount-Min Sketchมักประมาณเกินจาก hash collisionไม่เก็บ key ให้ค้น top-k ได้เองถ้าไม่มีโครงสร้างเสริม
จำนวนสมาชิกไม่ซ้ำHyperLogLogค่าประมาณเชิงสถิติบอกไม่ได้ว่าสมาชิกแต่ละตัวคือใคร
Quantile / MedianQuantile sketch เช่น KLL หรือ t-digest ตามกรณีคลาดเคลื่อนด้าน rank หรือ tail ตามโครงสร้างไม่เก็บ distribution ดิบครบทั้งหมด

Mergeability: คุณสมบัติสำคัญในระบบกระจาย

Summary ที่ดีในระบบกระจายควรรวมผลจากหลาย partition ได้ โดยไม่ต้องดึง raw event ทั้งหมดกลับมา เช่น Count-Min Sketch และ HyperLogLog สามารถ merge summary จากหลายเครื่องได้ภายใต้เงื่อนไขที่ตรงกัน คุณสมบัตินี้เชื่อมกับ Monoidification จากบท Functional Programming: ถ้าสถานะมี operation รวมที่ associative เราสามารถ aggregate เป็นชั้น ๆ ลดข้อมูลก่อนข้ามเครือข่าย และกู้คืนงานได้ง่ายขึ้น

Exact หรือ Approximate ไม่ใช่คำถามทางเทคนิคล้วน ๆ

ยอดเงินในบัญชีอาจต้อง exact และตรวจสอบย้อนหลังได้ แต่จำนวนผู้ชมโดยประมาณบน dashboard อาจยอมคลาดเคลื่อน 1% เพื่อออกเร็วและประหยัดทรัพยากร การเลือกต้องระบุ error budget, latency budget, cost budget และผลกระทบเมื่อผิด ไม่ใช่สรุปว่า approximate “ด้อยกว่า” เพราะบางครั้งคำตอบที่แม่น 100% แต่สายไปหนึ่งวันไม่มีประโยชน์

หยุดคิดก่อนเดินต่อ

ถ้ามี Sensor 10 ล้านตัวและต้องนับ alarm ใน 24 ชั่วโมงล่าสุดต่อ Sensor เราควรเก็บ event ทุกตัวหรือไม่ คำตอบขึ้นกับอัตรา alarm, ความแม่นที่ต้องการ, การตรวจสอบย้อนหลัง, งบ state และว่าต้อง query ค่า k หลายขนาดหรือเพียง 24 ชั่วโมงคงที่ อย่าเลือก DGIM เพียงเพราะเป็นหัวข้อในบท ให้เริ่มจากสัญญาของคำตอบก่อนเสมอ

8กรณีศึกษา: Kafka + Spark Streaming + Elasticsearch

บริบทจากกรณีศึกษาที่ใช้ประกอบวิชา
ระบบ CDN/Security ต้อง aggregate access log ที่มีอัตราเข้าระดับหลายแสน request ต่อวินาทีและมีข้อมูลหลาย TB ต่อวัน ตัวอย่างเดิมระบุช่วงหนึ่งราว 200,000 request/วินาที หรือประมาณ 50 MB/s และอาจมี log หลายสิบล้านรายการต่อช่วงประมวลผล เมื่อแยกตาม dimension จำนวนมากและหลายระดับเวลา ปริมาณ intermediate aggregate อาจขยายเร็วกว่าจำนวน event ดิบ ตัวเลขเหล่านี้ควรอ่านเป็นบริบทของระบบ ณ เวลานั้น ไม่ใช่ benchmark ที่ทุกคลัสเตอร์จะทำได้เหมือนกัน
องค์ประกอบบทบาท
Kafkaรับ event เข้ามาแบบ streaming เป็น message queue กลาง ที่ทนต่อ throughput สูงและ decouple ผู้ผลิต/ผู้บริโภคข้อมูล
Spark Streamingอ่านจาก Kafka ผ่าน custom high-performance consumer ประมวลผล aggregation แบบ multi-stage เพื่อลดภาระในแต่ละขั้น
Elasticsearchเก็บผลลัพธ์ aggregate ท้ายสุดสำหรับ ad-hoc query และ real user monitoring dashboard
กุญแจสำคัญของการรองรับสเกลนี้: Multi-Stage Aggregation
แทนที่จะ aggregate ข้อมูลดิบทั้งหมดในขั้นตอนเดียว (ซึ่งจะสร้างคอขวดมหาศาล) ระบบแบ่งการ aggregate เป็นหลายชั้น (เช่น aggregate ระดับ executor ก่อน แล้วค่อย aggregate ข้าม executor อีกที) — หลักการเดียวกับ combiner ใน MapReduce (บทที่ 4) ที่ลดข้อมูลก่อนส่งข้ามเครือข่าย เพียงแต่ทำในบริบทของ streaming แทน batch

ขยาย 300 มิติอย่างไรไม่ให้เกิด Explosion

ถ้า event หนึ่งถูกแตกเป็น aggregate record หลายร้อยชุดอย่างตรงไปตรงมา จำนวน record ระหว่างทางอาจสูงกว่า input หลายร้อยเท่า วิธีแก้คือรวมภายใน partition ก่อน ส่งเฉพาะ partial aggregate ตาม key ที่ต้องการ และแยก query ตาม SLA แทนการคำนวณทุก dimension ทุก granularity ในเส้นเดียว

Kafka ทำหน้าที่เป็น Log ไม่ใช่เพียงท่อส่งข้อความ

Kafka เก็บ record ตาม partition พร้อม offset ทำให้ consumer อ่านต่อหรือ replay ช่วงเดิมได้ Producer และ consumer จึงไม่ต้องทำงานด้วยความเร็วเท่ากันทุกขณะ อย่างไรก็ตาม Kafka ไม่ได้ทำให้ downstream ถูกต้องเอง เราต้องกำหนด key เพื่อรักษาลำดับภายใน partition, retention ให้ยาวพอสำหรับ recovery และตรวจว่า consumer lag โตเกิน SLA หรือไม่

Elasticsearch เหมาะกับการค้น แต่ต้องออกแบบการเขียนซ้ำ

ผล aggregate ที่เขียนไป sink อาจถูก retry หลัง failure ถ้าใช้ document ID แบบสุ่ม การ retry จะสร้างเอกสารซ้ำ แต่ถ้าสร้าง ID แบบ deterministic จาก window + dimensions การเขียนเดิมสามารถ upsert ทับได้ การทำ sink ให้ idempotent จึงเป็นส่วนหนึ่งของความถูกต้อง ไม่ใช่ optimization เสริม

9สถาปัตยกรรม Streaming: Source → Compute → Sink

ระบบ Streaming จริงไม่ได้มีเพียง Spark job หนึ่งตัว แต่เป็นสายตั้งแต่ producer, message log, stream processor, state/checkpoint ไปจน sink และผู้ใช้ปลายทาง ปัญหาที่เกิดในส่วนหนึ่งส่งผลต่อส่วนอื่น เช่น sink ช้าทำให้ processor ช้า consumer lag เพิ่ม และ retention ของ source อาจหมดก่อนกู้ระบบเสร็จ

ส่วนประกอบหน้าที่ตัวชี้วัดสำคัญ
Producerสร้าง event, timestamp, key และ event IDอัตราส่ง, error, retry, clock skew
Message Log / Brokerเก็บ event ตาม partition และรองรับ replayingress, partition skew, retention, consumer lag
Stream Processorแปลง Join Aggregate ตรวจ pattern และรักษา stateinput rate, processing rate, batch duration, state size
Checkpoint / State Storeบันทึก progress และ state สำหรับ recoverycheckpoint latency, size, failure, restore time
Sinkรับผลลัพธ์ไปใช้ค้น ตัดสินใจ หรือเก็บระยะยาวwrite latency, rejection, duplicate, upsert cost

Partition Key กำหนดทั้งลำดับและการกระจายงาน

ถ้า key ด้วย account_id event ของบัญชีเดียวกันมักไป partition เดียว จึงรักษาลำดับต่อบัญชีและทำ stateful processing ได้ง่าย แต่บัญชียอดนิยมอาจกลายเป็น hot partition ถ้าใช้ random key กระจายดีขึ้นแต่ลำดับและ state ต่อบัญชีซับซ้อนขึ้น Key จึงเป็นการตัดสินใจเชิง semantic และ performance พร้อมกัน

Backpressure: เมื่อปลายทางกินช้ากว่าต้นทางผลิต

ระบบที่รับ 100,000 event/s แต่ประมวลผลได้ 80,000 event/s จะสะสมหนี้ 20,000 event ทุกวินาที ต่อให้ยังไม่ล้ม Latency จะโตและ retention ถูกกินเรื่อย ๆ ทางแก้มีทั้งเพิ่มทรัพยากร ลดงานต่อ event ปรับ partition จำกัด input rate ลดความถี่ output หรือยอมลดรายละเอียดของคำตอบ

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

Capacity Planning จากอัตราสูงสุด ไม่ใช่ค่าเฉลี่ยอย่างเดียว

Traffic อาจพุ่งช่วงแคมเปญหรือเหตุการณ์ฉุกเฉิน เราควรดู peak rate, burst duration, record size, serialization, amplification จาก Join/Aggregate และเวลาที่ sink ชะลอ ถ้าระบบรองรับค่าเฉลี่ยได้พอดี จะไม่มีพื้นที่เหลือสำหรับ failure หรือ replay

10ความถูกต้องเมื่อข้อความซ้ำและระบบล้ม

คำว่า Exactly-once มักถูกใช้กว้างเกินจริง เราต้องถามว่า exactly-once ที่ส่วนใด: การอ่าน offset, การอัปเดต state, หรือผลข้างเคียงที่ sink ภายนอก ระบบอาจประมวลผล state อย่างถูกต้อง แต่ส่งอีเมลซ้ำสองฉบับหลัง retry ได้

Semanticsสิ่งที่รับประกันโดยแนวคิดผลที่อาจเกิด
At-most-onceไม่ retry หลังส่งหรือประมวลผลล้มไม่ซ้ำ แต่ event อาจหาย
At-least-onceretry จนสำเร็จไม่ควรหาย แต่เกิดซ้ำได้
Exactly-once processing semanticsผลเชิงตรรกะเหมือน event มีผลครั้งเดียวภายในขอบเขตที่ระบบรองรับต้องอาศัย replay, checkpoint และ sink semantics ที่เข้ากัน

Checkpoint จำอะไร

Structured Streaming ใช้ checkpoint เก็บ progress เช่น offset ที่ประมวลผลแล้ว รวมถึง metadata และ state ของ stateful operator ตาม query การ Restart ด้วย checkpoint เดิมช่วยให้ query อ่านต่อและกู้ state แต่ checkpoint ผูกกับโครงสร้าง query หลายส่วน การแก้ query บางชนิดแล้วใช้ checkpoint เดิมอาจไม่รองรับ จึงต้องมีแผน migration ไม่ใช่เปลี่ยนโค้ดแล้วหวังว่าจะอ่านต่อได้เสมอ

Idempotency: ทำซ้ำแล้วผลเหมือนเดิม

ถ้า sink รับ key เดิมแล้ว upsert ค่าเดิม การ retry ไม่ทำให้ผลสะสมซ้ำ เช่นเขียนยอดของ window ด้วย key (window_start, customer_id) แต่คำสั่ง “เพิ่มยอดอีก 100” ไม่ idempotent ถ้าถูกเรียกสองครั้ง เราอาจเปลี่ยนเป็นเขียนยอดรวมเวอร์ชันล่าสุด หรือใช้ transaction/deduplication ที่ sink รองรับ

Event ID และ Deduplication

Producer ควรสร้าง Event ID ที่คงเดิมเมื่อ retry ไม่ใช่ UUID ใหม่ทุกครั้ง มิฉะนั้น downstream แยกไม่ออกว่าเป็นเหตุการณ์ใหม่หรือสำเนา State สำหรับ dedup ต้องมีขอบเขตเวลา เพราะการจำ ID ตลอดประวัติทำให้ state โตไม่มีวันจบ ขอบเขตนี้ต้องยาวกว่าระยะที่ duplicate มีโอกาสกลับมา

Exactly-once ไม่ได้ทำให้โลกภายนอกย้อนเวลา
ถ้า stream processor เรียก API โอนเงิน ส่ง SMS หรือเปิดวาล์ว แล้วล้มก่อนบันทึกว่าเสร็จ การ replay อาจเรียกซ้ำ เราต้องใช้ idempotency key, transactional outbox, two-phase protocol หรือ compensation ตามระบบ เป้าหมายไม่ใช่ติดป้าย Exactly-once แต่ทำให้ผลทางธุรกิจไม่ผิดเมื่อมี retry

End-to-End Correctness

ความถูกต้องตลอดสายต้องดู producer, broker, processor และ sink ร่วมกัน ถ้า producer สร้าง event ซ้ำแต่ processor เชื่อว่าทุก ID ใหม่ หรือ sink append อย่างเดียวโดยไม่มี dedup ความรับประกันภายใน Spark ไม่พอ การเขียนเอกสารสถาปัตยกรรมควรระบุ failure scenario แล้วไล่ว่าแต่ละขั้น replay อะไรและผลลัพธ์จะเป็นอย่างไร

11เมื่อยุคเปลี่ยน: จาก DStream สู่ Structured Streaming

แล้ว · DStream API (Spark 1.x-2.x ตอนต้น)
ตอนนี้ · Structured Streaming (Spark 2.x+/3.x/4.x)
DStream คือ RDD ของ RDD — API แยกจาก DataFrame/SQL โดยสิ้นเชิง ต้องเรียนรู้ชุด API ใหม่แยกต่างหาก
Structured Streaming ใช้ DataFrame API ตัวเดียวกับ batch processing (บทที่ 9) — มองว่า stream คือ "ตารางที่เพิ่มแถวใหม่ตลอดเวลา (unbounded table)" โค้ด batch และ streaming แทบเหมือนกันทุกประการ ต่างแค่ตอนอ่าน/เขียนข้อมูล
Micro-batch เป็นโมเดลเดียวที่มี — latency ต่ำสุดจำกัดอยู่ที่ระดับหลักวินาที (ขนาด batch ที่เล็กที่สุดที่ยังมี overhead คุ้มค่า)
Structured Streaming เพิ่มโหมด Continuous Processing ที่ให้ latency ระดับมิลลิวินาที (แม้ยังมีข้อจำกัดด้าน operator ที่รองรับ) — ให้เลือกใช้ตาม trade-off ระหว่าง latency กับ throughput ที่ต้องการ
Kafka + Spark Streaming เป็นสถาปัตยกรรมอ้างอิงหลักสำหรับ real-time pipeline
ปัจจุบันมีทั้ง Spark Structured Streaming, Apache Flink และระบบเฉพาะทางอื่น การเลือกควรดู latency, stateful workload, ecosystem, ความชำนาญของทีม และการปฏิบัติการจริง Spark เด่นตรงการใช้ DataFrame/SQL และเครื่องมือร่วมกับงาน Batch ได้ ส่วนระบบ event-at-a-time อาจเหมาะกับงาน stateful ที่ต้องการ latency ต่ำกว่าในบางกรณี

Unbounded Input Table และ Result Table

Structured Streaming อธิบาย stream เป็นตาราง input ที่มีแถวใหม่เพิ่มตลอดเวลา Query ทำงานเหมือน DataFrame query แล้วอัปเดต Result Table ตาม event ที่เข้ามา ความคิดนี้ช่วยให้ Filter, Select, GroupBy และ Join มีภาษาเดียวกับ Batch แต่ไม่ได้หมายความว่า operator Batch ทุกตัวใช้กับ Stream ได้โดยไม่มีเงื่อนไข เพราะบาง operation ต้องเห็นข้อมูลทั้งหมดก่อนจึงตอบได้

Output Mode: จะส่งส่วนใดของ Result Table ออกไป

Modeสิ่งที่ส่งออกในแต่ละ Triggerเหมาะกับข้อควรระวัง
Appendเฉพาะแถวใหม่ที่ถือว่าสรุปแล้วตาม semantics ของ queryStateless result หรือ window ที่ใช้ watermark จนปิดได้ผลที่ยังอาจเปลี่ยนไม่สามารถ append เป็น final ได้ทันที
Updateเฉพาะแถวที่ถูกเพิ่มหรือเปลี่ยนตั้งแต่ trigger ก่อนDashboard และ sink ที่รองรับ upsert/updateSink ต้องระบุ key และจัดการ update อย่างถูกต้อง
CompleteResult Table ทั้งชุดทุก triggerAggregate state ขนาดเล็กที่ต้องการ snapshot เต็มแพงมากเมื่อ result โต และไม่เหมาะกับ cardinality สูง

Output Mode ไม่ใช่เพียงรูปแบบการเขียน แต่ผูกกับความหมายว่าแถวใด “เปลี่ยนได้อีก” ตัวอย่าง window aggregate ใน Append Mode ต้องรอ watermark ผ่านจนระบบมองว่า window ไม่ควรได้รับข้อมูลที่ยอมรับเพิ่มแล้ว จึงส่งแถวสุดท้ายออก

Trigger: ประมวลผลบ่อยเพียงใด

val query = counts.writeStream
  .outputMode("update")
  .format("console")
  .option("checkpointLocation", "/checkpoints/event-counts")
  .trigger(Trigger.ProcessingTime("10 seconds"))
  .start()

Trigger 10 วินาทีไม่ได้รับประกันว่าทุกผลลัพธ์ออกภายใน 10 วินาที ถ้า batch ใช้เวลา 25 วินาที trigger ถัดไปไม่สามารถย้อนเวลาแก้กำลังประมวลผลที่ไม่พอได้ Latency ยังรวมเวลารอใน source, scheduling, Shuffle, state store และ sink ด้วย

Continuous Processing ต้องอ่าน Guarantee ให้ครบ

โหมด Continuous Processing ถูกออกแบบเพื่อลด latency อย่างมากสำหรับ query ที่รองรับ แต่มี operator จำกัดและใช้ at-least-once fault-tolerance semantics ต่างจาก default micro-batch ที่รองรับ exactly-once processing semantics ในขอบเขตที่เอกสารกำหนด ดังนั้นประโยค “Continuous เร็วกว่า” ยังไม่พอ ต้องถามต่อว่างานของเรารองรับหรือไม่และ sink รับ duplicate ได้อย่างไร

Stream-Static Join กับ Stream-Stream Join

การ Join stream กับตาราง static เช่น event กับข้อมูลประเภทสินค้า โดยทั่วไปง่ายกว่า เพราะ static side มีขอบเขต แต่ถ้าข้อมูลอ้างอิงเปลี่ยน เราต้องรู้ว่า query จะเห็น version ใหม่เมื่อใด ส่วน Stream-Stream Join ต้องเก็บเหตุการณ์ทั้งสองฝั่งที่ยังรอคู่ จึงต้องมีเงื่อนไขเวลาและ watermark เพื่อกำหนดว่าแถวเก่าเมื่อใดไม่มีคู่ใหม่ที่เป็นไปได้แล้ว

val impressions = impressionStream
  .withWatermark("impression_time", "2 hours")

val clicks = clickStream
  .withWatermark("click_time", "3 hours")

val matched = impressions.as("i").join(
  clicks.as("c"),
  expr("""
    i.ad_id = c.ad_id AND
    c.click_time BETWEEN i.impression_time
                     AND i.impression_time + INTERVAL 1 HOUR
  """)
)

เงื่อนไขเวลาบอกว่า click จะจับคู่กับ impression ได้ภายในหนึ่งชั่วโมง ระบบจึงมีเหตุผลล้าง state เมื่อ watermark ของอีกฝั่งเดินผ่านระยะที่คู่ใหม่เป็นไปไม่ได้ ถ้า Join เพียง ad_id โดยไม่มีขอบเขตเวลา state อาจต้องรอคู่ไปตลอดชีวิตของ query

Streaming Query ไม่ควรอ่านด้วย Code อย่างเดียว

ควรตรวจ query progress ซึ่งมี input rows per second, processed rows per second, batch duration, event-time statistics, watermark และ state operator metrics ถ้า state โตต่อเนื่องทั้งที่ watermark เดิน อาจหมายถึง key cardinality โต, เงื่อนไขล้างไม่ทำงานตามคาด หรือ query ไม่มีขอบเขตที่ระบบใช้ล้างได้

12การวัดและแก้ปัญหา Streaming Pipeline

การปรับ Streaming เริ่มจากแยกอาการ ไม่ใช่เพิ่ม executor ทันที Latency สูงอาจเกิดจาก source lag, query plan, data skew, state store, garbage collection หรือ sink ที่รับไม่ทัน แต่ละสาเหตุต้องแก้คนละจุด

อาการหลักฐานที่ควรดูสมมุติฐานแรก
Input rate สูงกว่า processing rate ต่อเนื่องConsumer lag และ batch durationกำลังประมวลผลไม่พอหรือ query/sink เป็นคอขวด
Task ส่วนน้อยช้ามากTask duration, Shuffle read, records ต่อ partitionData skew หรือ hot key
State โตไม่หยุดnumRowsTotal, memoryUsedBytes, watermarkไม่มี watermark/TTL ที่ใช้ได้ หรือ cardinality เพิ่มไม่สิ้นสุด
Batch ช้าบางช่วงGC, spill, checkpoint และ sink latencyBurst, compaction, autoscaling หรือ external service ช้า
ตัวเลขเปลี่ยนย้อนหลังมากLate-event distribution และ output modeแหล่งข้อมูลมาช้าหรือ watermark ยาวตามการออกแบบ
ตัวเลขต่ำกว่า Batch reconciliationLate events, parse errors, dead-letter countsเหตุการณ์ถูกทิ้ง มาช้า หรือแปลงไม่สำเร็จ

Latency ต้องแยกเป็นช่วง

End-to-end latency อาจแบ่งเป็นเวลา Producer → Broker, รอใน Broker, Broker → Processor, การคำนวณ, State/Checkpoint, Sink write และเวลาที่ผู้ใช้เห็นผล ถ้าวัดเพียง batch duration เราอาจประกาศว่าใช้ 2 วินาที ทั้งที่ event รอใน Kafka มาแล้ว 20 นาที

Throughput กับ Latency ดึงกันคนละทาง

Batch ใหญ่ช่วย amortize scheduling และ I/O overhead ทำให้ throughput ดี แต่ event รอนานขึ้น Batch เล็กตอบเร็วแต่ overhead สูง การออกแบบต้องมี SLO ชัด เช่น 99% ของ event ปรากฏใน dashboard ภายใน 30 วินาทีที่อัตรา 200,000 event/s ไม่ใช่บอกเพียงว่าเป็น Real-time

Load Test ต้องรวม Failure และ Replay

การยิงข้อมูลคงที่ในภาวะปกติไม่พอ ควรทดสอบ burst, broker partition หยุด, executor หลุด, sink ช้า, restart จาก checkpoint, schema เปลี่ยน และ replay ข้อมูลเก่า ระบบอาจรับ live traffic ได้ดีแต่พังกู้คืน เพราะต้องประมวลผล backlog พร้อม traffic ใหม่

Observability ของข้อมูลกับระบบเป็นคนละชุด

System Observability

CPU, memory, GC, network, lag, batch duration, checkpoint latency, state size และ error rate บอกว่าระบบทำงานทันหรือไม่

Data Observability

Null rate, schema drift, event-time delay, duplicate rate, distribution และ business total บอกว่าคำตอบยังมีความหมายหรือไม่

Pipeline อาจเขียวทุก dashboard ทางระบบ แต่ข้อมูล region หนึ่งหายไปทั้งวัน การ monitor เพียง infrastructure จึงตอบไม่ได้ว่าผลลัพธ์ถูกต้อง

13กรณีศึกษา: ตรวจจับธุรกรรมผิดปกติแบบต่อเนื่อง

สมมุติธนาคารต้องแจ้งเตือนเมื่อบัญชีมีธุรกรรมจำนวนมากผิดปกติภายในช่วงสั้น พร้อมตรวจ pattern การเข้าสู่ระบบจากประเทศใหม่ โจทย์นี้รวม Window, State, CEP, Deduplication และ ML ไว้ด้วยกัน

ขั้นที่ 1: กำหนด Event Contract

แต่ละ event มี event_id, account_id, event_time, ingestion_time, amount, country, device และ schema version Event ID คงเดิมเมื่อ producer retry ส่วน timestamp ใช้ UTC และมี rule แยกค่าที่อยู่ไกลเกินจริง

ขั้นที่ 2: Deduplicate ก่อนสร้าง State ธุรกิจ

ระบบตัด duplicate ตาม Event ID ภายในช่วงเวลาที่ครอบคลุม retry ที่คาดไว้ ถ้าไม่ทำ จำนวนธุรกรรมและ feature จะสูงเกินจริง แต่ state ของ ID ก็ต้องมี watermark/TTL มิฉะนั้นโตตามจำนวนธุรกรรมตลอดไป

ขั้นที่ 3: สร้าง Feature หลาย Window

Window สั้นต้องการความสด ส่วน baseline ระยะยาวอาจใช้ summary หรือ precomputed profile ไม่จำเป็นต้องเก็บ raw event ทุกตัวไว้ใน state เดียว

ขั้นที่ 4: Rule กับ Model ทำงานคนละหน้าที่

กฎ เช่น “ผิด PIN 5 ครั้งแล้วโอนเงินภายใน 3 นาที” เหมาะกับ CEP เพราะลำดับมีความหมาย ส่วนโมเดลให้ risk score จาก feature หลายมิติ Rule ที่มีความเสี่ยงสูงอาจ block ทันที ขณะที่ score ปานกลางส่งให้ตรวจสอบ การรวมสองแบบช่วยให้มีทั้ง pattern ที่อธิบายได้และความยืดหยุ่นทางสถิติ

ขั้นที่ 5: Sink ต้องไม่สร้าง Alert ซ้ำ

สร้าง Alert ID จาก account, rule/model version และช่วงเวลา แล้ว upsert ไปยังระบบ case management ถ้า processor retry alert เดิมจะไม่เปิดคดีซ้ำ การส่ง SMS หรือระงับบัญชีควรใช้ idempotency key และมี audit log แยก

ขั้นที่ 6: ผลลัพธ์มาช้าก็ยังมีประโยชน์

ธุรกรรมที่มาช้าเกิน watermark อาจไม่เปลี่ยน alert แบบทันที แต่ส่งเข้า late-event table เพื่อ reconciliation และปรับปรุง model data ภายหลัง เราแยกเส้นทางตอบสนองเร็วออกจากเส้นทางบัญชีที่ต้องครบ แทนการบังคับให้ระบบเดียวตอบสอง SLA ที่ขัดกัน

บทเรียนจากกรณีศึกษา
Real-time fraud detection ไม่ได้จบที่โมเดลเร็ว สิ่งยากกว่าคือการสร้าง feature ที่ไม่ซ้ำ ไม่ใช้อนาคต มีเวลาอ้างอิงถูกต้อง รักษา state ได้ และไม่ลงโทษลูกค้าซ้ำเมื่อระบบ retry ความถูกต้องของ algorithm กับความถูกต้องของระบบต้องมาพร้อมกัน

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

Streaming เปลี่ยนโจทย์จากการคำนวณข้อมูลที่รู้ขอบเขต ไปเป็นการตัดสินใจจากข้อมูลที่ยังมาไม่ครบ เราจึงต้องแยก Event Time ออกจาก Processing Time เลือก Window ตามความหมายของคำถาม ใช้ Watermark เพื่อสร้างขอบเขตการรอ และกำหนด lifecycle ของ State อย่างชัดเจน

Smart Window Counting แสดงให้เห็นอีกมุมหนึ่งว่าเราไม่จำเป็นต้องเก็บอดีตทุกเหตุการณ์เพื่อให้ตอบคำถามได้ ถ้าโจทย์ยอมรับ approximation เราสามารถรักษา summary ที่ใช้พื้นที่เล็กมาก เช่น DGIM สำหรับนับเลข 1 ใน sliding window หลักสำคัญไม่ใช่จำชื่อ Algorithm แต่คือรู้ว่า summary เก็บอะไร สูญเสียอะไร รวมข้ามเครื่องได้หรือไม่ และ error ที่ยอมรับได้เท่าไร

ถ้าจำได้เพียงแปดเรื่อง ให้จำเรื่องเหล่านี้

  1. Stream ไม่มีจุดจบ แต่คำถามต้องมีขอบเขตผ่าน Window, Session หรือ State rule
  2. Event Time บอกโลกจริง ส่วน Processing Time บอกสภาพของระบบ อย่าใช้แทนกันโดยไม่รู้ตัว
  3. Watermark คือกลไกจัดการความคืบหน้าและ State ไม่ใช่คำสัญญาง่าย ๆ ว่าข้อมูลเกิน N นาทีจะถูกทิ้งทุกครั้ง
  4. State ทำให้เชื่อมอดีตกับปัจจุบันได้ แต่ต้องมีเงื่อนไขล้างและแผนกู้คืน
  5. เมื่ออัตราเข้ามากกว่าอัตราประมวลผล Broker เพียงซื้อเวลา Backlog ยังโตอยู่
  6. Exactly-once ต้องพิจารณาตลอดสาย โดยเฉพาะ Sink และผลข้างเคียงภายนอก
  7. Approximation เป็นสัญญาทางวิศวกรรม ไม่ใช่คำตอบชั้นสอง ต้องระบุ Error, Latency และ Cost Budget
  8. ระบบ Real-time ที่ดีต้องวัดทั้ง System Observability และ Data Observability

ขั้นตอนถัดไป คือข้อมูลแบบกราฟและ PageRank เราจะเปลี่ยนจากเหตุการณ์ที่เรียงตามเวลาไปสู่ข้อมูลที่ความสัมพันธ์ระหว่างโหนดเป็นหัวใจ แม้โจทย์ต่างกัน แต่แนวคิดเดิมยังตามไปด้วย ได้แก่การแบ่งข้อมูล การลดการเคลื่อนย้าย State การคำนวณซ้ำแบบ incremental และการแลกความแม่นกับทรัพยากรในระบบขนาดใหญ่

คำถามซ้อมสอบ
  1. อธิบายความแตกต่างระหว่าง DSMS, Stream Analytics และ CEP พร้อมยกตัวอย่าง State ที่แต่ละงานต้องเก็บ
  2. Event Time, Ingestion Time และ Processing Time ต่างกันอย่างไร และการเลือกผิดส่งผลต่อ Window อย่างไร
  3. เปรียบเทียบ Tumbling, Sliding และ Session Window พร้อมอธิบายปัญหาที่ขอบหน้าต่าง
  4. Watermark มีไว้ทำอะไร เหตุใดจึงไม่ควรอธิบายง่าย ๆ ว่า “ข้อมูลที่ช้ากว่าเกณฑ์จะถูกทิ้งแน่นอน”
  5. อธิบายว่าฟังก์ชัน inverse reduce ช่วย Sliding Window ได้อย่างไร และเหตุใด MAX หรือ MEDIAN จึงจัดการยากกว่า SUM
  6. อธิบายกฎของ DGIM การรวม Bucket และวิธีประมาณจำนวนเลข 1 ใน k ตำแหน่งล่าสุด
  7. เหตุใด DGIM จึงใช้พื้นที่ระดับ logarithmic แต่ยอมให้คำตอบคลาดเคลื่อน และจะแลกหน่วยความจำเพื่อเพิ่มความแม่นได้อย่างไร
  8. เปรียบเทียบ DGIM, Bloom Filter, Count-Min Sketch และ HyperLogLog ว่ารักษาข้อมูลใดและตอบคำถามใด
  9. จากกรณีศึกษา Log Aggregation อธิบายว่าทำไม Multi-stage Aggregation จึงลด Network และ State ได้
  10. At-most-once, At-least-once และ Exactly-once Processing Semantics ต่างกันอย่างไร และเหตุใด Sink ยังสำคัญ
  11. ออกแบบ Deduplication สำหรับ Event ที่ Retry ได้ โดยระบุ Event ID, State และเวลาที่ล้าง State
  12. Append, Update และ Complete Output Mode ต่างกันอย่างไร และสัมพันธ์กับ Watermark อย่างไร
  13. อธิบายเหตุผลที่ Stream-Stream Join ต้องมีเงื่อนไขเวลาและ Watermark เพื่อควบคุม State
  14. ออกแบบ Pipeline ตรวจจับ Fraud ตั้งแต่ Kafka ถึง Sink พร้อมระบุ Hot Key, Backpressure, Late Data และ Idempotency