- ตลอด 1 ปีที่ผ่านมา ผู้เขียนพยายามทำความเข้าใจอย่างลึกซึ้งว่าจะทำให้แอปพลิเคชัน Rails ที่ใช้ SQLite ทำงานได้ทั้งมีประสิทธิภาพและเสถียรอย่างไร
- ในกระบวนการนี้ได้เรียนรู้บทเรียนหลายอย่าง และต้องการนำมาแบ่งปัน
- บทความนี้จะอธิบายสาเหตุของปัญหาและวิธีแก้ไข
ปัญหาของ SQLite และ Rails
- โดยพื้นฐานแล้ว แอปพลิเคชัน Rails ที่ใช้ SQLite ยังไม่พร้อมใช้งานได้ทันที
- แต่ด้วยการปรับแต่งและจูนเล็กน้อย ก็สามารถสร้างแอปพลิเคชันที่ทั้งเร็วและเสถียรได้
- ใน Rails 8 มีเป้าหมายให้การตั้งค่าเริ่มต้นเพียงอย่างเดียวก็พร้อมสำหรับใช้งาน production
เดโมแอปพลิเคชัน "Lorem News"
- จะใช้งานเดโมแอปชื่อ "Lorem News" เพื่ออธิบายปัญหาและแนวทางแก้
- แอปนี้เป็นโคลนของ Hacker News ที่ให้ผู้ใช้เขียนโพสต์และคอมเมนต์ได้
การทดสอบประสิทธิภาพ
- ทดสอบประสิทธิภาพด้วย CLI สำหรับโหลดเทสต์
oha และเส้นทาง benchmarking ภายในแอปพลิเคชัน
- วัดประสิทธิภาพทั้งจากคำขอเดี่ยวและคำขอพร้อมกันหลายรายการ
ปัญหาหลัก: ข้อยกเว้น SQLITE_BUSY
- SQLite ใช้ write lock เพื่ออนุญาตให้มีงานเขียนได้เพียงครั้งละหนึ่งงาน
- เมื่อมีหลาย connection พยายามแย่ง write lock พร้อมกัน จะเกิดข้อยกเว้น
SQLITE_BUSY
- เพื่อแก้ปัญหานี้ จำเป็นต้องใช้ immediate transaction
Immediate transaction
- โดยปกติ SQLite จะใช้โหมด deferred transaction
- หากใช้ immediate transaction ระบบจะพยายามขอ write lock ทันที และหากล้มเหลวก็สามารถ retry ได้
- สามารถใช้ gem
sqlite3-ruby เพื่อตั้งค่าโหมด transaction เริ่มต้นเป็น immediate mode ได้
การตั้งค่า timeout
- สามารถลดข้อยกเว้น
SQLITE_BUSY ได้ด้วยการตั้งค่า timeout ในไฟล์ database.yml
- ใช้การตั้งค่า
busy_timeout ของ SQLite เพื่อ retry การขอ write lock ได้
ปัญหา GVL (Global VM Lock)
- gem
sqlite3-ruby จะไม่ปล่อย GVL ระหว่างเรียกใช้โค้ด C ของ SQLite
- สิ่งนี้ทำให้ประสิทธิภาพด้าน concurrency ลดลง
- สามารถใช้
busy_handler เพื่อปล่อย GVL และปรับปรุงประสิทธิภาพได้
การ reimplement busy_timeout
- มีการ reimplement
busy_timeout เพื่อให้ทุก query ถูก retry ด้วยความถี่เท่ากัน
- วิธีนี้ช่วยไม่ให้ query ที่เก่ากว่าหมดเวลา timeout
การปรับปรุงประสิทธิภาพ
- เพื่อเพิ่มประสิทธิภาพ ควรใช้การตั้งค่าต่อไปนี้
- ใช้ immediate transaction
- ตั้งค่า timeout
- ใช้
busy_handler
- ใช้โหมด WAL (Write-Ahead Logging)
- แยก connection pool สำหรับอ่าน/เขียน
สรุปโดย GN⁺
- บทความนี้กล่าวถึงปัญหาด้านประสิทธิภาพของแอปพลิเคชัน Rails ที่ใช้ SQLite และแนวทางแก้ไข
- สามารถปรับปรุงประสิทธิภาพได้ด้วยวิธีต่าง ๆ เช่น immediate transaction, การตั้งค่า timeout, การปล่อย GVL, การใช้โหมด WAL และการแยก connection pool สำหรับอ่าน/เขียน
- บทความนี้มีประโยชน์มากสำหรับนักพัฒนาที่ใช้งาน SQLite และ Rails
- สำหรับโปรเจ็กต์อื่นที่มีความสามารถคล้ายกัน ผู้เขียนแนะนำ PostgreSQL และ MySQL
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
แนะนำโปรเจกต์ Litestack ของ Oldmoe
ขอบคุณสำหรับบทความที่เขียนอย่างละเอียด
แนะนำสำหรับคนที่ทำงานเกี่ยวกับ SQLite
คำถามเกี่ยวกับระบบ analytics แบบ FOSS
ปัญหา GVL ของ gem
sqlite3-rubysqlite3-rubyไม่ปล่อย GVL ระหว่างที่เรียก SQLiteextraliteจะปล่อย GVL ระหว่างที่บล็อกอยู่ และโดยทั่วไปเร็วกว่าโดยไม่มีปัญหาเรื่อง concurrencyการตั้งค่าสำหรับเว็บเซอร์วิสส่วนตัว
PRAGMA journal_mode = WALPRAGMA busy_timeout = 5000PRAGMA synchronous = NORMALPRAGMA cache_size = 1000000000PRAGMA foreign_keys = truePRAGMA temp_store = memoryBEGIN IMMEDIATEคำถามเกี่ยวกับ Django
ข้อสงสัยเกี่ยวกับค่าตั้งต้นของ
busy_timeoutbusy_timeoutแบบค่าตั้งต้นจึงมีการหน่วงเวลาที่เหมือนลงโทษ query ที่เก่ากว่าความเห็นเกี่ยวกับการใช้ SQLite กับ Rails
ขอบคุณสำหรับการแก้ปัญหาการรวมเข้ากับ Rails