ข้าม​ไป​ยัง​เนื้อหา

"Event-Driven" หมายความ​ว่า​อะไร​กัน​แน่?

เกี่ยว​กับ​บท​แปล​นี้

บทความ​นี้​เป็น บท​แปล​ภาษา​ไทย​อย่าง​ไม่​เป็น​ทางการ ของ “What do you mean by “Event-Driven”?” โดย Martin Fowler เผยแพร่​ครั้ง​แรก​บน martinfowler.com เมื่อ 7 กุมภาพันธ์ 2017

แปล​และ​เรียบเรียง​เพื่อ​การ​ศึกษา — ไดอะแกรม​ประกอบ​และ​ตัวอย่าง​เชิง​เล่า​เรื่อง (เช่น ระบบ​ประกันภัย) เป็น​สิ่ง​ที่​ผู้​แปล​สร้าง​ขึ้น​ใหม่​เพื่อ​ช่วย​ความ​เข้าใจ ต้นฉบับ​ไม่มี​ไดอะแกรม​และ​ไม่​ได้​ยก​ตัวอย่าง​เหล่า​นี้ ส่วน​ศัพท์​เทคนิค​คง​ไว้​เป็น​ภาษา​อังกฤษ​พร้อม​คำ​อธิบาย​ไทย​เมื่อ​พบ​ครั้ง​แรก ลิขสิทธิ์​ของ​เนื้อหา​ต้นฉบับ​เป็น​ของ​ผู้​เขียน

ใน​ช่วง​ไม่​กี่​ปี​ที่​ผ่าน​มา เรา​ได้​เห็น​ความ​สนใจ​อย่าง​มาก​ต่อ​แนวคิด​การ​สร้าง​ระบบ​รอบ ๆ “event” (เหตุการณ์) หลาย​คน​สนับสนุน​ให้​ใช้​สถาปัตยกรรม​แบบ event-driven กับ​ระบบ​ของ​ตัวเอง โดย​ชู​ข้อดี​เรื่อง​การ​ลด coupling (ความ​ผูก​ติด​กัน​ระหว่าง​ส่วน​ต่าง ๆ) ความ​ยืดหยุ่น​ใน​การ​ปรับ​ขยาย และ​ความ​คล่องตัว​ใน​การ​ต่อยอด

แต่​เช่น​เดียว​กับ​คำ​ฮิต​อื่น ๆ พอ​คำ​ว่า “event-driven” ถูก​ใช้​กัน​แพร่หลาย มัน​ก็​เริ่ม​ถูก​ใช้​อย่าง​พร่ำเพรื่อ​จน​ความหมาย​พร่า​เลือน

ไม่​นาน​มา​นี้​ผู้​เขียน​ได้​ชวน​เพื่อน​ร่วม​งาน​หลาย​คน​ที่​ทำงาน​ใน​ด้าน​นี้​มา​รวม​ตัว​กัน​จัด workshop เพื่อ​แลกเปลี่ยน​ประสบการณ์​กัน ตลอด​การ​พูด​คุย​เรา​กลับ​พบ​ปัญหา​ร่วม​กัน​อย่าง​หนึ่ง นั่น​คือ​ผู้คนใช้​คำ​ว่า “event-driven” เพื่อ​สื่อ​ถึง​สิ่ง​ที่​แตก​ต่าง​กัน​อยู่​หลาย​อย่าง

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

ปัญหา​ไม่​ได้​อยู่​ที่ pattern เหล่า​นี้​แต่ละ​อัน แต่ละ​อัน​ล้วน​มี​ประโยชน์​ใน​บริบท​ของ​มัน​เอง ปัญหา​อยู่​ที่​การ เหมา​รวม มัน​เข้า​ด้วย​กัน​ภาย​ใต้​ร่ม​คำ​เดียว เพราะ​เมื่อ​คน​สอง​คน​พูด​คำ​ว่า “event-driven” แต่​ใน​หัวคิด​ถึง pattern คนละ​อัน บทสนทนา​ก็​คลาดเคลื่อน​กัน​ตั้งแต่​ต้น

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

4 pattern ที่​มัก​ถูก​เหมา​รวม​ว่า​เป็น event-driven
  1. Event Notification (การ​แจ้ง​เตือน​ด้วย​เหตุการณ์) — ระบบ​ส่ง​สัญญาณ​ออก​ไป​บอกว่า​มี​บาง​อย่าง​เปลี่ยน​ไป โดย​ไม่​คาด​หวัง​คำ​ตอบ​กลับ
  2. Event-Carried State Transfer (การ​ส่ง​ต่อ​สถานะ​ไป​พร้อม​กับ​เหตุการณ์) — event พก “สถานะ” (state) ที่​ผู้รับ​ต้อง​ใช้​มา​ให้​ครบ ผู้รับ​จึง​ไม่​ต้อง​ย้อน​กลับ​ไป​ถาม​แหล่ง​ข้อมูล​ต้นทาง
  3. Event Sourcing (การ​ใช้​เหตุการณ์​เป็น​แหล่ง​ความ​จริง) — เก็บ​ทุก​การ​เปลี่ยนแปลง​ไว้​เป็น​ลำดับ​ของ event แล้ว​สามารถ “เล่น​ซ้ำ” (replay) เพื่อ​สร้าง​สถานะ​ปัจจุบัน​ขึ้น​ใหม่​ได้​เสมอ
  4. CQRS (การ​แยก​ความ​รับผิดชอบ​ระหว่าง​การ​สั่ง​งาน​กับ​การ​สอบถาม) — แยก model ที่​ใช้ “เขียน/สั่ง​งาน” (command) ออก​จาก model ที่​ใช้ “อ่าน” (query)

