ทำไม Pantip ปิดปรับปรุงบ่อย บทเรียน ความเจ็บปวด และคราบน้ำตาจาก MongoDB

หลังจากที่ Pantip ปิดเพื่อปรับปรุงฐานข้อมูลไปเมื่อวันที่ 28 กันยายน เป็นเวลาถึง 22 ชั่วโมงเต็ม แต่หลังจากนั้นก็ยังมีช่วงเวลาปิดตอนกลางคืนระหว่างตีสองถึงหกโมงเช้าอยู่ทุกคืน จนหลายคนบ่นหงุดหงิดว่าทำไมเข้าเว็บไม่ได้ บล็อกนี้จะขอเล่าถึงบทเรียนที่เราได้รับจาก MongoDB

บางคนอาจเคยได้ยินมาก่อนหน้านี้ว่า Pantip เปลี่ยนระบบฐานข้อมูลจาก MySQL มาเป็น MongoDB คำถามคือทำไมเราถึงเลือกเปลี่ยนฐานข้อมูลจาก MySQL ที่ Web Developer คนไหนก็เขียนเป็น มาเป็น MongoDB ที่หาคนไทยที่เขียนเป็นได้น้อยมาก? เหตุผลหลักก็คือเรามีโจทย์ว่าอยากเก็บกระทู้ทั้งหมดไว้ในฐานข้อมูล กระทู้ไม่มีวันหมดอายุเหมือนสมัยก่อน ดังนั้น Scaling จึงเป็นเรื่องที่สำคัญมาก ซึ่งเราเชื่อว่าฐานข้อมูลที่ออกแบบมาเป็น Document-oriented มีความสามารถด้าน Scaling ที่ดีกว่าฐานข้อมูลแบบ Relational (ก่อนตัดสินใจใช้ MongoDB เรามีการทดลองด้านประสิทธิภาพด้วยการยิงโหลดทั้ง Read และ Write เพื่อวัดผลว่าจะรองรับได้แค่ไหน ปรากฎว่า MongoDB ให้ผลที่น่าอัศจรรย์แบบที่เราไม่เคยเห็นบน MySQL)

ส่วนเหตุผลรองลงมาที่เราเลิกใช้ MySQL ก็เพราะหลังจากที่ถูกซื้อไปโดย Oracle เราก็ไม่มั่นใจในอนาคตของมัน และที่เราตัดสินใจใช้ MongoDB ก็เพราะ Foursquare ใช้เก็บข้อมูลการเช็คอินของผู้ใช้ และ Craigslist ใช้เก็บข้อมูลประกาศมากกว่าสองพันล้านรายการ

เมื่อตัดสินใจว่าจะร่วมหอลงโรงกับ MongoDB แล้ว เราก็เริ่มเรียนรู้วิธีการเขียน Query บน MongoDB ซึ่งก็ไม่ยากมากเมื่อเทียบกับ MySQL แต่ที่ยากกว่าคือการออกแบบ Database Schema เพราะ MongoDB มีความยืดหยุ่นในการออกแบบสูงกว่า MySQL ในโลกของ MySQL เรามี Primary Key, Foreign Key สำหรับเชื่อมข้อมูลหลายตารางเข้าด้วยกัน แต่ในโลกของ MongoDB เราอาจกระจายข้อมูลไว้ในหลายตาราง (MongoDB เรียกว่า Collection) แล้วเชื่อมกันด้วย Reference ก็ได้ หรือเราจะเก็บเป็น Embedded Document ก็ได้ ซึ่งแต่ละแบบก็มีข้อดีข้อเสียแตกต่างกันไป ทำให้เราต้องตัดสินใจ “Trade-off” (เมื่ออยู่ในโลกของ MongoDB จงทำความคุ้นเคยกับคำนี้เอาไว้)

การเป็น Web Developer ทั่วไป แค่มี Skill ด้านการเขียน Query กับการออกแบบ Schema ก็ถือว่าหรูแล้ว (บางที่การออกแบบ Schema เป็นงานของ SA) แต่ในโลกของ MongoDB การมี Skill เพียงเท่านี้ถือว่ายังไม่พอ เพราะคุณต้องมี Skill ด้าน Scaling ด้วย

