1 คะแนน โดย GN⁺ 2024-09-13 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้ SQLite บน Rails จะยังไม่เพียงพอสำหรับใช้งานในสภาพแวดล้อม production ด้วยค่าเริ่มต้นล้วน ๆ แต่เมื่อเพิ่มการตั้งค่าบางอย่างก็สามารถสร้างแอปพลิเคชันที่ มีประสิทธิภาพสูงและทนทานต่อการกู้คืน ได้
  • เมื่อมีโหลดการเขียนพร้อมกัน จะเกิด SQLITE_BUSY จาก write lock ระดับ global ของ SQLite และ flow การทำ write transaction ทั่วไปของ Rails ไม่ค่อยเข้ากับโหมด deferred ที่เป็นค่าเริ่มต้น
  • ตั้งแต่ sqlite3-ruby 1.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 ทันที
  • IMMEDIATE transaction จะพยายามขอ 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 ได้
  • หลังการเปลี่ยนแปลงนี้ การทดสอบโหลดแบบง่ายแทบไม่พบ 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 จะไม่โยน exception BUSY ทันที แต่จะรอเป็นจำนวนมิลลิวินาทีที่กำหนดและพยายามขอ 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_handler
    • busy_timeout คือ implementation หนึ่งของ busy_handler ที่ SQLite จัดให้
    • sqlite3-ruby มี binding สำหรับฟังก์ชัน C sqlite3_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-ruby main 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
  • การผสมผสานระหว่าง “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 ความคิดเห็น

 