pattern แรก​และ​เป็น​ที่​คุ้น​เคย​ที่สุด​คือ Event Notification (การ​แจ้ง​เตือน​ด้วย​เหตุการณ์) แนวคิด​คือ เมื่อ​มี​บาง​อย่าง​เปลี่ยนแปลง​ภายใน domain ของ​ระบบ​หนึ่ง ระบบ​นั้น​จะ​ส่ง event message (ข้อความ​เหตุการณ์) ออก​ไป​เพื่อ แจ้ง​ให้​ระบบ​อื่น​รู้ ว่า​มี​การ​เปลี่ยนแปลง​เกิด​ขึ้น

หัวใจ​สำคัญ​คือ ระบบ​ต้นทาง​ที่​ส่ง event ออก​ไป​นั้น ไม่​ได้​คาด​หวัง​คำ​ตอบ​กลับ และ​มัก​ไม่รู้​ด้วย​ซ้ำ​ว่า​ใคร​เป็น​ผู้รับ

หมายเหตุ​ผู้​แปล: ที่มา​ของ​ตัวอย่าง

ตัวอย่าง​ระบบ​ประกันภัย (Customer Management / Insurance Quoting) ที่​ใช้​เดิน​เรื่อง​ใน​หัวข้อ​นี้​และ​หัวข้อ​ถัด​ไป รวม​ถึง​ไดอะแกรม​ประกอบ​ทั้งหมด เป็น​สิ่ง​ที่ผู้​แปล​สร้าง​ขึ้น​เอง​เพื่อ​ช่วย​ให้​เห็น​ภาพ ไม่​ปรากฏ​ใน​บทความ​ต้นฉบับ

ต้นฉบับ​ของ Fowler ไม่​ได้​ยก​ตัวอย่าง​ใด​สำหรับ Event Notification และ​สำหรับ Event-Carried State Transfer ก็​เพียง​เอ่ย​ถึง “ระบบ​จัดการ​ลูกค้า​ที่​ยิง event ออก​ไป​เมื่อ​ลูกค้า​แก้ไข​ข้อมูล เช่น ที่​อยู่” อย่าง​กว้าง ๆ เท่านั้น

ลอง​นึกถึง​ตัวอย่าง​ของ​บริษัท​ประกันภัย สมมติ​ว่า​ลูกค้า​คน​หนึ่ง​ย้าย​ที่​อยู่ การ​เปลี่ยน​ที่​อยู่​นี้​ถูก​จัดการ​โดย​ระบบ Customer Management (ระบบ​จัดการ​ข้อมูล​ลูกค้า) แต่​การ​ย้าย​ที่​อยู่​อาจ​ส่ง​ผล​ต่อ​เบี้ย​ประกัน​ของ​ลูกค้า​คน​นั้น​ด้วย ซึ่ง​เป็น​เรื่อง​ของ​ระบบ Insurance Quoting (ระบบ​คำนวณ​เบี้ย​ประกัน)

ใน​แบบ​ดั้งเดิม code ใน​ระบบ​จัดการ​ลูกค้า​อาจ​เรียก​ไป​ยัง​ระบบ​คำนวณ​เบี้ย​ประกัน​โดยตรง

แต่​ด้วย event notification ระบบ​จัดการ​ลูกค้า​จะ​เพียง​ประกาศ event ออก​ไป​ว่า “Customer Address Changed” (ที่​อยู่​ลูกค้า​เปลี่ยน​แล้ว) ส่วน​ระบบ​คำนวณ​เบี้ย​ประกัน​ที่​สนใจ event ชนิด​นี้​ก็​คอย​ฟัง​อยู่ แล้ว​ตอบ​สนอง​ด้วย​ตัวเอง

flowchart LR
  CM["Customer Management"] -->|"emits Customer Address Changed"| BUS["Event Bus"]
  BUS -->|"notify"| INS["Insurance Quoting"]
  BUS -->|"notify"| OTHER["Other Consumers"]
  INS -.->|"query back for details"| CM

ภาพ 1 — Event Notification: ระบบ Customer Management ประกาศ event ออก​ไป​โดย​ไม่รู้​ว่า​ใคร​ฟัง ระบบ​ที่​สนใจ (เช่น Insurance Quoting) รับ event มา​แล้ว​ทำงาน​ต่อ​ของ​ตัวเอง โดย​ทั่วไป event จะ​พก​ข้อมูล​ติด​มา​แค่​พอ​บอกว่า “เกิด​อะไร​ขึ้น” กับ id สำหรับ​อ้างอิง​เท่านั้น ถ้า​ผู้รับ​ต้องการ​รายละเอียด​เพิ่ม​ก็​ย้อน​กลับ​ไป query ที่​ต้นทาง (ลูกศร​ประ)

ประโยชน์​ที่​ชัดเจน​ของ pattern นี้​คือ​ระดับ​ของ decoupling (การ​ลด​ความ​ผูก​ติด​กัน​ระหว่าง​ส่วน​ต่าง ๆ) ที่​มัน​มอบ​ให้ ระบบ​ต้นทาง​ไม่​จำเป็น​ต้อง​รู้​อะไร​เลย​เกี่ยว​กับ​ผู้รับ เรา​สามารถ​เพิ่ม​ผู้รับ​ราย​ใหม่​เข้า​มา​ฟัง event เดิม​ได้​โดย​ไม่​ต้อง​แก้ code ของ​ต้นทาง​แม้แต่​บรรทัด​เดียว