ผมขอปูพื้นฐานด้านการ Scaling ก่อนนะครับ เว็บ Pantip มีจำนวนกระทู้และความคิดเห็นใหม่เป็นหลักหลายหมื่นต่อวัน และมีจำนวนการเปิดอ่านกระทู้เป็นหลักสิบล้านต่อวัน ถ้าเราเอาทั้งหมดนี้ใส่ลงไปในเซิร์ฟเวอร์เพียงเครื่องเดียว เซิร์ฟเวอร์เครื่องนั้นก็คงเป็นเทพจุติลงมาเกิดเป็นคอมพิวเตอร์ แล้วถ้า Pantip มีจำนวนข้อความใหม่เป็นหลักแสนต่อวัน มีจำนวนการเปิดอ่านกระทู้เป็นหลักร้อยล้านต่อวัน เราจะทำยังไงกับเซิร์ฟเวอร์เทพเครื่องนี้ดี?

1. อัด CPU อัด RAM อัด Storage เพื่อเปลี่ยนเซิร์ฟเวอร์เทพให้เป็นเซิร์ฟเวอร์บิดาแห่งเทพทุกสถาบัน หรือเราเรียกว่า Vertical Scaling หรือ Scale Up

2. ซื้อเซิร์ฟเวอร์โง่ๆ หลายๆ ตัว มาใส่เพิ่มแบบง่ายๆ แล้วให้ระบบมันกระจายโหลดไปที่เซิร์ฟเวอร์แต่ละตัวได้แบบฉลาดๆ หรือเราเรียกว่า Horizontal Scaling หรือ Scale Out

สิ่งที่ Database ทุกตัวทำได้อยู่แล้วคือ Vertical Scaling แต่สิ่งที่ MongoDB ถนัดมากคือ Horizontal Scaling ซึ่ง MongoDB เรียกว่า Sharding

การทำ Sharding คือการบอก MongoDB ว่า ข้อมูลกระทู้นี้ให้เก็บเครื่องหมายเลข 1 นะ ส่วนข้อมูลอีกกระทู้ให้เอาไปเก็บที่เครื่องหมายเลข 2 เวลาที่มีคนเปิดอ่านกระทู้นี้ก็ไปดูที่เครื่อง 1 แต่ถ้าอ่านอีกกระทู้ก็ไปดูเครื่อง 2

การแบ่งข้อมูลเป็น Shard

สิ่งที่จะบอก MongoDB ว่าให้เอาข้อมูลนี้ไปเขียนที่เครื่องไหน หรือบอกให้อ่านข้อมูลนี้จากเครื่องไหน เรียกว่า Shard Key ซึ่งเป็นฟิลด์หนึ่งที่อยู่ใน Collection

การเลือก Shard Key ให้ดี จะนำมาซึ่งความผาสุขของปวงชนชาวเว็บ แต่ถ้าเลือกผิด สิ่งที่ตามมาคือความเจ็บปวดและคราบน้ำตา

แบ่งข้อมูลด้วย Shard Key

อย่างที่บอกไปตอนแรกว่าการเขียน Query และออกแบบ Schema ให้ดีได้ก็ถือว่าหรูแล้ว แต่พอมาถึงขั้น Scaling มันเป็นสิ่งที่ทีม Developer แทบไม่มีประสบการณ์เลย อาศัยอ่านจาก Manual ก็นึกภาพออกบ้าง ไม่ออกบ้าง

สิ่งที่เราต้องตัดสินใจเมื่อโหลดของเว็บเพิ่มสูงขึ้นจนต้องทำ Scaling ก็คือ “จะเลือกฟิลด์ไหนเป็น Shard Key ดี?”

ผมขอยกตัวอย่างแบบเกือบเหมือนจริงนะครับ Schema การเก็บข้อมูลความคิดเห็นของแต่ละกระทู้ เราเก็บแยกเป็น 1 ความคิดเห็นต่อ 1 Document (Document คล้ายๆ Record ใน MySQL) โดยแต่ละ Document มีฟิลด์ชื่อ topic_id ที่เก็บเลขกระทู้ซึ่งใช้บอกว่าความคิดเห็นนี้อยู่ในกระทู้เบอร์อะไร และฟิลด์ชื่อ comment_no สำหรับเก็บลำดับความคิดเห็นนี้ในกระทู้นั้นๆ

