- แอปแบบ Local-First สัญญาว่าจะให้ การตอบสนองที่รวดเร็ว และ ความเป็นส่วนตัวพื้นฐานโดยปริยาย แต่ในทางปฏิบัติ การรองรับออฟไลน์อย่างแท้จริงนั้นทำได้ยากมาก
- สาเหตุใหญ่ที่สุดคือ ความซับซ้อนของการซิงก์ เพราะเมื่อมีการเปลี่ยนแปลงข้อมูลพร้อมกันจากหลายอุปกรณ์ สุดท้ายระบบต้อง ลู่เข้าไปสู่สถานะเดียวกันอย่างแม่นยำ
- มีความท้าทายทางเทคนิคหลักอยู่ 2 ประการคือ ความไม่แน่นอนของลำดับเวลา และ ความขัดแย้งของข้อมูล
- เพื่อแก้ปัญหานี้ จำเป็นต้องนำแนวทางออกแบบระบบกระจายอย่าง Hybrid Logical Clocks(HLCs) และ CRDTs มาใช้
- การใช้ส่วนขยายบนพื้นฐานของ SQLite สามารถมอบสถาปัตยกรรมการซิงก์ที่ เชื่อถือได้และเรียบง่าย และนำไปใช้ได้บนทุกแพลตฟอร์ม
คำสัญญาและความเป็นจริงของแอปแบบ Offline-First
- แอปแบบ Offline-First ชูจุดเด่นเรื่อง การตอบสนองทันที, ความเป็นส่วนตัวที่มีมาให้โดยพื้นฐาน, และ ใช้งานได้โดยไม่ต้องรอโหลดแม้ในเครือข่ายที่ไม่เสถียร
- แต่ในความเป็นจริง แอปส่วนใหญ่ยังรองรับออฟไลน์ได้ไม่สมบูรณ์ โดยมากเพียงแค่เก็บการเปลี่ยนแปลงไว้ในเครื่องชั่วคราวแล้วค่อยส่งเมื่อมีการเชื่อมต่อเครือข่ายภายหลัง
- วิธีทำแบบนี้มีความน่าเชื่อถือต่ำ และมักลงเอยด้วยข้อความเตือนอย่างเช่น "การเปลี่ยนแปลงอาจไม่ถูกบันทึก"
ความยากพื้นฐานของการซิงก์
- เมื่อต้องสร้างแอปแบบ Local-First ก็แทบเลี่ยงไม่ได้ที่จะต้องสร้าง ระบบกระจาย
- หลายอุปกรณ์อาจแก้ไขข้อมูลอย่างอิสระขณะออฟไลน์ และเมื่อกลับมาเชื่อมต่อกันอีกครั้ง ก็ต้อง ลู่เข้าสู่สถานะเดียวกันอย่างแม่นยำ
- เพื่อให้ทำเช่นนั้นได้ มี ความท้าทายใหญ่ 2 ข้อ
- ความไม่แน่นอนของลำดับเหตุการณ์
- ความขัดแย้งกับข้อมูลเดียวกัน
1. ความไม่แน่นอนของลำดับเหตุการณ์
- เหตุการณ์จากหลายอุปกรณ์เกิดขึ้นคนละช่วงเวลา และสถานะสุดท้ายอาจเปลี่ยนไปตามลำดับของเหตุการณ์
- ตัวอย่าง: อุปกรณ์ A ตั้งค่า x=3, อุปกรณ์ B ตั้งค่า x=5 → ต่างฝ่ายต่างเปลี่ยนระหว่างออฟไลน์ แล้วเมื่อซิงก์กันภายหลัง อาจเกิดผลลัพธ์ที่ต่างกันได้
- ฐานข้อมูลแบบรวมศูนย์เดิมแก้ปัญหานี้ด้วย ความสอดคล้องเข้มงวด แต่แนวทางนี้ต้องอาศัยการซิงก์ระดับโกลบอล จึงไม่เหมาะกับระบบแบบ Local-First
- สุดท้ายแล้ว จำเป็นต้อง กำหนดลำดับที่เหมาะสมให้กับแต่ละเหตุการณ์แม้อยู่ในสภาพแวดล้อมแบบกระจายและเปลี่ยนแปลงตลอดเวลา จึงต้องมีวิธีกำหนดลำดับโดยไม่พึ่งนาฬิกากลาง
การนำ Hybrid Logical Clocks(HLCs) มาใช้
- Hybrid Logical Clocks(HLCs) คือ อัลกอริทึมที่เรียบง่ายแต่มีประสิทธิภาพ ซึ่งช่วยให้อุปกรณ์แต่ละเครื่องตกลงลำดับของเหตุการณ์กันได้ในทางปฏิบัติ
- HLC ใช้งานโดย ผสานข้อมูลเวลาจริงเข้ากับตัวนับเชิงตรรกะ
- ตัวอย่างเช่น:
- อุปกรณ์ A บันทึกเหตุการณ์ตอนเวลา 10:00:00.100 ดังนั้น HLC คือ (10:00:00.100, 0)
- อุปกรณ์ B ที่ได้รับข้อความ แม้นาฬิกาของตัวเองจะช้ากว่า ก็จะเลื่อน HLC เป็น (10:00:00.100, 1)
- ด้วยเหตุนี้ จึงสามารถกำหนดลำดับเหตุการณ์ได้อย่างถูกต้อง โดยไม่ขึ้นกับความต่างของนาฬิกาจริงระหว่างสองอุปกรณ์
2. ปัญหาความขัดแย้ง
- แค่จัดลำดับให้ถูกต้องยังไม่พอ เพราะ เมื่ออุปกรณ์ต่างกันแก้ไขข้อมูลเดียวกันอย่างอิสระ ความขัดแย้งย่อมเกิดขึ้นอย่างหลีกเลี่ยงไม่ได้
- ระบบส่วนใหญ่มักบังคับให้นักพัฒนาต้องเขียนโค้ดแก้ความขัดแย้งเอง ซึ่งนำมาสู่ ความเสี่ยงต่อข้อผิดพลาดและภาระในการดูแลรักษา
การใช้ CRDTs
- แนวทางที่ดีที่สุดคือการใช้ Conflict-Free Replicated Data Types(CRDTs)
- CRDTs รับประกันว่า ไม่ว่าจะซิงก์ตามลำดับใด หรือแม้มีการนำไปใช้ซ้ำ สถานะของแต่ละอุปกรณ์ก็จะลงเอยเหมือนกันเสมอ
- กลยุทธ์ CRDT ที่ง่ายที่สุดคือ Last-Write-Wins(LWW)
- ใส่ timestamp ให้กับทุกการอัปเดต
- ระหว่างซิงก์ ให้เลือกค่าที่มี timestamp ใหม่กว่า
ข้อดีของ SQLite
- ในการสร้างแอปแบบ Local-First จำเป็นต้องมี ฐานข้อมูลภายในเครื่องที่เบาและเชื่อถือได้ และ SQLite คือทางเลือกที่เหมาะที่สุด
- หากทำฟังก์ชันการซิงก์ผ่านส่วนขยายของเฟรมเวิร์กที่อิง SQLite จะได้ข้อดีดังนี้
- การนำข้อความมาใช้ทำได้ง่าย: อ่านค่าปัจจุบัน → หาก timestamp ใหม่กว่าให้เขียนทับ → หากไม่ใช่ให้ข้าม
- วิธีนี้ รับประกันการลู่เข้าสู่สถานะเดียวกันของทุกอุปกรณ์ โดยไม่ขึ้นกับลำดับการซิงก์
ความหมายของสถาปัตยกรรมนี้
- โครงสร้างนี้ทำให้เกิดการซิงก์ที่ เรียบง่ายและเชื่อถือได้สูง
- แม้ออฟไลน์ต่อเนื่องหลายสัปดาห์ก็ยังเชื่อถือได้โดยไม่สูญหายของข้อมูล
- มีคุณสมบัติแบบกำหนดได้แน่นอนที่ลู่เข้าสู่สถานะสุดท้ายเสมอ
- แก้ปัญหาได้ด้วยเพียง ส่วนขยาย SQLite ขนาดเล็ก โดยไม่ต้องพึ่งพา dependency หนัก ๆ
- รองรับ ทุกแพลตฟอร์มหลัก เช่น iOS, Android, macOS, Windows, Linux, WASM
ข้อเสนอแนะสำหรับนักพัฒนา
- ควรหลีกเลี่ยงการ "แกล้งทำเป็น" รองรับโหมดออฟไลน์ด้วยคิวคำขอแบบง่าย ๆ
- ควรยอมรับแนวคิด eventual consistency และใช้ เทคโนโลยีระบบกระจายที่ผ่านการพิสูจน์แล้ว เช่น HLC และ CRDT
- แทนที่จะใช้เฟรมเวิร์กขนาดใหญ่และซับซ้อน ควรมุ่งสู่โครงสร้างที่ เล็กและไม่ผูกกับ dependency
- ผลลัพธ์คือแอปจะได้ประโยชน์อย่าง เปิดใช้งานได้ทันที, ใช้งานออฟไลน์ได้, และ มีความเป็นส่วนตัวพื้นฐานโดยปริยาย
แนะนำโอเพนซอร์ส SQLite-Sync
- หากสนใจเอนจิน Offline-First แบบข้ามแพลตฟอร์มที่พร้อมใช้ใน production ได้ทันที สามารถดูส่วนขยายโอเพนซอร์ส SQLite-Sync ได้
1 ความคิดเห็น
ความเห็นจาก Hacker News
Cache-Controlให้ถูกต้องใน API response และทำให้ network layer ปฏิบัติตาม ก็ช่วยแก้ปัญหาได้มาก แบบนี้ถ้าเปลี่ยนอายุ cache ที่ฝั่งเซิร์ฟเวอร์ก็มีผลได้ทันทีโดยไม่ต้องอัปเดตแอป