ความ​รับผิดชอบ​ถูก​แยก​ออก​จาก​กัน​อย่าง​สวยงาม domain ของ​ระบบ​ต้นทาง​ไม่​ปะปน​ไป​ด้วย​ตรรกะ​ของ​ระบบ​ปลายทาง

หาก​สนใจ​โครงสร้าง​เชิง code ของ​แนวคิด​นี้ ดู​เพิ่ม​ได้ที่ Domain Events และ Event Aggregator

event ต่าง​จาก command อย่างไร

จุด​ที่​ต้อง​ระวัง​คือ​ความ​ต่าง​ระหว่าง event กับ command (คำ​สั่ง) event คือ​การ​ประกาศ​ว่า มี​บาง​อย่าง​เกิด​ขึ้น​ไป​แล้ว เช่น “ที่​อยู่​ถูก​เปลี่ยน​แล้ว” มัน​เป็นการ​บอก​ข้อเท็จจริง ไม่​ได้​สั่ง​ให้​ผู้รับ​ราย​ใด​ทำ​อะไร​เป็นการ​เฉพาะ ส่วน command คือ​การ​ร้องขอ​ให้​ผู้รับ​ที่​เจาะจง​ลงมือ​ทำงาน​บาง​อย่าง

เส้น​แบ่ง​ที่​ดี​คือ ต้นทาง​แคร์​ผลลัพธ์​หรือ​ไม่ ถ้า​ต้นทาง​ไม่​แคร์​ว่า​จะ​เกิด​อะไร​ตาม​มา นั่น​คือ event ที่แท้​จริง แต่​ถ้า​ต้นทาง​แอบ​คาด​หวัง​ว่า​ผู้รับ​ต้อง​ทำ​สิ่ง​ใด​สิ่ง​หนึ่ง มัน​ก็​คือ command ที่​ปลอม​ตัว​มา​ใน​คราบ event

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

แต่ event notification ก็มี ข้อ​เสีย สำคัญ​อยู่ นั่น​คือ มัน​ไม่มี​ที่ไหน​บอก​ภาพ​รวม​ของ​พฤติกรรม​ทั้ง​ระบบ​ไว้​เลย เมื่อ​ลอง​มอง​ว่า “พอที่​อยู่​เปลี่ยน แล้ว​เกิด​อะไร​ขึ้น​บ้าง” คำ​ตอบ​จะ​กระจัดกระจาย​อยู่​ตาม event handler หลาย​ตัว​ที่​แยก​กัน​คนละ​ที่

ไม่มี​จุด​ใด​จุด​หนึ่ง​ใน code ที่​เรา​เปิด​ดู​แล้ว​เห็น flow (ลำดับ​การ​ทำงาน) ทั้งหมด​ได้ ตรรกะ​ของ flow กลาย​เป็น​สิ่ง​ที่ “เกิด​ขึ้น​เอง” (emergent) จาก​การ​ที่ handler แต่ละ​ตัว​ตอบ​สนอง event ของ​มัน

ความ​สามารถ​ใน​การ​เพิ่ม handler ได้​อิสระ ซึ่ง​ช่วย​เรา​ตอน​สร้าง​ระบบ กลับ​กลาย​เป็น​อุปสรรค​ตอน​ต้อง​ทำความ​เข้าใจ​และ debug ระบบ เพราะ​เรา​ไม่​อาจ​ไล่ flow จาก​การ​อ่าน code ตรง ๆ ได้​อีก

ทาง​แก้​คือ​ต้อง​ลงทุน​กับ monitoring และ logging ที่​ดี​พอ​จะ​ประกอบ​ร่าง flow ที่​เกิด​ขึ้น​จริง​กลับ​ขึ้น​มา​ได้ นี่​ไม่​ได้​แปล​ว่า event notification เป็น​สิ่ง​ไม่​ดี แต่​มัน​เรียกร้อง​วินัย​และ​เครื่องมือ​สังเกตการณ์​ที่​เหมาะสม​เพื่อ​ชดเชย​ความ​ชัดเจน​ของ flow ที่​หาย​ไป

Event-Carried State Transfer — การ​ส่ง​ต่อ​สถานะ​ไป​พร้อม​กับ​เหตุการณ์

หัวข้อ​ที่​มีชื่อ​ว่า “Event-Carried State Transfer — การ​ส่ง​ต่อ​สถานะ​ไป​พร้อม​กับ​เหตุการณ์”

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

Event-Carried State Transfer (การ​ส่ง​ต่อ​สถานะ​ไป​พร้อม​กับ​เหตุการณ์) แก้​ปัญหา​นี้​ด้วย​การ ใส่​ข้อมูล​สถานะ​ที่​ผู้รับ​จำเป็น​ต้อง​ใช้​ลง​ไป​ใน event ให้​ครบ ตั้งแต่​ต้น

