- OpenRun เป็นแพลตฟอร์มสำหรับ deploy เว็บแอปที่ใช้เป็นเครื่องมือภายใน โดยเก็บไฟล์สแตติก·โค้ดแอป·ไฟล์ตั้งค่าไว้ใน SQLite แทนระบบไฟล์ เพื่อจัดการสถานะการ deploy โดยมีฐานข้อมูลเป็นศูนย์กลาง
- จุดประสงค์หลักคือป้องกันไม่ให้มีการเสิร์ฟหน้าเว็บที่พังระหว่างเปลี่ยนเวอร์ชัน ด้วยการจัดการการอัปเดตแอปที่มีหลายไฟล์เปลี่ยนพร้อมกันเป็น transaction เดียว
- ใช้ SHA256 hash ก่อนการบีบอัดเป็น primary key เพื่อลดการเก็บไฟล์ซ้ำระหว่างเวอร์ชันของแอป และระหว่างแอป staging·preview·production
- วิธีเก็บด้วย SQLite ทำให้ rollback, backup, การเก็บ hash สำหรับ ETag และการเก็บแบบ Brotli compression ทำได้ง่ายขึ้น และหากจำเป็นก็สามารถรองรับข้อมูล GZip หรือแบบไม่บีบอัดร่วมกันได้ด้วยการเพิ่มคอลัมน์
- ปัจจุบันทำงานบน single node และเมื่อรองรับ multi-node มีแผนใช้ shared Postgres ร่วมกับ local SQLite file cache เพื่อลด latency
วิธีเก็บไฟล์ของ OpenRun
- OpenRun เป็นแพลตฟอร์ม deploy แบบโอเพนซอร์สสำหรับเครื่องมือภายในที่เน้น code-first และ deploy เว็บแอปด้วยแนวทาง GitOps ไปยัง single node หรือ Kubernetes cluster
- แทนที่จะวางคอนเทนต์สแตติกไว้ในระบบไฟล์เหมือนเว็บเซิร์ฟเวอร์ทั่วไป OpenRun เก็บข้อมูลของแอป เช่น ไฟล์สแตติก·โค้ดแอป·ไฟล์ตั้งค่า ไว้ใน SQLite
- เนื่องจาก metadata ของแอปถูกสร้างขึ้นแบบไดนามิก การเก็บไว้ในฐานข้อมูลจึงเป็นธรรมชาติ และถ้าจัดการไฟล์ในชั้นจัดเก็บเดียวกันด้วย ก็จะจัดการสถานะการ deploy ร่วมกันได้ง่าย
- เมื่อสร้างและอัปเดตแอป ไฟล์จะถูกอัปโหลดจาก GitHub หรือดิสก์ในเครื่องเข้าสู่ฐานข้อมูล SQLite
- ใช้ระบบไฟล์ในเครื่องเฉพาะใน development mode เท่านั้น
เหตุผลที่เลือก SQLite
- ข้อดีที่ใหญ่ที่สุดคือ transactional updates
- สามารถรวมการเปลี่ยนแปลงหลายไฟล์ไว้ใน transaction เดียวเพื่อประมวลผลได้
- ด้วย isolation จึงไม่เสิร์ฟเว็บแอปที่พังระหว่างการอัปเดต
- หากเกิดข้อผิดพลาดในการ deploy สามารถ rollback ในระดับ transaction ของฐานข้อมูลได้
- แม้มีหลายแอปถูกอัปเดตพร้อมกัน ก็สามารถย้อนกลับได้ในครั้งเดียว
- ง่ายกว่าวิธีค้นหาและจัดการไฟล์ที่เปลี่ยนไปในระบบไฟล์
- OpenRun ทำ version management สำหรับทุกการอัปเดตโดยอัตโนมัติ และข้อมูลไฟล์ถูกเก็บในตารางตาม schema ต่อไปนี้
CREATE TABLE files (sha text, compression_type text, content blob, create_time datetime, PRIMARY KEY(sha));
- เนื่องจากใช้ SHA256 hash ของคอนเทนต์ก่อนการบีบอัดเป็น primary key เนื้อหาไฟล์เดียวกันจึงถูกเก็บเพียงครั้งเดียวข้ามหลายเวอร์ชัน
- แต่ละ production app มี staging app และอาจมี preview apps หลายตัว จึงอาจเกิดไฟล์ซ้ำได้
- การจัดเก็บบน SQLite ช่วยไม่ให้เก็บไฟล์ที่มีคอนเทนต์เดียวกันซ้ำแม้ข้ามแอป
Backup, caching และการบีบอัด
- สามารถ backup สถานะของระบบทั้งหมด metadata และไฟล์ได้ด้วยเครื่องมือ backup ของ SQLite เช่น Litestream
- หากเก็บ content SHA ที่จำเป็นสำหรับ header ETag เพื่อ browser caching ไว้ครั้งเดียวตอนอัปโหลดไฟล์ ก็ไม่จำเป็นต้องคำนวณซ้ำในภายหลัง
- เนื้อหาไฟล์ถูกเก็บในตาราง SQLite ในรูปแบบบีบอัดด้วย Brotli
- ในวิธีที่ใช้ฐานข้อมูล สามารถเพิ่มคอลัมน์ในตาราง
files เพื่อเก็บข้อมูลที่บีบอัดด้วย GZip หรือข้อมูลแบบไม่บีบอัดได้ด้วย
ประสิทธิภาพและแผน multi-node
- วิธีใช้ฐานข้อมูล SQLite ใน OpenRun ให้ประสิทธิภาพที่ดี
- ไม่มี implementation แบบเทียบเท่าที่ใช้ระบบไฟล์ จึงไม่ได้ทำ benchmark เปรียบเทียบโดยตรง
- ตาม benchmark ของทีม SQLite ใน workload บางประเภท SQLite อาจให้ประสิทธิภาพดีกว่าการใช้ระบบไฟล์โดยตรง
- ปัจจุบัน OpenRun ทำงานบน single node
- หากในอนาคตเพิ่มการรองรับ multi-node มีแผนใช้ฐานข้อมูล Postgres แบบ shared สำหรับเก็บ metadata และข้อมูลไฟล์ แทน local SQLite
- วิธีนี้อาจเกิดปัญหา latency ได้
- เพื่อหลีกเลี่ยง latency ในการเข้าถึง Postgres มีแผนใช้ฐานข้อมูล SQLite ในเครื่องเป็น file cache
ทำไมวิธีใช้ระบบไฟล์จึงพบได้บ่อยกว่า
- เหตุผลหนึ่งที่เว็บเซิร์ฟเวอร์ส่วนใหญ่ใช้ระบบไฟล์คือ ความสะดวก
- สามารถคัดลอกและอัปเดตไฟล์ด้วยเครื่องมือระบบไฟล์ที่มีอยู่แล้ว เช่น rsync, tar
- อีกเหตุผลคือภูมิหลังทางประวัติศาสตร์
- ระบบไฟล์ถูกใช้งานมาตั้งแต่ก่อนจะมีฐานข้อมูลเชิงสัมพันธ์แบบ in-process ที่ดี
- หากจะใช้ฐานข้อมูลเป็นที่เก็บไฟล์ จำเป็นต้องมี API interface สำหรับอัปโหลดไฟล์ และไม่ใช่วิธีที่ทำได้เสมอไป
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมเคยทดลองไอเดียนี้เมื่อหลายปีก่อน และบางส่วนได้รับแรงบันดาลใจจากบทความ “35% Faster Than The Filesystem”: https://www.sqlite.org/fasterthanfs.html
บันทึกในตอนนั้นอยู่ที่นี่: https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sq...
ผมทำ https://datasette.io/plugins/datasette-media เป็นปลั๊กอินสำหรับเสิร์ฟไฟล์สแตติกจาก SQLite ใน Datasette และมันก็ทำงานได้ดี แต่พูดตามตรง หลังจากทำเสร็จแล้วก็ไม่ได้ใช้มากนัก
แนวคิดที่เกี่ยวข้องอีกอย่างคือการเสิร์ฟ tile แผนที่จาก SQLite และ https://datasette.io/plugins/datasette-tiles ก็ทำหน้าที่นั้นอยู่ ปรากฏว่ารูปแบบ MBTiles แท้จริงแล้วคือฐานข้อมูล SQLite ที่เต็มไปด้วยไฟล์ PNG
ถ้าจะทดลองใช้ SQLite สำหรับเสิร์ฟไฟล์ เครื่องมือ CLI “sqlite-utils insert-files” อาจมีประโยชน์ในการตั้งค่าฐานข้อมูลเริ่มต้น: https://sqlite-utils.datasette.io/en/stable/cli.html#inserti...
แฮชของเนื้อหาต้องสร้างเพียงครั้งเดียวตอนอัปโหลดไฟล์ และไม่จำเป็นต้องสร้างใหม่ทุกครั้งที่เว็บเซิร์ฟเวอร์รีสตาร์ต หรือมีขั้นตอน build เพื่อเปลี่ยนชื่อไฟล์จริง แม้จะนำไปใช้กับไฟล์ในระบบไฟล์แบบไดนามิกได้เช่นกัน (ดูการทำงานของ embedFS ใน https://github.com/benbjohnson/hashfs) แต่ฐานข้อมูลทำให้เรื่องนี้ง่ายขึ้นเล็กน้อย
requests-cache ถ้าจำไม่ผิดจะแคช request ใน SQLite ด้วย
(date, URI): https://github.com/requests-cache/requests-cache/blob/main/r...ค้นหา pyfilesystem SQLite: https://www.google.com/search?q=pyfilesystem+sqlite
ค้นหา sendfile mmap SQLite: https://www.google.com/search?q=sendfile+mmap+sqlite
https://github.com/adamobeng/wddbfs เป็น “ผู้ให้บริการ webdavfs ที่อ่านเนื้อหาฐานข้อมูล sqlite ได้”
น่าจะมีวิธีดี ๆ ในการสร้างระบบไฟล์บน SQLite โดยวางสิทธิ์ไฟล์ของ Unix และสิทธิ์ของ xattrs (แอตทริบิวต์ไฟล์แบบขยาย) ไว้ด้านบน
SQLite จะเร็วหรือสะดวกกว่าเช่น ngx_http_memcached_module.c ไหม? สงสัยด้วยว่า SQLite มี ACL ระดับเซลล์หรือไม่
เวลาอ่านไฟล์สแตติก ทุก request ต้องเปิดไฟล์ อ่าน แล้วปิดไฟล์ ทำให้ถึงแม้ชั้นระบบไฟล์จะแคชเนื้อหาไฟล์ไว้แล้ว ก็ยังเกิด context switch มากกว่า ถ้าอยากให้เร็วขึ้น วิธีที่เหมาะสมคือเพิ่ม caching frontend ไม่ใช่เปลี่ยนทุกอย่างให้เป็นฐานข้อมูล ซึ่งเร็วกว่า SQLite และดูแลรักษารวมถึงแก้ปัญหาได้ง่ายกว่าด้วย
รวมถึงระบบไฟล์ที่ทำงานทั้งหมดใน user space ด้วย ส่วน FUSE ไม่นับ เพราะการเรียกต้องผ่าน kernel
คำกล่าวที่ว่า “การอัปเดตแบบทรานแซกชัน” เป็นประโยชน์หลักนั้นมีข้อจำกัด ไม่ว่าเซิร์ฟเวอร์จะใช้ SQLite หรือระบบไฟล์ สิ่งนั้นเพียงอย่างเดียวก็ไม่สามารถป้องกันเว็บแอปที่พังระหว่างการอัปเดตได้
แต่ละหน้าในเบราว์เซอร์เป็น ต้นไม้ ของรีซอร์สที่ดึงมาด้วยคำขอ HTTP แยกกัน จึงไม่ใช่เป้าหมายของระบบทรานแซกชัน/การอัปเดตแบบอะตอมิกฝั่งเซิร์ฟเวอร์ ต่อให้ฝั่งเซิร์ฟเวอร์เปลี่ยนรีซอร์สทั้งหมดแบบทรานแซกชัน เบราว์เซอร์ก็ยังอาจเห็นชุดผสมระหว่างรีซอร์สเก่ากับรีซอร์สใหม่ได้
วิธีแก้โดยทั่วไปคือให้รีซอร์สย่อยทั้งหมดของหน้า (บันเดิล JavaScript, สไตล์ชีต, มีเดีย ฯลฯ) ใช้ชื่อ (URL) ที่มีแฮชของเนื้อหาหรือเวอร์ชันอยู่ ถ้าเอกสาร HTML รากโหลดเวอร์ชัน X รีซอร์สย่อยทั้งหมดก็ต้องโหลดเวอร์ชัน X ที่สอดคล้องกันด้วย
นอกจากนี้ เวลาจะอัปเดตจาก X เป็น Y หลังจากเริ่มเสิร์ฟหน้า Y แล้ว ก็ยังต้องให้บริการรีซอร์สย่อยของ X ต่อไปอีกระยะหนึ่ง หากไม่เก็บไว้จนกว่าจะแน่ใจอย่างสมเหตุสมผลว่าไม่มีเบราว์เซอร์ใดยังโหลดหน้า X อยู่ หน้า X ก็อาจพังได้
ดังนั้นถ้าต้องการเอา HTML รากกับรีซอร์สย่อยใส่ไว้ในบันเดิลเดียวที่ถูกสลับแบบอะตอมิก กลับจะไม่ดี เพราะรีซอร์สย่อยรุ่นก่อนอาจยังถูกอ้างอิงอยู่แต่ถูกลบทิ้งไปแล้ว
ในบางกรณี อาจต้องการจัดการเวอร์ชันของรีซอร์สย่อยบางอย่าง เช่น ไฟล์มีเดีย แยกจากเอกสาร HTML หากต้องการอัปเดตโดยไม่ทำให้แคชขององค์ประกอบโครงสร้างแอปอย่างก้อน JavaScript หรือสไตล์ชีตทั้งหมดเป็นโมฆะ ระบบ build ของหน้าก็อาจต้องคำนึงถึงเรื่องนี้ด้วย
ตอนที่บริษัทใหญ่แห่งหนึ่งทดลองเรื่องนี้ (ตอนนั้นเห็นเว็บส่วนใหญ่พอสมควร) ผู้ใช้ส่วนใหญ่ (มากกว่า 80%) อยู่ในเว็บแอปประมาณ 2~3 วัน มีโอกาสสูงที่จะเอนเอียงเพราะมีคนเปิดแท็บทิ้งไว้ตลอดสุดสัปดาห์
จุด 95% อยู่ที่ประมาณ 2 สัปดาห์ และ 100% อยู่ที่ประมาณ 600 วัน กล่าวคือมีผู้ใช้ที่เปิดแท็บทิ้งไว้เกือบ 2 ปี
ถ้าตั้งเป้า 100% ก็ต้องรอนานทีเดียว ตัวเลขทั้งหมดนี้อาศัยความทรงจำ และตอนนี้ก็ไม่ได้ทำงานที่บริษัทนั้นแล้ว
สถานการณ์ที่ผู้ใช้อยู่ในหน้าเดิมนาน ๆ แล้วได้รับลิงก์เสีย เป็นปัญหาที่ใกล้กับฝั่ง SPA มากกว่า
โดยรวมเห็นด้วย แต่สิ่งที่การอัปเดตแบบทรานแซกชันช่วยป้องกันได้เป็นเพียงปัญหาเกี่ยวกับการอัปเดตบางประเภทเท่านั้น ประสบการณ์ที่พังยังอาจเกิดจากปัญหาอื่นในระดับแอปได้
การเสิร์ฟเวอร์ชันเก่าของเนื้อหาสแตติกที่อ้างอิงด้วยแฮชของเนื้อหาต่อไปนั้นทำได้ แต่ใน Clace ยังไม่ได้ implement ไว้ในปัจจุบัน
เคล็ดลับสำคัญคืออัปโหลดการเปลี่ยนแปลงที่ไม่ใช่ HTML ก่อนการเปลี่ยนแปลง HTML เพื่อไม่ให้มีการอ้างอิงไฟล์ก่อนที่ไฟล์นั้นจะมีอยู่ ถ้าอยากทำให้แอปซับซ้อนที่สุด ก็อาจใช้อัลกอริทึม depth-first search กับการอัปโหลดได้ แต่ถ้าให้ความสำคัญกับสุขภาพจิต การลดรูปปัญหาแล้วเลือกอัปโหลด asset ก่อนในแอปน่าจะดีกว่า
ตอนทำงานที่บริษัทพัฒนาเกมเล็ก ๆ ในปี 2011/2012 ผมแนะนำให้ย้าย asset ที่เล็กกว่า 100KB ทั้งหมดไปไว้ใน DB sqlite3 และสร้าง “ไฟล์ pak” แล้วเก็บ offset ของไฟล์เหล่านั้นไว้ใน DB sqlite3
การตัดสินใจนี้ได้รับอิทธิพลจากการนำเสนอ postmortem ของ Richard Hipp ที่เขากล่าวว่าย้อนกลับไปแล้วน่าจะดีกว่าถ้าปฏิบัติกับ BLOB เหมือน inode โดยวางไว้ที่ offset ถัดไปในฐานข้อมูล และ append BLOB ต่อท้ายไฟล์
การโหลด asset เร็วมาก ๆ เพราะเป็นเกมมือถือ จึงมี asset ที่ไม่ได้อยู่ใน DB เพียงส่วนน้อยมาก การได้เห็นภายหลังว่าคนอื่น ๆ นำแนวทางนี้ไปใช้มากขึ้นก็เป็นเรื่องน่าสนใจ
ข้อดีอีกอย่างที่มักถูกมองข้ามคือสามารถแนบ เมทาดาทา ไว้ข้างเนื้อหาได้แทบไม่จำกัด ทำให้ค้นหาไฟล์ที่ “คล้ายกัน” ได้ด้วย query ฐานข้อมูล
เราใส่เมทาดาทาลงใน DB เป็นจำนวนมาก และเท่าที่จำได้ไฟล์ pak สุดท้ายมีขนาด 200MB ส่วนฐานข้อมูลประมาณ 20MB ขอย้ำอีกครั้งว่านี่เป็นเกมมือถือ
สิ่งที่แย่ที่สุดฝั่งไคลเอนต์คือ internal join ซ้อนกันสองชั้นตัวหนึ่งที่ลดไม่ได้เพราะความซับซ้อนฝั่งเซิร์ฟเวอร์ น่าหงุดหงิดที่เราไม่ได้ implement เซิร์ฟเวอร์เอง และฝ่ายที่ทำงานด้วยก็พัฒนาซอฟต์แวร์ได้แย่มาก ถึงขั้นเปลี่ยนสเปกแบ็กเอนด์ทั้งหมดโดยไม่แจ้ง ทำให้ build พังแบบกะทันหันอยู่บ่อย ๆ
สำหรับ replay ของเกมก็ใช้ฐานข้อมูล sqlite3 แยกต่างหาก และหลังจบแมตช์สามารถเล่นเกมทั้งเกมซ้ำเพื่อดูว่าคู่แข่งแต่ละคนทำอะไรบ้างได้ วิธีนี้ดีมากสำหรับการทดสอบอัตโนมัติด้วย
ในระบบควบคุมการเปลี่ยนแปลง lix ก็ลงเอยด้วยการใส่ไฟล์ไว้ใน SQLite แทนที่จะจัดการระบบไฟล์กับ git บทความนี้พูดถึงปัญหาที่เราเจอ: https://opral.substack.com/i/150054233/breaking-git-compatib...
ปัญหาอย่าง file locking และ concurrency นั้น SQLite ช่วยแก้ให้ได้
เมื่อใช้ SQLite ก็สามารถ query ไฟล์ด้วย SQL แทน API ระบบไฟล์ที่ต่างกันตามแพลตฟอร์มได้
SQL query สามารถเขียนแบบ type-safe ได้ด้วย Kysely https://kysely.dev/ โดยไม่ต้องใช้ ORM
เพียงแต่ต้องระวังว่า SQLite database จะไม่เล็กลงถ้าไม่ vacuum โดยพื้นฐานแล้วมันคือการคัดลอกข้อมูลไปยังไฟล์แยกแล้วลบต้นฉบับ
เป็นงานที่ต้องทำเองในจังหวะที่สมเหตุสมผลภายในแอปพลิเคชัน ดังนั้นเวลานำไปใช้แบบเขียนและลบข้อมูลไบนารี ต้องระวังการใช้พื้นที่ดิสก์
น่าสนใจที่ CMS ตัวสร้าง static site ที่ผมทำทำงานตรงกันข้ามกับวิธีในบทความนี้พอดี
ระหว่างพัฒนา/อัปเดตเว็บไซต์ หน้าและบทความทั้งหมดเป็น entry ในฐานข้อมูล SQLite และถูกจัดการผ่านเว็บอินเทอร์เฟซที่แสดงเวอร์ชันของเว็บไซต์ที่แก้ไขได้
จากนั้นจึง dump เว็บไซต์ออกเป็นหน้า static ลงระบบไฟล์เพื่อ deploy โดยตรง หรือดาวน์โหลดเป็น zip แล้วอัปโหลดไป deploy ที่อื่น รวมถึงบริการโฮสติ้งแบบ static ล้วน
ตาม “Appropriate Uses For SQLite” https://www.sqlite.org/whentouse.html ของ SQLite ทราฟฟิกเว็บที่ SQLite รองรับได้ขึ้นอยู่กับว่าไซต์ใช้งานฐานข้อมูลหนักแค่ไหน
โดยทั่วไป ไซต์ที่มี 100K hits ต่อวันหรือน้อยกว่านั้นควรทำงานได้ดีด้วย SQLite ตัวเลข 100K/วันเป็นค่าประเมินแบบระมัดระวัง ไม่ใช่เพดานสูงสุดแบบตายตัว SQLite เคยมีกรณีที่รองรับทราฟฟิกมากกว่านั้น 10 เท่าด้วย
เว็บไซต์ SQLite (https://www.sqlite.org/) เองก็ใช้ SQLite แน่นอน และเมื่อปี 2015 รองรับคำขอ HTTP ประมาณ 400K~500K ครั้งต่อวัน โดยในนั้น 15~20% เป็นหน้าแบบไดนามิกที่แตะฐานข้อมูล คอนเทนต์ไดนามิกใช้คำสั่ง SQL ประมาณ 200 คำสั่งต่อเว็บเพจ
โครงสร้างนี้รันอยู่บน VM เดียวที่แชร์เครื่องเซิร์ฟเวอร์จริงกับ VM อื่นอีก 23 เครื่อง แต่ก็ยังรักษา load average ให้อยู่ต่ำกว่า 0.1 ได้เป็นส่วนใหญ่ อ้างอิง: https://news.ycombinator.com/item?id=33975635
ถ้าเป็น workload ที่เน้นอ่านเป็นหลักอย่างการเสิร์ฟไฟล์ static SQLite น่าจะรองรับได้มากกว่านั้นมาก หากตั้งค่า header สำหรับแคชคอนเทนต์ไว้ เบราว์เซอร์ก็จะแคชคอนเทนต์ ทำให้เซิร์ฟเวอร์ต้องรับคำขอเฉพาะจากไคลเอนต์ใหม่เท่านั้น
ใน use case ส่วนใหญ่ SQLite ไม่น่าจะเป็นคอขวด
ไอเดียที่จะเสิร์ฟคอนเทนต์ static ด้วย SQLite โดยอ้างอิงแค่หน้า “35% Faster Than The Filesystem” ปี 2017 นั้น พูดให้ดีหน่อยก็ดู ยังไม่สุกงอม
เว็บเซิร์ฟเวอร์สมัยใหม่อย่าง Nginx ใช้กลยุทธ์ที่เหมาะที่สุดสำหรับการจัดการไฟล์ static ตั้งแต่ sendfile ไปจนถึงงาน io_uring และ splice และทำงานอยู่ใน thread pool ที่ออกแบบมาดีบนพื้นฐานที่เหมาะกับความต้องการ ไม่ว่าจะเป็น epoll, kqueue หรือ eventport
ในทางกลับกัน สิ่งที่ดีที่สุดที่ SQLite ให้ได้โดยพื้นฐานก็ประมาณการรองรับ memory-mapped I/O (https://www.sqlite.org/mmap.html)
วิธีนี้อาจเหมาะกับบริการแบบไคลเอนต์เดียว เช่น เว็บแอปที่โฮสต์ในเครื่องเดียวกัน (ดู https://github.com/electron/asar ด้วย) แต่สำหรับเว็บไซต์ขนาดใหญ่ ก็เหมือนกับที่คอมเมนต์อื่น ๆ ว่า คือพยายามแก้ปัญหาที่ไม่ได้มีอยู่จริง
ผมทำงานคำนวณเชิงวิทยาศาสตร์ประสิทธิภาพสูงอยู่มาก และโดยเฉพาะเมื่อเข้าถึงข้อมูลแบบขนาน วิธีที่ยืดหยุ่นและเร็วที่สุดมักเป็น ฐานข้อมูล SQLite แบบอ่านอย่างเดียวบน ramdisk
มันให้ความรู้สึกแฮ็กมาก ๆ แต่ตั้งค่าง่ายและเร็วที่สุดเท่าที่เคยหาเจอมา
ผมเคยเห็นเพื่อนในสายดาราศาสตร์บอกว่าคนจำนวนมากในวงการวิทยาศาสตร์ควรทำความคุ้นเคยกับฐานข้อมูล ไม่อย่างนั้นสุดท้ายก็จะทุ่มแรงมหาศาลไปสร้างฐานข้อมูลของตัวเองที่ห่วยโดยไม่รู้ตัว
เหตุผลที่แนวทางนี้ไม่แพร่หลายกว่านี้ก็คือ ระบบไฟล์เก่งเรื่องการจัดการไฟล์อยู่แล้ว
ถ้าต้องการอัปเดตแบบ atomic ก็ checkout ลงไดเรกทอรีใหม่แล้วเปลี่ยน symbolic link ก็พอ
ผมเคยเห็นหลายเวอร์ชันที่ใช้ฐานข้อมูลเหมือนระบบไฟล์ ซึ่งก็มีข้อดีอยู่บ้าง แต่เวลามีอะไรพังขึ้นมามันก็เหมือนฝันร้าย
แบบนั้นยังใช้ของอย่าง btrfs เพื่อ deduplication ในระดับ filesystem ได้ด้วย
ข้ออ้างที่ว่าเวลามีการอัปเดตแอป ไฟล์จำนวนมากอาจเปลี่ยนไป การใช้ฐานข้อมูลจึงทำให้ทุกการเปลี่ยนแปลงเป็น atomic ด้วย transaction ได้ และป้องกันไม่ให้เสิร์ฟเว็บเพจที่เสียระหว่างเปลี่ยนเวอร์ชันนั้นมีปัญหา
เพราะไฟล์ SQLite จะล็อกการอ่านระหว่างเขียนเพื่อให้ได้ isolation แบบ serializable ดังนั้นข้อสรุปจึงกลายเป็นว่าควรทำงานกับฐานข้อมูลบนไฟล์ offline แล้วค่อยสลับไฟล์เก่าใน production กับไฟล์ใหม่จะดีกว่า
นั่นสุดท้ายก็ใกล้เคียงกับการใช้ไฟล์ tar หรือใช้ไดเรกทอรีแยกต่างหากสำหรับสลับไปยังคอนเทนต์ใหม่
ไฟล์ static เสิร์ฟแบบ static นั้นง่ายกว่ามาก ไม่จำเป็นต้องเสิร์ฟผ่านโปรแกรมที่ต้องจัดการการเชื่อมต่อ SQLite แบบเรียลไทม์และพยายามทำเวทมนตร์ “concurrency ตอนอัปเดต” แปลก ๆ ปัญหานี้แก้ได้โดยไม่ยากเลย
การจัดการ CMS ด้วยฐานข้อมูล SQLite นั้นใช้ได้ แต่ถ้าคอนเทนต์เป็น static และต้องเสิร์ฟแบบเรียลไทม์ ก็ควรใช้ ไฟล์ static ดีกว่า