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

Transaction Log Tailing

คุณมี Transactional Outbox: การเปลี่ยนแปลงทางธุรกิจ commit เคียงข้างแถว outbox ใน local transaction เดียว ตอนนี้คุณต้องการ relay — คอมโพเนนต์ที่นำแถว outbox ที่ commit แล้วไป publish ยัง broker คุณต้องการให้การส่งรวดเร็วและเพิ่มภาระบนฐานข้อมูลแอปพลิเคชันให้น้อยที่สุดเท่าที่จะเป็นไปได้

relay แบบที่ชัดเจนที่สุดจะ query ตาราง outbox ซ้ำ ๆ เพื่อหาแถวใหม่ แต่ทุก query เสียงานของฐานข้อมูลไม่ว่าจะมีแถวใหม่หรือไม่ และความสดของการส่งถูกจำกัดด้วยความถี่ที่คุณ poll poll บ่อยคุณก็ใส่ภาระให้ฐานข้อมูล poll น้อยข้อความก็นั่งรอ ยังมีกับดักความถูกต้องที่แยบยลอีกอย่าง: คุณต้องสังเกตแถวตามลำดับ commit และไม่พลาดแม้แต่แถวเดียว

ดังนั้นแรงต่าง ๆ คือ: คุณต้องการ latency ต่ำและ overhead ของฐานข้อมูลต่ำ คุณต้อง publish ทุกแถว outbox ที่ commit แล้วเพียงครั้งเดียว จากมุมมองของ relay และคุณคงไม่อยากรัน query loop ที่ยุ่งวุ่นวายกับ primary database ของคุณ

relational database ทุกตัวมีบันทึกการเปลี่ยนแปลงที่ commit แล้วซึ่งเรียงลำดับและคงทนอยู่แล้ว เพื่อใช้ทำ recovery และ replication ของตัวเอง นั่นคือ transaction log เช่น write-ahead log ของ PostgreSQL หรือ binlog ของ MySQL Transaction log tailing เปลี่ยน relay ให้กลายเป็น consumer ของ log ชุดนั้น แทนที่จะ query ตาราง outbox ตัว relay จะอ่าน stream ของ log แล้วคัดเฉพาะ insert ที่ลงตาราง outbox ออกมา publish ไปยัง broker เรียงตามลำดับ commit ทันทีที่ transaction commit

วิธีนี้เรียกว่า change data capture (CDC) เครื่องมืออย่าง Debezium ต่อเข้ากับ log ถอดรหัสออกมา แล้วปล่อย change event หนึ่งอันต่อหนึ่งแถว เพราะ relay อ่าน replication stream แทนที่จะรัน query จึงแทบไม่เพิ่มภาระให้ primary และแทบไม่เพิ่ม latency ตัว relay จะจำตำแหน่งของตัวเองใน log ไว้เป็น offset เช่น LSN หลัง restart จึงกลับมาทำงานต่อตรงจุดที่ค้างไว้พอดี

flowchart LR
  subgraph DB[Service Database]
    OT[(outbox table)]
    LOG[(transaction log / WAL)]
    OT -->|insert is recorded in| LOG
  end
  CDC[CDC Connector]
  B[(Message Broker)]
  LOG -->|stream of committed changes| CDC
  CDC -->|publish outbox inserts in commit order| B
  CDC -.->|store offset / LSN| OFF[(offset store)]
relay tail transaction log ผ่าน CDC และ publish insert ของ outbox โดยไม่ต้อง polling

log tailing เป็น pattern เชิง infrastructure เป็นหลัก คุณรัน CDC connector แทนที่จะเขียน query loop เอง ตัว relay จึงมาจากการ configure ไม่ใช่การเขียน code ตัวอย่างข้างล่างคือ connector แบบ Debezium ที่เฝ้าตาราง outbox โดยจับ insert แล้วส่งแต่ละ event ไปยัง topic ที่ได้จาก metadata ของ outbox

# CDC connector: tail the WAL and publish outbox inserts to the broker.
name: order-outbox-connector
config:
connector.class: io.debezium.connector.postgresql.PostgresConnector
plugin.name: pgoutput
database.hostname: orders-db
database.dbname: orders
database.server.name: orders
# Only capture the outbox table.
table.include.list: public.outbox
# The outbox is append-only; ignore deletes from pruning.
tombstones.on.delete: false
# Route by the event's aggregate type and key by aggregate id,
# using the outbox event-router transform.
transforms: outbox
transforms.outbox.type: io.debezium.transforms.outbox.EventRouter
transforms.outbox.table.field.event.key: aggregate_id
transforms.outbox.route.by.field: aggregate_type
transforms.outbox.route.topic.replacement: ${routedByValue}.events