Schema ของความคิดเห็น

เราเลือก Shard Key แบบ Compound จากสองฟิลด์นี้ โดยเราคาดหวังว่าเมื่อมีความคิดเห็นใหม่เข้ามา MongoDB จะนำข้อมูลไปเก็บลงใน Shard แบบกระจายกันไป เวลามีคนอ่านกระทู้เยอะๆ อย่างตอนบ่ายสามหรือสามทุ่ม เราก็หวังว่าข้อมูลจะถูกอ่านแบบกระจายด้วย

อนิจจา… โลกความจริงไม่ได้สวยงามอย่างที่ฝันไว้ เพราะเมื่อเราเริ่ม Scaling ไปได้สักพัก เราก็พบว่าโหลดมันหนักอยู่ที่ Shard เดียว ขณะที่อีก Shard นอนกลางวันสบายใจเฉิบ

หลังจากขอคำปรึกษาจาก MongoDB, Inc. เราก็ได้รู้ว่า Shard Key ที่เราเลือก มันทำให้เกิดปัญหาที่เรียกว่า Hot Chunk นี่เป็นการเสียน้ำตาครั้งแรก

ปัญหา Hot Chunk เกิดขึ้นเมื่อเราเลือก Shard Key ที่ “คาดเดาได้” ซึ่งคำว่าคาดเดาได้นั้นหมายถึงเรารู้ว่าค่ามันมีแนวโน้มที่จะเพิ่มขึ้นเรื่อยๆ เพราะกระทู้ Pantip เพิ่มขึ้นทุกวินาที ความคิดเห็นใหม่ที่เข้ามามักจะเกิดขึ้นกับกระทู้ใหม่ๆ ดังนั้น ข้อมูล topic_id และ comment_no จึงมีแนวโน้มเพิ่มขึ้น ซึ่ง MongoDB มันจะเอาข้อมูลที่มี Shard Key มากกว่าไปเก็บไว้ใน Chunk ขวาสุดตลอด (Chunk คือหน่วยเก็บข้อมูลที่อยู่ใน Shard ถ้าหนึ่ง Shard มีพื้นที่ 1 TB โดยแต่ละ Chunk มีขนาด 64 MB แปลว่า Shard นี้สามารถเก็บข้อมูลได้ 16,384 Chunk)

เมื่อความคิดเห็นใหม่มักจะเกิดในกระทู้ใหม่ Shard Key ที่เลือกไว้คือ topic_id และ comment_no ก็เลยมีค่าเพิ่มขึ้นเรื่อยๆ (คาดเดาได้) ทำให้ MongoDB จับโยนให้ Chunk ขวาสุด ก็กลายเป็นว่าโหลดมันไปหนักอยู่ที่ Shard เดียว นี่แหละคือปัญหา Hot Chunk

Hot Chunk

Hot Chunk จะยิ่งหนักขึ้นเรื่อยๆ ตามปริมาณความคิดเห็นใหม่ที่เข้ามา แปลว่าถ้าเราปล่อยปัญหานี้ไว้ให้นานกว่านี้ มันจะยิ่งแก้ไขยากกว่านี้ เราเลยตัดสินใจหยุดพัฒนาฟีเจอร์ใหม่ที่เกี่ยวข้องกับ Database เพื่อแก้ปัญหานี้ให้ได้ก่อน ซึ่งการแก้ปัญหาก็ค่อนข้างโหดร้าย เพราะเราต้องเปลี่ยน Shard Key ใหม่ แล้ว MongoDB มันไม่ยอมให้เปลี่ยน Shard Key ไง วิธีที่จะเปลี่ยน Shard Key ได้ก็คือ 1. ปิดเว็บ 2. Dump ข้อมูลออกมา 3. ลบข้อมูลออก 4. สร้าง Collection ใหม่ 5. Restore ข้อมูลเข้าไป 6. กำหนด Shard Key ใหม่ นี่คือคำอธิบายว่าวันที่ 28 กันยายน เราปิดเว็บ 22 ชั่วโมงไปเพื่ออะไร (ตอนนั้น Collection ที่เก็บความคิดเห็นมีขนาดประมาณ 14 ล้าน Document และยังมี Collection อื่นๆ ที่เรามีการเปลี่ยน Shard Key ใหม่ไปพร้อมกันอีกมากกว่า 100 ล้าน Document)

