วิธีและเหตุผลของ SQLite on Rails เพื่อประสิทธิภาพสูงสุด
(fractaledmind.github.io)- แม้ SQLite บน Rails จะยังไม่เพียงพอสำหรับใช้งานในสภาพแวดล้อม production ด้วยค่าเริ่มต้นล้วน ๆ แต่เมื่อเพิ่มการตั้งค่าบางอย่างก็สามารถสร้างแอปพลิเคชันที่ มีประสิทธิภาพสูงและทนทานต่อการกู้คืน ได้
- เมื่อมีโหลดการเขียนพร้อมกัน จะเกิด
SQLITE_BUSYจาก write lock ระดับ global ของ SQLite และ flow การทำ write transaction ทั่วไปของ Rails ไม่ค่อยเข้ากับโหมด deferred ที่เป็นค่าเริ่มต้น - ตั้งแต่
sqlite3-ruby1.6.9 สามารถเปลี่ยนโหมด transaction เริ่มต้นในdatabase.ymlเป็น IMMEDIATE ได้ ทำให้การรอและ retry เมื่อขอ write lock ไม่สำเร็จปลอดภัยขึ้น busy_timeoutจะยังจับ Ruby GVL ไว้ระหว่างรอ ทำให้ความขนานของ Puma แย่ลง ดังนั้นถ้าใช้ retry ทุก 1ms โดยปล่อย GVL ด้วยsleepในbusy_handlerจะช่วยลด latency แบบ long tail ได้- WAL mode, IMMEDIATE transaction, busy handler ที่ปล่อย GVL, ค่าเริ่มต้นของ SQLite ใน Rails 7.1 และการ แยก connection pool สำหรับอ่าน/เขียน แบบเลือกใช้ได้ คือแกนหลักของการปรับปรุงประสิทธิภาพ SQLite on Rails
ข้อจำกัดที่เห็นได้จากการตั้งค่า Rails + SQLite พื้นฐาน
- หากต้องการรันแอปพลิเคชัน Rails บน SQLite ให้มีประสิทธิภาพดีและทนทานต่อการกู้คืน การใช้เพียงค่าเริ่มต้นในปัจจุบันยังไม่พอ
- เป้าหมายคือทำให้ประสบการณ์เริ่มต้นของ SQLite ใน Rails 8 อยู่ในสถานะ พร้อมสำหรับสภาพแวดล้อม production
- แอปพลิเคชันเดโมคือ Lorem News
- เป็น clone พื้นฐานสไตล์ Hacker News มีผู้ใช้ โพสต์ และคอมเมนต์
- เนื้อหาประกอบด้วย Lorem Ipsum
- การทดสอบโหลดใช้ oha load testing CLI และ benchmarking routes ที่ฝังอยู่ในแอป
- เมื่อส่ง request แบบเรียงลำดับไปยัง endpoint
post#createเป็นเวลา 5 วินาที ค่า RPS จะคงที่และ request ทั้งหมดสำเร็จ - เมื่อส่ง request พร้อมกัน 4 รายการไปยัง endpoint เดียวกันเป็นเวลา 5 วินาที request บางส่วนจะคืนค่า 500 response
SQLITE_BUSY และ IMMEDIATE transaction
- ปัญหาแรกที่พบใน log คือ exception
SQLITE_BUSY - SQLite ใช้ write lock กับ database เพื่ออนุญาตให้มีงานเขียนได้ครั้งละหนึ่งรายการเท่านั้น
- ในแต่ละครั้งมี connection เดียวเท่านั้นที่ถือ write lock ได้
- หาก connection อื่นถือ lock อยู่ แล้ว connection ใหม่พยายามขอ write lock จะเกิด
SQLITE_BUSY
- เมื่อแอป Rails รับโหลดพร้อมกันมากขึ้น สัดส่วน request ที่ล้มเหลวด้วย
SQLITE_BUSYก็เพิ่มขึ้นด้วย - โหมด transaction เริ่มต้นของ SQLite คือ deferred และจะยังไม่ขอ lock จนกว่าจะเกิดงานเขียนจริง
- มีข้อได้เปรียบด้านประสิทธิภาพในสภาพแวดล้อมที่มี connection เดียว หรือมี transaction แบบอ่านอย่างเดียวจำนวนมาก
- แต่แอป Rails ใน production ใช้หลาย connection จากหลาย thread และ Rails จะครอบ query เขียนด้วย transaction จึงขัดกับค่าเริ่มต้นนี้
- หากขอ write lock ไม่สำเร็จกลาง transaction SQLite จะไม่สามารถ retry query นั้นได้อย่างปลอดภัยโดยไม่ทำลาย serializable isolation จึงโยน exception ทันที
IMMEDIATEtransaction จะพยายามขอ write lock ตั้งแต่ตอนเริ่ม transaction- SQLite สามารถนำ write query เข้า queue แล้วค่อยลองขอ lock อีกครั้งภายหลังได้
- สำหรับรูปแบบ write transaction ของ Rails วิธีนี้เหมาะกว่า deferred
- gem
sqlite3-rubyรองรับการตั้งค่าโหมด transaction เริ่มต้นตั้งแต่ 1.6.9- Rails ส่ง key ระดับบนสุดของ
database.ymlไปยังการ initialize database ของsqlite3-ruby - จึงสามารถตั้งค่าใน
database.ymlให้ transaction ของ SQLite ใน Rails ทำงานเป็น IMMEDIATE mode ได้
- Rails ส่ง key ระดับบนสุดของ
- หลังการเปลี่ยนแปลงนี้ การทดสอบโหลดแบบง่ายแทบไม่พบ error 500 แม้มีโหลดพร้อมกัน แต่เมื่อมี request พร้อมกัน 16 รายการ ก็ยังกลับมาพบ error บางส่วน
busy_timeout, GVL และ custom busy_handler
- คอขวดถัดไปคือปรากฏการณ์ที่ p99 latency เพิ่มขึ้นอย่างรวดเร็ว เมื่อจำนวน request พร้อมกันเข้าใกล้หรือเกินจำนวน Puma worker
- เวลาประมวลผล request จริงยังคงเสถียรแม้มีโหลดพร้อมกัน 3 เท่าของจำนวน Puma worker แต่เมื่อเริ่มมี request ที่ใช้เวลาประมาณ 5 วินาที ก็เกิด response 500 จาก
SQLITE_BUSYตามมาด้วย - เวลา 5 วินาทีนี้ตรงกับค่า timeout ใน
database.ymlและถูก map ไปเป็นการตั้งค่าbusy_timeoutของ SQLite busy_timeoutจะไม่โยน exceptionBUSYทันที แต่จะรอเป็นจำนวนมิลลิวินาทีที่กำหนดและพยายามขอ write lock ใหม่- SQLite จะพยายามขอ lock ใหม่ด้วยรูปแบบคล้าย exponential backoff
- จะโยน exception
BUSYก็ต่อเมื่อยังขอ lock ไม่ได้ภายใน timeout - เว็บแอปพลิเคชันสามารถเปิดหลาย connection ได้โดยไม่ต้องจัดลำดับการเขียนเอง แต่ปล่อยให้ SQLite จัดการ
- คอขวดเกิดจากการที่ SQLite ฝังอยู่ใน process ของ Ruby และ
sqlite3-rubyไม่ปล่อย Ruby GVL เมื่อเรียกใช้โค้ด C ของ SQLite- ขณะที่ Puma worker หนึ่งรอการคืนค่าจาก database query มันยังจับ GVL ไว้ต่อ
- ทำให้ Puma worker อื่นส่ง write query ไปยัง SQLite พร้อมกันได้ยากด้วยซ้ำ
- คุณสมบัติการเขียนแบบเรียงลำดับของ SQLite ทำให้การประมวลผล request ของ Rails มีลักษณะเป็นเส้นตรงมากขึ้น และลด throughput ลงอย่างมาก
- SQLite มี hook ระดับต่ำกว่า
busy_timeoutชื่อbusy_handlerbusy_timeoutคือ implementation หนึ่งของbusy_handlerที่ SQLite จัดให้sqlite3-rubyมี binding สำหรับฟังก์ชัน Csqlite3_busy_handlerจึงสามารถสร้าง Ruby callback ที่ถูกเรียกเมื่อ query เข้า queue ได้
- หากใช้
Kernel.sleepใน Ruby callback จะสามารถ ปล่อย GVL ระหว่างที่ query รอ retry write lock ได้ - วิธีนี้ช่วยปรับปรุง p99 latency อย่างมากภายใต้โหลดพร้อมกัน แต่ใน p99.99 latency ยังเหลือปรากฏการณ์ที่ request ที่ช้าที่สุดจะยิ่งช้าลงเมื่อโหลดพร้อมกันเพิ่มขึ้น
long tail ของ exponential backoff และการ retry ทุก 1ms
- logic
busy_timeoutของ SQLite ที่นำมา implement ใหม่ด้วย Ruby มีโครงสร้างที่ยิ่ง query รอนานก็ยิ่งเสียเปรียบ - ในการ retry ช่วงแรก เวลารอจะน้อย แต่ยิ่งจำนวนครั้งที่ callback ถูกเรียกเพิ่มขึ้น เวลารอก็จะมากขึ้น
- การรอครั้งแรกจะรอ 1ms
- การเรียกครั้งที่สิบจะรอ 50ms
- ตั้งแต่ครั้งที่ 12 เป็นต้นไปจะรอครั้งละ 100ms
- หากเวลารอสะสมเกิน timeout 5000ms จะเกิด exception
- หากมี write query ใหม่เข้ามาอย่างต่อเนื่อง query ใหม่สามารถ retry เพื่อขอ lock ได้บ่อยกว่าด้วยเวลารอที่สั้นกว่า
- ระหว่างที่ query เก่าซึ่งรอมาแล้วสามครั้งกำลังรอ 10ms query ใหม่สามารถผ่านการรอ 1ms, 2ms, 5ms และ retry ได้สามครั้ง
- backoff แบบเพิ่มขึ้นนี้ทำให้ query เก่ามีโอกาสสูงขึ้นที่จะไม่ได้ write lock และ timeout
- ทางแก้คือทำให้ทุก query retry ด้วยความถี่เท่ากัน ไม่ขึ้นกับอายุของ query
- การเปลี่ยนแปลงนี้ถูกรวมไว้ใน
sqlite3-rubymain branch แล้ว- ฟีเจอร์นี้ยังไม่รวมอยู่ใน tagged release ณ เวลาที่เขียนบทความ
- Ruby callback ปล่อย GVL ระหว่างรอด้วย
sleep - sleep เพียง 1ms เสมอ
- หลังการเปลี่ยนแปลงนี้ กราฟ p99.99 latency ใน benchmark จะแบนลง
- ยังมี jump อยู่เมื่อ concurrency เกินครึ่งหนึ่งของจำนวน Puma worker
- หลังจากนั้น long tail latency จะแบนอยู่ที่ระดับประมาณ 0.5 วินาที
WAL mode และการแยก connection pool สำหรับอ่าน/เขียน
- เงื่อนไข 4 ข้อที่จำเป็นต่อประสิทธิภาพของ SQLite on Rails คือ IMMEDIATE transaction, busy handler ที่ปล่อย GVL, การตั้งค่า SQLite ที่เหมาะสม และ WAL mode
- write-ahead log ช่วยให้ SQLite รองรับงานอ่านพร้อมกันหลายรายการได้
- rollback journal mode ที่เป็นค่าเริ่มต้นอนุญาตให้มี query ได้ครั้งละหนึ่งรายการเท่านั้น ไม่ว่าจะอ่านหรือเขียน
- WAL mode อนุญาตให้มี reader หลายตัวพร้อมกัน แต่ writer ยังมีได้ครั้งละหนึ่งตัวเท่านั้น
- ตั้งแต่ Rails 7.1 Rails ใช้ค่าเริ่มต้นที่ดีกว่าสำหรับ database SQLite
- การตั้งค่าเหล่านี้สำคัญต่อการทำให้ SQLite ทำงานได้ดีในบริบทของเว็บแอปพลิเคชัน
- รายละเอียดการตั้งค่าและเหตุผลอยู่ใน บล็อกโพสต์ แยกต่างหาก
- performance lever ข้อที่ห้าแบบเลือกใช้ได้คือการแยก read-only pool และ write-only pool
- SQLite WAL mode รองรับ read connection หลายรายการและ write connection หนึ่งรายการ
- หาก Active Record connection pool เต็มไปด้วย write connection งานอ่านอาจถูกบล็อกโดยไม่จำเป็น
- สามารถใช้ การรองรับ multiple databases ของ Rails เพื่อให้การตั้งค่า reader และ writer ชี้ไปยัง database SQLite เดียวกันได้
- ในความเป็นจริงไม่ได้เป็น database แยก แต่ชี้ไปยัง database เดี่ยวเดียวกัน
- ผลลัพธ์คือสามารถสร้าง connection pool และการตั้งค่า connection ที่แยกจากกันได้
- reader connection pool ประกอบด้วย connection แบบอ่านอย่างเดียว ส่วน writer connection pool มีเพียง connection เดียว
- ตั้งค่า Active Record model ให้เชื่อมต่อกับ connection pool ที่เหมาะสมตาม role
- ใช้ automatic role switching ของ Rails เพื่อให้ connection เริ่มต้นของทุก web request เป็น reader pool และสลับไป writer pool เฉพาะเมื่อต้องเขียน database
- เนื่องจากใช้ database เดียวกัน จึงไม่ต้องมี
delayเพื่อรับประกันread your own writes - patch เมธอด
transactionของ ActiveRecord adapter เพื่อให้ transaction เชื่อมต่อกับ writer database
- เนื่องจากใช้ database เดียวกัน จึงไม่ต้องมี
- การผสมผสานระหว่าง “deferred requests” กับ connection pool ที่แยกกันนี้ แสดงให้เห็นการปรับปรุงประสิทธิภาพตามค่า RPS แบบง่ายในการทดสอบ endpoint comment create
การปรับปรุงที่แพ็กเป็น gem
- ไม่จำเป็นต้อง implement การปรับปรุงทั้งหมดเองในแอป Rails
- ติดตั้ง
activerecord-enhancedsqlite3-adapterเพื่อใช้การปรับปรุงที่เกี่ยวข้องได้ - ฟีเจอร์ connection pool ที่แยกกันเป็นฟีเจอร์ทดลองที่ใหม่กว่า จึงต้องเพิ่มการตั้งค่าเพื่อ opt-in
- แนวทางนี้รวมเครื่องมือ เทคนิค และค่าเริ่มต้นสำหรับใช้งาน SQLite ในสภาพแวดล้อม production ของ Rails ให้รวดเร็วและยืดหยุ่น
- Rails เป็นเว็บแอปพลิเคชันเฟรมเวิร์กที่ทำงานร่วมกับ SQLite ได้ดี และระบบนิเวศของเครื่องมือกับ gem ที่เกี่ยวข้องกำลังเติบโต
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าคุณเป็นคนที่ตั้งใจจะใช้ SQLite + Rails โปรเจกต์ Litestack ของ Oldmoe (X/GitHub) ก็คุ้มค่ามากที่จะดู
Litestack เป็น Ruby gem ที่ใช้ประโยชน์จากความเป็นฐานข้อมูลแบบฝังตัวของ SQLite ในแอป Ruby และ Ruby on Rails เพื่อให้ SQL database, แคชที่รวดเร็ว, คิวงาน, message broker, เครื่องมือค้นหา full-text และแพลตฟอร์ม metrics อยู่ในแพ็กเกจเดียว
ตอนนี้ใช้อยู่ในโปรเจกต์และพอใจมาก ลิงก์คือ https://github.com/oldmoe/litestack
นึกไม่ออกเลยว่าการเขียนบทความละเอียดขนาดนี้ต้องใช้เวลากี่วัน
เป็นบทความที่มีประโยชน์สำหรับคนที่กำลังคิดเรื่อง การขยายสเกลเว็บแอปพลิเคชันด้วย SQLite ไม่ใช่แค่ Rails
คนที่ทำงานกับ SQLite ควรอ่านบทความนี้ ไม่ว่าจะใช้ภาษา หรือเฟรมเวิร์กอะไรก็ตาม
ในฐานะคนที่เมื่อหลายปีก่อนต้องค้นหาเรื่องพวกนี้ส่วนใหญ่ด้วยตัวเอง ขอบคุณที่รวบรวมไว้ให้
กำลังสร้างระบบ analytics แบบ FOSS อยู่ และต้องการให้ติดตั้งง่าย จึงอยากส่งข้อมูล event ไปยัง ฐานข้อมูล SQLite แยกต่างหาก ที่แยกจากข้อมูลหลักของแอป
แม้แต่เว็บไซต์ที่ค่อนข้างยุ่งในระดับปกติก็อาจมี event มากกว่า 1000 รายการต่อวินาที เลยกังวลเรื่องการขยายสเกล
อยากรู้ว่าการรวบรวม event ไว้ในหน่วยความจำของเซิร์ฟเวอร์ แล้วทำ การเขียนแบบเป็นชุด ทุก 1 วินาที เป็นวิธีที่สมเหตุสมผลในการหลีกเลี่ยงข้อจำกัดเรื่องการเขียนหลายรายการของ SQLite หรือไม่ หรือมีไอเดียที่ดีกว่านี้ไหม
พิสูจน์ด้วย benchmark ได้ง่าย และยังสามารถใส่รายการทั้งหมดในชุดเดียวกันไว้ใน transaction เดียวได้ด้วย
ถ้าเป็นการประมวลผลแบบเป็นชุด โดยพื้นฐานแล้วจะมีเธรดหนึ่งที่ดึง batch มาแล้วทำการเขียนจริง จึงเข้ากันได้ดีกับ ข้อจำกัดของ SQLite ที่ให้เขียนพร้อมกันได้ 1 รายการ
แต่ความซับซ้อนจะเพิ่มขึ้นเล็กน้อย ต้องตัดสินใจว่าจะทำอย่างไรถ้าการเขียนชุดเดียวไม่เสร็จภายใน 1 วินาที จะให้ขนาดคิวที่เก็บในหน่วยความจำไม่จำกัดหรือไม่ ถ้าไม่จำกัด จะมั่นใจได้ไหมว่าเมื่อเซิร์ฟเวอร์โหลดเกินจะไม่ตายเพราะ OOM ถ้าจำกัด จะยอมให้รายการสูญหายหรือไม่ ถ้าจะทิ้ง จะทิ้งรายการไหน และกำหนดขีดจำกัดคิวไว้เท่าไร
คำถามเหล่านี้เป็นสิ่งที่ต้องเจอในแทบทุกระบบที่ต้องใช้คิว และจะมีประโยชน์ทั้งกับสถานการณ์ตอนนี้และในอนาคต
ถ้าไม่มั่นใจในประสิทธิภาพด้าน concurrency ของ transaction ใน SQLite ก็ให้ serialize การเขียนทั้งหมดไว้ในเธรดหรือโปรเซสเฉพาะได้
ถ้าใช้ฟีเจอร์ async-insert[0] ของ ClickHouse ก็ไม่ต้องกังวลเรื่องการประมวลผล event แบบเป็นชุดฝั่งแอปพลิเคชัน
ถ้ากำลังมองหาโซลูชันแบบ embedded ก็ใช้ chDB ที่อิงกับ ClickHouse ได้
[0] https://clickhouse.com/blog/asynchronous-data-inserts-in-cli...
จะมี worker สำหรับอ่านมากเท่าไรก็ได้ ส่วน writer ก็รับ input จากสิ่งอย่างคิว
แค่ทำแบบนี้ ประสิทธิภาพก็มักจะน่าทึ่งจริง ๆ
ผมยังไม่ค่อยเข้าใจกระแสที่จะใช้ SQLite เป็นฐานข้อมูลแบ็กเอนด์สำหรับโปรดักชัน
SQLite นั้นยอดเยี่ยมสำหรับฐานข้อมูลแอปฝั่งไคลเอนต์ขนาดเล็กที่ฝังตัวได้ เช่น สมุดรายชื่อในมือถือ แต่แม้แต่นักพัฒนาของมันเองก็ปฏิเสธมาโดยตลอดที่จะให้มันขยายเกินขอบเขตนั้น
ตัวอย่างเช่น ไม่เพิ่ม native type ที่มีประโยชน์อย่าง date/time หรือ UUID เพราะจะทำให้โค้ดและขนาดของอ็อบเจกต์ที่ฝังมาด้วยใหญ่ขึ้น และผลคือมันติดอยู่กับสภาพที่ “ทุกอย่างเป็นสตริง”
referential integrity เปิดใช้ได้ก็จริง แต่ตัวเลือก constraint ก็จำกัดมาก
ผมไม่เข้าใจว่าทำไมถึงยังพยายามยัดมันเข้าไปในบทบาทที่ไม่เหมาะและไม่ได้รับการรองรับอย่างจริงจัง
เหตุผลนั้นเรียบง่ายมาก ใน access pattern ที่เน้นการอ่าน SQLite เร็วมหาศาล และเร็วพอที่จะทำให้โค้ดเข้าถึงฐานข้อมูลง่ายลงได้ เช่น หลายครั้ง N+1 query ก็ไม่ได้เป็นปัญหาจริง ๆ
อีกทั้ง SQLite ยังตัดเลเยอร์หนึ่งออกจากสถาปัตยกรรมแบบ N-tier และลดองค์ประกอบที่อาจพังได้ลงตามนั้นด้วย ถ้าคุณเคยดูแล Postgres หรือ MySQL เองโดยตรง ก็จะรู้ว่ามีหลายอย่างที่ผิดพลาดได้จริง ๆ
มันไม่ใช่ตัวเลือกที่สมบูรณ์แบบสำหรับทุกแอป หรือแม้แต่แอปจำนวนมาก แต่กระแสตอนนี้น่าจะเป็นการ ปรับสมดุล ต่อความคิดที่ผิดอย่างชัดเจนว่า SQLite เหมาะเฉพาะกับ “ฐานข้อมูลแอปไคลเอนต์แบบฝังตัวขนาดเล็ก” เท่านั้น
เมื่อมองแบบนั้น โครงสร้างที่มี DBMS อีกตัวอยู่ข้าง ๆ DBMS ที่จัดการข้อมูลชุดเดียวกันก็ดูค่อนข้างตลก
ดังนั้นทิศทางจึงแยกเป็นให้ไคลเอนต์เชื่อมต่อกับ Postgres โดยตรง หรือไม่ก็ตัด Postgres ออกแล้วโฟกัสกับ DBMS ของตัวเองมากขึ้น
ถ้าเลือกข้อ 2 SQLite ก็เป็นเอนจินที่สร้างทับได้สะดวก แม้ไม่สมบูรณ์แบบ แต่มันคือเครื่องมือที่เรามีตอนนี้
การตระหนักรู้นี้เพิ่งแพร่กระจายในวงกว้างได้ไม่นาน จึงมีการทดลองจำนวนมากเพื่อดูว่าอะไรทำได้และอะไรทำไม่ได้
เป็นวัฏจักรธรรมชาติของวงการคอมพิวติ้ง สิ่งเก่ากลับมาใหม่อีกครั้ง
Postgres ในที่นี้จะอ่านแทนด้วย MySQL, MSSQL, Oracle หรือ DBMS อื่นก็ได้
การทำให้เรียบง่ายแบบนี้ให้ประโยชน์หลายด้าน ไม่ใช่แค่กับนักพัฒนา SQLite หรือฮาร์ดแวร์สเปกต่ำ แต่กับนักพัฒนาแอปด้วย
เอกสารเรียบง่ายขึ้น learning curve สั้นลง พื้นที่ผิวของบั๊กและขนาดไบนารีก็ลดลง
ซอฟต์แวร์ยุคนี้มีแนวโน้มจะเพิ่มความเทอะทะและความซับซ้อนให้ทุกอย่าง ผมดีใจมากที่ยังมีบางโปรเจกต์อย่าง SQLite ที่ต่อต้านแนวโน้มนั้น
บทความดีมาก และสงสัยว่ามีเอกสารคล้ายกันสำหรับ Django ไหม
ArchiveBox ใช้ SQLite ผ่าน Django และเจอปัญหาแบบเดียวกับที่บทความอธิบายเกี่ยวกับ Rails อยู่ค่อนข้างบ่อย
ถ้ามีโซลูชันในเลเยอร์ SQLite ที่ไม่ต้อง serialize การเขียนทั้งหมดผ่านเส้นทางอื่นของแอปก็คงดี
gem
sqlite3-rubyไม่ปล่อย GVL ระหว่างเรียก SQLite ตามการออกแบบ ดูจากคอมเมนต์ใน issue ที่ลิงก์ไว้ https://github.com/sparklemotion/sqlite3-ruby/issues/287#iss... ดูเหมือนเคยสงสัยว่าต้นทุนในการ acquire lock กลับมาสูง แต่ไม่ได้พิสูจน์พอคิดถึง workaround ทั้งหมดนี้แล้วก็รู้สึกน่าสงสัยอยู่
ถ้าเป็นวัฒนธรรม extension ของ Python ผมคิดว่าน่าจะออกแบบกลับกัน เลยอยากรู้ว่าจริง ๆ ที่นั่นทำกันอย่างไร
นอกจากนี้ ใน issue ที่ลิงก์ยังมีคอมเมนต์ว่า “gem extralite เป็น SQLite client ทางเลือกที่ปล่อย GVL ระหว่าง blocking และมีเนื้อหาเกี่ยวกับ concurrency อยู่ที่ https://github.com/digital-fabric/extralite?tab=readme-ov-fi... โดยทั่วไปเร็วกว่า gem นี้มากและไม่มีปัญหา concurrency” ด้วย
มีการยืนยันแล้วว่าถ้าปล่อย GVL ง่าย ๆ ทุกครั้งที่
stepของ SQLite virtual machine จะทำให้ ประสิทธิภาพ single-thread แย่ลงมากการหาจุดกึ่งกลางระหว่างประสิทธิภาพ single-thread กับ multi-thread นั้นยุ่งยาก
ใน Rails เรารู้ว่าเป็น multi-thread เพราะมี connection pool แต่ gem ระดับล่างยังถูกใช้กันมากในไลบรารีและเครื่องมืออื่น ๆ ที่ทำงานในสภาพแวดล้อมแบบ single-thread ด้วย
ค่าปรับแต่งบางอย่างที่ผมคงไว้ในเว็บเซอร์วิสเล่น ๆ ส่วนตัวมีดังนี้
PRAGMA journal_mode = WAL;PRAGMA busy_timeout = 5000;PRAGMA synchronous = NORMAL;PRAGMA cache_size = 1000000000;PRAGMA foreign_keys = true;PRAGMA temp_store = memory;และใช้ transaction แบบ BEGIN IMMEDIATE
https://kerkour.com/sqlite-for-servers
cache_sizeกับmmap_sizeอย่างไรผมชอบทั้ง SQLite และ Rails แต่เรื่องนี้ดูคล้ายกับการใช้ MS Access ในสภาพแวดล้อมโปรดักชัน
ยิ่งไปกว่านั้น ตอนนี้คอมพิวเตอร์และดิสก์เร็วขึ้นมากเมื่อเทียบกับหลายสิบปีก่อนที่การใช้ Access แพร่หลายกว่า
ถ้าเน้นการอ่าน ไม่ใช่แค่ SQLite แม้แต่ Jet ก็น่าจะไปถึงระดับคำขอหลายหมื่นครั้งต่อวินาทีได้ค่อนข้างง่าย
แอปพลิเคชันส่วนใหญ่ไม่ได้มีผู้ใช้พร้อมกันถึงหลักแสน ดังนั้น SQLite อาจเหมาะมาก
จุดแข็งอีกอย่างของ SQLite คือมีไคลเอนต์ในแทบทุกแพลตฟอร์มและภาษา
การเก็บถาวร การสำรองข้อมูล และการพกพา ก็เป็นกรณีใช้งานที่เหมาะกับ SQLite เช่นกัน ผมเคยผลักดันอย่างหนักให้ใช้ SQLite แยกเป็นรายกล่องในโปรเจ็กต์ที่มีการป้อนข้อมูลจำกัดอยู่ในช่วงเวลาหนึ่ง และตอนนี้ก็ยังรู้สึกว่านั่นน่าจะเป็นทางเลือกที่ดีกว่า
แทนที่จะสร้างสคีมาที่ซับซ้อนและฟังก์ชันส่งออก/เก็บถาวรแบบกำหนดเอง แค่คัดลอกไฟล์เดียวก็ใช้เป็นไฟล์เก็บถาวรหรือสำรองข้อมูลได้ และไม่ต้องคำนึงถึงการเปลี่ยนแปลงสคีมาที่เกิดขึ้นเมื่อเวลาผ่านไปอย่างลึกซึ้งนัก
แล้วแต่สถานการณ์ แต่สำหรับหลายปัญหา นี่เป็นวิธีแก้ที่ค่อนข้างดี คล้ายกับที่ในแอปพลิเคชันส่วนใหญ่ PostgreSQL หรือ DBMS เชิงสัมพันธ์อื่น ๆ มักเป็นตัวเลือกที่ดีกว่า NoSQL ที่ขยายขนาดได้มากกว่า
เรามีแนวโน้มจะออกแบบเกินความจำเป็น และตอนนี้ประสิทธิภาพด้านคอมพิวต์กับ I/O ก็กำลังเข้าใกล้ระดับที่ทำให้ความพยายามแบบนั้นสมเหตุสมผลน้อยลงเรื่อย ๆ
เป็นบทความที่มีประโยชน์มากและเขียนได้ดี
สงสัยว่าทำไมเมธอด
busy_timeoutค่าเริ่มต้นถึงมี การหน่วงแบบเอ็กซ์โปเนนเชียล ที่ลงโทษคิวรีเก่าเหตุผลที่ทำให้มันสมเหตุสมผลในฐานะค่าเริ่มต้นคืออะไร?