GN⁺ 2024-09-13
ความคิดเห็นจาก 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

    • Rails 8 มีแผนจะใช้ฐานข้อมูลเป็นค่าเริ่มต้นสำหรับแคช, คิว และการ broadcast ผ่าน WebSocket: https://fly.io/ruby-dispatch/the-plan-for-rails-8/
  • นึกไม่ออกเลยว่าการเขียนบทความละเอียดขนาดนี้ต้องใช้เวลากี่วัน
    เป็นบทความที่มีประโยชน์สำหรับคนที่กำลังคิดเรื่อง การขยายสเกลเว็บแอปพลิเคชันด้วย SQLite ไม่ใช่แค่ Rails

  • คนที่ทำงานกับ SQLite ควรอ่านบทความนี้ ไม่ว่าจะใช้ภาษา หรือเฟรมเวิร์กอะไรก็ตาม
    ในฐานะคนที่เมื่อหลายปีก่อนต้องค้นหาเรื่องพวกนี้ส่วนใหญ่ด้วยตัวเอง ขอบคุณที่รวบรวมไว้ให้

  • กำลังสร้างระบบ analytics แบบ FOSS อยู่ และต้องการให้ติดตั้งง่าย จึงอยากส่งข้อมูล event ไปยัง ฐานข้อมูล SQLite แยกต่างหาก ที่แยกจากข้อมูลหลักของแอป
    แม้แต่เว็บไซต์ที่ค่อนข้างยุ่งในระดับปกติก็อาจมี event มากกว่า 1000 รายการต่อวินาที เลยกังวลเรื่องการขยายสเกล
    อยากรู้ว่าการรวบรวม event ไว้ในหน่วยความจำของเซิร์ฟเวอร์ แล้วทำ การเขียนแบบเป็นชุด ทุก 1 วินาที เป็นวิธีที่สมเหตุสมผลในการหลีกเลี่ยงข้อจำกัดเรื่องการเขียนหลายรายการของ SQLite หรือไม่ หรือมีไอเดียที่ดีกว่านี้ไหม

    • ผมว่าค่อนข้างสมเหตุสมผลและเป็นไอเดียที่ดี เคย implement วิธีคล้ายกันในหลายระบบ และโดยทั่วไปการประมวลผลแบบเป็นชุดช่วยลด overhead ต่อรายการได้
      พิสูจน์ด้วย benchmark ได้ง่าย และยังสามารถใส่รายการทั้งหมดในชุดเดียวกันไว้ใน transaction เดียวได้ด้วย
      ถ้าเป็นการประมวลผลแบบเป็นชุด โดยพื้นฐานแล้วจะมีเธรดหนึ่งที่ดึง batch มาแล้วทำการเขียนจริง จึงเข้ากันได้ดีกับ ข้อจำกัดของ SQLite ที่ให้เขียนพร้อมกันได้ 1 รายการ
      แต่ความซับซ้อนจะเพิ่มขึ้นเล็กน้อย ต้องตัดสินใจว่าจะทำอย่างไรถ้าการเขียนชุดเดียวไม่เสร็จภายใน 1 วินาที จะให้ขนาดคิวที่เก็บในหน่วยความจำไม่จำกัดหรือไม่ ถ้าไม่จำกัด จะมั่นใจได้ไหมว่าเมื่อเซิร์ฟเวอร์โหลดเกินจะไม่ตายเพราะ OOM ถ้าจำกัด จะยอมให้รายการสูญหายหรือไม่ ถ้าจะทิ้ง จะทิ้งรายการไหน และกำหนดขีดจำกัดคิวไว้เท่าไร
      คำถามเหล่านี้เป็นสิ่งที่ต้องเจอในแทบทุกระบบที่ต้องใช้คิว และจะมีประโยชน์ทั้งกับสถานการณ์ตอนนี้และในอนาคต
    • ไม่ใช่ว่า SQLite เขียนไม่ได้เลย เพียงแต่รองรับ write transaction หนึ่งรายการ ในแต่ละครั้งเท่านั้น
      ถ้าไม่มั่นใจในประสิทธิภาพด้าน concurrency ของ transaction ใน SQLite ก็ให้ serialize การเขียนทั้งหมดไว้ในเธรดหรือโปรเซสเฉพาะได้
    • ที่นี่ทำงานได้ดี และทำให้มุมมองต่อ SQLite เปลี่ยนไป Hipp ก็ร่วมทำงานด้วย และ SQLite สามารถทำประสิทธิภาพได้มหาศาล: https://use.expensify.com/blog/scaling-sqlite-to-4m-qps-on-a...
    • ข้อมูล analytics โดยทั่วไปเน้นการเขียน จึงแนะนำ ClickHouse
      ถ้าใช้ฟีเจอร์ async-insert[0] ของ ClickHouse ก็ไม่ต้องกังวลเรื่องการประมวลผล event แบบเป็นชุดฝั่งแอปพลิเคชัน
      ถ้ากำลังมองหาโซลูชันแบบ embedded ก็ใช้ chDB ที่อิงกับ ClickHouse ได้
      [0] https://clickhouse.com/blog/asynchronous-data-inserts-in-cli...
    • การเขียนแบบเป็นชุดน่าจะเป็นไอเดียที่ดี แต่แนวทางที่ดีที่สุดเมื่อทำเรื่องแบบนี้ด้วย SQLite คือใช้ WAL และมี writer ที่กำหนดไว้เพียงตัวเดียว
      จะมี worker สำหรับอ่านมากเท่าไรก็ได้ ส่วน writer ก็รับ input จากสิ่งอย่างคิว
      แค่ทำแบบนี้ ประสิทธิภาพก็มักจะน่าทึ่งจริง ๆ
  • ผมยังไม่ค่อยเข้าใจกระแสที่จะใช้ SQLite เป็นฐานข้อมูลแบ็กเอนด์สำหรับโปรดักชัน
    SQLite นั้นยอดเยี่ยมสำหรับฐานข้อมูลแอปฝั่งไคลเอนต์ขนาดเล็กที่ฝังตัวได้ เช่น สมุดรายชื่อในมือถือ แต่แม้แต่นักพัฒนาของมันเองก็ปฏิเสธมาโดยตลอดที่จะให้มันขยายเกินขอบเขตนั้น
    ตัวอย่างเช่น ไม่เพิ่ม native type ที่มีประโยชน์อย่าง date/time หรือ UUID เพราะจะทำให้โค้ดและขนาดของอ็อบเจกต์ที่ฝังมาด้วยใหญ่ขึ้น และผลคือมันติดอยู่กับสภาพที่ “ทุกอย่างเป็นสตริง”
    referential integrity เปิดใช้ได้ก็จริง แต่ตัวเลือก constraint ก็จำกัดมาก
    ผมไม่เข้าใจว่าทำไมถึงยังพยายามยัดมันเข้าไปในบทบาทที่ไม่เหมาะและไม่ได้รับการรองรับอย่างจริงจัง

    • ก่อนอื่น ผมไม่แน่ใจว่า Richard Hipp จะคิดเหมือนกันหรือไม่ว่า SQLite “ตั้งใจมาแต่แรก” สำหรับบทบาทแบบไหน
      เหตุผลนั้นเรียบง่ายมาก ใน access pattern ที่เน้นการอ่าน SQLite เร็วมหาศาล และเร็วพอที่จะทำให้โค้ดเข้าถึงฐานข้อมูลง่ายลงได้ เช่น หลายครั้ง N+1 query ก็ไม่ได้เป็นปัญหาจริง ๆ
      อีกทั้ง SQLite ยังตัดเลเยอร์หนึ่งออกจากสถาปัตยกรรมแบบ N-tier และลดองค์ประกอบที่อาจพังได้ลงตามนั้นด้วย ถ้าคุณเคยดูแล Postgres หรือ MySQL เองโดยตรง ก็จะรู้ว่ามีหลายอย่างที่ผิดพลาดได้จริง ๆ
      มันไม่ใช่ตัวเลือกที่สมบูรณ์แบบสำหรับทุกแอป หรือแม้แต่แอปจำนวนมาก แต่กระแสตอนนี้น่าจะเป็นการ ปรับสมดุล ต่อความคิดที่ผิดอย่างชัดเจนว่า SQLite เหมาะเฉพาะกับ “ฐานข้อมูลแอปไคลเอนต์แบบฝังตัวขนาดเล็ก” เท่านั้น
    • สุดท้ายก็จะตระหนักว่า “API server” แท้จริงแล้วเป็น DBMS มาตั้งแต่ต้น
      เมื่อมองแบบนั้น โครงสร้างที่มี DBMS อีกตัวอยู่ข้าง ๆ DBMS ที่จัดการข้อมูลชุดเดียวกันก็ดูค่อนข้างตลก
      ดังนั้นทิศทางจึงแยกเป็นให้ไคลเอนต์เชื่อมต่อกับ Postgres โดยตรง หรือไม่ก็ตัด Postgres ออกแล้วโฟกัสกับ DBMS ของตัวเองมากขึ้น
      ถ้าเลือกข้อ 2 SQLite ก็เป็นเอนจินที่สร้างทับได้สะดวก แม้ไม่สมบูรณ์แบบ แต่มันคือเครื่องมือที่เรามีตอนนี้
      การตระหนักรู้นี้เพิ่งแพร่กระจายในวงกว้างได้ไม่นาน จึงมีการทดลองจำนวนมากเพื่อดูว่าอะไรทำได้และอะไรทำไม่ได้
      เป็นวัฏจักรธรรมชาติของวงการคอมพิวติ้ง สิ่งเก่ากลับมาใหม่อีกครั้ง
      Postgres ในที่นี้จะอ่านแทนด้วย MySQL, MSSQL, Oracle หรือ DBMS อื่นก็ได้
    • UUID ก็ใช้เป็นสตริงหรือ BLOB ส่วนวันที่ก็ใช้เป็นสตริง หรือ timestamp แบบจำนวนเต็ม/ทศนิยมไม่ได้หรือ?
      การทำให้เรียบง่ายแบบนี้ให้ประโยชน์หลายด้าน ไม่ใช่แค่กับนักพัฒนา 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” ด้วย

    • การอภิปรายที่ละเอียดกว่านี้ดูได้ที่ https://github.com/sparklemotion/sqlite3-ruby/pull/528 และ https://github.com/digital-fabric/extralite/pull/46
      มีการยืนยันแล้วว่าถ้าปล่อย 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 ในสภาพแวดล้อมโปรดักชัน

    • ไม่ได้ต่างกันมากนัก แต่ในสถานการณ์ที่เน้นการอ่าน SQLite มีประสิทธิภาพดีกว่า Jet (MS Access) มาก
      ยิ่งไปกว่านั้น ตอนนี้คอมพิวเตอร์และดิสก์เร็วขึ้นมากเมื่อเทียบกับหลายสิบปีก่อนที่การใช้ Access แพร่หลายกว่า
      ถ้าเน้นการอ่าน ไม่ใช่แค่ SQLite แม้แต่ Jet ก็น่าจะไปถึงระดับคำขอหลายหมื่นครั้งต่อวินาทีได้ค่อนข้างง่าย
      แอปพลิเคชันส่วนใหญ่ไม่ได้มีผู้ใช้พร้อมกันถึงหลักแสน ดังนั้น SQLite อาจเหมาะมาก
      จุดแข็งอีกอย่างของ SQLite คือมีไคลเอนต์ในแทบทุกแพลตฟอร์มและภาษา
      การเก็บถาวร การสำรองข้อมูล และการพกพา ก็เป็นกรณีใช้งานที่เหมาะกับ SQLite เช่นกัน ผมเคยผลักดันอย่างหนักให้ใช้ SQLite แยกเป็นรายกล่องในโปรเจ็กต์ที่มีการป้อนข้อมูลจำกัดอยู่ในช่วงเวลาหนึ่ง และตอนนี้ก็ยังรู้สึกว่านั่นน่าจะเป็นทางเลือกที่ดีกว่า
      แทนที่จะสร้างสคีมาที่ซับซ้อนและฟังก์ชันส่งออก/เก็บถาวรแบบกำหนดเอง แค่คัดลอกไฟล์เดียวก็ใช้เป็นไฟล์เก็บถาวรหรือสำรองข้อมูลได้ และไม่ต้องคำนึงถึงการเปลี่ยนแปลงสคีมาที่เกิดขึ้นเมื่อเวลาผ่านไปอย่างลึกซึ้งนัก
      แล้วแต่สถานการณ์ แต่สำหรับหลายปัญหา นี่เป็นวิธีแก้ที่ค่อนข้างดี คล้ายกับที่ในแอปพลิเคชันส่วนใหญ่ PostgreSQL หรือ DBMS เชิงสัมพันธ์อื่น ๆ มักเป็นตัวเลือกที่ดีกว่า NoSQL ที่ขยายขนาดได้มากกว่า
      เรามีแนวโน้มจะออกแบบเกินความจำเป็น และตอนนี้ประสิทธิภาพด้านคอมพิวต์กับ I/O ก็กำลังเข้าใกล้ระดับที่ทำให้ความพยายามแบบนั้นสมเหตุสมผลน้อยลงเรื่อย ๆ
    • ถ้ามี อะไรสักอย่างแบบ MS Access สำหรับเว็บแอปก็คงดี ผมลองใช้ผลิตภัณฑ์สร้างเว็บไซต์มาหลายตัวแล้ว แต่ไม่มีอะไรใช้งานได้เป็นธรรมชาติเท่า Access
    • สงสัยว่าทำไมถึงเป็นแบบนั้น โดยเฉพาะถ้าเป็นแอปพลิเคชันที่เน้นการอ่าน แล้วกำลังกังวลเรื่องข้อจำกัดด้านการขยายขนาดแบบไหนกัน?
    • สำหรับ Pieter Levels วิธีนี้ใช้ได้ผล แน่นอนว่าถ้าจำนวนผู้ใช้เกินเกณฑ์บางอย่างก็จะเริ่มมีปัญหา
  • เป็นบทความที่มีประโยชน์มากและเขียนได้ดี
    สงสัยว่าทำไมเมธอด busy_timeout ค่าเริ่มต้นถึงมี การหน่วงแบบเอ็กซ์โปเนนเชียล ที่ลงโทษคิวรีเก่า
    เหตุผลที่ทำให้มันสมเหตุสมผลในฐานะค่าเริ่มต้นคืออะไร?