หลังจากที่เรา Restore ข้อมูลเสร็จแล้ว เรารีบเปิดให้บริการทันที ซึ่งตอนนั้นข้อมูลยังรวมอยู่ใน Shard เดียวกันอยู่ เพราะเรายังไม่ได้ทำ Balancing เพื่อกระจายข้อมูล ส่งผลให้ตอนนั้นเข้าเว็บแทบไม่ค่อยได้ การทำ Balancing ใช้เวลานานมาก ไม่ใช่แค่หลักชั่วโมงหรือหลักวัน แต่เป็นหลักสัปดาห์หรืออาจจะหลักเดือน เราเคยลองทำ Balancing ในขณะที่กำลังเปิดให้บริการเว็บอยู่ ก็พบปัญหาว่าข้อมูลเละ เปิดกระทู้แล้วเจอความคิดเห็นเบิ้ลกัน ก็เลยตัดสินใจว่าจะปิดเว็บช่วงตีสองถึงหกโมงเช้าเพื่อทำ Balancing ซึ่งตอนนี้ก็ยังทำไม่เสร็จ แถมเรายังเจอปัญหาใหม่เข้า…

ปัญหาใหม่ที่เราพบคือการเลือก Shard Key ผิดอีกแล้ว แต่คราวนี้ไม่ใช่ Hot Chunk เพราะรอบนี้เราใช้ Hashed Shard Key ซึ่งเป็นวิธีการใช้ Shard Key แบบ Random ที่ไม่มีทางคาดเดาได้ วิธีง่ายๆ ที่เราทำคือการเอา topic_id และ comment_no ไปเข้าฟังก์ชั่น md5() เราก็จะได้ข้อมูลอะไรก็ไม่รู้ ประกอบด้วยตัวเลขและตัวอักษร 32 ตัว มากบ้าง น้อยบ้าง ปนเปกันไป ไม่มีรูปแบบ

Shard Key ใหม่นี้ช่วยกระจายการเขียนข้อมูลได้อย่างยอดเยี่ยม ข้อมูลถูกกระจายระหว่างสอง Shard แบบสมบูรณ์ แต่ปัญหาใหม่ของเราคือข้อมูลมันกระจายตัวมากเกินไป หน้าแรกของกระทู้แสดง 100 ความคิดเห็น เวลา Query เราก็จะหยิบความคิดเห็นที่มี topic_id ตามที่กำหนด และระบุว่า comment_no ตั้งแต่ 1 ถึง 100 แต่ Document ที่เก็บความคิดเห็นถูกเก็บแบบกระจายอย่างสมบูรณ์ ทำให้ต้องอ่านข้อมูลจากหลาย Chunk บางกระทู้อ่านมากถึง 93 Chunk เพื่อใช้สร้างเป็นกระทู้เพียง 1 กระทู้ แสดงไปให้ผู้ใช้เพียง 1 คน แต่ Active User ในแต่ละวินาทีมีตั้งไม่รู้กี่ร้อยกี่พัน ส่งผลให้ I/O ทำงานหนักมาก ธรรมชาติของ Harddisk เหมาะกับงานอ่านข้อมูลแบบ Sequential แต่พอเจอการอ่านแบบ Random ก็แทบจะไปไม่ไหว เลยต้องแก้ขัดด้วยการซื้อ SSD มาใส่ ซึ่งช่วยบรรเทาปัญหาลงได้พอสมควร

Hashed Shard Key

วิธีแก้เรื่องนี้ก็คือต้องเหนื่อยเปลี่ยน Shard Key กันอีกรอบ คราวนี้ต้องเลือก Shard Key ที่อยู่กึ่งกลางระหว่างการกระจายตัวอย่างสมบูรณ์และการคาดเดาได้ ซึ่งก็คือเราต้องการให้ Document ที่เก็บความคิดเห็นในกระทู้เดียวกัน กระจุกตัวอยู่ใน Chunk เดียวกัน แต่ถ้าเป็นความคิดเห็นในอีกกระทู้ ก็ให้ไปอยู่ Chunk อื่น