connector จำตำแหน่งของตัวเองใน log ไว้ พอ restart จึงกลับมาทำงานต่อจาก offset ที่ commit ล่าสุด แทนที่จะ replay ทุกอย่างใหม่ทั้งหมด:

offset: { "lsn": 24197848, "txId": 5912, "ts_usec": 1718900000000000 }
resume → continue streaming WAL after lsn 24197848

สิ่งที่คุณได้:

  • ไม่ต้อง polling และ latency ต่ำ ข้อความถูก publish แทบจะทันทีที่ transaction ต้นทาง commit เพราะ relay อ่าน replication stream แบบ live อยู่แล้ว
  • ภาระน้อยที่สุดบน primary การอ่าน log เป็น path ราคาถูกเดียวกันกับที่ replica ใช้อยู่แล้ว ไม่มี query loop ที่ยุ่งวุ่นวายกระหน่ำตาราง outbox
  • การเรียงลำดับที่ถูกต้องโดยอัตโนมัติ transaction log เรียงตามลำดับ commit อยู่แล้ว ดังนั้น relay จึง publish ตามลำดับนั้นโดยไม่ต้องทำงานเพิ่ม

สิ่งที่คุณต้องแลก:

  • ภาระเชิงปฏิบัติการ คุณต้องรันและ monitor CDC connector อย่าง Kafka Connect, Debezium หรือเทียบเท่า ต้องให้สิทธิ์ replication และตั้ง log retention ให้ดี ไม่งั้น relay ตามไม่ทันแล้วเสียตำแหน่งไป
  • การผูกติดกับฐานข้อมูล connector เจาะจงกับรูปแบบ log ของฐานข้อมูลของคุณ การเปลี่ยนฐานข้อมูลหมายถึงการเปลี่ยน connector
  • ยังคงเป็น at-least-once การ restart connector สามารถปล่อยการเปลี่ยนแปลงล่าสุดที่ยังไม่ถูก ack ออกมาซ้ำ ดังนั้น consumer ต้องยังคงเป็น idempotent
  • Transactional Outbox — จัดหาแถวที่ relay นี้ tail
  • Polling Publisher — ทางเลือก relay ที่ง่ายกว่าเมื่อ CDC มากเกินไปที่จะทำงาน
  • Idempotent Consumer — จำเป็นเพราะ log tailing ก็ส่งแบบ at least once เช่นกัน
ข้อดีข้อแลกเปลี่ยน
latency ต่ำมาก — detect change แทบ real-timeผูกกับ database-specific mechanism (binlog, WAL, change stream)
ไม่เพิ่ม load บน database จาก pollingต้องการ expertise ในการ configure CDC tool (Debezium)
reliable — event มาจาก database log โดยตรงlog format เปลี่ยนเมื่ออัปเกรด database version
เหมาะกับ high-throughput systemschema เปลี่ยนอาจ break CDC pipeline

ใช้ CDC กับ Database ที่ไม่รองรับดี — เปิด binlog บน database ที่ไม่ได้ออกแบบมาสำหรับ replication อาการ:

  • database performance ลดเพราะ binlog overhead
  • CDC pipeline ไม่ stable — disconnect บ่อย
  • ตรวจสอบ compatibility ก่อน adopt

Schema Change โดยไม่วางแผน — เปลี่ยน column name หรือ type โดยไม่แจ้ง consumer อาการ:

  • CDC pipeline break หลัง migration
  • consumer downstream ได้รับ event ที่ deserialize ไม่ได้
  • ใช้ schema registry (Confluent Schema Registry) เพื่อ manage compatibility

💡 ตัวอย่างจากของจริง

Debezium (Red Hat):

  • open source CDC platform ที่ใช้แพร่หลายที่สุด
  • รองรับ MySQL, PostgreSQL, MongoDB, SQL Server
  • Netflix, Shopify, Zalando ใช้ Debezium ใน production

MongoDB Change Streams:

  • built-in CDC สำหรับ MongoDB
  • ไม่ต้องใช้ third-party tool — subscribe change stream ได้โดยตรงจาก application
relay แบบ transaction-log-tailing อ่านอะไรเพื่อหาข้อความใหม่?
ทำไม log tailing จึงแทบไม่เพิ่มภาระให้ primary database?
relay ติดตามอะไรเพื่อให้กลับมาทำงานต่อได้ถูกต้องหลัง restart?