กลับ​ไป​ที่​ตัวอย่าง​เดิม แทนที่ event “Customer Address Changed” จะ​พก​มา​แค่ id ของ​ลูกค้า มัน​จะ​พก​ข้อมูล​ลูกค้า​ที่​เปลี่ยนแปลง (เช่น ที่​อยู่​ใหม่​ทั้ง​ชุด) ติด​มา​ด้วย เมื่อ​ระบบ​คำนวณ​เบี้ย​ประกัน​ได้​รับ event นี้ มัน​ก็​มี​ทุก​อย่าง​ที่​ต้อง​ใช้​อยู่​แล้ว ไม่​ต้อง​ย้อน​กลับ​ไป​ถาม​ระบบ​จัดการ​ลูกค้า​อีก

โดย​ทั่วไป​ผู้รับ​จะ​เก็บ สำเนา​ข้อมูล (replica) ของ​สิ่ง​ที่​ตน​สนใจ​ไว้​ใน​ฝั่ง​ตัวเอง แล้ว​คอย​อัปเดต​สำเนา​นั้น​ทุก​ครั้ง​ที่​มี event เข้า​มา

flowchart LR
  CM["Customer Management<br/>source of truth"] -->|"event carries full customer data"| INS
  subgraph INS["Insurance Quoting"]
    direction TB
    LOGIC["Quoting Logic"]
    COPY["Local Replica of Customer Data"]
    LOGIC --> COPY
  end

ภาพ 2 — Event-Carried State Transfer: event พา​ข้อมูล​ลูกค้า​ที่​จำเป็น​มา​ให้​ครบ ระบบ Insurance Quoting จึง​เก็บ​สำเนา​ข้อมูล​ลูกค้า​ไว้​ใน​ฝั่ง​ตัวเอง และ​ทำงาน​ต่อ​ได้​โดย​ไม่​ต้อง​ย้อน​ถาม​ต้นทาง สังเกต​ว่า​ไม่มี​ลูกศร query ย้อน​กลับ​เหมือน​ภาพ​แรก​อีก​แล้ว

ข้อดี​ของ​แนวทาง​นี้​มา​จาก​การ​ที่​ผู้รับ​ไม่​ต้อง​พึ่ง​ต้นทางใน​เวลา​จริง อย่าง​แรก​คือ ลด​ภาระ (load) ที่​ตก​กับ​ระบบ​ต้นทาง เพราะ​ไม่มี​การ query ย้อน​กลับ​เข้า​มารบกวน​อีก

อย่าง​ที่​สอง​คือ เพิ่ม​ความ​พร้อม​ใช้งาน (availability) และ​ความ​ทนทาน (resilience) ของ​ผู้รับ เพราะ​แม้​ระบบ​ต้นทาง​จะ​ล่ม​ชั่วคราว ผู้รับ​ก็​ยัง​ทำงาน​ต่อ​ได้​ด้วย​สำเนา​ข้อมูล​ที่​ตน​ถือ​อยู่ กล่าว​คือ​มัน​ลด coupling ใน​เชิง​เวลา (temporal coupling) ลง​ไป​อีก​ขั้น

แต่​ทุก​อย่าง​มี​ราคา​ของ​มัน ข้อ​เสีย​คือ​ตอน​นี้​เรา​มี ข้อมูล​จำลอง​ซ้ำ (replicated data) กระจาย​อยู่​หลาย​ที่ ทุก​ระบบ​ที่​รับ event ต่าง​เก็บ​สำเนา​ข้อมูล​ที่​ตน​สนใจ​ไว้​เอง นั่น​หมาย​ถึง​พื้นที่​จัด​เก็บ​ที่​มาก​ขึ้น และ​ที่​สำคัญ​กว่า​นั้น​คือ​เรา​เข้า​สู่​โลก​ของ eventual consistency (ความ​สอดคล้อง​ที่​ตาม​มา​ทีหลัง)

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

pattern ที่​สาม​ชื่อ Event Sourcing (การ​ใช้​เหตุการณ์​เป็น​แหล่ง​ความ​จริง) และ​มัก​เป็น​ตัว​ที่​สร้าง​ความ​สับสน​มาก​ที่สุด

แนวคิด​หลัก​คือ เก็บ​ทุก​การ​เปลี่ยนแปลง​ที่​เกิด​กับ​สถานะ​ของ application ไว้​เป็น​ลำดับ​ของ event ใน​บันทึก​ที่​เรียก​ว่า event log แล้ว​เรา​จะ​สามารถ สร้าง​สถานะ​ปัจจุบัน​ขึ้น​ใหม่​ได้​เสมอ​ด้วย​การ​เล่น event ทั้งหมด​ซ้ำ (replay) ตั้งแต่​ต้น​จน​จบ

หัวใจ​ของ event sourcing คือ​การ​สลับ​บทบาท​ของ “แหล่ง​ความ​จริง” event log ต่างหาก​คือ system of record (แหล่ง​บันทึก​ที่​ถือว่า​เชื่อถือ​ได้​จริง) ส่วน​สถานะ​ปัจจุบัน​ที่​เรา​ใช้งาน​อยู่​นั้น​เป็น​เพียง ผลลัพธ์​ที่ derive (สืบทอด) ออก​มา จาก event เท่านั้น เปรียบ​ได้​เหมือน cache ที่​สร้าง​ขึ้น​เพื่อ​ความ​สะดวก

ถ้า​ลบ​สถานะ​ปัจจุบัน​ทิ้ง​ไป​ทั้งหมด เรา​ก็​สร้าง​มัน​กลับ​ขึ้น​มา​ใหม่​จาก event log ได้​เสมอ