Shard ที่ควรจะเป็น

ตอนนี้มีแนวทางแล้วว่า Shard Key ที่เหมาะสมควรใช้ตัวไหน และจะใช้เทคนิค Pre-splitting หรือการตั้งใจระบุไปเลยว่า Shard Key ไหนจะไปอยู่ที่ Shard ไหน โดยที่ไม่ต้องให้ MongoDB คิดให้ ซึ่งน่าจะช่วยให้ Down Time รอบนี้น้อยกว่า 22 ชั่วโมง ตอนนี้กำลังอยู่ระหว่างศึกษาว่าค่า Shard Key ที่อยู่กึ่งกลางที่สุดคือค่าไหน มีความเป็นไปได้ว่ารอบนี้อาจใช้เวลาแค่ 4 ชั่วโมง ก็คือการหาวันปิดเว็บช่วงตีสองถึงหกโมงเช้าในคืนที่ไม่มีบอล

ส่วน Balancing ที่ทำทุกคืนติดต่อกันมาหลายคืน ล่าสุดตอนนี้หยุดทำแล้วเพื่อรอการแก้ Shard Key อีกครั้ง ดังนั้นตั้งแต่คืนนี้สามารถเข้า Pantip ได้ตามปกติแล้วครับ

สรุปว่าตอนนี้เรารู้สึกยังไงกับ MongoDB? มันยังน่าใช้อยู่มั้ย? ส่วนตัวผมมองว่าเราต้องเข้าใจจุดแข็งจุดอ่อนของมัน มีหลายคนเคยถามผมว่าเขาควรใช้ MongoDB บ้างดีมั้ย ผมมักจะตอบว่าต้องดูว่า App ที่คุณทำเป็นยังไง คือถ้าทำพวก Banking ต้องมี Transaction ผมจะแนะนำว่าไม่ควรใช้ เพราะ MongoDB ตัดฟีเจอร์ Transaction ออกเพื่อแลกกับความเร็วที่เพิ่มขึ้น (Trade-off) หรือถ้าคุณทำ App ที่มีจำนวนผู้ใช้ในเวลาเดียวกันไม่มาก ไม่แคร์เรื่องการ Scaling ทั้ง Read และ Write ผมว่าใช้ MySQL หรือ MariaDB ก็ดีอยู่แล้ว แต่ถ้า App ของคุณมีลักษณะการเก็บข้อมูลเป็นเอกสาร เช่น กระทู้ บล็อก ประกาศขายสินค้า ที่ไม่ต้องเอาเอกสารไปสร้าง Relation กับข้อมูลอื่นๆ มาก และมีความต้องการขยายข้อมูลออกไปเรื่อยๆ ไม่มีการลบข้อมูลเก่าออกจากระบบ มีผู้ใช้เข้าใช้งานหนาแน่น แบบนี้ค่อยลองพิจารณา MongoDB ครับ

และจำไว้ว่าสิ่งที่คุณจำเป็นต้องรู้เกี่ยวกับ MongoDB คือ

1. CRUD Operations เรียนรู้ว่าจะสร้างข้อมูลยังไง เรียกดูข้อมูลยังไง แก้ไขข้อมูลยังไง และลบข้อมูลยังไง

2. Data Modelling เรียนรู้ว่าจะออกแบบโครงสร้างฐานข้อมูลยังไงให้เหมาะกับพฤติกรรมการใช้งานของ App มี Trade-off มากมายที่ต้องตัดสินใจในขั้นตอนนี้ อยากให้อ่านได้เร็วต้องแลกกับการแก้ไขข้อมูลที่ยุ่งยากขึ้น อยากให้ยืดหยุ่นสูงก็ต้องแลกกับความเร็วในการอ่านที่ลดลง

3. Scaling เรียนรู้ว่า App เรามีรูปแบบการเกิดของข้อมูลใหม่ยังไง มีรูปแบบการอ่านข้อมูลยังไง หาจุดที่เหมาะสมให้พบ เลือก Shard Key ให้ดี เลือกผิดทีเหนื่อยจุงเบย

ถ้าอ่านแล้วชอบ ฝากแชร์ด้วยนะครับ
  •  
  •  
  •  
  •  
  •  
  •  
  •  

,