- ทีมฐานข้อมูลของ Figma สรุปเส้นทางตลอดเก้าเดือนในการทำ horizontal sharding ให้กับสแตก Postgres และวิธีที่ทำให้รองรับการขยายตัวได้แทบไร้ขีดจำกัด
เส้นทางสู่การทำ horizontal sharding ของสแตก Postgres ที่ Figma
- ขนาดของสแตกฐานข้อมูลของ Figma เพิ่มขึ้นเกือบ 100 เท่านับตั้งแต่ปี 2020: นี่เป็นปัญหาเชิงบวกที่สะท้อนการขยายตัวของธุรกิจ แต่ขณะเดียวกันก็สร้างความท้าทายทางเทคนิคด้วย ในปี 2020 บริษัทใช้งานฐานข้อมูล Postgres เดี่ยวบนอินสแตนซ์แบบกายภาพที่ใหญ่ที่สุดของ AWS และภายในปลายปี 2022 ก็ได้สร้างสถาปัตยกรรมแบบกระจายที่รวมทั้งแคช รีดรีพลิกา และฐานข้อมูลหลายชุดที่แยกแบบ vertical partitioning
- การแยกแนวตั้ง: แยกกลุ่มตารางที่เกี่ยวข้องกันออกเป็น vertical partition ของตัวเอง เพื่อให้ได้ประโยชน์ด้านการสเกลแบบค่อยเป็นค่อยไปและคงเผื่อพื้นที่ไว้เพียงพอให้ทันกับการเติบโต ตัวอย่างเช่น แยกกลุ่มตารางที่เกี่ยวข้องกันอย่าง “ไฟล์ Figma” หรือ “องค์กร” ออกเป็น vertical partition ของตัวเอง
- การเปลี่ยนไปสู่ horizontal sharding: ตระหนักว่าการแยกแนวตั้งเพียงอย่างเดียวมีข้อจำกัด หลังจากความพยายามในการสเกลช่วงแรกที่เน้นลดการใช้ CPU ก็เริ่มติดตามคอขวดที่หลากหลายในฟลีตที่ใหญ่ขึ้นและมีความหลากหลายมากขึ้น วัดขีดจำกัดในการสเกลฐานข้อมูลในหลายมิติ ตั้งแต่ CPU และ IO ไปจนถึงขนาดตารางและจำนวนแถวที่ถูกเขียน การระบุขีดจำกัดเหล่านี้มีความสำคัญต่อการคาดการณ์ว่าแต่ละชาร์ดยังเหลือเผื่อพื้นที่ได้มากแค่ไหน
- ข้อจำกัดของขนาดตาราง: บางตารางมีขนาดถึงหลายเทราไบต์และหลายพันล้านแถว จนไปถึงระดับที่ฐานข้อมูลเดี่ยวจัดการได้ยาก ที่ขนาดระดับนี้ ความน่าเชื่อถือจะได้รับผลกระทบระหว่างงาน vacuum ของ Postgres (งานเบื้องหลังที่จำเป็นเพื่อป้องกันไม่ให้ transaction ID หมดจนระบบหยุดทำงาน) ตารางที่มีการเขียนมากที่สุดก็กำลังจะเกินค่า IOPS สูงสุดที่ Amazon Relational Database Service (RDS) รองรับในไม่ช้า ปัญหาเหล่านี้แก้ไม่ได้ด้วยการแยกแนวตั้ง และจำเป็นต้องมีทางออกที่ใหญ่กว่าเพื่อป้องกันไม่ให้ฐานข้อมูลพัง
การวางรากฐานเพื่อการขยายขนาด
- ลดผลกระทบต่อฝั่งนักพัฒนาให้ต่ำที่สุด: รองรับโมเดลข้อมูลเชิงสัมพันธ์ที่ซับซ้อนส่วนใหญ่ไว้ภายใน เพื่อให้นักพัฒนาแอปพลิเคชันสามารถโฟกัสกับการสร้างฟีเจอร์ใหม่ที่น่าสนใจบน Figma แทนการรีแฟกเตอร์โค้ดเบสส่วนใหญ่
- การขยายแบบโปร่งใส: ทำให้เมื่อมีการสเกลในอนาคต ไม่จำเป็นต้องแก้ไขเพิ่มเติมในชั้นแอปพลิเคชัน กล่าวคือ หลังจากทำงานเตรียมความพร้อมครั้งแรกเพื่อให้ตารางเข้ากันได้แล้ว การขยายในอนาคตควรเกิดขึ้นอย่างโปร่งใสต่อทีมผลิตภัณฑ์
- หลีกเลี่ยง backfill ที่มีต้นทุนสูง: หลีกเลี่ยงโซลูชันที่ต้องทำ backfill กับตารางขนาดใหญ่ของ Figma หรือกับทุกตาราง เมื่อพิจารณาจากขนาดตารางและข้อจำกัดด้าน throughput ของ Postgres แล้ว backfill แบบนั้นจะใช้เวลาหลายเดือน
- ค่อยเป็นค่อยไป: ระบุแนวทางที่สามารถทยอยเปิดใช้ได้ เพื่อลดความเสี่ยงของการเปลี่ยนแปลงสำคัญในโปรดักชัน วิธีนี้ช่วยลดความเสี่ยงของการหยุดชะงักครั้งใหญ่ และทำให้ทีมฐานข้อมูลรักษาความน่าเชื่อถือของ Figma ได้ระหว่างการย้ายระบบ
- หลีกเลี่ยงการย้ายแบบทางเดียว: รักษาความสามารถในการ rollback ได้แม้หลังจากทำ physical sharding เสร็จแล้ว ซึ่งช่วยลดความเสี่ยงจากการติดอยู่ในสถานะที่ไม่ดีเมื่อเกิดตัวแปรที่ไม่รู้มาก่อน
- รักษาความสอดคล้องของข้อมูลอย่างแข็งแกร่ง: หลีกเลี่ยงโซลูชันที่ซับซ้อน เช่น double-writes ซึ่งทำได้ยากหากไม่มี downtime หรืออาจกระทบต่อความสอดคล้องของข้อมูล ต้องการโซลูชันที่สเกลได้โดยมี downtime แทบเป็นศูนย์
- ใช้ประโยชน์จากจุดแข็งของทีม: ภายใต้แรงกดดันของเดดไลน์ที่กระชั้นชิด ทีมเลือกแนวทางที่สามารถทยอยเปิดใช้ได้มากที่สุดเท่าที่เป็นไปได้ สำหรับตารางที่เติบโตเร็วที่สุด ทีมพยายามใช้ความเชี่ยวชาญและทักษะที่มีอยู่แล้ว
การสำรวจตัวเลือกที่เป็นไปได้
- ทบทวนตัวเลือกฐานข้อมูลสำหรับ horizontal sharding: มีทั้งโซลูชันโอเพนซอร์สและแบบ managed ยอดนิยมหลากหลายสำหรับฐานข้อมูล horizontal sharding ที่เข้ากันได้กับ Postgres หรือ MySQL โดยมีการประเมิน CockroachDB, TiDB, Spanner และ Vitess อย่างไรก็ตาม การย้ายไปยังฐานข้อมูลทางเลือกเหล่านี้จะต้องใช้การย้ายข้อมูลที่ซับซ้อนเพื่อรับประกันความสอดคล้องและความน่าเชื่อถือระหว่างที่เก็บข้อมูลของฐานข้อมูลสองระบบที่ต่างกัน
- ใช้ประโยชน์จากความเชี่ยวชาญที่มีอยู่: ตลอดหลายปีที่ผ่านมา ทีมได้สร้างความเชี่ยวชาญจำนวนมากในการรัน RDS Postgres ให้เสถียรและมีประสิทธิภาพ หากย้ายระบบ ก็จะต้องสร้างความเชี่ยวชาญเฉพาะทางขึ้นมาใหม่ตั้งแต่ต้น ระดับการเติบโตที่รุนแรงมากทำให้เวลาที่เหลืออยู่มีเพียงไม่กี่เดือน
- ตัดตัวเลือกฐานข้อมูล NoSQL ออก: อีกโซลูชันที่มักถูกเลือกเมื่อบริษัทเติบโตคือฐานข้อมูล NoSQL แต่สถาปัตยกรรมปัจจุบันมีโมเดลข้อมูลเชิงสัมพันธ์ที่ซับซ้อนมากซึ่งสร้างอยู่บน Postgres และ API ของ NoSQL ไม่รองรับความหลากหลายระดับนี้ ทีมต้องการให้นักวิศวกรโฟกัสกับการปล่อยฟีเจอร์ที่ยอดเยี่ยมและสร้างผลิตภัณฑ์ใหม่ แทนที่จะต้องเขียนแบ็กเอนด์แอปพลิเคชันเกือบทั้งหมดใหม่ ดังนั้น NoSQL จึงไม่ใช่โซลูชันที่ใช้ได้จริง
- พิจารณาสร้างโซลูชัน horizontal sharding บนโครงสร้างพื้นฐาน RDS Postgres เดิม: สำหรับทีมขนาดเล็ก การสร้างฐานข้อมูลเชิงสัมพันธ์แบบ horizontal sharding ทั่วไปขึ้นใหม่ภายในองค์กรไม่มีความคุ้มค่า เพราะจะเท่ากับต้องแข่งขันกับเครื่องมือที่ชุมชนโอเพนซอร์สขนาดใหญ่หรือผู้ขายฐานข้อมูลเฉพาะทางสร้างไว้ อย่างไรก็ตาม เนื่องจากเป็นการปรับ horizontal sharding ให้เหมาะกับสถาปัตยกรรมเฉพาะของ Figma การรองรับฟีเจอร์ที่เล็กกว่ามากก็อาจเพียงพอแล้ว ตัวอย่างเช่น ทีมตัดสินใจไม่รองรับธุรกรรมข้ามชาร์ดแบบที่รับประกันความเป็นอะตอม เพราะมีวิธีจัดการความล้มเหลวของธุรกรรมข้ามชาร์ดได้อยู่แล้ว ทีมเลือกกลยุทธ์ colocation เพื่อลดการเปลี่ยนแปลงที่ต้องทำในชั้นแอปพลิเคชันให้น้อยที่สุด ทำให้สามารถรองรับชุดย่อยของ Postgres ที่เข้ากันได้กับลอจิกผลิตภัณฑ์ส่วนใหญ่ นอกจากนี้ยังทำให้การรักษา backward compatibility ระหว่าง Postgres แบบชาร์ดและแบบไม่ชาร์ดทำได้ง่าย และหากต้องเผชิญกับตัวแปรที่ไม่รู้มาก่อน ก็สามารถ rollback กลับไปใช้ Postgres แบบไม่ชาร์ดได้ง่าย
เส้นทางสู่ horizontal sharding
- การนำ horizontal sharding มาใช้: horizontal sharding คือกระบวนการแบ่งตารางเดียวหรือกลุ่มตารางออก แล้วกระจายข้อมูลไปยังอินสแตนซ์ฐานข้อมูลจริงหลายตัว ผ่านกระบวนการนี้ ตารางที่ถูกทำ horizontal sharding ในชั้นแอปพลิเคชันจะสามารถรองรับจำนวนชาร์ดเท่าใดก็ได้ในชั้นกายภาพ และยังสามารถขยายเพิ่มเติมได้เสมอเพียงแค่ทำการแยก physical shard ซึ่งทำงานเบื้องหลังอย่างโปร่งใส โดยมี downtime ต่ำมากและแทบไม่ต้องเปลี่ยนแปลงในระดับแอปพลิเคชัน ความสามารถนี้ทำให้ Figma สามารถนำหน้าคอขวดด้านการสเกลฐานข้อมูลที่เหลืออยู่ และกำจัดหนึ่งในความท้าทายการสเกลครั้งใหญ่สุดท้ายของบริษัทได้ หากการแยกแนวตั้งช่วยให้เร่งความเร็วได้เหมือนขึ้นทางด่วน horizontal sharding ก็เหมือนการยกเลิกการจำกัดความเร็วและทำให้บินได้
- ความซับซ้อนของ horizontal sharding: horizontal sharding ซับซ้อนกว่าความพยายามด้านการสเกลก่อนหน้านี้ไปอีกระดับ เมื่อมีการแบ่งตารางออกไปอยู่บนฐานข้อมูลจริงหลายตัว คุณสมบัติด้านความน่าเชื่อถือและความสอดคล้องจำนวนมากที่มักมองว่าเป็นเรื่องปกติในฐานข้อมูล SQL แบบ ACID จะหายไป ตัวอย่างเช่น บาง SQL query อาจรองรับได้อย่างไม่มีประสิทธิภาพหรืออาจทำไม่ได้เลย และโค้ดแอปพลิเคชันต้องถูกปรับให้ให้ข้อมูลเพียงพอเพื่อ route query ไปยังชาร์ดที่ถูกต้องอย่างมีประสิทธิภาพ การเปลี่ยน schema ต้องประสานกันเพื่อให้ทุกชาร์ดซิงก์กันอยู่เสมอ และ foreign key รวมถึงดัชนีที่ต้องไม่ซ้ำกันในระดับ global ก็ไม่สามารถให้ Postgres บังคับใช้ได้อีกต่อไป ธุรกรรมจะครอบคลุมหลายชาร์ด ทำให้ไม่สามารถใช้ Postgres บังคับธุรกรรมได้อีก ตอนนี้อาจเกิดกรณีที่การเขียนไปยังฐานข้อมูลบางตัวสำเร็จ แต่อีกบางตัวล้มเหลวได้ ลอจิกผลิตภัณฑ์จึงต้องระมัดระวังให้ทนทานต่อ “partial commit failure” เหล่านี้ (ลองนึกภาพการย้ายทีมจากองค์กรหนึ่งไปอีกองค์กรหนึ่ง แต่ข้อมูลหายไปแค่ครึ่งเดียว!)
- ความพยายามหลายปีสู่ horizontal sharding: ทีมรู้อยู่แล้วว่าการไปให้ถึง horizontal sharding อย่างเต็มรูปแบบจะเป็นความพยายามหลายปี จึงต้องส่งมอบคุณค่าแบบค่อยเป็นค่อยไปพร้อมลดความเสี่ยงของโครงการให้มากที่สุด เป้าหมายแรกคือทำให้ตารางที่ค่อนข้างเรียบง่ายแต่มีทราฟฟิกสูงมากถูกชาร์ดในโปรดักชันให้เร็วที่สุด ซึ่งไม่เพียงพิสูจน์ความเป็นไปได้ของ horizontal sharding เท่านั้น แต่ยังช่วยยืดเผื่อพื้นที่ให้กับฐานข้อมูลที่รับโหลดหนักที่สุดได้ด้วย จากนั้นจึงค่อยสร้างฟีเจอร์เพิ่มเติมพร้อมกับชาร์ดกลุ่มตารางที่ซับซ้อนยิ่งขึ้น แม้แต่ชุดความสามารถที่เรียบง่ายที่สุดก็ยังต้องใช้ความพยายามอย่างมาก ตั้งแต่ต้นจนจบ ทีมใช้เวลาราวเก้าเดือนในการชาร์ดตารางแรก
แนวทางเฉพาะของเรา
- Colos: ทำ horizontal sharding ให้กับกลุ่มตารางที่เกี่ยวข้องกันด้วย colocation (เรียกสั้น ๆ แบบกันเองว่า “colo”) ซึ่งใช้ shard key และเลย์เอาต์ physical sharding เดียวกัน ช่วยให้ผู้พัฒนามี abstraction ที่คุ้นเคยสำหรับการทำงานกับตารางที่ถูก shard ในแนวนอน
- Logical sharding: แยกแนวคิด “logical sharding” ในชั้นแอปพลิเคชันออกจาก “physical sharding” ในชั้น Postgres โดยใช้ view เพื่อปล่อยใช้งาน logical sharding ที่ปลอดภัยกว่าและมีต้นทุนต่ำกว่า ก่อนจะทำการสลับระบบแบบ distributed physical failover ที่มีความเสี่ยงมากกว่า
- เอนจินคิวรีของ DBProxy: สร้างบริการ DBProxy เพื่อดักจับ SQL query ที่สร้างจากชั้นแอปพลิเคชัน และ route query แบบไดนามิกไปยังฐานข้อมูล Postgres หลายตัว ภายใน DBProxy มี query engine ที่สามารถ parse และ execute คิวรี horizontal sharding ที่ซับซ้อนได้ ทำให้สามารถเพิ่มความสามารถอย่าง dynamic load balancing และ request hedging ผ่าน DBProxy ได้
- ความพร้อมของ shadow application: เพิ่มเฟรมเวิร์ก “shadow application readiness” ที่สามารถคาดการณ์ได้ว่า production traffic จริงจะทำงานอย่างไรภายใต้ shard key ที่เป็นไปได้หลายแบบ ช่วยให้ทีมผลิตภัณฑ์เห็นภาพชัดเจนว่าจำเป็นต้อง refactor หรือตัด logic ฝั่งแอปพลิเคชันส่วนใดเพื่อเตรียมระบบให้พร้อมสำหรับ horizontal sharding
- การทำ logical replication ทั้งชุด: ไม่จำเป็นต้องทำ “filtered logical replication” ที่คัดลอกเฉพาะบางส่วนของข้อมูลไปยังแต่ละ shard แต่เลือกคัดลอกชุดข้อมูลทั้งหมดแทน จากนั้นอนุญาตให้อ่าน/เขียนได้เฉพาะกับส่วนย่อยของข้อมูลที่อยู่ใน shard นั้น
การนำ sharding ไปใช้งาน
- ความสำคัญของการเลือก shard key: หนึ่งในการตัดสินใจที่สำคัญที่สุดของ horizontal sharding คือจะใช้ shard key อะไร เพราะ horizontal sharding เพิ่มข้อจำกัดหลายอย่างให้กับ data model โดยยึดตาม shard key ตัวอย่างเช่น คิวรีส่วนใหญ่ต้องมี shard key เพื่อให้ route request ไปยัง shard ที่ถูกต้องได้ ข้อจำกัดของฐานข้อมูลบางอย่าง เช่น foreign key จะทำงานได้ก็ต่อเมื่อ foreign key นั้นเป็น sharding key ด้วย นอกจากนี้ shard key ต้องกระจายข้อมูลไปยังทุก shard อย่างสม่ำเสมอ เพื่อหลีกเลี่ยง hotspot ที่ทำให้เกิดปัญหาด้านความน่าเชื่อถือหรือกระทบต่อ scalability
- แนวทางที่ปรับให้เหมาะกับ data model ของ Figma: Figma ทำงานบนเบราว์เซอร์ และผู้ใช้จำนวนมากสามารถร่วมมือกันบนไฟล์ Figma เดียวกันได้พร้อมกัน ซึ่งหมายความว่าระบบขับเคลื่อนด้วย relational data model ที่ค่อนข้างซับซ้อนซึ่งเก็บ file metadata, organization metadata, comment, file version และข้อมูลอื่น ๆ ใน data model เดิมไม่มีตัวเลือกเดียวที่เหมาะอย่างชัดเจน จึงเคยพิจารณาใช้ sharding key เดียวกันกับทุกตาราง แต่จะต้องสร้าง composite key เพื่อเพิ่ม sharding key แบบรวมศูนย์ เพิ่มคอลัมน์ให้ทุก schema ของตาราง รัน backfill ที่มีต้นทุนสูงเพื่อเติมข้อมูล และจากนั้นต้อง refactor product logic จำนวนมาก แทนที่จะทำเช่นนั้น Figma เลือกปรับแนวทางให้เข้ากับ data model เฉพาะตัวของตน โดยเลือก shard key เพียงไม่กี่ตัว เช่น UserID, FileID และ OrgID ซึ่งแทบทุกตารางใน Figma สามารถ shard ได้ด้วยคีย์เหล่านี้
- การนำ Colos มาใช้: นำแนวคิด colocation มาใช้เพื่อมอบ abstraction ที่เป็นมิตรกับนักพัฒนาผลิตภัณฑ์ ตารางภายใน colo เดียวกันรองรับทั้ง cross-table join และ transaction แบบเต็มรูปแบบได้ ตราบใดที่ยังจำกัดอยู่กับ shard key เดียว โค้ดแอปพลิเคชันส่วนใหญ่เดิมก็โต้ตอบกับฐานข้อมูลในลักษณะนี้อยู่แล้ว จึงช่วยลดงานที่นักพัฒนาแอปพลิเคชันต้องทำเพื่อปรับตารางให้พร้อมสำหรับ horizontal sharding
- การรับประกันการกระจายข้อมูลอย่างสม่ำเสมอ: หลังจากเลือก shard key แล้ว ยังต้องทำให้มั่นใจว่าข้อมูลถูกกระจายอย่างเท่าเทียมไปยังฐานข้อมูล backend ทุกตัว น่าเสียดายที่ shard key จำนวนมากที่เลือกใช้นั้นเป็น auto-increment หรือใช้ ID ที่มี Snowflake timestamp prefix ซึ่งจะทำให้เกิด hotspot อย่างหนักจนข้อมูลส่วนใหญ่อาจไปกองอยู่ใน shard เดียว แม้จะเคยสำรวจการย้ายไปใช้ ID ที่สุ่มมากขึ้น แต่ก็ต้องอาศัยการย้ายข้อมูลที่มีต้นทุนสูงและใช้เวลานาน แทนที่จะทำเช่นนั้น จึงตัดสินใจใช้ hash ของ shard key สำหรับการ route หากเลือก hash function ที่มีความสุ่มเพียงพอ ก็สามารถรับประกันการกระจายข้อมูลได้อย่างสม่ำเสมอ ข้อเสียอย่างหนึ่งคือ range scan บน shard key จะมีประสิทธิภาพลดลง เพราะคีย์ที่ต่อเนื่องกันจะถูก hash ไปยัง shard ฐานข้อมูลที่ต่างกัน อย่างไรก็ตาม รูปแบบคิวรีนี้ไม่ได้พบบ่อยใน codebase ของพวกเขา จึงเป็น trade-off ที่ยอมรับได้
ทางออกแบบ "logical"
- ลดความเสี่ยงของการปล่อยใช้ horizontal sharding: เพื่อลดความเสี่ยงของการปล่อยใช้ horizontal sharding พวกเขาต้องการแยกกระบวนการทางกายภาพในการแบ่ง shard ออกจากกระบวนการเตรียมตารางในชั้นแอปพลิเคชัน จึงแยก “logical sharding” ออกจาก “physical sharding” วิธีนี้ทำให้สามารถแยกสองส่วนของ migration ออกจากกัน เพื่อนำไปใช้งานอย่างอิสระและลดความเสี่ยงได้ logical sharding ช่วยสร้างความมั่นใจให้กับ serving stack ผ่านการปล่อยใช้แบบ low-risk และค่อย ๆ เพิ่มสัดส่วน เมื่อพบ bug การ rollback logical sharding ทำได้เพียงเปลี่ยน config อย่างง่าย ขณะที่การ rollback งานฝั่ง physical shard แม้จะทำได้ แต่ต้องมีการประสานงานที่ซับซ้อนกว่าเพื่อรับประกัน data consistency
- พฤติกรรมหลังทำ logical sharding: เมื่อตารางถูกทำ logical sharding แล้ว การอ่านและการเขียนทั้งหมดจะทำงานราวกับว่าถูก shard ในแนวนอนอยู่แล้ว ทั้งในแง่ reliability, latency และ consistency ระบบดูเหมือนทำงานแบบ horizontal sharding แม้ว่าข้อมูลจริงยังอยู่บนโฮสต์ฐานข้อมูลเดียว เมื่อมั่นใจแล้วว่า logical sharding ทำงานตามคาด จึงค่อยทำงาน physical sharding ซึ่งเป็นกระบวนการคัดลอกข้อมูลจากฐานข้อมูลเดียวไป shard บน backend หลายตัว จากนั้น route traffic การอ่านและเขียนใหม่ให้ผ่านฐานข้อมูลชุดใหม่
เอนจินคิวรีที่ทำได้จริง
- การออกแบบ backend stack ใหม่เพื่อรองรับ horizontal sharding: ในช่วงแรก บริการแอปพลิเคชันสื่อสารกับ PGBouncer ซึ่งเป็นชั้น connection pooling โดยตรง แต่ horizontal sharding ต้องการความสามารถที่ซับซ้อนกว่ามากในการ parse, plan และ execute คิวรี เพื่อรองรับสิ่งนี้ จึงสร้างบริการใหม่ด้วย golang ชื่อ DBProxy โดย DBProxy อยู่ระหว่างชั้นแอปพลิเคชันกับ PGBouncer ภายในมี logic สำหรับ load balancing, observability ที่ดีขึ้น, การรองรับ transaction, การจัดการ database topology และ lightweight query engine
- องค์ประกอบหลักของ query engine:
- Query parser: อ่าน SQL ที่ส่งมาจากแอปพลิเคชันและแปลงเป็น abstract syntax tree (AST)
- Logical planner: parse AST และดึงประเภทของคิวรีใน query plan (เช่น insert, update เป็นต้น) พร้อม logical shard ID ออกมา
- Physical planner: แมปคิวรีจาก logical shard ID ไปยังฐานข้อมูลจริง และ rewrite คิวรีเพื่อให้รันบน physical shard ที่เหมาะสม
- แนวทางแบบ "scatter-gather": ทำงานเหมือนเกมซ่อนหาในฐานข้อมูลทั้งระบบ คือส่งคิวรีไปทุก shard (scatter) แล้วรวบรวมคำตอบกลับมาจากแต่ละตัว (gather) ฟังดูสนุก แต่ถ้าใช้มากเกินไปกับคิวรีที่ซับซ้อน ฐานข้อมูลก็อาจช้าราวกับหอยทากได้
- การทำคิวรีในโลกของ horizontal sharding: คิวรีแบบ single-shard จะถูกกรองด้วย shard key เดียว query engine เพียงแค่ดึง shard key ออกมาและ route คิวรีไปยังฐานข้อมูลจริงที่เหมาะสม โดย “ผลัก” ความซับซ้อนของการ execute คิวรีลงไปให้ Postgres จัดการ แต่ถ้าคิวรีไม่มี shard key query engine จะต้องทำ “scatter-gather” ที่ซับซ้อนกว่า ในกรณีนี้จะต้อง fan out คิวรีไปยังทุก shard ก่อน (ขั้น scatter) แล้วจึง aggregate ผลลัพธ์กลับมา (ขั้น gather)
- การทำ SQL compatibility ให้เรียบง่ายขึ้น: หากบริการ DBProxy รองรับ SQL compatibility แบบครบถ้วน มันก็จะมีหน้าตาคล้ายกับ query engine ของฐานข้อมูล Postgres มากเกินไป พวกเขาจึงต้องการทำ API ให้เรียบง่ายและลดความซับซ้อนของ DBProxy ให้มากที่สุด พร้อมลดภาระของนักพัฒนาแอปพลิเคชันที่ต้อง rewrite คิวรีที่ไม่รองรับ เพื่อกำหนดว่า subset ที่เหมาะสมคืออะไร พวกเขาจึงสร้างเฟรมเวิร์ก “shadow planning” ที่สามารถนิยาม sharding schema ที่เป็นไปได้ของตาราง และรันขั้นตอน logical planning บน production traffic แบบเรียลไทม์ได้ จากนั้นบันทึกคิวรีและ query plan ที่เกี่ยวข้องลงในฐานข้อมูล Snowflake เพื่อทำการวิเคราะห์แบบออฟไลน์ จากข้อมูลนี้ พวกเขาเลือกภาษาคิวรีที่รองรับ 90% ของคิวรีที่พบบ่อยที่สุด โดยหลีกเลี่ยงความซับซ้อนในกรณีเลวร้ายที่สุดของ query engine ตัวอย่างเช่น อนุญาตทั้ง range scan และ point query ทั้งหมด แต่ join จะอนุญาตได้เฉพาะเมื่อทำระหว่างสองตารางใน colo เดียวกันและทำบน sharding key
มุมมองในอนาคต
- การห่อหุ้ม logical shard: ทีมต้องตัดสินใจว่าจะห่อหุ้ม logical shard อย่างไร โดยได้สำรวจการแยกข้อมูลด้วยการใช้ฐานข้อมูล Postgres แยกกัน หรือใช้ Postgres schema แยกกัน แต่น่าเสียดายที่วิธีนี้ทำให้การทำ logical sharding ยังต้องมีการเปลี่ยนแปลงข้อมูลทางกายภาพ ซึ่งซับซ้อนไม่ต่างจากการแบ่ง physical shard เอง
- การแสดง shard ผ่าน Postgres view: ทีมจึงเลือกแสดง shard เป็น Postgres view แทน โดยในแต่ละตารางสามารถสร้าง view ได้หลายตัว และแต่ละตัวจะสอดคล้องกับชุดย่อยของข้อมูลใน shard ที่กำหนด ซึ่งมีหน้าตาประมาณนี้:
CREATE VIEW table_shard1 AS SELECT * FROM table WHERE hash(shard_key) >= min_shard_range AND hash(shard_key) < max_shard_range) การอ่านและเขียนทั้งหมดจะทำผ่าน view เหล่านี้
- การสร้าง sharded view ครอบบนฐานข้อมูลกายภาพเดิมที่ยังไม่ sharding: วิธีนี้ทำให้สามารถทำ logical sharding ได้ก่อนที่จะต้องทำงาน physical reshard ที่มีความเสี่ยง แต่ละ view จะถูกเข้าถึงผ่านบริการ connection pooler แบบ sharded ของตัวเอง โดย connection pooler ยังคงชี้ไปยัง physical instance เดิมที่ยังไม่ sharding ทำให้ระบบดูเหมือนถูก sharding แล้ว ทีมค่อย ๆ ปล่อยการอ่านและเขียนแบบ sharded ผ่าน feature flag ของ query engine เพื่อลดความเสี่ยง และสามารถ rollback ได้ทุกเมื่อภายในไม่กี่วินาทีด้วยการส่งทราฟฟิกกลับไปยังตารางหลัก ก่อนลงมือทำ reshard ครั้งแรก ทีมจึงมั่นใจในความปลอดภัยของ topology แบบ sharded นี้ได้
- ความเสี่ยงของการพึ่งพา view: view เพิ่ม performance overhead และในบางกรณีอาจเปลี่ยนวิธีที่ Postgres query planner ใช้ optimize query ไปอย่างมีนัยสำคัญ เพื่อพิสูจน์แนวทางนี้ ทีมได้รวบรวม query corpus จาก production ที่ผ่านการทำให้ปลอดข้อมูลแล้ว และรัน load test ทั้งกรณีที่ใช้ view และไม่ใช้ view จากผลที่ได้พบว่าในกรณีส่วนใหญ่ view เพิ่ม overhead ด้านประสิทธิภาพเพียงเล็กน้อย และในกรณีแย่ที่สุดก็ยังต่ำกว่า 10% นอกจากนี้ ทีมยังสร้าง shadow read framework ที่ส่งทราฟฟิกการอ่านแบบเรียลไทม์ทั้งหมดผ่าน view แล้วเปรียบเทียบทั้งประสิทธิภาพและความถูกต้องกับ query แบบไม่ใช้ view ผลลัพธ์ยืนยันได้ว่า view เป็นทางออกที่ใช้งานได้จริงโดยมีผลกระทบต่อประสิทธิภาพเพียงเล็กน้อย
การแก้ปัญหา topology
- ให้ DBProxy เข้าใจ topology สำหรับการ route query: ระบบจำเป็นต้องให้ DBProxy เข้าใจ topology ของตารางและฐานข้อมูลกายภาพ เนื่องจากมีการแยกแนวคิดระหว่าง logical sharding และ physical sharding จึงต้องมีวิธีแทน abstraction เหล่านี้ไว้ใน topology
- การแมปตารางและ shard key: จำเป็นต้องมีวิธีแมปตาราง
users ไปยัง shard key คือ user_id และมีวิธีแมป logical shard ID (123) ไปยังฐานข้อมูล logical และ physical ที่ถูกต้อง
- vertical partitioning และการพึ่งพาไฟล์คอนฟิกที่ hardcode: ใน vertical partitioning ระบบอาศัยไฟล์คอนฟิกแบบ hardcode ที่เรียบง่ายเพื่อแมปตารางไปยังพาร์ทิชันที่เกี่ยวข้อง แต่การเปลี่ยนไปสู่ horizontal sharding ต้องการระบบที่ซับซ้อนกว่านั้น
- การเปลี่ยน topology แบบไดนามิกและความจำเป็นที่ DBProxy ต้องอัปเดตสถานะอย่างรวดเร็ว: ระหว่างการ split shard topology อาจเปลี่ยนแบบไดนามิก จึงจำเป็นให้ DBProxy อัปเดตสถานะได้อย่างรวดเร็วเพื่อหลีกเลี่ยงการ route request ไปยังฐานข้อมูลผิดตัว
- ความเข้ากันได้ย้อนหลังของการเปลี่ยน topology: การเปลี่ยน topology ทั้งหมดต้องรองรับ backward compatibility และต้องไม่มีการเปลี่ยนแปลงที่กระทบเส้นทางสำคัญของเว็บไซต์
- การสร้าง database topology ที่ห่อหุ้ม metadata ของ horizontal sharding ที่ซับซ้อน: ทีมได้สร้าง database topology ที่ห่อหุ้ม metadata อันซับซ้อนของ horizontal sharding และให้การอัปเดตแบบเรียลไทม์ได้ภายในเวลาไม่ถึง 1 วินาที
- การแยก logical topology ออกจาก physical topology เพื่อทำให้การจัดการฐานข้อมูลง่ายขึ้น: ช่วยลดต้นทุนและลดความซับซ้อนด้วยการใช้จำนวนฐานข้อมูลกายภาพให้น้อยลงในสภาพแวดล้อมที่ไม่ใช่ production ขณะยังคง logical topology แบบเดียวกับ production
- การบังคับใช้ invariants ภายใน topology ผ่านไลบรารี topology: ทีมรักษาความถูกต้องของระบบระหว่างการสร้าง horizontal sharding ด้วยการบังคับใช้ invariants ใน topology เช่น shard ID ทุกตัวต้องถูกแมปไปยังฐานข้อมูลกายภาพเพียงหนึ่งเดียวเท่านั้น
งาน physical sharding
- ขั้นตอนสุดท้ายหลังจากเตรียมตารางสำหรับ sharding เสร็จ: คือการ failover ทางกายภาพจากฐานข้อมูลที่ยังไม่ sharding ไปยังฐานข้อมูลที่ sharding แล้ว แม้จะสามารถนำ logic เดิมจำนวนมากกลับมาใช้ซ้ำสำหรับ horizontal sharding ได้ แต่ก็ยังมีความแตกต่างสำคัญบางจุด เช่น การย้ายจากฐานข้อมูลแบบ 1 ต่อ 1 ไปเป็น 1 ต่อ N
- ความจำเป็นในการเพิ่มความทนทานของกระบวนการ failover: ทีมต้องทำให้กระบวนการ failover มีความทนทานมากขึ้นเพื่อรับมือกับ failure mode แบบใหม่ที่งาน sharding อาจสำเร็จเพียงบางส่วนของฐานข้อมูลเท่านั้น
- ได้ลดความเสี่ยงส่วนใหญ่ไปแล้วระหว่าง vertical partitioning: เนื่องจากความเสี่ยงจำนวนมากถูกลดทอนไปตั้งแต่ช่วง vertical partitioning แล้ว ทีมจึงเดินหน้าไปสู่ physical sharding ครั้งแรกได้เร็วกว่าเดิมมาก
ตอนนี้อยู่ตรงไหนของเส้นทาง horizontal sharding
- การลงทุนระยะยาวหลายปีใน horizontal sharding: หลังตระหนักว่าจำเป็นต้องลงทุนหลายปีใน horizontal sharding เพื่อรองรับการขยายตัวในอนาคตของ Figma ทีมจึงเปิดใช้ตาราง horizontal sharding ตารางแรกในเดือนกันยายน 2023
- ดำเนินการ failover ได้สำเร็จ: บรรลุการ failover สำเร็จ โดยมีช่วงที่ฐานข้อมูล primary พร้อมใช้งานได้เพียงบางส่วนชั่วคราว 10 วินาที และไม่มีผลกระทบต่อความพร้อมใช้งานของ replica หลัง sharding แล้วก็ไม่พบ regression ด้าน latency หรือ availability
- การจัดการ shard ที่ซับซ้อนมากขึ้น: ฐานข้อมูลที่มีอัตราการเขียนสูงสุดถูก sharding ด้วย shard ที่ค่อนข้างเรียบง่าย แต่ในปีนี้ทีมมีแผนจะ sharding ฐานข้อมูลที่ซับซ้อนขึ้นเรื่อย ๆ ซึ่งประกอบด้วยหลายสิบตารางและจุดเรียกใช้งานในโค้ดนับพันจุด
- จำเป็นต้องทำ horizontal sharding ให้กับทุกตารางของ Figma: เพื่อปลดข้อจำกัดด้านการสเกลที่เหลืออยู่สุดท้ายและรองรับการเติบโตอย่างแท้จริง โลกที่ถูก horizontal sharding อย่างสมบูรณ์จะให้ประโยชน์หลายด้าน เช่น ความน่าเชื่อถือที่ดีขึ้น ต้นทุนที่ลดลง และความเร็วในการพัฒนาที่สูงขึ้น
- ปัญหาที่ยังต้องแก้:
- รองรับการอัปเดต schema ที่ทำ horizontal sharding แล้ว
- สร้าง ID ที่ไม่ซ้ำกันทั่วโลกสำหรับ primary key ที่ทำ horizontal sharding
- รองรับธุรกรรมข้าม shard แบบ atomic สำหรับ use case สำคัญทางธุรกิจ
- ดัชนีแบบ globally unique ที่กระจายตัวอยู่หลายจุด (ปัจจุบันรองรับเฉพาะดัชนีที่มี shard key รวมอยู่ด้วย)
- เพิ่มความเร็วในการพัฒนาด้วยโมเดล ORM ที่เข้ากันได้กับ horizontal sharding อย่างราบรื่น
- งาน reshard แบบอัตโนมัติเต็มรูปแบบที่สามารถสั่ง split shard ได้ด้วยการคลิกปุ่ม
- การทบทวนแนวทาง horizontal sharding บน RDS ที่ใช้อยู่เดิมอีกครั้ง: เส้นทางนี้เริ่มต้นขึ้นเมื่อ 18 เดือนก่อนภายใต้แรงกดดันด้านกำหนดเวลาที่ตึงตัวมาก ขณะเดียวกัน NewSQL store ก็มีการพัฒนาและเติบโตต่อเนื่อง ปัจจุบันทีมมีเวลามากพอที่จะประเมิน trade-off ใหม่ระหว่างการเดินหน้าต่อบนเส้นทางเดิมกับการย้ายไปใช้โซลูชันแบบ open source หรือ managed service
- ความคืบหน้าที่น่าตื่นเต้นในเส้นทาง horizontal sharding: แม้ยังมีความท้าทายอีกมากที่เพิ่งเริ่มต้นขึ้น ทีมคาดว่าจะมีการเจาะลึกองค์ประกอบต่าง ๆ ของ horizontal sharding stack เพิ่มเติมในอนาคต หากคุณสนใจโปรเจกต์ลักษณะนี้ ทีมกำลังเปิดรับสมัครอยู่
ความเห็นของ GN⁺
- ทีมฐานข้อมูลของ Figma ต้องการก้าวข้ามข้อจำกัดด้าน scalability ของฐานข้อมูลผ่าน horizontal sharding ซึ่งเป็นก้าวสำคัญต่อการเติบโตและการรักษาประสิทธิภาพของเครื่องมือทำงานร่วมกันบนคลาวด์
- horizontal sharding นำมาซึ่งความท้าทายใหม่ทั้งด้านการจัดการข้อมูลและการ optimize query ซึ่งต้องการความรู้และทักษะใหม่จากทั้งผู้ดูแลฐานข้อมูลและนักพัฒนา
- แม้ horizontal sharding จะช่วยเพิ่ม scalability ของฐานข้อมูลได้อย่างมาก แต่ก็ต้องมีแนวทางใหม่ในการจัดการ query ที่ซับซ้อนและการรักษาความสอดคล้องของข้อมูล
- โปรเจกต์โอเพนซอร์สที่มีความสามารถคล้ายกันคือ CitusDB ซึ่งให้ความสามารถในการขยาย Postgres database ในแนวนอน
- เมื่อนำเทคโนโลยี horizontal sharding มาใช้ ต้องพิจารณาทั้งความซับซ้อนของ data model ประสิทธิภาพของ query ความยืดหยุ่นของระบบ และการบำรุงรักษา ซึ่งหมายถึงการหาสมดุลระหว่าง scalability ของฐานข้อมูลกับความง่ายในการดูแลจัดการ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตารางขนาดใหญ่และข้อจำกัด IOPS ของ RDS
ผลลัพธ์ของการทำชาร์ดและต้นทุน
เวลาและต้นทุนที่ใช้ไปกับการทำชาร์ด
การเปรียบเทียบต้นทุนกับ YugabyteDB
ข้อเสนอให้แยกฐานข้อมูลตามลูกค้า
การสร้างเวอร์ชัน PG ที่คล้ายกับ Vitess ของ MySQL
การพิจารณา FoundationDB
แนวทางที่มองการชาร์ดเหมือนการแฮ็ก
ข้อสงสัยเรื่องการไม่ใช้ส่วนขยาย Citus
ความเป็นไปได้ในการใช้ Aurora Limitless
ความเข้าใจเกี่ยวกับฐานข้อมูล NoSQL
jsonbแต่เมื่อมีโมเดลข้อมูลที่ดีอยู่แล้ว ก็ไม่จำเป็นต้องใช้มากนักความเป็นผู้ใหญ่ของการชาร์ดและการพิจารณาโซลูชัน NewSQL
เทคโนโลยี Spanner ของ Google และการประเมินของ Figma