ตัวอย่าง​ที่​ช่วย​ให้​เห็น​ภาพ​ชัด​ที่สุด​คือ version control system อย่าง Git เพราะ repository ไม่​ได้​เก็บ​แค่ “สถานะ​ล่าสุด​ของ file” แต่​เก็บ​ทุก commit (ทุก​การ​เปลี่ยนแปลง) ไว้​เป็น​ลำดับ ส่วน working copy ที่​เรา​เห็น​ใน​เครื่อง​นั้น​เป็น​เพียง​สิ่ง​ที่ derive ออก​มา​จาก​การ​เล่น commit ทั้งหมด​ซ้ำ

อีก​ตัวอย่าง​คือ บัญชี​แบบ ledger ของ​นัก​บัญชี ที่​ไม่มี​การ​ลบ​หรือ​แก้​รายการ​เก่า มี​แต่​การ​เพิ่ม​รายการ​ใหม่​เข้าไป ยอด​คง​เหลือ​เป็น​ผล​รวม​ที่​คำนวณ​จาก​รายการ​ทั้งหมด

flowchart LR
  CMD["Command"] --> APP["Application Logic"]
  APP -->|"append event"| LOG["Event Log<br/>system of record"]
  LOG -->|"replay"| STATE["Current State<br/>derived"]
  LOG -.->|"periodically"| SNAP["Snapshot"]
  SNAP -.->|"speed up rebuild"| STATE

ภาพ 3 — Event Sourcing: คำ​สั่ง​เข้า​มา​ที่​ตรรกะ​ของ app ซึ่ง​ไม่​ได้​เขียน​ทับ​สถานะ​โดยตรง แต่ append (ต่อ​ท้าย) event ลงใน event log ที่​เป็น​แหล่ง​ความ​จริง สถานะ​ปัจจุบัน​เป็น​เพียง​ผล derive จาก​การ replay event ทั้งหมด ส่วน snapshot คือ​ภาพนิ่ง​ของ​สถานะ ณ ช่วง​เวลา​หนึ่ง​ที่​ทำ​ไว้​เป็น​ระยะ เพื่อ​ไม่​ต้อง replay ตั้งแต่ event แรก​ทุก​ครั้ง

ความ​เข้าใจ​ผิด​ที่​พบ​บ่อย​เกี่ยว​กับ event sourcing

event sourcing ไม่​จำเป็น​ต้อง​เป็น asynchronous หลาย​คน​เข้าใจ​ว่า​มัน​ต้อง​มา​คู่​กับ​การ​สื่อสาร​แบบ​ไม่​ประสาน​เวลา​เสมอ แต่​ที่​จริง​ไม่ใช่ ลอง​นึกถึง​การ commit ลง git repository ใน​เครื่อง​ตัวเอง หรือ​การ commit เข้า version control แบบ​รวม​ศูนย์​อย่าง SVN นั่น​ล้วน​เป็นการ​ทำงาน​แบบ synchronous ทั้งสิ้น

ไม่ใช่​ทุก​ส่วน​ของ​ระบบ​ที่​ต้อง​รับรู้​หรือ​แตะ event log อีก​ความ​เข้าใจ​ผิด​หนึ่ง​คือ​คิด​ว่า​ทุก​คน​ที่​ใช้​ระบบ event-sourced จะ​ต้อง​เข้าใจ​และ​เข้าถึง event log เพื่อ​ดึง​ข้อมูล

ความ​จริง​คือ​ส่วน​ใหญ่​ของ​ระบบ​ทำงาน​อยู่​บน “สำเนา​ใช้งาน” (working copy) ที่ derive ออก​มา​ได้​ก็​พอ มี​เพียง​องค์​ประกอบ​ที่​จำเป็น​ต้อง​ใช้​ข้อมูล​จาก event log จริง ๆ เท่านั้น​ที่​ควร​เข้าไป​ยุ่ง​กับ​มัน​โดยตรง

เปรียบ​เหมือน​ตอน​นี้​ผู้​เขียน​พิมพ์​ต้นฉบับ​อยู่​ใน​โปรแกรม​แก้ไข​ข้อความ​ที่​ไม่รู้​อะไร​เลย​เกี่ยว​กับ commit ทั้งหลาย​ใน source tree มัน​เพียง​มอง​เห็น​ว่า​มี file อยู่​บน disk เท่านั้น

เรื่อง​วิธี​จัด​เก็บ​ภายใน​ไม่​เกี่ยว​กับ​การ​เป็น event sourcing บาง​คน​แย้ง​ว่า git ไม่ใช่ event sourcing เพราะ​ภายใน .git/objects มัน​เก็บ​เป็น snapshot ของ file และ tree (และ​ใช้ packfile ที่​ผสม snapshot กับ delta เพื่อ​ประสิทธิภาพ)

แต่​การ​ที่​ระบบ​จะ​เก็บ​ข้อมูล​ภายใน​เป็น “การ​เปลี่ยนแปลง” หรือ “ภาพนิ่ง” นั้น​ไม่​เกี่ยว​กับ​ว่า​มัน​เป็น event-sourced หรือ​ไม่

ตราบ​ใด​ที่ git ยัง​เรียก​ดู​รายการ​การ​เปลี่ยนแปลง​ทั้งหมด​ออก​มา​ให้​เรา​ได้​ตาม​ต้องการ มัน​ก็​ยัง​เป็น event sourcing อยู่ดี ไม่​ว่า​เบื้องหลัง​จะ​จัด​เก็บ​ด้วย​กลยุทธ์​ใด​ก็ตาม

