lobste.rs เปลี่ยนจาก MariaDB มาใช้ SQLite
(lobste.rs)- หลังจาก lobste.rs เปลี่ยนจาก MariaDB มาใช้ SQLite ก็สามารถรับมือกับทราฟฟิกที่พุ่งสูงในวันจันทร์ได้อย่างเสถียร และพบว่าใช้ CPU·หน่วยความจำน้อยลง พร้อมการตอบสนองที่รู้สึกได้ว่าเร็วขึ้น
- หากปิด VPS ของ MariaDB ที่แยกไว้ต่างหาก จะทำให้ ค่าใช้จ่าย VPS ลดลงครึ่งหนึ่ง และปิดภารกิจไมเกรชันที่เริ่มมาตั้งแต่ปี 2019 ได้สำเร็จ
- การ deploy ครั้งแรก rollback เพราะแม้มีเฉพาะทราฟฟิกแบบอ่านอย่างเดียว CPU ทุกตัวก็แตะ 100% แต่หลังแก้ full table scan 2 จุดบนตารางขนาดใหญ่และปัญหา N+1 ก็ deploy ใหม่ได้สำเร็จ
- ต้องแก้ข้อจำกัดด้าน ความเข้ากันได้ หลายอย่าง เช่น การทำ
regexp·if·stddevด้วย user-defined function ของ SQLite, การไม่รองรับ unsigned bigint และNOCASEที่จัดการได้เฉพาะ ASCII - การเปลี่ยนฐานข้อมูลพื้นฐานโดยไม่เข้าถึง production database ต้องอาศัย ข้อมูลทดสอบขนาดสมจริง, test suite, เช็กลิสต์ deploy·rollback และการทำงานร่วมกันอย่างใกล้ชิด
จากการหารือในปี 2019 สู่การเปลี่ยนมาใช้ SQLite
- การหารือเรื่องไมเกรชันเริ่มขึ้นในปี 2019 จาก issue #539
- ตอนนั้นมีการเสนอ MySQL เป็นทางเลือกโดยคำนึงถึงความเข้ากันได้กับ MariaDB แต่ก็มีการหารือเรื่องย้ายไป PostgreSQL อยู่ก่อนแล้ว
- ในปี 2025 Rahul กล่าวถึง การเข้าซื้อ MariaDB โดย K1 ทำให้มีการหารือรายละเอียดการไมเกรตไป PostgreSQL ต่อเนื่อง
- ในเดือนกุมภาพันธ์ Rahul ถามว่า lobsters สามารถรันบน SQLite ได้หรือไม่ และตั้งแต่เดือนมิถุนายน 2025 ก็เริ่มเข้าร่วมโปรเจกต์อย่างจริงจัง
- PR แรก ในเดือนสิงหาคม 2025 หยุดค้างไป แล้วถูก GitHub ปิดในฐานะ PR เก่า จึงเปิดใหม่ไม่ได้
- PR ที่สอง รวมการทดสอบประสิทธิภาพ, สคริปต์ไมเกรตข้าม DB ที่ทำขึ้นเอง และการดีบักด้านความถูกต้องครบถ้วนของข้อมูล
- ในบรรดาสคริปต์แปลง MariaDB/MySQL→SQLite ที่มีอยู่ ไม่มีตัวใดตอบโจทย์ จึงเขียนสคริปต์แยกเอง
การ deploy ครั้งแรกล้มเหลวและการแก้คอขวดด้านประสิทธิภาพ
- การ deploy ครั้งแรกวันที่ 21 กุมภาพันธ์ดำเนินไปพร้อม เช็กลิสต์การ deploy ที่เตรียมไว้ แต่ทันทีหลัง deploy แม้มีเพียงทราฟฟิกสถานะอ่านอย่างเดียว CPU ทุกตัวก็แตะ 100%
- หาสาเหตุไม่พบจึง rollback
- เนื่องจากไม่สามารถเข้าถึง production DB ได้ จึงยากที่จะจำลองปัญหาประสิทธิภาพจริงล่วงหน้า
- สองวันหลังความล้มเหลว มีการเปิด PR ที่สามและเป็น PR สุดท้าย เพื่อแก้ปัญหาที่เกี่ยวกับการค้นหา และเพิ่ม สคริปต์สร้างข้อมูลจำนวนมาก ที่สร้างข้อมูลขนาดครึ่งหนึ่งของข้อมูล lobsters จริงในเครื่อง local
- การสร้างข้อมูลใช้เวลา หนึ่งสัปดาห์
- คอขวดของการ deploy ครั้งแรกคือ full table scan จากสอง query บนตารางขนาดใหญ่ และปัญหา N+1 อีกหนึ่งจุด
- มีการนำ การเปลี่ยนแปลง 1, การเปลี่ยนแปลง 2, การเปลี่ยนแปลง 3 ที่แก้ปัญหานี้เข้าไป
- ในเช้าวัน deploy ยังเพิ่ม slow query log เพื่อให้ติดตามปัญหาประสิทธิภาพเพิ่มเติมได้
ผลการ deploy ใหม่และการลดต้นทุนดำเนินงาน
- ในการ deploy ครั้งที่สองวันที่ 11 กรกฎาคม มีการเตรียมเช็กลิสต์ deploy·rollback อีกครั้ง และหลังเปลี่ยนแล้วไซต์ยังทำงานตามปกติ โดยการใช้ CPU และหน่วยความจำคงที่อย่างเสถียร
- ระหว่างเฝ้าดู metric ของไซต์และ IRC ปัญหาที่ถูกรายงานถูกจัดการทันทีด้วย การแก้ไข 1, การแก้ไข 2
- แม้ทราฟฟิกพุ่งสูงในวันจันทร์ก็ไม่มีปัญหา และพบว่าใช้ CPU·หน่วยความจำน้อยลง พร้อมการตอบสนองที่รู้สึกได้ว่าเร็วขึ้น
- หากปิด MariaDB VPS จะทำให้ ค่าใช้จ่าย VPS ลดลงครึ่งหนึ่ง และปิด issue เรื่องไมเกรชันได้
ข้อจำกัดของ SQLite ที่ต่างจาก MariaDB
- ใช้การรองรับ user-defined function (UDF) ของ SQLite gem เพื่อทำ
regexp,if,stddevที่ SQLite ไม่มี- ทำให้ไม่ต้องเพิ่ม workaround เพื่อไมเกรต SQL มากเกินไป
- SQLite ไม่รองรับ unsigned bigint ที่ MariaDB ใช้กับ ID บางส่วน จึงเปลี่ยนเป็น bigint
- กฎการเรียงลำดับก็จำกัดกว่า MariaDB
- ใน MariaDB ใช้
utf8mb4_general_ciแต่ใน SQLite ใช้NOCASE NOCASEไม่รองรับการ case folding ของ UTF ทั้งหมด และ จัดการได้เฉพาะอักขระ ASCII
- ใน MariaDB ใช้
- สำหรับตาราง full-text search ของ SQLite วิธีที่เหมาะคือ Contentless-Delete Tables ซึ่งเป็นวิธีที่แนะนำแต่ไม่ใช่ค่าเริ่มต้น
- การตั้งค่า PRAGMA เริ่มต้น ของ Rails ทำงานได้ตามปกติใน lobsters
- database migration แตกต่างกันไปตามแต่ละ DB จึงย้าย migration เดิมไปยังไดเรกทอรี old_migrations และทำให้
db:migrateยังทำงานต่อได้
- database migration แตกต่างกันไปตามแต่ละ DB จึงย้าย migration เดิมไปยังไดเรกทอรี old_migrations และทำให้
บทเรียนจากการทดสอบและการทำงานร่วมกัน
- ใน codebase ของ lobsters พบ search parser และ heinous_inline_partials ซึ่งเป็นแฮ็กเพื่อเพิ่มความเร็วในการ render
- ด้วย test suite จึงสามารถตรวจสอบความถูกต้องของการย้ายไป SQLite ได้โดยไม่ต้องทดสอบด้วยมือครั้งใหญ่
- การสื่อสารที่ราบรื่นระหว่างผู้มีส่วนร่วมเป็นตัวตัดสินความสำเร็จของไมเกรชัน
- การเปลี่ยนฐานข้อมูลพื้นฐานในสภาพที่เข้าถึง production DB ไม่ได้นั้นยากมาก
- หากต้องทำงานแบบเดียวกันอีกครั้ง ควรเตรียม ขนาดข้อมูลที่สมจริง ไว้ล่วงหน้า
- หากตั้งค่าให้การทดสอบล้มเหลวเมื่อเกิด full table scan ได้ ก็น่าจะจับปัญหาประสิทธิภาพในการ deploy ครั้งแรกได้ล่วงหน้า
- จำเป็นต้องมีเครื่องมือที่สามารถสร้างชุดข้อมูลคล้าย production ได้ง่าย โดยไม่ต้องเขียนเองหรือรอนานเป็นสัปดาห์
1 ความคิดเห็น
ความคิดเห็นใน Lobste.rs
@thomas0 รับหน้าที่ดูแลการไมเกรตครั้งใหญ่ได้อย่างละเอียดรอบคอบ, @355E3B ช่วยเรื่องการวางแผนและการดำเนินงาน และด้วยผู้ร่วมพัฒนาที่เกี่ยวข้องกับ #539 กับ #lobsters ทำให้ช่วงเวลาแบบอ่านอย่างเดียวผ่านไปได้อย่างสนุก
เดิมทีใน #539 วางแผนจะใช้ PostgreSQL แต่ผู้ที่อาสามาช่วยลงมือทำจริงใช้ SQLite และไม่อยากใช้โซลูชันที่ใหญ่และซับซ้อนเกินความต้องการที่คาดไว้ จึงเลือก SQLite โดยปกติ PostgreSQL เป็นตัวเลือกเริ่มต้น แต่ก็มีภาระที่ต้องรัน ปรับแต่ง และดูแลบริการแยกต่างหาก
น่าประหลาดใจที่หลังไมเกรตแล้ว การใช้ CPU และ RAM ลดลงทั้งคู่ โดยมีกราฟรายละเอียดอยู่ที่ ช่วงท้ายของ #539
ทุกครั้งที่เห็นค่าเริ่มต้นของ SQLite ก็รู้สึกแปลกใจ อยากให้มีโหมดสำหรับโปรเจกต์ใหม่ที่เปิดใช้ WAL, foreign key,
synchronous=NORMALฯลฯ เป็นค่าเริ่มต้น และน่าจะยังมีค่าเริ่มต้นอื่น ๆ ที่ควรปรับปรุงได้อีกสงสัยว่า กลยุทธ์การสำรองข้อมูล ของฐานข้อมูล SQLite เป็นอย่างไร อยากรู้ว่าใช้เครื่องมืออย่าง litestream หรือใช้วิธีอื่น
สงสัยว่า lobste.rs จัดการ งานอัปเดต อย่างไร
SQLite มี ฟังก์ชัน iif อยู่แล้ว แต่การรัน ฟังก์ชันที่ผู้ใช้กำหนดเอง ในบริเวณเดียวกันนั้นก็น่าสนใจเช่นกัน
เคยคิดว่า SQLite ไม่รองรับ concurrency ในระดับเดียวกับ PostgreSQL หรือ MySQL แต่เมื่อดู เอกสารภาพรวม แล้ว ควรเข้าใจว่าตอนนี้สามารถใช้จากหลายโปรเซสได้ แต่ lobste.rs ต้องรันบนเซิร์ฟเวอร์เดียวใช่ไหม
อยากรู้ด้วยว่าโปรเซสต่าง ๆ ซิงโครไนซ์กันภายใน SQLite ด้วยวิธีอย่าง shared memory หรือ
mmapทำให้เขียนพร้อมกันได้เพียงรายการเดียวแต่เปิดให้อ่านได้หลายรายการหรือไม่ และงานเขียนที่รออยู่จะรอคิวตามลำดับหรือไม่สำหรับการรอเขียน ดูเหมือนว่าจะไม่ได้ใช้คิว shared memory แบบชัดเจน แต่ใช้วิธีรอสั้น ๆ แล้วลองใหม่ ไม่แน่ใจว่าการใช้ SQLite จากหลายเครื่องเป็นไปไม่ได้โดยสิ้นเชิงหรือไม่ แต่แนวทางง่าย ๆ อย่างการแชร์ไฟล์ผ่าน NFS นั้นไม่แนะนำอย่างชัดเจน ดังนั้นการเข้าใจว่าเป็น เซิร์ฟเวอร์เดียว จึงเป็นการประมาณที่สมเหตุสมผล และสมัยนี้เครื่องเดียวก็รองรับสเกลได้ค่อนข้างมากแล้ว
busy_timeoutทำงาน แหล่งอ้างอิงคือ https://fractaledmind.github.io/2024/04/… และ https://kerkour.com/sqlite-for-servers รวมถึง https://www.sqlite.org/lang_transaction.html#immediate โดยดูรายละเอียดได้จากส่วนSQLITE_BUSYของลิงก์ที่สองและจากลิงก์ที่สามเป็นการอัปเกรดที่น่าประทับใจ ซึ่งมีการ整理รายละเอียดไว้ดีมากใน PR, issue และ Gist สงสัยว่าการเปลี่ยนแปลงนี้ทำให้เกิด ความแตกต่างด้านฟังก์ชัน อย่างการค้นหาในไซต์หรือไม่ และขนาดบนดิสก์ของไฟล์ SQLite ปัจจุบันเท่าไร
จำได้ว่าเคยมีบอกไว้ที่ไหนสักแห่งว่าไฟล์ SQLite มีขนาด 3.8GB