Event sourcing ขอให้คุณหยุดเขียนทับข้อมูลของคุณ ในแอปพลิเคชันแบบ CRUD ดั้งเดิม การอัปเดตที่อยู่จัดส่งของผู้ใช้หมายถึงการค้นหาแถวข้อมูล เปลี่ยนแปลงค่า และทิ้งสถานะก่อนหน้าไป แต่ Event sourcing เลือกใช้เส้นทางที่ต่างออกไป มันจะจัดเก็บทุกการเปลี่ยนแปลงในรูปแบบของข้อเท็จจริงที่ไม่สามารถเปลี่ยนแปลงได้ (immutable fact) เช่น ผู้ใช้สร้างบัญชี, อัปเดตที่อยู่, หรือยืนยันอีเมล สถานะปัจจุบันของระบบไม่ได้ถูกจัดเก็บไว้โดยตรง แต่จะถูกคำนวณจากการนำเหตุการณ์ (events) เหล่านี้มาเล่นซ้ำ (replay) ตามลำดับ
รูปแบบนี้ช่วยแก้ปัญหาที่เกิดขึ้นจริง ประวัติการตรวจสอบ (audit trails) จะกลายเป็นผลพลอยได้ที่ได้มาฟรีๆ คุณสามารถสร้างสถานะของคำสั่งซื้อขึ้นมาใหม่ ณ ช่วงเวลาใดก็ได้ในอดีต คุณสามารถดีบั๊กได้โดยการเล่นซ้ำสิ่งที่เกิดขึ้นจริง สิ่งที่ต้องแลกมาคือความซับซ้อน เพราะตอนนี้คุณต้องจัดการกับสตรีมของข้อเท็จจริง (streams of facts), โมเดลสำหรับอ่านข้อมูล (read models) และความสอดคล้องในท้ายที่สุด (eventual consistency) แทนที่จะเป็นเพียงแถวข้อมูลธรรมดา
PostgreSQL สามารถทำหน้าที่เป็น event store ของคุณได้ ซึ่งทีมส่วนใหญ่ก็ใช้งานมันอยู่แล้ว มันมี ACID transactions, JSONB สำหรับข้อมูลที่มีความยืดหยุ่น และเครื่องมือสำรองข้อมูลที่เชื่อถือได้ คุณไม่จำเป็นต้องนำ Kafka, Cassandra หรือฐานข้อมูล event-store เฉพาะทางมาใช้ตั้งแต่วันแรก เพียงแค่ใช้ Postgres instance มาตรฐาน คุณก็จะได้ทั้งการรับประกันด้านธุรกรรม (transactional guarantees) และประวัติการตรวจสอบที่ Event sourcing ต้องการ โดยไม่ต้องขยายโครงสร้างพื้นฐานให้ใหญ่โตขึ้น
รูปแบบของ Postgres Event Store
Schema สามารถเรียบง่ายจนน่าตกใจ อย่างน้อยที่สุด คุณต้องมีตารางที่ใช้วิธีการเพิ่มเหตุการณ์ (append) และไม่เคยอัปเดตข้อมูลเดิมในที่เดิม การออกแบบที่ใช้งานได้จริงจะมีลักษณะดังนี้:
idเป็น bigserial หรือ UUID เพื่อใช้ในการจัดลำดับทั่วทั้งระบบ (global ordering)stream_idเพื่อจัดกลุ่มเหตุการณ์ที่เกี่ยวข้อง เช่น การเปลี่ยนแปลงทั้งหมดของผู้ใช้หรือคำสั่งซื้อเพียงรายการเดียวevent_typeเป็นข้อความธรรมดา (plain text):UserEmailChanged,PaymentReceived,InventoryAdjustedpayloadเป็น JSONB ซึ่งเก็บข้อมูลเฉพาะสำหรับเหตุการณ์นั้นๆoccurred_atพร้อมความละเอียดของเขตเวลา (timezone)versionต่อหนึ่ง stream เพื่อบังคับใช้ optimistic concurrency
คุณสามารถบังคับใช้กฎความไม่เปลี่ยนแปลง (immutability) ได้ในโค้ดของแอปพลิเคชันหรือด้วยข้อกำหนดของฐานข้อมูล (database constraint) การทำ unique index บน (stream_id, version) จะช่วยป้องกันไม่ให้ผู้เขียนสองรายเพิ่มหมายเลขลำดับเดียวกัน เมื่อมีคำสั่งเข้ามา คุณจะอ่านเวอร์ชันปัจจุบันของ stream นั้น เพิ่มค่าขึ้นหนึ่ง และแทรกเหตุการณ์ใหม่ลงไปภายใน transaction หากมีกระบวนการอื่นทำตัดหน้าคุณไปก่อน ข้อกำหนด unique จะล้มเหลว และคุณสามารถเลือกที่จะลองใหม่หรือปฏิเสธคำสั่งนั้น
ลองพิจารณาตัวอย่างที่เป็นรูปธรรม คุณกำลังรันระบบจัดการสินค้าคงคลัง (inventory system) แทนที่จะมีแถว inventory เพียงแถวเดียวพร้อมคอลัมน์ quantity คุณจะใช้วิธีเพิ่มเหตุการณ์ลงในตาราง inventory_events แทน เช่น ItemReceived จะเพิ่มจำนวนสิบหน่วย, ItemReserved จะลบออกสองหน่วย, และ ItemShipped จะลบออกสามหน่วย หากต้องการทราบจำนวนสต็อกปัจจุบันของ SKU-42 คุณก็แค่รวมผลรวมของ payload ในเหตุการณ์ที่เกี่ยวข้อง หากต้องการทราบสต็อกเมื่อสามวันที่แล้ว คุณก็แค่รวมผลรวมจนถึงช่วงเวลานั้น หากบั๊กในตรรกะการจัดส่งทำให้เกิดข้อผิดพลาดเมื่อวันอังคารที่ผ่านมา คุณสามารถนำเหตุการณ์เหล่านั้นมาเล่นซ้ำผ่านโค้ดที่ได้รับการแก้ไขแล้วเพื่อให้ได้สถานะที่แท้จริง ซึ่งคุณไม่สามารถทำแบบนี้ได้ด้วยคำสั่ง UPDATE ธรรมดา
หลักการที่จะช่วยให้คุณไม่ประสบปัญหา
การสร้างระบบบน Postgres ไม่ได้หมายความว่าคุณไม่จำเป็นต้องมีวินัย หลักการต่อไปนี้สามารถนำไปใช้กับระบบแบบ event-sourced ได้โดยตรง
ทำให้เรียบง่ายเข้าไว้. ความซับซ้อนทำลายความน่าเชื่อถือ จงห้ามใจไม่ให้สร้างเฟรมเวิร์กเหตุการณ์แบบครอบจักรวาล (generic event framework) ก่อนที่คุณจะส่งมอบเวิร์กโฟลว์ที่ใช้งานได้จริงเพียงหนึ่งอย่าง แค่ตารางเดียว, ฟังก์ชัน repository สำหรับเพิ่มเหตุการณ์ และ projection worker สำหรับสร้าง read models ก็เพียงพอที่จะพิสูจน์คุณค่าแล้ว ให้เพิ่มเครื่องมือต่างๆ ก็ต่อเมื่อปัญหาที่ชัดเจนปรากฏขึ้นเท่านั้น
เริ่มจากจุดเล็กๆ. อย่าเพิ่งเขียนระบบ Monolith ใหม่ทั้งหมด ให้เลือกหนึ่ง bounded context ที่ประวัติการตรวจสอบ (audit trail) ให้ความคุ้มค่าเมื่อเทียบกับภาระที่เพิ่มขึ้น เช่น ระบบบัญชีแยกประเภท (billing ledger), เอนจินเวิร์กโฟลว์ (workflow engine) หรือระบบจองสินค้าคงคลัง (inventory reservation system) ให้สร้างไปป์ไลน์นั้นตั้งแต่ต้นจนจบ ปล่อยให้มันรันในระบบจริง แล้วค่อยตัดสินใจว่าจะขยายต่อหรือไม่
กำหนดนิยามของความสำเร็จก่อน. Event sourcing ไม่ใช่สถาปัตยกรรมมาตรฐานที่ควรใช้กับทุกอย่าง แต่มันคือทางออกสำหรับความต้องการเฉพาะด้าน หากความต้องการของคุณคือการติดตามสถานะล่าสุดเท่านั้น CRUD จะรวดเร็วและประหยัดกว่า แต่ถ้าคุณต้องการการคิวรีข้อมูลตามช่วงเวลา (temporal queries), การตรวจสอบที่เข้มงวด หรือความสามารถในการสร้าง read models ใหม่ตามความต้องการ เมื่อนั้นการใช้ events จึงจะสมเหตุสมผล จงรู้ว่าคุณกำลังแก้ปัญหาอะไรก่อนที่จะเริ่มลงมือทำ
วัดผลก่อนที่จะปรับแต่ง (optimize). PostgreSQL สมัยใหม่บนฮาร์ดแวร์ระดับปานกลางสามารถรับเหตุการณ์ได้หลายพันรายการต่อวินาทีด้วยตารางแบบ append-only ง่ายๆ อย่าเพิ่งทำ sharding ให้กับ event store หรือนำระบบการแบ่งส่วน (partitioning) ที่ซับซ้อนมาใช้ จนกว่าการตรวจสอบ (monitoring) จะพิสูจน์ได้ว่าวิธีที่ง่ายกว่านั้นใช้ไม่ได้ผลแล้ว จงทำ index ในฟิลด์ที่คุณคิวรี และปรับแต่ง autovacuum สำหรับเวิร์กโหลดแบบ append-only จากนั้นจึงค่อยวัดผลอีกครั้ง
ทดสอบทุกอย่าง ทำ Unit test ให้กับ event handlers ของคุณ ทำ Integration test สำหรับเส้นทางการ append ข้อมูล และที่สำคัญที่สุดคือ ต้องทดสอบสถานการณ์ที่เกิดความล้มเหลว (failure scenarios) จะเกิดอะไรขึ้นเมื่อมีสองโหนดพยายาม append ข้อมูลลงใน stream เดียวกันพร้อมกัน? จะเกิดอะไรขึ้นเมื่อ projection worker เกิด crash ระหว่างการประมวลผลแบบ batch? จงเขียน test เพื่อตรวจสอบการจัดการ optimistic concurrency และการรับประกันการส่งข้อมูลแบบ at-least-once ของคุณ
เฝ้าระวังในระบบ production ตาราง events จะมีขนาดใหญ่ขึ้นเรื่อยๆ ซึ่งต่างจาก schema แบบ normalized ที่การ update จะทำให้จำนวนแถวคงที่ แต่ event sourcing ถูกออกแบบมาให้เป็นการเพิ่มข้อมูล (additive) โดยเฉพาะ จงติดตามขนาดของตาราง, disk I/O และความล่าช้า (lag) ระหว่าง write model และ read model projections ของคุณ และตั้งค่าการแจ้งเตือน (alerts) เมื่อเกิด projection lag ก่อนที่ผู้ใช้จะสังเกตเห็นข้อมูลที่ไม่อัปเดต (stale data)
เปลี่ยนงานที่ต้องทำด้วยมือให้เป็นระบบอัตโนมัติ การเปลี่ยน schema, การ rebuild projection หรือการ replay event ด้วยมือ คือระเบิดเวลาที่รอวันทำงาน จงเขียนสคริปต์สำหรับกลยุทธ์การ migration ของคุณ หากคุณมีการพัฒนา event schema ให้ทำระบบ upcasting หรือการแปลงข้อมูล (transformation) แบบอัตโนมัติ เพื่อให้ event เก่าสามารถถูก replay ผ่าน logic ใหม่ได้โดยไม่ต้องมีคนมาคอยจัดการตอนเที่ยงคืน
บันทึกการตัดสินใจของคุณ เขียนอธิบายว่าทำไมถึงต้องมี stream เฉพาะเจาะจงเหล่านี้, แต่ละ event type หมายถึงอะไร และเมื่อใดที่ทีมควรเลือกใช้ CRUD แทนที่จะใช้ events เนื่องจาก event sourcing เพิ่มภาระทางความคิด (cognitive load) เอกสารที่ดีจะช่วยป้องกันไม่ให้วิศวกรใหม่คาดเดาผิดพลาดและ append event ที่ผิดรูปแบบลงใน stream ที่สำคัญ
กับดักที่ทำให้เสียเวลาเป็นเดือนๆ
Event sourcing มักจะดูสวยงามในแผนภาพ แต่กลับสร้างความลำบากในระบบ production จงระวังกับดักเหล่านี้
การประเมินความซับซ้อนต่ำเกินไป การ replay event เพื่อสร้าง state ใหม่นั้นดูเรียบง่ายในเชิงแนวคิด แต่การจัดการเรื่อง idempotency, การทำ snapshotting เพื่อประสิทธิภาพ และการทำ compensating transactions ระหว่าง aggregates นั้นไม่ใช่เรื่องง่าย จงแบ่งระบบของคุณออกเป็นส่วนเล็กๆ และแก้ปัญหาทีละ stream
การออกแบบที่เกินความจำเป็น (Over-engineering) อย่าเพิ่งเตรียม Kafka cluster แบบหลายโหนด เพียงเพราะจินตนาการว่าปริมาณ event จะเยอะจนต้องใช้ในอนาคต เพราะ Postgres สามารถรองรับคุณได้ไกลกว่าที่คิดอย่างน่าประหลาดใจ จงเริ่มใช้โครงสร้างพื้นฐานใหม่ก็ต่อเมื่อคุณพบปัญหาคอขวด (bottleneck) ที่วัดผลได้และไม่สามารถแก้ไขได้ด้วยการตั้งค่าปัจจุบันเท่านั้น
การละเลยหนี้ทางเทคนิค (Technical debt) Schema ของ event เก่าๆ จะคงอยู่ตลอดไป หากคุณเปลี่ยน payload ของ OrderCreated คุณก็ยังมี event ในอดีตอีกสิบล้านรายการที่อยู่ในรูปแบบเดิม จงติดตามหนี้ก้อนนี้ วางแผนการสร้าง reader ที่รองรับเวอร์ชันเก่า (backward-compatible) หรือเขียนสคริปต์ migration อย่าปล่อยให้ภาระของ legacy events มาทำให้การพัฒนาฟีเจอร์ใหม่ๆ ล่าช้า
การเลือกเครื่องมือที่ทีมไม่สามารถดูแลได้ สถาปัตยกรรมที่ดีที่สุดจะล้มเหลวหากมีเพียงคนเดียวที่เข้าใจมัน หากทีมของคุณเชี่ยวชาญ Postgres และ SQL ก็ให้เริ่มจากตรงนั้น หากคุณจะนำ specialized event store มาใช้ ต้องมั่นใจว่าคุณมีความเชี่ยวชาญในการปฏิบัติงาน (operational expertise) มากพอที่จะ debug มันได้ตอนตีสอง
จุดเริ่มต้นที่นำไปใช้ได้จริง
หากแนวทางนี้เหมาะกับปัญหาของคุณ อย่ารอจนต้องรื้อระบบใหม่ในอีกห้าไตรมาสข้างหน้า ให้เริ่มตั้งแต่วันนี้
ตรวจสอบระบบปัจจุบันของคุณ หาจุดที่การมี audit trail จะช่วยแก้ปัญหาที่เกิดขึ้นจริงได้ บางทีอาจจะเป็น order state machine ที่ปัจจุบันเก็บแค่คอลัมน์ status เพียงคอลัมน์เดียว หรืออาจจะเป็นบัญชีแยกประเภททางการเงินที่การแก้ไขยอดคงเหลือต้องใช้วิธีการ patch ฐานข้อมูลด้วยมือ จงเลือกช่องว่างหนึ่งจุดที่การเขียนทับ state (overwriting state) เคยสร้างปัญหาให้กับคุณ
จากนั้น เลือกการปรับปรุงเล็กๆ หนึ่งอย่างที่คุณสามารถทำได้ในวันนี้ สร้างตาราง event หนึ่งตาราง ออกแบบหนึ่ง stream เขียนหนึ่ง projection ที่สร้าง read model จาก event เหล่านั้น แล้ว deploy มันภายใต้ feature flag เพื่อดูว่ามันสามารถจัดการกับ traffic จริงได้หรือไม่
Event sourcing ด้วย PostgreSQL ไม่ใช่เวทมนตร์ แต่มันคือเครื่องมือที่ใช้งานได้จริงสำหรับทีมที่ต้องการทราบไม่ใช่แค่ว่าสิ่งต่างๆ อยู่ที่ไหน แต่ต้องรู้ด้วยว่าสิ่งเหล่านั้นมาถึงจุดนี้ได้อย่างไร จงสร้างอย่างช้าๆ วัดผลอย่างตรงไปตรงมา และให้ความต้องการที่แท้จริงเป็นตัวนำทางสถาปัตยกรรมของคุณ