ประโยชน์ ของ event sourcing นั้น​ทรง​พลัง​มาก เพราะ​เมื่อ​เรา​ถือ​ประวัติ​ทุก​ก้าว​ไว้​ครบ:

  • การ​ตรวจสอบ​ย้อนหลัง​ที่​สมบูรณ์ (Audit) — เรา​มี​บันทึก​ทุก​สิ่ง​ที่​เคย​เกิด​ขึ้น​ใน​ระบบ ซึ่ง​มี​ค่า​มหาศาล​สำหรับ​งาน​ที่​ต้อง​ตรวจสอบ​ย้อนหลัง​หรือ​อยู่​ภาย​ใต้​ข้อ​กำหนด​ด้าน​กฎ​ระเบียบ (บัญชี​เดินสะพัด​ก็​คือ event source ของ​ยอด​คง​เหลือ​ใน​บัญชี​นั่นเอง)
  • สถานะ​ย้อน​เวลา (temporal / historic state) — เรา​สร้าง​สถานะ ณ จุด​เวลา​ใด​ใน​อดีต​ขึ้น​มา​ใหม่​ได้ ตอบ​คำถาม​อย่าง “เมื่อ​วัน​อังคาร​ที่​แล้ว​สถานะ​เป็น​อย่างไร” ได้​จริง และ​เข้าใจ​ว่า มา​ถึง​สถานะ​ปัจจุบัน​ได้​อย่างไร
  • สำรวจ​ทาง​เลือก (what-if / alternative history) — เรา​สามารถ replay event ชุด​เดิม​โดย​แทรก event สมมติ​เข้าไป เพื่อ​ทดลอง​ดู​ว่า “ถ้า​เหตุการณ์​เป็น​อีก​แบบ​จะ​ได้​ผลอย่างไร”
  • Memory Image (การ​เก็บ​สถานะ​ไว้​ใน​หน่วย​ความ​จำ) — เพราะ​สถานะ​สร้าง​ใหม่​จาก event ได้​เสมอ เรา​จึง​อาจ​เลือก​เก็บ​สำเนา​ใช้งาน​ไว้​ใน​หน่วย​ความ​จำ​ล้วน ๆ แบบ​ไม่​ต้อง​คงทน (non-durable) โดย​ไม่​ต้อง​พึ่ง​ฐาน​ข้อมูล​ใน​แบบ​เดิม ๆ

แต่​พลัง​นั้น​ก็​มา​พร้อม ปัญหา​และ​กับดัก ที่​ต้อง​ระวัง:

  • ความ​ไม่​คุ้น​เคย — นัก​พัฒนา​ส่วน​ใหญ่​ไม่​ชิน​กับ​วิธี​คิด​แบบ​นี้ มัน​เป็นกระบวน​ทัศน์ (paradigm) ที่​ต่าง​ไป​จาก​การ​เขียน​ทับ​สถานะ​ที่​เรา​คุ้น​เคย จึง​มี​เส้น​โค้ง​การ​เรียนรู้​ที่​ชัน แม้​ผู้​เขียน​เอง​ก็​อด​สงสัย​ไม่​ได้​ว่า​ความ​ซับซ้อน​ที่​คน​มัก​บ่นถึง​นี้ อาจ​มา​จาก​การ​แยก​ส่วน​ที่​ไม่​ดี​ระหว่าง​ส่วน​ที่ derive working copy กับ​ส่วน​ที่​ทำ domain logic มากกว่า​จะ​เป็น​ข้อ​เสีย​โดย​ธรรมชาติ​ของ event sourcing เอง
  • การ​ติดต่อ​กับ​ระบบ​ภายนอก — นี่​คือ​กับดัก​ใหญ่​ที่สุด ถ้า​ตรรกะ​ของ​เรา​ทำงาน​ที่​มี​ผลกระทบ​ต่อ​โลก​ภายนอก เช่น ส่ง​อีเมล หรือ​เรียก API ของ​ระบบ​อื่น การ replay event ใน​ภายหลัง​อาจ​ไป กระตุ้น​ผล​ข้าง​เคียง​เหล่า​นั้น​ซ้ำ โดย​ไม่​ตั้งใจ (ส่ง​อีเมล​ซ้ำ สั่ง​ของ​ซ้ำ) เรา​จึง​ต้อง​ออกแบบ gateway มา​คั่น​การ​ติดต่อ​ภายนอก​ให้​แยก​ออก​ไป เพื่อ​ให้​ตอน replay ไม่​ยิง​ผล​ข้าง​เคียง​เหล่า​นั้น​ออก​ไป​อีก และ​ถ้า​เรา​เรียก​ระบบ​ภายนอก​ที่​ให้​คำ​ตอบ​ต่าง​กัน​ใน​แต่ละ​เวลา (เช่น อัตรา​แลกเปลี่ยน) เรา​ต้อง บันทึก​คำ​ตอบ​ที่​ได้​รับ​ไว้​ด้วย เพื่อ​ให้การ replay ยัง​คง​ให้​ผลลัพธ์​เดิม
  • การ​จัดการ version ของ event (schema versioning) — เมื่อ​เวลา​ผ่าน​ไป โครงสร้าง​ของ event ย่อม​เปลี่ยนแปลง แต่ event เก่า​ใน​อดีต​ยัง​อยู่​ใน​บันทึก เรา​จึง​ต้อง​มี​กลยุทธ์​ให้ code version ใหม่​ยัง​ตีความ event รูปแบบ​เก่า​ได้​ถูกต้อง
  • ประสิทธิภาพ​ใน​การ​สร้าง​สถานะ​ใหม่ — เมื่อ event สะสม​จน​มี​จำนวน​มหาศาล การ replay ตั้งแต่ event แรก​จะ​ช้า​ลง​เรื่อย ๆ ทาง​แก้​ที่​นิยม​คือ​ทำ snapshot เก็บ​ภาพ​สถานะ​ไว้​เป็น​ระยะ แล้ว replay ต่อ​จาก snapshot ล่าสุด​เท่านั้น
  • การ​เปลี่ยน​ตรรกะ​ที่​ใช้ derive — ต้อง​ระวัง​ว่า​ถ้า​ตรรกะ​ที่​ใช้​แปลง event ให้​เป็น​สถานะ​เปลี่ยน​ไป การ replay ประวัติ​เดิม​ด้วย​ตรรกะ​ใหม่​อาจ​ให้​ผลลัพธ์​ที่​ต่าง​จาก​เดิม เท่ากับ “ประวัติศาสตร์” ถูก​ตีความ​ใหม่​โดย​ไม่​ได้​ตั้งใจ

