หยุดซิงค์ข้อมูลทุกอย่าง
(sqlsync.dev)- Graft คือ transactional storage engine แบบโอเพนซอร์สที่พยายามผสานความเรียบง่ายของ physical replication เข้ากับประสิทธิภาพของ logical replication แทนที่จะส่ง change log ทั้งหมดให้ไคลเอนต์ทุกตัว
- จัดการ Volume ที่ประกอบด้วย Page ขนาดคงที่ในหน่วย Snapshot โดยเซิร์ฟเวอร์ไม่ได้ส่งข้อมูลจริง แต่ส่ง
graftซึ่งเป็น bitset ที่บีบอัดของดัชนี page ที่เปลี่ยนแปลง - ไคลเอนต์ดู
graftแล้วดึงมาเฉพาะ page ที่จำเป็น และสามารถเลือกใช้ prefetching แบบ Leap, prefetching เฉพาะโดเมน หรือ proactive fetching ที่ดึงการเปลี่ยนแปลงทั้งหมดมาได้ - มีเป้าหมายเพื่อทำ partial replication ได้แม้ใน สภาพแวดล้อมที่มีข้อจำกัด เช่น เบราว์เซอร์ แอปมือถือ serverless function และ embedded environment โดยใช้งาน object storage และ edge server
- consistency model คือ Serializable Snapshot Isolation โดยจะปฏิเสธ commit ที่อิง Snapshot เก่า และให้ไคลเอนต์จัดการด้วยวิธีใดวิธีหนึ่งระหว่าง reset/replay, merge หรือ Volume fork
ปัญหา replication ที่ Graft ต้องการแก้
- partial replication ดูเหมือนง่ายหากซิงค์เฉพาะข้อมูลที่ต้องใช้ แต่ในการออกแบบจริง replication แต่ละแบบมีต้นทุนที่ชัดเจน
- logical replication ติดตามการเปลี่ยนแปลงทุกอย่างได้อย่างละเอียด แต่ทำให้ strong consistency ซับซ้อนขึ้น
- physical replication หลีกเลี่ยงความซับซ้อนนั้นได้ แต่ต้องซิงค์การเปลี่ยนแปลงทั้งหมดแม้บางส่วนจะถูกทิ้งในภายหลัง
- Graft คือ transactional storage engine แบบโอเพนซอร์สที่สร้างขึ้นโดยมีเป้าหมายคือ lazy synchronization, partial replication, strong consistency, horizontal scalability และ durability ของ object storage
- จุดเริ่มต้นมาจากประสบการณ์กับ SQLSync
- SQLSync เป็น database stack ที่ปรับให้เหมาะกับ frontend สร้างบน SQLite และใช้แนวคิดจาก Git กับ distributed systems ใน sync engine
- SQLSync มีโครงสร้างที่ replicate change log ทั้งหมดไปยังไคลเอนต์ทุกตัว ซึ่งใช้ได้ดีบนเซิร์ฟเวอร์ แต่ไม่เหมาะกับสภาพแวดล้อม edge และเบราว์เซอร์
- เป้าหมายของ Graft คือให้ไคลเอนต์ซิงค์ตามจังหวะที่ต้องการ ดึงเฉพาะสิ่งที่จำเป็น และ replicate ข้อมูลใด ๆ ที่ edge และอุปกรณ์ออฟไลน์ได้ด้วย strong consistency
การออกแบบที่อยู่ระหว่าง full replication กับ schema-aware diff
- แนวทางเดิมแบ่งได้เป็นสองสายหลัก
- full replication: ซิงค์ dataset ทั้งหมดไปยังไคลเอนต์แต่ละตัว จึงไม่เหมาะในทางปฏิบัติกับสภาพแวดล้อมที่มีข้อจำกัด เช่น serverless function หรือเว็บแอป
- schema-aware diff: ติดตามการเปลี่ยนแปลงเชิงตรรกะระดับแถวหรือฟิลด์ เช่น CDC หรือ CRDT แต่ต้องผสานเข้ากับแอปพลิเคชันอย่างลึก และทำให้ครอบคลุมข้อมูลใด ๆ ได้ยาก
- Graft ไม่ขึ้นกับ schema เหมือน full replication
- ไม่รู้และไม่สนใจชนิดของข้อมูลที่จัดเก็บ แต่ replicate page ที่บรรจุ byte อยู่
- ขณะเดียวกันก็ส่งคำอธิบายแบบบีบอัดให้ไคลเอนต์ว่าอะไรเปลี่ยนไปตั้งแต่ซิงค์ครั้งล่าสุด เหมือน logical replication
- abstraction หลักคือ Volume
- Volume คือคอลเลกชันแบบ sparse และเรียงลำดับของ Page ขนาดคงที่
- ไคลเอนต์อ่านและเขียน Volume ที่ Snapshot ใด Snapshot หนึ่งผ่าน transaction API
- ภายใน Graft จะจัดเก็บและ replicate เฉพาะสิ่งที่จำเป็น และใช้ object storage เป็น backend ที่ทนทานและขยายขนาดได้
lazy synchronization: ตามให้ทันในเวลาที่ไคลเอนต์ต้องการ
- Graft ออกแบบโดยตั้งสมมติฐานว่า edge client ตื่นขึ้นมาเป็นครั้งคราว เครือข่ายไม่เสถียร และมีเวลารันสั้น
- ไม่พึ่งพา replication อย่างต่อเนื่อง แต่ให้ไคลเอนต์เลือกเองว่า จะซิงค์เมื่อไร
- การซิงค์เริ่มจากคำถามว่า “มีอะไรเปลี่ยนไปตั้งแต่ Snapshot ล่าสุด”
- เซิร์ฟเวอร์ไม่ส่งข้อมูลจริง แต่ตอบกลับด้วย
graftซึ่งเป็น bitset ที่บีบอัดของดัชนี page ที่เปลี่ยนแปลงgraftทำหน้าที่เป็นคำแนะนำสำหรับต่อการเปลี่ยนแปลงใหม่เข้ากับ Snapshot เดิม- ไคลเอนต์รู้ได้ว่า page ใดนำกลับมาใช้ซ้ำได้ และ page ใดต้องดึงมาเมื่อจำเป็น
- เนื่องจาก
graftเป็น metadata ของการเปลี่ยนแปลง ไม่ใช่ตัวข้อมูล อำนาจควบคุมว่าจะดึงอะไรและเมื่อไรจึงยังอยู่กับไคลเอนต์
partial replication และ prefetching
- ในแท็บเบราว์เซอร์ แอปมือถือ และ serverless function การดาวน์โหลด dataset ทั้งหมดเพื่อประมวลผล query บางส่วนทำได้ยาก
- หลังได้รับ
graftไคลเอนต์จะตัดสินว่า page ใดยังใช้ได้อยู่ และ page ใดต้องดึงมา - เพราะดึงมาเฉพาะ page ที่จำเป็นแบบเลือกได้ จึง replicate เฉพาะข้อมูลที่จะใช้งานจริงได้
- Graft รองรับ prefetching หลายแบบเพื่อลด latency ในการเข้าถึง page
- general-purpose prefetching: prefetcher ในตัวที่อิงอัลกอริทึม Leap ระบุ access pattern และคาดการณ์การเข้าถึง page ในอนาคต
- domain-specific prefetching: แอปพลิเคชันใช้ความรู้เกี่ยวกับข้อมูลที่ถูกเรียกดูบ่อย เช่น โปรไฟล์ผู้ใช้ เพื่อดึง page ที่เกี่ยวข้องมาล่วงหน้าได้
- proactive fetching: หากจำเป็น สามารถดึงการเปลี่ยนแปลงทั้งหมดมา ซึ่งเท่ากับย้อนกลับไปเป็น full replication โดยเฉพาะมีประโยชน์กับ workload ของ Graft ฝั่งเซิร์ฟเวอร์
- page ถูก host โดยตรงบน object storage จึงใช้เป็นฐาน replication ที่มี durability และ scalability
การ deploy ที่ edge และ embedded client
- edge replication ของ Graft ไม่ได้มีเป้าหมายแค่ว่าจะซิงค์ข้อมูลใด แต่รวมถึงการวางข้อมูลไว้ในตำแหน่งที่ต้องใช้ด้วย
- page ถูกให้บริการจาก object storage ผ่าน fleet ของ global edge server
- hot page ที่ถูกเข้าถึงบ่อยสามารถถูก cache ใกล้กับไคลเอนต์ได้
- มีเป้าหมายให้ latency ต่ำและตอบสนองได้ดี ไม่ว่าผู้ใช้จะอยู่ที่ใดในโลก
- Graft client ออกแบบให้เบาและฝังเข้าไปได้
- มี dependency น้อยและ runtime ขนาดเล็ก
- สามารถผสานเข้ากับสภาพแวดล้อมอย่างเบราว์เซอร์ อุปกรณ์ แอปมือถือ และ serverless function ได้
- edge caching ทำให้เกิดปัญหา consistency และ conflict handling ดังนั้น Graft จึงมี strong consistency model มาพร้อมกัน
consistency model และ conflict handling
- Graft ใช้ Serializable Snapshot Isolation เป็น consistency model
- ไคลเอนต์ได้รับมุมมองข้อมูลที่ถูก isolate และ consistent ณ Snapshot หนึ่ง โดย read สามารถดำเนินพร้อมกันได้โดยไม่รบกวนกัน
- write ถูก serialize อย่างเข้มงวด ทำให้ transaction ทั้งหมดมีลำดับที่ consistent ในระดับ global
- ด้วยคุณสมบัติ offline-first และ delayed replication ไคลเอนต์อาจพยายาม commit โดยอิง Snapshot ที่เก่า
- หากยอมรับ commit แบบนี้โดยไม่มีเงื่อนไข strict serializability จะเสียหาย
- Graft จะปฏิเสธ commit นั้นอย่างปลอดภัย และให้ไคลเอนต์เลือกวิธีจัดการ
- ตัวเลือกทั่วไปของไคลเอนต์มีสามแบบ
- Reset and replay: ดึง Snapshot ล่าสุดมา แล้วนำ local transaction มา apply ใหม่ก่อนลองอีกครั้ง
- ข้อมูล global ยังคงอยู่ในสถานะ strict serializable
- ฝั่ง local จะได้รับประสบการณ์แบบ Optimistic Snapshot Isolation โดย read จะเห็น Snapshot ที่ consistent ภายใน แต่ Snapshot นั้นอาจถูกทิ้งหาก commit ถูกปฏิเสธ
- Merge: merge local state เข้ากับ Snapshot ล่าสุดของเซิร์ฟเวอร์
- ในกรณีนี้ consistency model ระดับ global อาจลดลงเป็น snapshot isolation
- Volume fork: สร้าง Volume ใหม่แบบถาวรเพื่อแยกออกไป
- ยังคงรักษา global serializability ได้
- Reset and replay: ดึง Snapshot ล่าสุดมา แล้วนำ local transaction มา apply ใหม่ก่อนลองอีกครั้ง
แอปพลิเคชันที่สร้างได้
- แอป offline-first: Graft สามารถรับผิดชอบการซิงค์ในแอปที่ทำงานออฟไลน์บางส่วน เช่น โน้ต การจัดการงาน และแอป CRUD
- เมื่อใช้ร่วมกับ conflict handler ก็สามารถทำฟีเจอร์ multiplayer บนข้อมูลใด ๆ ได้
- ข้อมูลข้ามแพลตฟอร์ม: แชร์ข้อมูลบนแพลตฟอร์มมือถือ อุปกรณ์ และเว็บ พร้อมลดการผูกติดกับ vendor ได้
- stateless read replica: เปิด database replica โดยไม่มี local state ดึง metadata ของ Snapshot ล่าสุด แล้ว query ได้ทันที
- ไม่จำเป็นต้องดาวน์โหลดข้อมูลทั้งหมดหรือ replay log
- replication สำหรับข้อมูลใด ๆ: Graft มุ่งเน้นที่ page replication จึงไม่ยุ่งกับรูปแบบข้อมูลภายใน page
- สามารถใช้กับข้อมูลอย่างฐานข้อมูล SQLite, โมเดล AI, ไฟล์ Parquet, Lance และ Geospatial tilesets ได้
SQLite extension libgraft
- วิธีที่ง่ายที่สุดในตอนนี้ในการใช้ Graft คือ
libgraftซึ่งเป็น native SQLite extension libgraftใช้ได้ทุกที่ที่ SQLite ทำงาน และ replicate เฉพาะบางส่วนของฐานข้อมูลที่ไคลเอนต์ใช้งานจริง- implement SQLite VFS เพื่อ intercept การอ่านและเขียนฐานข้อมูล
- ให้ semantics ของ transaction และ concurrency แบบเดียวกับที่ SQLite มีใน WAL mode
- ความสามารถที่มีให้ ได้แก่
- asynchronous replication กับ object storage
- delayed partial replication ที่ edge และบนอุปกรณ์
- Serializable Snapshot Isolation
- point-in-time restore
- ดูเอกสารได้ที่ เอกสาร SQLite บน GitHub
การมีส่วนร่วมและแผน managed service
- Graft พัฒนาแบบเปิดบน GitHub
- รับ issue, discussion และ Pull Request พร้อมมี contribution guide
- มี Discord และอีเมลเป็นช่องทางพูดคุย
- มีแผนเปิดตัว Graft Managed Service และมีลิงก์สำหรับสมัคร waitlist
roadmap
- Graft ผ่านงานวิจัยมา 1 ปี การ iterate หลายครั้ง และการเปลี่ยนทิศทางครั้งใหญ่หนึ่งครั้ง แต่ยังมีงานเหลืออีกมาก
- รายการที่วางแผนไว้มีดังนี้
- รองรับ WebAssembly: ทำให้ใช้ Graft ในเบราว์เซอร์ได้ โดยตั้งเป้ารองรับ official Wasm build ของ SQLite, wa-sqlite และ sql.js
- ผสาน Graft กับ SQLSync: หลังรองรับ Wasm แล้ว มีแผนแยกชั้น mutation, rebase และ query subscription ของ SQLSync ออกมา แล้ววางบนฐานข้อมูล replication ของ Graft
- ขยาย client library: ต้องการ native Graft client wrapper สำหรับ Python, JavaScript, Go และ Java
- low-latency write: ปัจจุบันงาน push จะถูก block จนกว่าจะ commit เข้า object storage อย่างสมบูรณ์
- ทดลอง S3 express zone
- วางกลุ่ม consensus ที่ทนทานและ latency ต่ำไว้หน้า object storage
- garbage collection, checkpointing, compaction: จำเป็นเพื่อเพิ่มประสิทธิภาพ query ให้สูงสุด ลดพื้นที่สูญเปล่า และลบถาวร
- authentication และ authorization: เป็นงานขอบเขตกว้าง ตั้งแต่บัญชี managed service ไปจนถึงสิทธิ์อ่าน/เขียน Volume แบบละเอียด
- Volume forking: service สามารถทำ zero-copy fork ได้โดย copy reference ของ Segment ไปยัง Volume ใหม่ แต่ local fork ในปัจจุบันต้อง copy page ทั้งหมด
- conflict handling: มีแผนให้กลยุทธ์แก้ conflict ในตัวและ extension point โดยกลยุทธ์แรกคือ auto-merge transaction ที่ไม่ทับซ้อนกัน
เปรียบเทียบกับโซลูชัน SQLite replication
- ข้อมูลเปรียบเทียบรวบรวมจากเอกสารและบทความบล็อก พร้อมระบุข้อสงวนว่าอาจไม่แม่นยำทั้งหมด
-
mvSQLite
- mvSQLite implement ชั้น VFS แบบ custom ที่เก็บ SQLite page ลงใน FoundationDB โดยตรง
- Graft กับ mvSQLite คล้ายกันตรงที่ versioning ระดับ page ทำให้ทำ lazy fetch และมุมมองฐานข้อมูลแบบ partial ได้
- ความแตกต่างคือที่เก็บข้อมูลและวิธีติดตามการเปลี่ยนแปลงของ page
- mvSQLite พึ่งพา FoundationDB และทุก node ต้องเข้าถึง cluster โดยตรง
- changeset ที่อิง Splinter ของ Graft เป็นแบบ self-contained จึง deploy ง่ายกว่า และไม่ต้อง query FoundationDB โดยตรงเพื่อรู้ version ของ page ที่เปลี่ยน
-
Litestream
- Litestream คือโซลูชัน streaming backup ที่ replicate SQLite WAL frame ไปยัง object storage อย่างต่อเนื่อง
- Graft ผสานเข้ากับกระบวนการ commit ของ SQLite โดยตรงผ่าน custom VFS ทำให้ทำ delayed partial replication และ distributed write ได้
- ทั้งสอง replicate page ไปยัง object storage และรองรับ point-in-time restore
-
cr-sqlite
- cr-sqlite คือ SQLite extension ที่เปลี่ยน table เป็น CRDT เพื่อให้ logical replication ระดับแถวทำได้
- มี automatic conflict resolution แต่ต้องรู้ schema และต้องผสานในระดับแอปพลิเคชัน
- Graft ไม่ขึ้นกับ schema และเข้ากันได้กับ SQLite extension ใด ๆ รวมถึง custom data structure แต่เพื่อ global serializability แอปพลิเคชันต้องจัดการ conflict resolution อย่างชัดเจน
-
Cloudflare Durable Objects with SQLite Storage
- การรวม Durable Objects กับ SQLite ทำให้วางฐานข้อมูลที่มี strong consistency และ durability สูงซึ่งครอบด้วย business logic ไว้บนเครือข่าย edge ของ Cloudflare ได้
- ภายในคล้าย Litestream ตรงที่ replicate SQLite WAL ไปยัง object storage และ checkpoint เป็นระยะ
- Graft เปิดเผย replication เป็น first-class feature และมุ่งทำ replication กับ edge อย่างมีประสิทธิภาพ
-
Cloudflare D1
- Cloudflare D1 คือ managed SQLite database ที่เข้าถึงผ่าน HTTP API
- Graft เป็นโมเดลแบบ distributed ที่ embed ข้อมูลใน client application และ replicate ไปยัง edge โดยตรง
-
Turso & libSQL
-
rqlite & dqlite
-
Verneuil
- Verneuil replicate SQLite Snapshot ไปยัง read replica แบบ asynchronous ผ่าน object storage โดยให้ความสำคัญกับ reliability
- ตั้งใจหลีกเลี่ยงกลไกที่ลด replication lag หรือเพิ่ม freshness ให้มากที่สุด
- Graft ทำงานใกล้เคียงกับ multi-writer distributed database มากกว่า และเน้น selective real-time partial replication
ยังไม่มีความคิดเห็น