มาแก้ปัญหา Firestore กับปัญหาราคา Read pricing สุดจี๊ด
/ 7 min read
สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
บทความต้นฉบับ (ย้ายมาจาก medium) https://blog.mikelopster.dev/3426619fc2ab
วันนี้พามาชวนคุยเรื่องเทคโนโลยีสุดฮิตอย่าง Firebase กัน สำหรับใครก็ตามที่กำลังเริ่มเขียนเว็บ หรือ เขียนเว็บมาระดับนึงแล้วคงจะเคยผ่าน Firebase กันมาบ้าง Firebase เป็นเทคโนโลยีที่เรียกได้ว่าเป็น Backend as a Service หากใครก็ตามต้องการระบบหลังบ้านสักตัว และไม่ต้องการที่จะมา setup เองจากจุดเริ่มต้น Firebase ก็ถือเป็นสิ่งที่ตอบโจทย์ในเรื่องนี้ มีหมดตั้งแต่ Database, Realtime Database, Authenticaion รวมถึง Backend function อย่าง Cloud function อยู่ด้วย
ทีนี้ พอตอนที่จะเอาไปใช้จริง คนส่วนนึงมาจมอยู่กับปัญหา Firestore Pricing มันแพง (Firestore คือ เทคโนโลยี NoSQL ของ Firebase) เนื่องจาก การคิด Firestore นั้น จะคิดทั้งหมดตั้งแต่ read, write ยั้น delete แน่นอนว่าเมื่อ user เพิ่มขึ้น มันก็เลี่ยงไม่ได้ที่จำนวน read มันจำเพิ่มขึ้นตามจำนวนคนที่เพิ่มขึ้น
เพื่อให้เห็นภาพมากขึ้น สมมุติว่าเรามี user 4 คน และตอนนี้ข้อมูลระบบเรามี 100 documents ที่จะต้อง read มาให้ user เห็น เมื่อ user เปิดเว็บขึ้นมา แต่ละ user ก็จะเกิดการ read ครั้งละ 100 documents (ตามภาพด้านบน) = ถ้า user ยิ่งเปิดเยอะ หรือ ถ้าจำนวน user ยิ่งเยอะขึ้น (โดยที่เราไม่ได้ทำอะไรเลย) Pricing ก็จะโดนคิดเพิ่มไปเรื่อยๆตามจำนวน Read ที่เพิ่มขึ้น ทีนี้เราจะทำยังไงกับเรื่องนี้ดี
เราจะลองมาหาไอเดียในการลด Pricing กัน
ในฐานะที่เป็นผู้ใช้ Firebase เหมือนกัน เรามาลองชวนคุยกันดีกว่า ว่าเราจะสามารถหาวิธีลด Pricing หรือจำนวนการ read ของ Firestore ได้ยังไงกันบ้าง โดยข้อจำกัดของผมคือ ผมจะใช้แค่เทคโนโลยีภายใน Firebase เท่านั้น ผมเลยเกิดเป็น 2 ไอเดียนี้ขึ้นมาคือ
- ไอเดียที่ 1 ใช้ Firebase firestore แพ็คเป็น bundle ไว้ แล้วค่อยนำ bundle นั้นโหลดมาใช้ทีเดียว (อารมณ์ zip file แล้วโหลดมาเป็น file ใหญ่ทีเดียวเพื่อใช้)
- ไอเดียที่ 2 ใช้ Firestore PersistentLocalCache จะได้ไม่ต้อง read ซ้ำบ่อยๆ
โดยตัวอย่างที่ผมจะหยิบมาใช้นั้น ผมจะลองเลียนแบบ Social network อย่างง่ายโดย เราจะใช้ Firebase Firestore เก็บ content ซึ่งจะเก็บแค่ description, category และ tag (เป็น Array) ไว้
ไอเดียที่ 1 ใช้ Firebase firestore bundle ร่วมกับ Cloud storage
ไอเดียนี้ เราจะเปลี่ยนจากส่งเป็น document เปลี่ยนมาเป็นส่งมาเป็น file แทน วิธีการคือ
ฝั่ง Backend
- เราจะทำการ read ข้อมูลออกมาครั้งหนึ่งเอาไว้ (อาจจะเป็นจังหวะหลังแก้ไฟล์ก็ได้ แต่ในตัวอย่างนี้ผมจะ seed file เข้าไปแล้ว save bundle เลย)
- หลังจากนั้น เอาข้อมูลที่ read ได้มา save เป็น bundle file (Firestore มี feature ที่ทำแบบนี้ได้นะ) เก็บใส่ Cloud storage ไว้
ฝั่ง Frontend
- ทำการโหลด bundle จาก path file
- แล้วใช้คำสั่งของ Firestore (loadBundle) ทำการโหลดข้อมูลจาก bundle file เข้ามาไว้ในตัวแปรของ Firestore ไว้
ไอเดียจะเป็นประมาณภาพด้านล่างนี้
เราจะลองมา code กัน จะมีทั้งหมด 2 ฝั่งคือ
- ฝั่งที่ทำการ save bundle (ขาที่มีการ update ข้อมูล) = ฝั่ง Backend ผมจะใช้เป็น Nodejs ต่อกับ firebase-admin เพื่อทำการ read / write ทั้ง Cloud storage และ Firestore
- ฝั่งที่ทำการใช้ bundle (ดึงมาใช้) = ฝั่ง Frontend
เริ่มจากฝั่ง Backend ก่อน ผมสร้าง functions/firebaseConfig.js ขึ้นมาเพื่อใช้เป็นตัวแปรกลางสำหรับการเรียกใช้ Firestore (db) และ Cloud storage (storage) ขึ้นมา
มาที่ไฟล์หลัก functions/server.js ผมจะแบบออกเป็น 2 path ก่อนคือ
- ผมจะทำการเพิ่ม code ส่วนการ seed data ก่อน (เพราะตอนนี้เรายังไม่มี data อะไรเลย) ผมก็ใช้ไอเดียง่ายๆเลยว่า เราจะสร้าง list ของ category, tag ขึ้นมาและทำการวนลูปสร้างสัก 100 ตัวขึ้นมา
ผมใช้ Firebase Emulator ในการรัน project นี้ ทีนี้ผมลองรันดูก็จะได้ผลลัพธ์ประมาณนี้ออกมา ตามด้านล่างนี้
- ok ข้อมูลพร้อมและ เรามาสร้าง code สำหรับสร้าง bundle กัน ไอเดียคือ เราจะดึงข้อมูล posts ออกมาทั้งหมดและนำไป save bundle file ไว้ที่ cloud storage
เมื่อลองรัน code ดูเราจะได้ file bundle ออกมา และมันก็ได้ถูก upload ขึ้น cloud storage เป็นที่เรียบร้อย
ok Backend เรียบร้อย move มาฝั่ง Frontend เราจะ set src/config.js ไว้แบบนี้ เพื่อทำการใช้งาน Firestore (ฝั่ง Frontend จะใช้แค่ตัวนี้)
ที่ไฟล์หลัก src/script.js เราจะลองทดสอบเรียกใช้ bundle ดูได้การระบุ path bundle เข้าไปตรงๆ
และนี่ก็คือผลลัพธ์ของการ load ผ่าน bundle และข้อมูลที่ได้ออกมา
จะเห็นว่าเราสามารถหยิบผลลัพธ์ที่เรา save ใน bundle ออกมาได้เรียบร้อย เราจะต๊ะจุดนี้กันไว้ก่อน เราจะไปต่อที่วิธีที่ 2 กันก่อน
ไอเดียที่ 2 ใช้ PersistentLocalCache ของ Firebase firestore
ผมลองนั่งดู และก็ไปเจอว่า Firebase Firestore มี feature อย่าง persistentLocalCache ที่ช่วยทำให้ข้อมูล Firestore สามารถมา save เป็น cache ใน local browser ได้ ซึ่งมันจะมีวิธี สามารถโหลดมาแค่รอบแรกครั้งเดียว และจะนับ cost แค่ตอน snapshot sync (ตอนที่มีข้อมูลใหม่เพิ่มมา) ได้
ก็จะอารมณ์ประมาณภาพล่างนี้
มาลองปรับ src/config.js ให้เป็นตามนี้กัน รอบนี้ส่วนที่แก้จะมีแค่
กลับมาที่ src/script.js เราจะเพิ่ม function loadPost() เข้ามาสำหรับดึงข้อมูล Posts ออกมาทั้งจาก cache / server ด้วยคำสั่ง onSnapshot (ใส่ includeMetadataChanges เพื่อให้จังหวะแรกสุดดึงข้อมูลออกมาทั้งหมดได้ และถ้ามี cache มันก็จะเลือกดึงมาจาก cache มาแทน)
นี่คือผลลัพธ์ของเรื่องราวดีๆนี้
จากด้านบนนี้ ผมลอง clear cache ทิ้งก่อนเริ่ม และทำการเริ่มใช้ function loadPosts()
- ครั้งแรก ข้อมูลจะโหลดจาก server มาก่อน ทั้ง 100 ตัว
- แต่พอ refresh อีกรอบ ข้อมูลจะโหลดจาก cache มาแทนทั้ง 100 ตัวแล้ว = browser ได้ save สิ่งนี้เก็บไว้แล้วเรียบร้อย
ความดีงามของวิธีนี้คือ มันจะทำการ handle เรื่อง cache ให้เองได้เลย โดยเพิ่มเพียงแค่ config Firestore เองนี่แหละ
คำถามคือ เราควรเลือกใช้ bundle หรือ PersistententLocalCache ดี ?
ก่อนที่เราจะตอบคำถามนี้ เราจะพาทุกคนมาลองวิเคราะห์ pricing ของแต่ละวิธีกันก่อน
นี่คือไอเดียที่เรามี ทีนี้ผมจะลองแตกการคำนวนบางอย่างออกมาเพื่อให้เห็นว่า ถ้าใช้ Firestore ตามปกติ และ ถ้าใช้ Cloud storage เทียบกัน ราคาจะต่างกันประมาณไหน
ภาพการคำนวณด้านล่างนี้คือ
- สมมุติ Firestore: user 1 คนที่ปริมาณการ read ที่ 10k ต่อเดือน
- สมมุติ Cloud storage: user 1 คนจะโหลด bundle มาใหม่ เดือนละ 1 ครั้ง
- ตรงที่ระบายสีเหลือง = ราคา ของ Firestore กับ Cloud storage เทียบในโจทย์เดียวกัน ซึ่งจะสังเกตุเห็นว่าด้วยสมมุติฐานนี้ Cloud storage จะที่ pricing ที่ถูกกว่า
ดูเผิน way ของ Cloud storage เหมือนจะราคาดีกว่า แต่ว่า ราคาจะแพงขึ้นมากเมื่อเราต้องมีการโหลด bundle แบบถี่ๆ เช่น ถ้าเกิด 1 เดือนเรามี update 10 ครั้ง แปลว่า user ต้องโหลด bundle มาใหม่ 10 รอบ จะกลายเป็นว่าวิธี bundle จะแพงกว่าแทน
ทีนี้ ผมก็เลยทดลองเพิ่มอีก 1 อย่าง ผมสงสัยว่า “แล้วตอนโหลด bundle มา มันได้ทำการเก็บ cache ไว้ไหม ?” คำตอบของเรื่องนี้คือ “มันจะเก็บไว้เมื่อเราเปิด mode cache ด้วย PersistententLocalCache” ผมจะลองยกตัวอย่างจาก code ตัวนี้ ผมลองปรับ code src/script.js เพื่อลองทดสอบ assumption นี้
และนี่คือผลลัพธ์ของเรื่องนี้
ผลลัพธ์นี้คือ
- ผมได้ทำการทดสอบรัน loadPostsFromCache() ก่อนหนึ่งรอบ เพื่อ recheck ก่อนว่า ข้อมูลไม่ได้มีอยู่ใน cache จริงๆ (ผมทำการ clear cache ทิ้งก่อน)
- จากนั้น ผมได้ทำการ loadPostsFromBundle() → loadPostsFromCache() ก็จะเจอว่าทั้ง 2 คำสั่งได้ข้อมูลชุดเดียวกันออกมาได้ = จังหวะโหลด bundles มันได้ save ลง cache ไว้แล้วเรียบร้อย
แน่นอนครับว่า ผลลัพธ์นี้เป็นจริงกับคำสั่ง onSnapshot ที่เราใช้ในไอเดียที่ 2 เช่นกัน มันจะเปลี่ยนมาโหลดจาก cache ทั้งหมดแทน
ผมก็เลยได้ข้อสรุปจากการทดลองทั้งหมดนี้ว่า
- [กรณี Firebase ล้วน] โหลดจาก bundle มาไว้ก่อน (ไอเดียที่ 1) แต่ถ้าไม่ได้มี update ใหญ่แล้ว ให้เปลี่ยนมาทำการ subscribe (onSnapshot) เพื่อเปลี่ยนมาดึงจาก cache ให้ตอนจังหวะดึงข้อมูลมา update เป็นปริมาณที่ไม่มากแทน (ไอเดียที่ 2) มันจะทำให้เรา weight ราคาระหว่าง Cloud storage และ Firestore ให้เหมาะสมซึ่งกันและกันได้
- [กรณีที่ไม่ใช้ Firebase] ถ้ารู้สึก pricing ของ Cloud storage มันแพง ก็เปลี่ยนไปใช้ตัวอื่นแทนได้ (อาจจะใช้เป็น CDN ที่ทำ caching ในตัวเอง) ก็จะยังคงสามารถใช้วิธี bundle แบบไม่แพงมากได้
- bundle จะมีข้อพิจารณาตอนเวลาเขียน bundle ด้วย ยิ่งข้อมูลเยอะมันใช้เวลาพอสมควรตอนเขียน bundle ขึ้นมา = ไม่แนะนำให้ใช้กับเคส realtime มากเกินไป
และนี่คือภาพที่สมบูรณ์ของเรื่องราวนี้
แชร์ไว้เผื่อใครสนใจอยากลองเข้าไปดูวิธีคำนวนของเรา
https://docs.google.com/spreadsheets/d/12b4t_lRpEFW4WxkT5xPuWvkjkVQ5TU-yMjrI4xolWOs/edit?usp=sharing
ใครสนใจเพิ่มเติมดู code ผ่าน github กันได้
https://github.com/mikelopster/firebase-bundle-cache-expirement
ข้อสรุปของเรื่องนี้
สำหรับผม ผมสนุกกับการทดลองนี้มาก 555 ผมก็ยังไม่แน่ใจนะว่าตัวเองค้นพบ solution แบบสุดทางแล้วหรือยัง ถ้าใครที่แวบมาอ่านก็มาแชร์กันเพิ่มเติมได้เพราะปัญหา Firebase Firestore เป็นปัญหาที่ผมเคยชนกับมันอยู่จริง และมันเคยทำเอาผม pain จนอยากจะเลิกใช้ Firebase ไปเลยก็ว่าได้ (แต่ยังไม่เลิกหรอก คนมันรักกันแล้ว จะให้เลิกกันมันคงยาก)
ถึงยังไงเทคโนโลยีอย่าง Firebase นั้นก็ยังถือว่าอำนวยความสะดวกสบายและยังเหมาะสมสำหรับการใช้งานเคสทั่วๆไปอยู่ดี แต่ถ้าจะต้องใช้ production ก็บริหาร cache ให้ดี หรือไม่งั้นก็ใช้ technology อื่นร่วมกันก็ได้ อาจจะทำให้เราสามารถทำระบบ production ที่สามารถใช้ pricing ที่เหมาะสมออกมาได้ดีกว่านี้ก็ได้นะ
Reference
- https://firebase.blog/posts/2021/04/firestore-supports-data-bundles/
- https://dev.to/moga/decrease-read-costs-of-firestore-using-firestore-data-bundles-30e9
- มาเรียน Canvas ผ่าน Pong game กันมี Video
มารู้จักกับ HTML Canvas ว่ามันคืออะไร ใช้ทำอะไรบ้าง และมารู้จักพื้นฐานเบื้องต้นของการทำ Animation บน Browser ว่ามีหลักการเป็นประมาณไหน
- รู้จักกับ Design Pattern - Creational (Part 1/3)มี Video
มาเรียนรู้รูปแบบการพัฒนา Software Design Pattern ประเภทแรก Creational กัน
- มาเรียนรู้พื้นฐาน Diagram แต่ละประเภทของงาน development กันมี Video
รู้จักกับ Diagram ที่ใช้บ่อยประเภทต่างๆว่ามีอะไรบ้าง และใช้สำหรับทำอะไรบ้าง
- รู้จักกับ Storybook และการทำ Component Specsมี Video
มาลองทำ Component Specs และ Interactive Test ผ่าน Storybook กัน