โดย​สรุป event sourcing เป็น pattern ที่​ทรง​พลัง​แต่​ซับซ้อน ควร​ใช้​ด้วย​ความ​เข้าใจ​ใน​กับดัก​ของ​มัน และ​ที่​สำคัญ มัน​เป็น pattern ที่ เป็น​อิสระ​จาก CQRS แม้​ทั้ง​สอง​จะ​มัก​ถูก​พูด​ถึง​คู่​กัน​จน​หลาย​คน​เข้าใจ​ผิด​ว่า​เป็น​เรื่อง​เดียวกัน

CQRS — การ​แยก​ความ​รับผิดชอบ​ระหว่าง​การ​สั่ง​งาน​กับ​การ​สอบถาม

หัวข้อ​ที่​มีชื่อ​ว่า “CQRS — การ​แยก​ความ​รับผิดชอบ​ระหว่าง​การ​สั่ง​งาน​กับ​การ​สอบถาม”

pattern สุดท้าย​คือ CQRS ย่อ​มา​จาก Command Query Responsibility Segregation (การ​แยก​ความ​รับผิดชอบ​ระหว่าง​การ​สั่ง​งาน​กับ​การ​สอบถาม) แนวคิด​คือ​การ​ใช้ model ข้อมูล​คนละ​ตัว​สำหรับ​การ​เขียน (command) กับ​การ​อ่าน (query) แทนที่​จะ​มี model เดียว​รับผิดชอบ​ทั้ง​สอง​หน้าที่​อย่าง​ที่​เรา​ทำ​กัน​เป็น​ปกติ

ฝั่ง command จะ​เป็น model ที่​ออกแบบ​มา​เพื่อ​จัดการ​การ​เปลี่ยนแปลง​สถานะ ส่วน​ฝั่ง query เป็น model ที่​ออกแบบ​มา​เพื่อ​ตอบ​คำถาม​หรือ​แสดง​ผล​โดย​เฉพาะ

flowchart TB
  UI["Client"] -->|"commands"| CM["Command Model<br/>write side"]
  UI -->|"queries"| QM["Query Model<br/>read side"]
  CM -->|"update projections"| QM
  CM --> WS["Write Store"]
  QM --> RS["Read Store"]

ภาพ 4 — CQRS: ฝั่ง​เขียน (Command Model) กับ​ฝั่ง​อ่าน (Query Model) ถูก​แยก​ออก​จาก​กัน​เป็น2 model การ​เปลี่ยนแปลง​ที่​เกิด​ฝั่ง​เขียน​จะ​ถูก propagate ไป​ปรับปรุง​มุมมอง​ข้อมูล (projection) ที่​ฝั่ง​อ่าน​ใช้ ทั้ง​สอง​ฝั่ง​อาจ​ใช้​ที่​จัด​เก็บ​ข้อมูล​คนละ​แบบ​ที่​เหมาะ​กับ​งาน​ของ​ตน ดู​สรุป​แบบ​อ้างอิง​ได้ที่​หน้า CQRS

CQRS มัก​ถูก​ใช้​คู่​กับ event sourcing เพราะ​เข้า​กัน​ได้​ดี event store เหมาะ​กับ​การ​เขียน​เชิง append ส่วน​ฝั่ง​อ่าน​ก็​สร้าง projection ที่​ปรับ​แต่ง​มา​เพื่อ​การ query โดย​เฉพาะ แต่​ขอ​ย้ำ​อีก​ครั้ง​ว่า ทั้ง​สอง​เป็น​คนละ pattern ใช้ CQRS โดย​ไม่​ทำ event sourcing ก็ได้ และ​ทำ event sourcing โดย​ไม่​ใช้ CQRS ก็ได้​เช่น​กัน

CQRS มี​ประโยชน์​จริง​ใน​บาง​สถานการณ์ เช่น เมื่อ model สำหรับ​อ่าน​กับ​เขียน​แตก​ต่าง​กัน​มาก​จน​การ​ยัด​รวม​เป็น model เดียว​ทำให้​ทุก​อย่าง​ยุ่งเหยิง หรือ​เมื่อ​ภาระ​งาน​ฝั่ง​อ่าน​กับ​ฝั่ง​เขียน​ไม่​สมดุล​กัน​อย่าง​รุนแรง​จน​ต้อง​ปรับ​ขยาย (scale) แยก​กัน

