วิธีต่าง ๆ ในการจับการเปลี่ยนแปลงใน Postgres
(blog.sequin.io)- หากต้องการส่งต่อการเปลี่ยนแปลงใน Postgres ไปยังระบบอื่นแบบเรียลไทม์ จำเป็นต้องใช้ CDC(Change Data Capture) และตัวเลือกแต่ละแบบตั้งแต่การแจ้งเตือนอย่างง่ายไปจนถึงการทำสำเนาบนฐาน WAL ก็มีความน่าเชื่อถือและภาระในการดูแลระบบต่างกันมาก
- Listen/Notify เป็นจุดเริ่มต้นที่เบาที่สุด แต่ด้วยข้อจำกัดเรื่องการส่งแบบ at-most-once, การแจ้งเตือนชั่วคราว, และเพย์โหลดขนาดสูงสุด 8000 ไบต์ จึงใกล้เคียงกับสัญญาณเสริมมากกว่าจะเป็น CDC หลัก
- การโพลตารางและ ตารางตรวจสอบ(audit table / outbox pattern) สามารถทำได้ด้วยตารางมาตรฐานและทริกเกอร์เท่านั้น แต่ต้องจัดการเรื่องการตรวจจับการลบ, diff, ลำดับการ commit, write amplification และ backpressure ด้วยตัวเอง
- การทำสำเนาเชิงตรรกะ(logical replication) เป็นวิธีที่ทรงพลังในการสตรีม insert/update/delete จาก WAL แต่แอปพลิเคชันต้องจัดการ replication slot, ack, การรีสตาร์ต และการรองรับปริมาณงานด้วยตัวเอง
- Sequin ใช้ logical replication ของ Postgres เป็นฐานเพื่อส่งต่อการเปลี่ยนแปลงไปยัง SQS, Kafka, Elasticsearch, Redis, HTTP endpoints และอื่น ๆ ช่วยลดภาระจากการต้องจัดการ replication slot โดยตรง
สถานการณ์ที่ต้องใช้ Postgres CDC
- Postgres เก่งมากในการจัดการข้อมูลที่เก็บอยู่ แต่ถ้าต้องการใช้การเปลี่ยนแปลงในตารางเพื่อทริกเกอร์เวิร์กโฟลว์ หรือสตรีมข้อมูลแบบเรียลไทม์ไปยังแหล่งเก็บข้อมูล ระบบ หรือบริการอื่น ก็จำเป็นต้องออกแบบ การเคลื่อนย้ายข้อมูล แยกต่างหาก
- Change Data Capture(CDC) คือแนวทางในการระบุและจับการเปลี่ยนแปลงในฐานข้อมูล แล้วส่งต่อไปยังระบบ downstream แบบเรียลไทม์
- มีหลายวิธีในการจับการเปลี่ยนแปลงจาก Postgres โดยแต่ละวิธีมีความยากในการติดตั้งใช้งาน ความน่าเชื่อถือ และภาระในการปฏิบัติการแตกต่างกัน
Listen/Notify: pub-sub ที่ง่ายที่สุด
- Listen/Notify ของ Postgres เป็นความสามารถสำหรับการสื่อสารระหว่างโปรเซส และทำงานตามแพตเทิร์น publish-subscribe
- เซสชันสามารถ
listenช่องทางหนึ่งไว้ และกิจกรรมในฐานข้อมูลหรือเซสชันอื่นสามารถส่งnotifyไปยังช่องทางนั้นได้ - สามารถใช้กับทริกเกอร์เพื่อจับการเปลี่ยนแปลงได้
- ตัวอย่างทริกเกอร์จะสร้าง JSON ของ
table,id,actionของเรคคอร์ดที่เปลี่ยนแปลงในจังหวะafter insert or update or deleteแล้วเรียกpg_notify('table_changes', payload::text)
- ตัวอย่างทริกเกอร์จะสร้าง JSON ของ
- ข้อจำกัดค่อนข้างชัดเจน
- มี semantics แบบ at-most-once และ listener ต้องเชื่อมต่ออยู่ในเวลาที่มีการส่งการแจ้งเตือน
- listener จะได้รับเฉพาะการแจ้งเตือนหลังจากเริ่ม subscribe เท่านั้น ดังนั้นแม้จะหลุดจากเครือข่ายเพียงชั่วคราวก็อาจพลาดการแจ้งเตือนได้
- ขนาดเพย์โหลดถูกจำกัดไว้ที่ 8000 ไบต์ และถ้าเกินคำสั่ง
notifyจะล้มเหลว - ขนาดเพย์โหลดยังนับรวมชื่อช่องทางด้วย และชื่อช่องทางก็อาจยาวได้สูงสุด 64 ไบต์เหมือน identifier ของ Postgres
- ใช้ได้กับการตรวจจับการเปลี่ยนแปลงพื้นฐานหรือการปรับแต่งการโพลตารางให้มีประสิทธิภาพขึ้น แต่สำหรับความต้องการ CDC ที่ซับซ้อนอาจไม่เหมาะนัก
การโพลตาราง: เรียบง่ายแต่ไม่เก่งเรื่องการลบและ diff
- วิธีจับการเปลี่ยนแปลงแบบ ทนทาน ที่ง่ายที่สุดคือการโพลตารางโดยตรง
- แต่ละตารางต้องมีคอลัมน์อย่าง
updated_atที่อัปเดตทุกครั้งเมื่อแถวถูกแก้ไข และหากต้องการก็สร้างด้วยทริกเกอร์ได้ - ใช้คู่ของ
updated_atกับidเป็นเคอร์เซอร์ และให้ลอจิกของแอปพลิเคชันเก็บและจัดการเคอร์เซอร์ - หากใช้การ subscribe กับ Notify ร่วมด้วย ก็สามารถแจ้งแอปพลิเคชันได้ว่ามีการแทรกหรือแก้ไขเรคคอร์ด เพื่อลดความถี่ในการโพล
- เนื่องจากการแจ้งเตือนของ Postgres เป็นแบบชั่วคราว จึงเหมาะจะใช้เป็นเพียงตัวเพิ่มประสิทธิภาพบนการโพลเท่านั้น
- มีข้อเสียหลักอยู่สามข้อ
- แถวที่ถูกลบจะไม่อยู่ในตารางแล้ว จึง ตรวจจับการลบ ไม่ได้
- วิธีแก้คือให้ delete trigger บันทึก
idและคอลัมน์ที่จำเป็นลงในตารางแยก เช่นdeleted_contactsแล้วให้แอปพลิเคชันโพลตารางนั้น - เราอาจรู้ว่าเรคคอร์ดถูกอัปเดตแล้ว แต่ไม่รู้ว่า อะไรเปลี่ยนไปบ้าง
- datetime และ sequence ของ Postgres อาจไม่เรียงตามลำดับ commit จึงอาจพลาดแถวที่ยังอยู่ระหว่าง commit ในระหว่างที่อ่านบล็อกตาม
updated_at
- เป็นตัวเลือกที่สมเหตุสมผลสำหรับการติดตามการเปลี่ยนแปลงแบบง่าย ๆ ที่การลบ, diff และการตกหล่นเป็นครั้งคราวไม่ใช่ปัญหาใหญ่
ตารางตรวจสอบ: เก็บบันทึกการเปลี่ยนแปลงด้วย outbox pattern
- แนวทาง ตารางตรวจสอบ(audit table) คือการบันทึกการเปลี่ยนแปลงลงในตาราง
changelogแยกต่างหาก และมักเรียกว่า outbox pattern changelogสามารถมีคอลัมน์ที่เกี่ยวข้องกับการเปลี่ยนแปลงได้ เช่นaction: เป็นinsert,update,deleteold:jsonbของเรคคอร์ดก่อนเปลี่ยนแปลง โดยจะว่างในกรณี insertvalues:jsonbของฟิลด์ที่เปลี่ยนแปลง โดยจะว่างในกรณี deleteinserted_at: เวลาที่เกิดการเปลี่ยนแปลง
- ในการติดตั้งใช้งาน ต้องมี trigger function ที่แทรกข้อมูลลง
changelogทุกครั้งที่มีการเปลี่ยนแปลง และมีทริกเกอร์สำหรับแต่ละตารางที่ต้องการเฝ้าดู - ยังสามารถใช้
changelogเป็นเหมือนคิวสำหรับการบริโภคข้อมูลได้ด้วย- worker ของแอปพลิเคชันจะดึงการเปลี่ยนแปลงจากตาราง
- สามารถใช้
for update skip lockedของ Postgres เพื่อให้ได้การประมวลผลแบบใกล้เคียง exactly-once - worker สามารถเปิดทรานแซกชัน ล็อกข้อมูลเป็นชุดด้วย
order by timestamp limit 100 for update skip lockedจากนั้นประมวลผล ลบเรคคอร์ดที่เสร็จแล้ว แล้วค่อย commit
- มีข้อเสียด้านการปฏิบัติการ
- การเขียนลงตารางเดียวก่อให้เกิด write amplification เพราะมีการเขียนหลายครั้งลงในตารางตรวจสอบ
- โดยทั่วไปจะมีอย่างน้อยสามครั้ง คือ insert เริ่มต้นลงตารางตรวจสอบ, update ระหว่างประมวลผล, และ delete หลังประมวลผลเสร็จ
- หากจะ fan-out ผ่าน worker ก็ต้องออกแบบเองให้เหมาะกับแอปพลิเคชัน
- ก่อนนำไปใช้ในโปรดักชันระดับใหญ่ มีโอกาสสูงที่จะต้องปรับ trigger function และโครงสร้างตาราง
- อาจต้องพิจารณานโยบายย่อย เช่น เวลาสูงสุดที่ worker จะถือครองการ checkout การเปลี่ยนแปลงไว้ได้
- หาก worker ประมวลผลไม่สำเร็จ ตารางตรวจสอบก็ยังคงสะสมข้อมูลต่อไป จึงจัดการ backpressure ได้ไม่ดีนัก
Foreign Data Wrapper: ตัวเลือกที่ใกล้เคียงกับการซิงก์ระหว่าง Postgres บางกรณี
- Foreign Data Wrapper(FDW) เป็นความสามารถที่ทำให้ฐานข้อมูล Postgres อ่านและเขียนแหล่งข้อมูลภายนอกได้
- ส่วนขยายแบบ FDW ที่รองรับแพร่หลายที่สุดคือ
postgres_fdw- สามารถเชื่อมฐานข้อมูล Postgres สองตัว และสร้างโครงสร้างคล้าย view ที่อ้างอิงตารางของอีกฐานข้อมูลหนึ่งจากอีกฝั่งได้
- ภายในแล้วฐานข้อมูล Postgres ตัวหนึ่งจะเป็นไคลเอนต์ และอีกตัวจะเป็นเซิร์ฟเวอร์
- เมื่อ query foreign table ฐานข้อมูลฝั่งไคลเอนต์จะส่งคำสั่งไปยังฐานข้อมูลฝั่งเซิร์ฟเวอร์ผ่าน Postgres wire protocol
- FDW ไม่ใช่วิธีจับการเปลี่ยนแปลงที่พบได้ทั่วไป และนอกเหนือจากสถานการณ์ที่เฉพาะมากก็ยากจะแนะนำ
- หากต้องการเขียนการเปลี่ยนแปลงจากฐานข้อมูล Postgres หนึ่งไปยังอีกฐานข้อมูล Postgres หนึ่ง FDW อาจเหมาะสม
- ตัวอย่างคือกรณีที่ใช้ฐานข้อมูลสำหรับงานบัญชีแยกจากฐานข้อมูลของแอปพลิเคชัน
- สามารถข้ามขั้นตอนจับการเปลี่ยนแปลงระหว่างกลาง แล้วสะท้อนข้อมูลโดยตรงระหว่างฐานข้อมูลด้วย
postgres_fdw
- ยังสามารถสร้าง FDW เองเพื่อ POST การเปลี่ยนแปลงไปยัง API ภายในได้
- เพราะเขียนไปยัง API ภายใน commit จึงสามารถให้ API ปฏิเสธการเปลี่ยนแปลงและ rollback commit ได้
- FDW ทรงพลัง แต่ไม่ค่อยเป็นตัวเลือกที่ดีที่สุดสำหรับงาน CDC และการเขียน FDW เองก็ถือว่าเป็นงานใหญ่ที่สุดแบบหนึ่งในบรรดาวิธีจับการเปลี่ยนแปลง
- การเขียน FDW เองง่ายขึ้นด้วยเครื่องมืออย่าง Supabase wrappers แต่ก็ยังเป็นงานใหญ่
ทำ logical replication เอง: CDC ทรงพลังบนฐาน WAL
- Postgres มีโปรโตคอลสำหรับการทำสำเนาฐานข้อมูล และหนึ่งในนั้นคือ logical replication
- logical replication สร้างอยู่บน WAL(write-ahead log) ของ Postgres
- มีการติดตามทุก insert, update, delete ของฐานข้อมูล
- การเปลี่ยนแปลงจะถูกสตรีมไปยัง subscriber
- ผู้ใช้ต้องสร้าง replication slot บน primary ก่อน
- ใช้รูปแบบ
pg_create_logical_replication_slot('<your_slot_name>', '<output_plugin>')
- ใช้รูปแบบ
output_pluginใช้ระบุปลั๊กอินที่จะถอดรหัสการเปลี่ยนแปลงจาก WALpgoutputเป็นปลั๊กอินเริ่มต้น และส่งออกเป็นรูปแบบไบนารีที่ไคลเอนต์เซิร์ฟเวอร์คาดหวังtest_decodingเป็นปลั๊กอินเอาต์พุตแบบง่ายที่แสดงการเปลี่ยนแปลงจาก WAL ในรูปแบบที่มนุษย์อ่านได้- แม้ไม่ใช่ปลั๊กอินที่มากับ Postgres แต่
wal2jsonก็เป็นปลั๊กอินยอดนิยม และ JSON ก็จัดการได้ง่ายกว่าเป็นจุดเริ่มต้นสำหรับแอปพลิเคชันเมื่อเทียบกับรูปแบบไบนารีของ Postgres
- หลังสร้าง replication slot แล้วก็สามารถเริ่มและบริโภคข้อมูลได้
- replication slot ใช้ส่วนของโปรโตคอล Postgres ที่ต่างจาก query ปกติ
- ไลบรารีฝั่งไคลเอนต์หลายตัวมีฟังก์ชันช่วยทำงานกับ replication slot
- ตัวอย่าง
psycopg2ใช้cursor.start_replication(...)และcursor.consume_stream(...)เพื่อบริโภคข้อความ WAL และใช้cursor.send_feedback(flush_lsn=msg.wal_end)เพื่อส่ง ack
- ไคลเอนต์ต้องส่ง ack ให้ข้อความ WAL ที่ได้รับ และ replication slot ทำงานคล้าย Kafka ที่มี offset
- logical replication เป็นวิธีที่ทนทานซึ่งถูกสร้างมาเพื่อ CDC โดยตรง แต่ก็ซับซ้อน
- replication slot และ replication protocol ไม่คุ้นเคยกับนักพัฒนาเท่ากับตารางและ query ปกติ
- จำเป็นต้องมีกลยุทธ์เพื่อไม่ให้พลาดข้อความระหว่างการรีสตาร์ต
- ต้องออกแบบให้รองรับข้อความปริมาณมากที่ส่งออกมาจาก Postgres ได้
Sequin: เครื่องมือ CDC ที่ห่อหุ้ม logical replication
- Sequin เป็นเครื่องมือ CDC ที่ส่งต่อการเปลี่ยนแปลงและแถวจาก Postgres ไปยังคิว สตรีม ดัชนีค้นหา แคช และ HTTP endpoints เป็นต้น
- ปลายทางรวมถึง SQS, Kafka, Elasticsearch, Redis, HTTP endpoints และอื่น ๆ
- Sequin ใช้ logical replication ของ Postgres ภายใน แต่ทำ abstraction ความซับซ้อนของโปรโตคอลระดับล่างไว้
- สามารถจับ insert, update, delete ได้ทั้งหมด และในกรณี update กับ delete จะจับทั้งค่า
newและoldของแถว - เงื่อนไขที่เหมาะจะพิจารณา Sequin มีดังนี้
- ต้องการ CDC แบบเรียลไทม์
- ต้องการสตรีมตรงไปยังปลายทางอย่าง SQS หรือ webhook โดยไม่มีระบบกลาง
- ต้องการฟีเจอร์อย่างการ backfill ข้อมูลเก่าและการกรองการเปลี่ยนแปลงด้วย SQL
whereclause - ต้องการทางเลือกที่ง่ายกว่าการจัดการ replication slot ด้วยตัวเอง
- ต้องการการรับประกันการประมวลผลแบบ exactly-once
- แต่ก็มีข้อเสีย
- Sequin ไม่ใช่ส่วนขยายภายใน Postgres แต่เป็น เครื่องมือของ third party ที่รันอยู่ข้างฐานข้อมูล
- เพราะไม่ใช่ extension จึงเข้ากันได้กว้างกับฐานข้อมูล Postgres หลายแบบ แต่ถ้าไม่ได้ใช้ Sequin Cloud ก็ต้องตั้งโครงสร้างพื้นฐานเพิ่มเอง
เกณฑ์ในการเลือก
- ในระยะเริ่มต้น Listen/Notify และการโพลตารางเหมาะสมที่สุด
- Listen/Notify เหมาะกับการจับเหตุการณ์ที่ไม่สำคัญมาก การทำต้นแบบ และการปรับปรุงการโพลให้มีประสิทธิภาพ
- การโพลเป็นทางแก้ที่ตรงไปตรงมาและใช้งานได้ดีกับกรณีใช้งานแบบง่าย
- ในระยะที่จริงจังขึ้น ตารางตรวจสอบอาจเป็นตัวเลือกระดับกลาง
- สามารถจับ payload ของ
newและoldของแถวได้ - หากทำอย่างถูกต้อง ก็สามารถได้ระบบประมวลผลแบบ exactly-once
- เมื่อขยายระบบ ปัญหา write amplification และการขาดการจัดการ backpressure จะเด่นชัดขึ้น และหากตั้งค่าด้วยมือผิดพลาดก็อาจทำข้อความตกหล่นได้
- สามารถจับ payload ของ
- ในระยะขยายระบบ logical replication เป็นแนวทางที่ใกล้เคียงกับคำตอบที่ทนทานที่สุด
- อย่างไรก็ตาม แนะนำให้ใช้เครื่องมืออย่าง Sequin มากกว่าการอ่านจาก slot โดยตรง
- FDW เป็นความสามารถที่น่าสนใจ แต่มีโอกาสน้อยที่จะตอบโจทย์ความต้องการ CDC ทั่วไป
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ทริกเกอร์ + ตารางประวัติ (ตาราง audit) คือคำตอบที่ถูกต้องใน 98% ของกรณี ถ้ายังไม่ได้ใช้ ก็เริ่มใช้ได้ตั้งแต่วันนี้ เป็นเทคนิคที่ผ่านการพิสูจน์มากว่า 30 ปีแล้ว
ตัวอย่างง่าย ๆ สำหรับการทำแบบ generic อยู่ที่ https://gist.github.com/slotrans/353952c4f383596e6fe8777db5d... เป็นแนวทางที่ยอมสละประสิทธิภาพด้านพื้นที่จัดเก็บ แล้วเลือก “ทำให้ง่าย”
ถ้าเก็บข้อมูลแบบ immutable ได้ก็คงดีมาก แต่ในฐานข้อมูลน่าจะมี ข้อมูลที่เปลี่ยนแปลงได้ จำนวนมหาศาล และมีโอกาสสูงว่าคุณกำลังลืมหลายสิ่งไปทุกวัน อย่าลืม ใช้ตารางประวัติซะ
อ้างอิง: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
ไม่ควรใช้ไลบรารีหรือเทคนิคติดตามประวัติในชั้นแอปพลิเคชันอย่าง Papertrail เพราะช้าและเกิดข้อผิดพลาดได้ง่าย อีกทั้งจับการเปลี่ยนแปลง DB ที่อ้อมผ่าน app stack ไม่ได้ การพยายามให้แอปเป็นคนประทับเวลา
updatedก็ผิดโดยพื้นฐาน เพราะแต่ละเว็บเซิร์ฟเวอร์มีนาฬิกาไม่ตรงกัน ต้องใช้ นาฬิกาของ DB และนั่นคือนาฬิกาเดียวที่ถูกต้องnow()ไว้ใน query เพื่อใช้ นาฬิกาของ DBแต่การซิงก์โดยอาศัย timestamp นี้อย่างเดียวไม่เพียงพอ เพราะ timestamp ถูกสร้าง ณ เวลาเริ่ม transaction ไม่ใช่เวลาที่ transaction commit
หาก poll ตารางแล้วกรองด้วย timestamp ล่าสุด อาจพลาดบาง transaction ที่ลำดับ commit สลับกันได้ อาจตั้ง buffer โดยย้อนเวลาไปอีกหลายนาทีแล้วกำจัดรายการซ้ำได้ แต่ใน PostgreSQL ระยะเวลาของ transaction ไม่มีขีดจำกัด และถ้าย้อนไปไกลเกินไปก็สิ้นเปลืองมาก ถ้าความถูกต้องและประสิทธิภาพสำคัญ วิธีนี้ไม่เหมาะ
หากใช้ log sequence number, เวลา DB และ
REPLICA IDENTITY FULLก็จะรวมสถานะก่อน/หลังการเปลี่ยนแปลงด้วย จากนั้นเมื่อ materialize collection ไปยังที่อย่าง Snowflake ก็จะได้ตารางซิงก์ที่ตามการอัปเดตของ source DB เป็นพื้นฐานจาก data lake พื้นฐานเดียวกัน ยังสามารถแปลงหรือ materialize ประวัติทั้งตารางเพื่อวัตถุประสงค์ด้าน audit ได้ด้วย จึงไม่ต้องไปติดตัวจับข้อมูลหรือ WAL reader เพิ่มที่ source DB อีก
ผมยังอธิบายวิธีของ SQLite ที่ใช้แพตเทิร์นคล้ายกัน แต่ทำแบบอิงคอลัมน์แทน JSON แยกไว้อีกด้วย: https://simonwillison.net/2023/Apr/15/sqlite-history/
บทความนี้สรุปแนวทางต่าง ๆ ที่ทำได้ด้วยฟีเจอร์พื้นฐานของ Postgres ไว้ได้กระชับดี
ในส่วน “จับการเปลี่ยนแปลงลงในตาราง audit” ที่บริษัทก่อนหน้านี้ เราใช้ แพตเทิร์น Temporal Tables ได้ผลดีมาก แม้ Postgres เองจะไม่มีในตัว ต่างจาก RDBMS รายใหญ่ตัวอื่น ๆ แต่มีแพตเทิร์นเรียบง่ายที่ใช้งานผ่านฟังก์ชัน SQL ได้: https://github.com/nearform/temporal_tables
สามารถดูสถานะของตาราง ณ เวลาหนึ่ง ๆ ได้ จึงตอบคำถามอย่าง “วันที่ 12 สิงหาคม การตั้งค่าของผู้ใช้รายนี้คืออะไร”, “เมื่อคืน 23:55 มีเรคคอร์ดที่ยังไม่ประมวลผลกี่รายการ”, “ขอให้แสดงความแตกต่างของ feature flag ระหว่างตอนนี้กับหนึ่งสัปดาห์ก่อน” ได้
เคยไปให้คำปรึกษากับบริษัทหนึ่งที่มี SQL Server แบบโมโนลิธขนาดใหญ่มาก มาก่อน แม้จะไม่ใช่ Postgres แต่ถ้าสมมติว่าเป็น Postgres ก็คงคล้ายกัน
มันถูกใช้งานมาหลายสิบปี และถูกใช้กับสารพัดงานภายในบริษัท แทบทุกแอปพลิเคชันและกระบวนการทางธุรกิจทั้งบริษัทต่างก็บันทึกข้อมูลลงในฐานข้อมูลนี้
ปัญหาคือมีแอปพลิเคชันจำนวนมากที่อ่าน DB นี้ และมีกระบวนการกับขั้นตอนที่แทรก·แก้ไขข้อมูลเยอะมาก จนเมื่อกระบวนการแทรก·แก้ไขฝั่ง upstream เปลี่ยนไปหรือมีของใหม่เพิ่มเข้ามา ก็เกิดเหตุที่ไปทำลาย invariant ระดับแอปพลิเคชัน แม้แต่กระบวนการปกติก็ทำงานต่างออกไปหากมีข้อมูลเสีย
การไล่หาสาเหตุทำได้ยากมาก เพราะสิ่งที่ต้องไปดูส่วนใหญ่เขียนไว้เมื่อ 10 ปีก่อน และพนักงานเหล่านั้นก็ออกจากบริษัทไปแล้ว
เลยสงสัยว่าจะจับการเปลี่ยนแปลงในฐานข้อมูล Postgres ให้อยู่ในรูป DAG บางแบบได้ไหม เพื่อให้รู้ว่ากระบวนการใดแทรก·แก้ไข·ลบข้อมูล และในอดีตมันมีพฤติกรรมอย่างไร รวมถึงแอปพลิเคชันต่าง ๆ อ่านข้อมูลนี้อย่างไร และสถิติของ query เปลี่ยนไปตามเวลาอย่างไร
ไม่แน่ใจว่ามีกรณีตัวอย่างแบบนี้มาก่อนหรือไม่ หรือควรใช้แนวทางใดจึงจะสร้างเครื่องมือแบบนี้ได้ เคยคิดจะทำอะไรคล้าย ๆ กันมาก่อน แต่ดูเหมือนเป็นขอบเขตที่ต้องเข้าใจระดับวิศวกรแกนหลักของ Postgres ถึงจะตัดสินใจได้ดี
แต่ในแต่ละการเปลี่ยนแปลงจะไม่ได้ข้อมูลต้นทางระดับ client มาด้วย
ถึงอย่างนั้นก็มีทางเลี่ยงได้ สตรีม logical replication สามารถรวมข้อความข้อมูลจากฟังก์ชัน
pg_logical_emit_messageได้ ดังนั้น client จึงใส่ metadata เองได้ อาจตั้งค่าให้ปล่อยตัวระบุ client ตอนเริ่มแต่ละ transaction ได้ด้วยlast updated by) ไว้ อาจเป็น antipattern ก็ได้ จึงอยากได้วิธีที่แข็งแรงกว่านี้แนวทางนี้ใช้ได้กับโซลูชันตระกูล SQL เกือบทั้งหมดที่ใช้ WAL หรือ trigger
เคยใช้วิธี trigger กับ SQL Server หลายครั้ง แต่ถ้า log ทุก query ก็มักจะช้าลง การออกแบบกลไก insert ที่ไม่ขวางงาน production นั้นไม่สมบูรณ์แบบ และอาจต้องใช้ sampling
ถ้าจะไปทาง “ตาราง audit” ก็ใช้ pgaudit ได้เลย เป็น extension ที่ผ่านการพิสูจน์ในการใช้งานจริง และถ้าใช้ AWS ก็ใช้บน RDS ได้ด้วย
https://github.com/pgaudit/pgaudit/blob/master/README.md
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appen...
https://github.com/arkhipov/temporal_tables
https://news.ycombinator.com/item?id=26748096
ไม่จำเป็นต้องทำก็ได้ การอยากได้สิ่งนี้หมายถึงการเปลี่ยน relation ของ Postgres ให้เป็น สัญญา ซึ่งจะทำให้ไม่มี service ใดสามารถ persist สถานะภายในของตัวเองได้
ถ้าทุ่มเทกับ Domain-Driven Design จริง ๆ ก็อาจเป็นไปได้ แต่ใช้ ระบบ event-driven ที่เบาและใช้งานได้จริงจะดีกว่า
อะไรสักอย่างแบบ event-driven ซับซ้อนกว่าพันเท่า
วิธี polling คอลัมน์
updated_atในรูปแบบที่ง่ายที่สุดนั้นไม่แข็งแรง เพราะไม่มีการรับประกันว่า transaction จะ commit ตามลำดับนั้นupdated_atของ Row 1 เป็น2023-09-22 12:00:01ถัดมาไม่นาน Transaction B เริ่มขึ้น ตั้งค่า
updated_atของ Row 2 เป็น2023-09-22 12:00:02แล้ว B commit ก่อนquery สำหรับ polling ทำงาน เห็น Row 2 เป็นการเปลี่ยนแปลงล่าสุดและอัปเดต cursor เป็น
2023-09-22 12:00:02จากนั้นถ้า A ค่อยมา commit ภายหลัง ก็จะพลาด Row 1วิธีง่าย ๆ ที่หลีกเลี่ยงปัญหานี้คือไม่ polling แบบเกือบ real-time เพราะสุดท้ายลำดับจะสอดคล้องกันเอง
ข้อเสนอที่แข็งแรงกว่าน่าจะเป็นการใช้ sequence เช่น มีคอลัมน์
updated_at_idxที่เพิ่มค่าทุกครั้งเมื่อ row ถูกเปลี่ยนสงสัยว่าถ้าใช้ before trigger ที่ใส่
now()timestamp ของupdated_atในสองแถวอาจไม่ตรงกับลำดับ commit ของ transaction ได้หรือไม่updated_atไม่จำเป็นต้องเท่ากับ commit timestamp แต่updated_atควรแสดงลำดับ commit ได้แม่นยำในระดับมิลลิวินาที/ไมโครวินาที_txidแทนupdated_atโดยให้ trigger ตั้งค่าเป็น transaction ID ปัจจุบัน จากนั้นตอน polling ก็ใช้txid_current()เพื่อตรวจว่า transaction ใด commit แล้วและ transaction ใดยังไม่ commitค่อนข้างหวาดเสียวและเกิด off-by-one ได้ง่ายมาก แต่ก็ทำงานได้ดีใน production มาหลายปีแล้ว
บทความยอดเยี่ยมมาก
ถ้าใช้ Elixir กับ Postgres ผมทำไลบรารีเล็ก ๆ ที่ฟังการเปลี่ยนแปลงของ WAL ด้วยแนวทางคล้ายกันไว้: https://github.com/cpursley/walex
วิธีเหล่านี้ทั้งหมดดูไม่ค่อยดีเท่าไร และโดยส่วนตัวผมมองว่า การโพลลิง ใช้งานได้จริงที่สุด
อยากให้ Postgres สร้างนวัตกรรมในด้านนี้
ผมคิดว่าจนกว่าจะถูกบรรจุในมาตรฐาน SQL ก็ยากที่จะเกิดแรงผลักดันในพื้นที่ kernel ของ DBMS เชิงสัมพันธ์ ทางเลือกมีมากและซับซ้อน อีกทั้งโซลูชันที่ประสบความสำเร็จใน user space ก็ไม่ได้มีภาระด้านประสิทธิภาพมากเกินไปนัก
อนึ่ง คนที่วิจัยด้านนี้โดยมากจะเอนเอียงไปทางแนวทาง ตาราง audit เพราะยังคงคุณสมบัติ ACID ที่สอดคล้องกันไว้ภายในฐานข้อมูล และทำให้ Postgres ยังคงเป็นจุดล้มเหลวเดียว แทนที่จะเพิ่มพร็อกซีหรืองานโพลลิงเข้ามา
ในโลกของข้อมูลมีช่องว่างขนาดใหญ่อยู่ แทนที่จะถามผลลัพธ์จากแหล่งเก็บข้อมูล คงจะดีถ้า ผลลัพธ์ของคิวรีถูก push แบบเพิ่มทีละส่วน
เราทำ real-time/streaming analytics กันมาก สามารถทำ stream processing ได้ และบางส่วนก็ประมวลผลเป็น materialized view ภายในแหล่งเก็บข้อมูลได้ แต่หลังจากข้อมูลเข้าไปอยู่ใน DB หรือ data lake แล้ว ถ้าต้องการดูการเปลี่ยนแปลงจาก downstream ก็แทบจะต้องกลับไปโพลลิงอีก
ถ้าต้องการตอบสนองเมื่อมีบางสถานการณ์เกิดขึ้นในข้อมูล หรืออัปเดตหน้าจอโดยไม่ต้อง refresh หน้า ก็แทบไม่มีโซลูชันที่สะอาด วิธีในบทความนี้ก็ดูใกล้เคียงกับ workaround มากกว่าฟีเจอร์ระดับ first-class
ถ้าต้องการทำรายงานที่อัปเดตแบบเรียลไทม์โดยไม่ต้อง refresh หน้า โดยทั่วไปจะกลายเป็นการโหลดข้อมูลจาก DB แล้วส่งการเปลี่ยนแปลงไปยัง GUI ผ่าน Kafka และ WebSocket แบบนั้นก็จะต้องดูแล Lambda architecture แปลก ๆ ที่วิเคราะห์บางส่วนด้วยโค้ด บางส่วนด้วย DB
มีนวัตกรรมในด้านนี้อยู่ KSQL และ Kafka Streams ส่งการเปลี่ยนแปลงออกมาได้, Materialize มี subscription และ ClickHouse มี live view แต่ฟีเจอร์จำนวนมากยังใหม่หรืออยู่ในขั้น preview และไม่ได้พอดีกับความต้องการนัก ผมลองใช้มาหมดแล้ว แต่รู้สึกว่ามันผลักภาระงานไปให้ developer มากเกินไป
ถ้ามีไลบรารีที่รับ change feed ได้ทันทีด้วยตัวเลือกอย่าง
[select * from orders with suscribe]ก็คงดี เป็นด้านที่สำคัญพอสมควรแต่ที่ผ่านมาได้รับความสนใจน้อยมีหลุมพรางใหญ่ของ replication ที่บทความไม่ได้พูดถึง และเพราะแบบนั้นผมจึงไม่ใช้ replication
Postgres พยายามรับประกันอย่างแข็งแรงมากว่าผู้บริโภคของ replication slot จะไม่พลาดข้อมูล ดังนั้นถ้าผู้บริโภคไม่ consume ข้อมูลจาก slot, Postgres ก็จะเก็บข้อมูลที่พลาดไว้ให้อย่างใจดีต่อไป และสุดท้ายก็ไปจนดิสก์เต็มแล้ว DB ล้ม ผมเจอมาแล้วกับ SaaS DB สองเจ้าระหว่างทำ prototype และวิธี recovery ก็มีแค่ต้องเปิด support ticket
ถ้า replication slot consumer หยุดอ่าน จะต้องมีการแจ้งเตือนดังขึ้นเสมอ
อีกเหตุผลหนึ่งคือ code path สำหรับดึง initial snapshot ของตารางกับ code path สำหรับอ่านการเปลี่ยนแปลงนั้นต่างกันโดยสิ้นเชิง การ initialize การอ่าน replication slot โดยไม่ให้พลาดการเปลี่ยนแปลงแม้แต่รายการเดียวไม่ใช่เรื่องเล็กน้อย
น่าเสียดายที่จากมุมมอง change capture แล้ว replication เป็นโซลูชันที่ hacky น้อยที่สุด
ผมใช้การโพลลิง แต่แทนที่จะใช้
updated_atผมเก็บ txidอยากรู้ว่าคุณต้องการพฤติกรรมแบบไหนมากกว่ากัน
ถ้าจัดการกับปริมาณข้อมูลขนาดใหญ่ ก็จะเริ่มอยากให้ initial snapshot กับการอ่านการเปลี่ยนแปลงถูกจัดการต่างกัน เพราะต้องทำงานอย่าง parallel initialization หรือการ initialize บนฐานของ physical backup ได้ อย่างไรก็ดี เข้าใจว่าฟีเจอร์ที่ช่วย stream ข้อมูลเดิมแบบเลือกได้หลังสร้าง slot อาจมีประโยชน์
ส่วนการ initialize การอ่าน replication slot เพื่อไม่ให้พลาดการเปลี่ยนแปลงนั้นไม่น่าจะยาก เลยอยากรู้ว่าติดตรงไหน
เมื่อไม่ต้องการการเปลี่ยนแปลงทั้งหมด temporary replication slot ที่ล้างตัวเองเมื่อการเชื่อมต่อหลุดก็มีประโยชน์เช่นกัน นอกจากนี้ยังมี configuration สำหรับตั้งค่าสูงสุดของ WAL ที่เก็บไว้เพื่อไม่ให้เซิร์ฟเวอร์ล้ม
อยากให้ช่วยอธิบายเพิ่มเติมว่าใช้ txid แทน
updated_atอย่างไร