คำ​เตือน​เรื่อง CQRS

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

หมายเหตุ​ผู้​แปล: หาก​จะ​นำ CQRS ไป​ใช้ คำ​แนะนำ​ที่​นิยม​กัน (เป็น​แนวทาง​เสริม​จาก​ฝั่ง DDD/DevIQ ไม่ใช่​ถ้อยคำ​ของ Fowler ใน​บทความ​นี้) คือ​จำกัด​ขอบเขต​การ​ใช้​ไว้​เฉพาะ​บาง Bounded Context ที่​ประโยชน์​คุ้ม​กับ​ความ​ซับซ้อน​ที่​เพิ่ม​ขึ้น​จริง ๆ ไม่ใช่​เหมา​เอา​ไป​ครอบ​ทั้ง​ระบบ

เพื่อ​ให้​เห็น​ว่าการ​เหมา​รวม pattern เหล่า​นี้​ก่อ​ปัญหา​จริง​อย่างไร ผู้​เขียน​ขอ​เล่า​เหตุการณ์​หนึ่ง

มี​ผู้จัดการ project คน​หนึ่ง​ซึ่ง​ทั้ง​เก่ง​และ​มาก​ประสบการณ์ เคย​บอก​กับ​ผู้​เขียน​ว่า event sourcing เป็น “หายนะ” ของ project เขา เพราะ​ทุก​ครั้ง​ที่​จะ​แก้​อะไร​สัก​อย่าง งาน​กลับ​เพิ่ม​ขึ้น​เป็น​สอง​เท่า เนื่องจาก​ต้อง​ไป​อัปเดต​ทั้ง model ฝั่ง​อ่าน​และ model ฝั่ง​เขียน

แต่​เพียง​ประโยค​ที่​ว่า “ต้อง​อัปเดต​ทั้ง model ฝั่ง​อ่าน​และ​ฝั่ง​เขียน” ก็​ส่อ​เค้า​ความ​สับสน​ให้​เห็น​แล้ว เพราะ​การ​แยก model อ่าน​กับ​เขียน​ออก​จาก​กัน​นั้น​คือ​ลักษณะ​ของ CQRS ไม่ใช่​ของ event sourcing ทั้ง​สอง​ถูก​จับ​มา​ปน​กัน​ตั้งแต่​ต้น

และ​เมื่อ​ผู้​เขียน​ไป​คุย​กับ tech lead ของ​ทีม​เดียวกัน เขา​กลับ​ชี้​ว่า​ต้นตอ​ที่แท้​จริง​คือ​การ​สื่อสาร​แบบ asynchronous ที่​มี​อยู่​หนาแน่น​ใน​ระบบ​ต่างหาก ซึ่ง​เป็น​ตัว​เพิ่ม​ความ​ซับซ้อน​ที่​รู้​กัน​ดี แต่​ก็​ไม่ใช่​สิ่ง​ที่​ผูก​ติด​มา​กับ event sourcing หรือ CQRS โดย​จำเป็น​แต่​อย่าง​ใด

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

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

pattern ทั้ง​สี่ — Event Notification, Event-Carried State Transfer, Event Sourcing และ CQRS — เป็น​แนวคิด​ที่​แตก​ต่าง​กัน แม้​จะ​ถูก​จับ​รวม​ไว้​ใต้​ร่ม​คำ​ว่า “event-driven” อยู่​บ่อย​ครั้ง ทั้ง​สี่​สามารถ​หยิบ​มา​ใช้​ร่วม​กัน​หรือ​แยก​กัน​ก็ได้ และ​แต่ละ​อัน​ก็​มี​ชุด​ข้อดี​ข้อ​เสีย​เป็น​ของ​ตัวเอง

ข้อคิด​สำคัญ​ที่​ผู้​เขียน​อยาก​ฝาก​ไว้​คือ เมื่อ​มี​ใคร​สัก​คน​พูด​ว่า “เรา​ควร​ทำ​ระบบ​ให้​เป็น event-driven” สิ่ง​แรก​ที่​ควร​ทำ​คือ ถาม​กลับ​ให้​ชัด​ว่า​เขา​หมาย​ถึง pattern ตัว​ไหน เพราะ​การ​เหมา​รวม​สี่​แนวคิด​นี้​เข้า​ด้วย​กัน​คือ​ต้นตอ​ของ​ความคิด​ที่​สับสน​และ​การ​ตัดสิน​ใจ​เชิง​สถาปัตยกรรม​ที่​ผิดพลาด

การ​รู้​ว่า​เรา​กำลัง​ใช้ pattern ใด และ​ใช้​เพราะ​เหตุผล​อะไร ต่างหาก​ที่​ทำให้​ประโยชน์​ของ​แนวคิด event-driven ปรากฏ​ออก​มา​ได้​จริง โดย​ไม่​ตกหลุมพราง​ของ​ความ​ซับซ้อน​ที่​ไม่​จำเป็น


📄 อ่าน​ต้นฉบับ​ภาษา​อังกฤษ: What do you mean by “Event-Driven”? — Martin Fowler, martinfowler.com (7 กุมภาพันธ์ 2017)