ขยายพื้นที่จัดเก็บออนไลน์อย่างรวดเร็วเพื่อรองรับผู้ใช้ ChatGPT กว่า 1 พันล้านคน
เราใช้ Python ปรับแพลตฟอร์มพื้นที่จัดเก็บข้อมูลสำหรับแอปพลิเคชัน Habitat อย่างไร เพื่อรองรับการเติบโตในระดับที่ไม่เคยเกิดขึ้นมาก่อน
โดย Jon Lee, Chaomin Yu และ Ben Ries สมาชิกฝ่ายเทคนิค
ผลิตภัณฑ์ทุกตัวของ OpenAI ต้องอาศัยการเข้าถึงข้อมูลที่รวดเร็วและเชื่อถือได้ ไม่ว่าจะเป็นตอนที่ผู้ใช้เข้าสู่ระบบ ตรวจสอบการตั้งค่า Codex หรือเริ่มบทสนทนาใหม่ใน ChatGPT การดำเนินการแต่ละอย่างอาจต้องค้นหาข้อมูลแยกกันหลายครั้งก่อนที่ผลิตภัณฑ์จะตอบสนองได้ หากคำขอเหล่านี้ทำงานช้า ผู้ใช้ก็จะรู้สึกว่าผลิตภัณฑ์ทำงานช้า และหากคำขอเหล่านี้ล้มเหลว ผลิตภัณฑ์ก็จะหยุดทำงานโดยสิ้นเชิง
Habitat คือแพลตฟอร์มพื้นที่จัดเก็บข้อมูลออนไลน์ที่เราสร้างขึ้นเพื่อให้ผลิตภัณฑ์ของ OpenAI เข้าถึงข้อมูลที่จำเป็นได้อย่างรวดเร็วและเชื่อถือได้ ปัจจุบัน Habitat รองรับคำขอมากกว่า 70 ล้านครั้งต่อวินาที เพื่อรองรับผลิตภัณฑ์ที่มีผู้ใช้งานมากกว่า 1 พันล้านคนต่อสัปดาห์ ครอบคลุมเกือบ 40 ภูมิภาคทั่วโลก Habitat เปิดตัวครั้งแรกเพื่อรองรับ GPTs ในงาน DevDay ปี 2566 โดยเริ่มต้นจากไลบรารี Python แบบง่ายๆ ที่ทำงานฝั่งไคลเอ็นต์และเชื่อมต่อกับฐานข้อมูลเพียงแห่งเดียว ปัจจุบัน Habitat ได้พัฒนาเป็นระบบแบบกระจายที่ซับซ้อนและรองรับข้อมูลมากกว่า 500 เพตะไบต์
ภาพ 01 · Habitat คืออะไร
แพลตฟอร์มพื้นที่จัดเก็บออนไลน์
Habitat คือแพลตฟอร์มพื้นที่จัดเก็บออนไลน์ที่เราสร้างขึ้น เพื่อให้ผลิตภัณฑ์ของ OpenAI เข้าถึงข้อมูลที่จำเป็นได้อย่างรวดเร็วและเชื่อถือได้
- คำขอ
- การตอบกลับ
- การเปลี่ยนแปลง (CDC)
การสร้างและดูแลโครงสร้างพื้นฐานในระดับนี้ไม่ใช่เรื่องง่าย แต่ก็ไม่ได้เป็นเรื่องที่ท้าทายเกินไป สิ่งที่ทำให้สถานการณ์ของเราแตกต่างคือ เราต้องขยายระบบด้วยความเร็วอย่างที่ไม่เคยเกิดขึ้นมาก่อน เพื่อรองรับการเติบโตของผู้ใช้และความต้องการใช้งานผลิตภัณฑ์ที่เพิ่มขึ้นอย่างมหาศาล พร้อมกับพัฒนาแพลตฟอร์มให้แข็งแกร่งและสมบูรณ์ขึ้นไปพร้อมกัน ปกติวิศวกรระบบจะสร้างเผื่อการเติบโตไว้ 10 เท่า และหวังว่าระบบนั้นจะรองรับไปได้อีกสองสามปี ระหว่างที่เตรียมรับการเติบโตอีก 10 เท่าในรอบถัดไป แต่สำหรับเรา การเติบโตมากกว่า 10 เท่าเกิดขึ้นทุกปีตลอดสามปีที่ผ่านมา การสร้างและดูแล Habitat จึงเต็มไปด้วยการตัดสินใจระยะสั้นและการจัดลำดับว่าจะต้องทำอะไรก่อนหลัง เราต้องเจาะลึกทุกองค์ประกอบของระบบเพื่อดึงศักยภาพจากระบบที่มีอยู่ให้ได้มากที่สุด พร้อมกับประคองระบบผ่านช่วงที่ความจุของพื้นที่จัดเก็บและทรัพยากรประมวลผลใกล้ถึงขีดจำกัด เพื่อให้มีเวลาสำหรับการลงทุนวางรากฐานที่จะรองรับระบบในระยะยาว
- 70M+
คำขอต่อวินาที
- 1B+
คนต่อสัปดาห์
- 500 PB+
ข้อมูล
เมื่อ OpenAI เติบโตขึ้น Habitat ก็ต้องเติบโตตามไปด้วย เริ่มจากการพัฒนาให้มีความน่าเชื่อถือเพียงพอสำหรับรองรับทราฟฟิกของผลิตภัณฑ์ที่มีความสำคัญต่อการทำงาน จากนั้นต้องเร็วพอสำหรับผู้ใช้ทั่วโลก และท้ายที่สุดต้องสามารถทำงานในระดับมหาศาลได้อย่างมีประสิทธิภาพ บทความนี้เป็นตอนแรกของบทความสองตอนที่เล่าถึงวิธีที่เราขยายระบบพื้นที่จัดเก็บข้อมูลออนไลน์ โดยในตอนนี้ เราจะเล่าว่า Habitat พัฒนามาอย่างไร เหตุใดเราจึงเปลี่ยนจากไลบรารีให้เป็นบริการ และเราผลักดันขีดความสามารถของบริการที่เขียนด้วย Python ซึ่งเป็นภาษาที่ไม่ค่อยใช้สำหรับสแต็กการให้บริการ จนกลายเป็นเลเยอร์แพลตฟอร์มพื้นที่จัดเก็บข้อมูลที่เชื่อถือได้อย่างไร
ในบทความครั้งต่อไป เราจะเจาะลึกถึงวิธีที่เราสร้างความน่าเชื่อถือให้กับระบบที่รองรับผู้ใช้งานหลายกลุ่มบนระบบเดียวกันในระดับใหญ่ กลยุทธ์แบบหลายชั้นที่เราใช้เพื่อเพิ่มประสิทธิภาพการอ่านข้อมูล และวิธีที่เราขยายความร่วมมือกับ Azure Cosmos DB เพื่อรองรับความต้องการในระดับที่ไม่เคยเกิดขึ้นมาก่อนได้อย่างน่าเชื่อถือ
Habitat เริ่มต้นจากแนวคิดง่ายๆ ว่า วิศวกรผลิตภัณฑ์ไม่ควรต้องกังวลเรื่องการจัดการฐานข้อมูล Habitat เปิดตัวครั้งแรกเพื่อรองรับ GPTs ในงาน DevDay ปี 2566 ในรูปแบบไลบรารี Python ขนาดเล็กที่ทำงานร่วมกับเซิร์ฟเวอร์หลักของ ChatGPT โดยรองรับการดำเนินการพื้นฐานเพียงไม่กี่อย่าง ซึ่งเบื้องหลังจะเชื่อมโยงไปยังระบบฐานข้อมูล Azure Cosmos DB
หน้าที่ของไลบรารีนี้คือช่วยให้ทีมผลิตภัณฑ์จัดเก็บและเรียกดูข้อมูลได้อย่างง่ายดาย โดยไม่จำเป็นต้องเข้าใจรายละเอียดของระบบที่ทำงานอยู่เบื้องหลัง Habitat จะจัดการงานที่จำเป็นให้ทั้งหมด ไม่ว่าจะเป็นการระบุว่าข้อมูลนั้นเป็นประเภทใด ควรดึงข้อมูลมาจากที่ใดหรือส่งไปที่ใด คำขอนั้นได้รับอนุญาตหรือไม่ และอื่นๆ
วิศวกรผลิตภัณฑ์ไม่จำเป็นต้องกังวลกับการค้นหา Schema การกำหนดเส้นทาง การตรวจสอบสิทธิ์ การเข้ารหัส การแปลงข้อมูลให้อยู่ในรูปแบบที่จัดเก็บหรือส่งต่อได้ การจัดรูปแบบคำขอ และการจัดการกลุ่มการเชื่อมต่อ พวกเขาไม่จำเป็นต้องคิดด้วยซ้ำว่าข้อมูลมาจากที่ใด ไม่ว่าจะเป็น Azure Cosmos DB แคช หรือพื้นที่จัดเก็บข้อมูลประเภทอื่น
ภาพ 02 · บริการ Habitat
โฟลว์คำขอ Habitat แบบย่อ
การแยกตรรกะพื้นที่จัดเก็บออกเป็นบริการอิสระทำให้เรามีจุดควบคุมเดียวสำหรับการติดตั้งใช้งาน การสังเกตการณ์ และการปรับปรุงแพลตฟอร์ม
- คำขอ
- การตอบกลับ
ไลบรารี Python นี้ตอบโจทย์ได้ดี ทำให้วิศวกรผลิตภัณฑ์ของ OpenAI นำ Habitat ไปใช้งานกันอย่างรวดเร็ว ทั้งที่ไม่มีการผลักดันจากส่วนกลางอย่างเป็นระบบให้เปลี่ยนจากการใช้ Postgres และ Azure Cosmos DB แบบบริการตนเองมาใช้ Habitat
เมื่อความต้องการของผลิตภัณฑ์เปลี่ยนแปลงไป นักพัฒนาผลิตภัณฑ์ก็สามารถเพิ่มการรองรับฟีเจอร์ต่างๆ ลงในไลบรารีที่ใช้ร่วมกันได้อย่างง่ายดาย เช่น การแคชฝั่งไคลเอ็นต์ การบีบอัดข้อมูล หรือการเข้ารหัส
ภายในกลางปี 2568 Habitat ในรูปแบบการใช้งานฝั่งไคลเอ็นต์ก็มาถึงขีดจำกัด เมื่อเลเยอร์ Habitat ซับซ้อนขึ้นและ OpenAI มีบริการเพิ่มขึ้น การเปลี่ยนแปลงโปรโตคอลที่เข้ากันได้กับเวอร์ชันก่อนหน้าก็ไม่สามารถทำได้อีกต่อไป
ครั้งหนึ่ง เราต้องการจำกัดผลกระทบหากภูมิภาคใดภูมิภาคหนึ่งหยุดให้บริการ เพื่อปกป้องชุดข้อมูลที่สำคัญที่สุดของเรา จึงย้ายชุดข้อมูลเหล่านั้นไปยังบัญชี Azure Cosmos DB หลายบัญชีที่กระจายอยู่ในภูมิภาคต่างๆ การดำเนินการนี้จำเป็นต้องเพิ่มตรรกะการกำหนดเส้นทางในไคลเอ็นต์ โดยซ่อนการทำงานใหม่ไว้หลังฟีเจอร์แฟล็กที่ยังปิดอยู่ จากนั้นจึงทยอยเผยแพร่การเปลี่ยนแปลงให้ครอบคลุมไคลเอ็นต์ทั้งหมด ก่อนเปิดฟีเจอร์แฟล็กเพื่อเริ่มใช้งาน
การประสานงานเพื่อนำการเปลี่ยนแปลงไปใช้กับบริการหลายสิบรายการ รวมถึงการทำงานร่วมกับแต่ละทีมให้ปรับใช้การเปลี่ยนแปลงดังกล่าว ต้องใช้เวลาหลายวัน ก่อนที่จะเปิดใช้งาน เราพบว่าควรเพิ่มการทดสอบแบบคู่ขนาน เพื่อตรวจสอบว่าการแบ่งข้อมูลทำงานได้ถูกต้อง กว่าจะนำการเปลี่ยนแปลงนี้ไปใช้ได้ครบก็ต้องใช้เวลาเพิ่มอีกสองสามวัน จากนั้นเราก็พบข้อผิดพลาดที่ต้องแก้ ซึ่งหมายความว่าต้องใช้เวลาเพิ่มอีกสองสามวันอีกครั้ง ในที่สุด เมื่อทุกอย่างพร้อมและเรากำลังจะเปิดฟีเจอร์แฟล็ก ทีมหนึ่งกลับนำบริการเวอร์ชันก่อนหน้ามาใช้งานด้วยเหตุผลอื่นที่ไม่เกี่ยวข้อง แต่บริการเวอร์ชันนั้นยังใช้ไคลเอ็นต์ที่มีข้อผิดพลาดอยู่ จึงทำให้ระบบหยุดให้บริการ ซึ่งเป็นเหตุการณ์ที่เราพยายามอย่างหนักมาตลอดเพื่อหลีกเลี่ยง
การเปลี่ยนแปลงไลบรารีฝั่งไคลเอ็นต์ทำให้เราต้องประสานงานที่ซับซ้อนกับบริการหลายสิบรายการ และเมื่อเวลาผ่านไป กระบวนการนี้ก็ยิ่งเปราะบาง ไม่มีประสิทธิภาพ และมีโอกาสเกิดปัญหาในการดำเนินงานมากขึ้น เพื่อลดจำนวนบริการที่ต้องเข้าไปจัดการทุกครั้งที่นำการเปลี่ยนแปลงใหม่ขึ้นใช้งานในอนาคต เราจึงตัดสินใจแยก Habitat ออกมาเป็นบริการของตัวเอง
การแยกตรรกะการจัดเก็บข้อมูลออกมาเป็นบริการที่ทำงานแยกต่างหาก ทำให้เรามีจุดศูนย์กลางเพียงแห่งเดียวสำหรับควบคุมการนำระบบขึ้นใช้งาน การติดตามสถานะและการทำงานของระบบ รวมถึงการปรับปรุงแพลตฟอร์ม แทนที่จะต้องจัดการการอัปเดตที่กระจัดกระจายอยู่ตามบริการต่างๆ เราสามารถปรับปรุงระบบจากส่วนกลางได้โดยตรง ทำให้ผลิตภัณฑ์ทุกตัวของ OpenAI ได้รับประโยชน์จากการปรับปรุงเหล่านั้นทันที
การรวมระบบไว้ในบริการส่วนกลางยังทำให้เรามีจุดควบคุมเพียงจุดเดียวสำหรับบังคับใช้มาตรการด้านความปลอดภัยและความเป็นส่วนตัวของข้อมูลอย่างเข้มงวดที่สุด Habitat เป็นจุดที่เราสามารถบังคับใช้นโยบายควบคุมการเข้าถึงจากส่วนกลาง บันทึกกิจกรรมเพื่อตรวจสอบย้อนหลัง และจำกัดการเข้าถึงทรัพยากรพื้นที่จัดเก็บข้อมูลที่อยู่เบื้องหลัง เช่น Azure Cosmos DB ได้ Habitat จึงมีบทบาทสำคัญในการปกป้องข้อมูลของผู้ใช้และป้องกันการเข้าถึงโดยไม่ได้รับอนุญาต ไม่ว่าจะมาจากภายนอก ภายใน หรือเอเจนต์
เรารู้ว่าจำเป็นต้องแยกระบบนี้ออกมาเป็นบริการ แต่ในตอนนั้นยังไม่อยากเลิกใช้ Python แม้จะรู้ว่าการใช้ Python ในลักษณะนี้ทำให้ระบบต้องใช้ทรัพยากรมากขึ้นก็ตาม สำหรับบริการที่ต้องรองรับปริมาณงานมหาศาล Python ทำให้การรับส่งข้อมูลผ่านเครือข่ายช้าลง และยิ่งขยายระบบก็ยิ่งต้องเพิ่ม CPU และหน่วยความจำมากกว่าการเรียกใช้ไลบรารีโดยตรง นอกจากนี้ เรายังรู้ว่าหากระบบต้องรองรับปริมาณงานเพิ่มขึ้นถึง 100 เท่า ข้อจำกัดด้านประสิทธิภาพของ Python จะกลายเป็นปัญหาที่ไม่อาจมองข้ามได้ และสุดท้ายเราก็คงต้องเขียนระบบใหม่แทบทั้งหมด
อย่างไรก็ตาม เรามองว่านี่เป็นการยอมสร้างหนี้ทางเทคนิคอย่างมีกลยุทธ์ เพราะเป้าหมายหลักในตอนนั้นไม่ใช่การลดต้นทุนหรือใช้ทรัพยากรให้มีประสิทธิภาพสูงสุด แต่เป็นการขจัดอุปสรรคที่ขัดขวางการทำงานของนักพัฒนาผลิตภัณฑ์และทำให้แพลตฟอร์มมีเสถียรภาพ การยอมแลกกับประสิทธิภาพบางส่วนจากการใช้ Python เป็นบริการในระยะสั้น ทำให้เรามีเวลาไปจัดการกับปัญหาที่เร่งด่วนกว่า วาง API หลักของระบบให้ชัดเจน และสร้างโครงสร้างพื้นฐานที่แข็งแกร่งรองรับการเติบโตในอนาคต
เรายังตัดสินใจบนความเสี่ยงที่ประเมินมาแล้วว่า โมเดลเขียนโค้ดของเราที่พัฒนาอย่างรวดเร็วจะช่วยลดความซับซ้อนทางเทคนิคในอนาคต เราเชื่อว่าเมื่อถึงเวลาที่ต้องย้ายระบบออกจาก Python อย่างสมบูรณ์ Codex และ GPT จะช่วยให้การย้ายระบบครั้งใหญ่นี้เป็นไปได้ และท้ายที่สุดสิ่งที่เราคาดไว้ก็เป็นจริง
การใช้ Python เพื่อรัน Habitat ในรูปแบบบริการอาจไม่ใช่ทางเลือกที่ดีที่สุดในแง่ประสิทธิภาพ แต่ก็เป็นทางเลือกที่จำเป็น Python ช่วยให้เราพัฒนาได้อย่างรวดเร็ว แต่ไม่ได้หมายความว่าเราจะละเลยเรื่องประสิทธิภาพและยอมรับเวลาแฝงที่เพิ่มขึ้นอย่างเห็นได้ชัดได้ เพราะคำขอหนึ่งครั้งจากผู้ใช้โดยเฉลี่ยอาจต้องเรียกฐานข้อมูลหลายร้อยครั้ง และผู้ใช้จะต้องรอจนกว่าการเรียกฐานข้อมูลที่ช้าที่สุดจะเสร็จสิ้น จากประสบการณ์ของเรา ความท้าทายสำคัญในการใช้บริการ Python ในระบบขนาดใหญ่ระดับนี้คือการจัดการกับเวลาแฝงของการทำงานส่วนที่ช้าที่สุด
Asyncio ช่วยให้ Python จัดการงานที่ต้องรอ I/O หลายงานพร้อมกันได้ แต่ไม่ได้ช่วยให้หลีกเลี่ยงข้อจำกัดของ Python GIL เพื่อประมวลผลด้วย CPU แบบขนาน นอกจากการส่งต่อคำขอซึ่งต้องใช้ I/O จำนวนมากแล้ว Habitat ยังรับผิดชอบงานที่ใช้ CPU สูงและงานเบื้องหลังอีกหลายอย่าง ได้แก่ การกำหนดเส้นทาง การบีบอัดข้อมูล การเข้ารหัส การตรวจสอบความถูกต้องของข้อมูล การตรวจสอบความพร้อมของบริการที่รับช่วงคำขอต่อ การส่งสำเนาคำขอไปยังอีกระบบเพื่อทดสอบโดยไม่กระทบคำขอจริง และการส่งคำขอสำรองเพื่อลดผลกระทบจากคำขอที่ตอบสนองช้า
เนื่องจากบริการของเรามีทั้งงานที่ใช้ CPU สูงและงานเบื้องหลังจำนวนมาก ความล่าช้าจากการจัดคิวงานของ asyncio จึงกลายเป็นปัจจัยหลักที่ทำให้คำขอที่ช้าที่สุดใช้เวลานานขึ้นได้ง่าย ก่อนที่เราจะปรับแต่งระบบเพื่อเปิดให้บริการครั้งแรก เราตรวจสอบข้อมูลการทำงานของคำขอที่มีเวลาแฝงตั้งแต่ระดับ p99 ขึ้นไป และพบว่าแม้ระบบจัดเก็บข้อมูลปลายทางจะตอบกลับอย่างรวดเร็ว แต่คำขอกลับหยุดรอบ่อยครั้ง เพราะ coroutine ที่รับผิดชอบต้องรอให้ asyncio จัดคิวกลับมาทำงานอีกครั้งเพื่อประมวลผลข้อมูลที่ตอบกลับมา
ภาพ 03 · ติดตามความล่าช้าของ asyncio
การทำงานพร้อมกันไม่ใช่การประมวลผล CPU แบบขนาน
asyncio ของ Python ช่วยประมวลผลคำขอพร้อมกันได้ แต่เธรด CPU จะเรียกใช้คำขอได้ทีละรายการเท่านั้น สิ่งนี้ส่งผลอย่างมากต่อเวลาแฝงของคำขอเมื่อมีงาน CPU จำนวนมาก
งาน CPU ต่ำ
ขั้นตอน Python สั้น เวลารอ I/O ซ้อนทับกันงาน CPU สูง
ขั้นตอนการประมวลผลด้วย Python ที่ใช้เวลานานทำให้การตอบกลับที่พร้อมแล้วต้องรอต่อไปสำหรับบริการที่พัฒนาด้วย Python ที่ OpenAI เราพบว่านอกจากการวัดค่ามาตรฐานที่บอกระดับการใช้ทรัพยากรและภาวะที่ทรัพยากรใกล้เต็มขีดความสามารถ ทั้งหน่วยความจำ CPU เครือข่าย และดิสก์แล้ว สิ่งสำคัญไม่แพ้กันคือการติดตามลูปการทำงานของ asyncio มีงานหนาแน่นเพียงใด แล้วจึงปรับแต่งระบบให้เหมาะสมตามข้อมูลที่ได้
เราวัดความล่าช้าในการจัดคิวของลูปการจัดการงานแบบเรียลไทม์ได้ โดยกำหนดให้งานเบื้องหลังทำงานเป็นระยะ แล้วบันทึกส่วนต่างระหว่างเวลาที่ควรเริ่มทำงานกับเวลาที่เริ่มทำงานจริง เมื่อระบบใช้ทรัพยากรในระดับสูงและมีงานที่ต้องใช้ทรัพยากรมากจำนวนมาก แม้จำนวนคำขอที่ทำงานพร้อมกันในแต่ละโปรเซสจะไม่สูงมาก ก็เพียงพอที่จะทำให้จังหวะการจัดคิวงานคลาดเคลื่อนไปอย่างมาก โดยอาจล่าช้าถึงหลายร้อยมิลลิวินาที และในบางกรณีอาจนานถึงหลายวินาที
ด้วยเหตุนี้ เราจึงจำกัดให้แต่ละโปรเซสรองรับคำขอพร้อมกันเพียงจำนวนเล็กน้อย แล้วชดเชยด้วยการเพิ่มจำนวนโปรเซส Python ที่ทำหน้าที่ประมวลผลงานขึ้นเป็นจำนวนมาก
เมื่อเปิดตัวบริการในช่วงแรก เราพบหนึ่งในต้นเหตุของความล่าช้าที่สูงใน asyncio จากการทำโปรไฟล์ CPU ของบริการขณะทำงานจริง ซึ่งความล่าช้านี้ส่งผลให้เวลาแฝงส่วนปลายสูงขึ้นตามไปด้วย ต้นเหตุดังกล่าวคือการแยกวิเคราะห์ JSON ของคอนฟิกูเรชันฟีเจอร์แฟล็กผ่าน Statsig เป็นระยะ โดย Statsig เป็นเครื่องมือที่ใช้จัดการฟีเจอร์แฟล็ก ทำการทดสอบ A/B และงานอื่นๆ
ตามค่าเริ่มต้น Statsig จะตรวจสอบการกำหนดค่าที่อัปเดตใหม่ทุกหนึ่งนาที และทุกโปรเซสจะทำเช่นนี้ตามจังหวะเวลาเดียวกันโดยไม่มีการสุ่มให้เหลื่อมกัน นอกจากนี้ ไฟล์การกำหนดค่ายังรวบรวมกฎทั้งหมดที่ใช้ในระบบจริงจากทุกบริการไว้ด้วย ในอีกส่วนหนึ่งของการออกแบบ เรากำหนดให้แต่ละพ็อดรันโปรเซส Python ได้สูงสุด 8 โปรเซส เพื่อเพิ่มการใช้ CPU และลดเวลาแฝง ผลคือในทุก หนึ่งนาที จะมีช่วงหนึ่งที่โปรเซสทั้งหมดในแต่ละพ็อดหยุดจัดการคำขอที่ยังประมวลผลไม่เสร็จพร้อมกัน แล้วใช้ CPU ไปกับการแยกวิเคราะห์ไฟล์การกำหนดค่าขนาดใหญ่แทน
เมื่อเราวิเคราะห์การทำงานของ CPU จนพบต้นตอของปัญหาแล้ว วิธีแก้ก็ตรงไปตรงมา เพียงลดขนาดไฟล์การกำหนดค่าให้เหลือเฉพาะส่วนที่เกี่ยวข้อง เพิ่มช่วงเวลาระหว่างการอัปเดตแต่ละครั้ง และกระจายเวลาเริ่มทำงานของงานเบื้องหลังประเภทนี้ไม่ให้เกิดขึ้นพร้อมกัน
เพื่อไม่ให้ asyncio เกิดความล่าช้าสูง การกระจายคำขอไปยังกระบวนการบนเซิร์ฟเวอร์อย่างสมดุลก็เป็นสิ่งสำคัญเช่นกัน หากไม่มีการปรับแต่งอย่างเหมาะสม การจัดการกลุ่มการเชื่อมต่ออาจให้ผลตรงกันข้ามและทำให้คำขอกระจายอย่างไม่สมดุลได้
เมื่อใช้การจัดการกลุ่มการเชื่อมต่อฝั่งไคลเอ็นต์ กระบวนการบนไคลเอ็นต์เพียงกระบวนการเดียวที่ส่งคำขอพร้อมกันจำนวนมาก อาจสร้างการเชื่อมต่อกับเซิร์ฟเวอร์เพียงไม่กี่รายการ และส่งผลให้ภาระงานทั้งหมดของกระบวนการนั้นถูกส่งไปยังกระบวนการบนเซิร์ฟเวอร์เพียงไม่กี่กระบวนการ ก่อนที่เราจะปรับวิธีการกระจายโหลด บริการของเรามีความแตกต่างด้านการใช้งานระหว่างแต่ละกระบวนการค่อนข้างมาก โดยกระบวนการบางส่วนที่อยู่ปลายสุดของการกระจายต้องรองรับคำขอพร้อมกันมากกว่าค่าเฉลี่ยถึง 5–10 เท่า
เราพบปัญหานี้โดยบังเอิญจากเหตุการณ์ครั้งหนึ่ง หลังจากหยุดไคลเอ็นต์ที่ทำให้บริการบางส่วนรับภาระหนักเกินไปแล้ว เราคาดว่าระบบจะกลับมาเป็นปกติ แต่กระบวนการบางส่วนกลับยังคงมีประสิทธิภาพลดลง แม้ช่วงที่มีทราฟฟิกพุ่งสูงจะผ่านไปนานแล้วก็ตาม ยิ่งไปกว่านั้น กระบวนการเหล่านั้นกลับยิ่งแย่ลงเรื่อยๆ เพราะได้รับคำขอมากขึ้นอย่างต่อเนื่อง จนสุดท้ายเราต้องเริ่มการทำงานใหม่ เมื่อใดก็ตามที่พ็อดเริ่มรับภาระหนักเกินไป จะมีพฤติกรรมบางอย่างของระบบที่ทำให้ทราฟฟิกยิ่งถูกส่งไปกระจุกอยู่ที่พ็อดนั้นมากขึ้น เพื่อนร่วมทีมบางคนของเราคุ้นเคยกับความล้มเหลวลักษณะนี้จากงานก่อนหน้านี้ ซึ่งเรียกว่าภาวะล้มเหลวแบบกึ่งเสถียร(เปิดในหน้าต่างใหม่)
เราสงสัยว่าต้นเหตุอยู่ที่กลุ่มการเชื่อมต่อ จึงทดสอบสมมติฐานนี้ด้วยการจำกัดระยะเวลาสูงสุดที่สามารถนำการเชื่อมต่อเดิมกลับมาใช้ซ้ำได้ ผลคือปัญหารุนแรงน้อยลงจริง ทำให้เรามั่นใจว่ากำลังตรวจสอบมาถูกทาง เมื่อเจาะลึกต่อไป เราพบว่า TCPConnector ของ aiohttp ใน Python ใช้วิธีนำการเชื่อมต่อกลับมาใช้ซ้ำแบบ LIFO เป็นค่าเริ่มต้น กล่าวคือ การเชื่อมต่อที่เพิ่งกลับเข้ากลุ่มล่าสุดจะถูกหยิบมาใช้กับคำขอถัดไปก่อน ปกติแล้ววิธีนี้ก็สมเหตุสมผล เพราะการเลือกใช้การเชื่อมต่อล่าสุดทำให้การเชื่อมต่อส่วนเกินที่สร้างขึ้นเพื่อรองรับช่วงที่มีคำขอจำนวนมากสามารถปิดตัวเองลงได้เมื่อไม่มีการใช้งานตามระยะเวลาที่กำหนด ซึ่งช่วยลดภาระในการคงการเชื่อมต่อส่วนเกินไว้ แต่ในกรณีของเรา วิธีนี้กลับทำให้เกิดภาวะล้มเหลวที่ยังคงดำเนินต่อไปแม้ต้นเหตุแรกเริ่มจะหมดไปแล้วในช่วงที่มีคำขอจำนวนมากเข้ามา คำขอที่ส่งไปยังเซิร์ฟเวอร์ที่ทำงานช้ากว่าและรับภาระหนักเกินไปจะเสร็จทีหลัง การเชื่อมต่อจากเซิร์ฟเวอร์เหล่านั้นจึงถูกส่งคืนเข้ากลุ่มทีหลัง และด้วยหลัก LIFO การเชื่อมต่อเหล่านี้ก็ถูกเลือกสำหรับคำขอถัดไปบ่อยขึ้น ส่งผลให้ทราฟฟิกค่อยๆ ไปกระจุกอยู่ที่พ็อดที่กำลังรับภาระหนักอยู่แล้วมากขึ้นเรื่อยๆ เมื่อเราปรับกลุ่มการเชื่อมต่อให้ใช้ FIFO แทน วงจรนี้ก็หยุดลง และยังช่วยลดความแตกต่างของจำนวนคำขอระหว่างกระบวนการต่างๆ เมื่อระบบมีภาระงานคงที่อีกด้วย
ภาพ 04A · การจัดการกลุ่มการเชื่อมต่อฝั่งไคลเอ็นต์
LIFO ส่งงานใหม่กลับไปยังกระบวนการที่ทำงานช้า
หลังจากมีคำขอจำนวนมากเข้ามาในช่วงเวลาสั้นๆ เซิร์ฟเวอร์ที่ทำงานช้ากว่าจะส่งการเชื่อมต่อกลับเข้ากลุ่มเป็นลำดับท้ายๆ เมื่อใช้ LIFO การเชื่อมต่อที่เพิ่งถูกส่งคืนจะถูกนำกลับมาใช้ก่อน จึงทำให้งานใหม่ยิ่งไปกระจุกตัวอยู่ที่เซิร์ฟเวอร์ที่ทำงานช้าเหล่านั้น
คำขอจำนวนมากชุดแรกถูกส่งไปยัง A, B และกระบวนการ C ที่ทำงานช้ากว่า
ภาพ 04B · การจัดการกลุ่มการเชื่อมต่อฝั่งไคลเอ็นต์
FIFO ช่วยตัดวงจรการนำการเชื่อมต่อเดิมกลับมาใช้ซ้ำ
หลังจากมีคำขอเข้ามาจำนวนมากในช่วงสั้นๆ FIFO จะยังคงมีการเชื่อมต่อที่ใช้งานอยู่มากกว่า แต่ช่วยกระจายภาระงานไปยังเซิร์ฟเวอร์ทุกเครื่องได้อย่างเท่าเทียม
คำขอจำนวนมากชุดแรกถูกส่งไปยัง A, B และกระบวนการ C ที่ทำงานช้ากว่า
ปัจจุบัน เราใช้ Istio และ Envoy เป็นหลักในการจัดการกลุ่มการเชื่อมต่อและกระจายโหลดโดยคำนึงถึงภาระงานของเซิร์ฟเวอร์ได้ดียิ่งขึ้นทั่วทั้งโครงสร้างพื้นฐานของ OpenAI ซึ่งช่วยให้เราหลีกเลี่ยงปัญหานี้ได้ตั้งแต่ต้น
ผลข้างเคียงอย่างหนึ่งของการปรับระบบให้ asyncio มีความล่าช้าต่ำ ประกอบกับการที่เรามีกระบวนการ Python จำนวนมาก คือระบบสามารถสร้างการเชื่อมต่อจำนวนมหาศาลจนทำให้ระบบปลายทางรับภาระหนักเกินไปได้ง่าย ซึ่งเป็นปรากฏการณ์ที่เรียกว่า “ภาวะการเชื่อมต่อถาโถม”
แม้แต่การนำระบบขึ้นใช้งานที่เกิดขึ้นเป็นประจำทุกวันก็สร้างปัญหาได้ หากปล่อยให้เกิดขึ้นเร็วเกินไป เพราะการเชื่อมต่อจำนวนมากจะถูกปิดและสร้างขึ้นใหม่ในเวลาใกล้เคียงกัน ทำให้ CPU ต้องรับภาระสูง นอกจากนี้ หากระบบสร้างการเชื่อมต่อแล้วไม่ปิดอย่างถูกต้อง จำนวนการเชื่อมต่ออาจเพิ่มขึ้นจน NAT gateway เต็มและทำให้เครือข่ายใช้งานไม่ได้ ปัญหาแบบนี้เกิดกับบริการอื่นได้เช่นกัน แต่ระบบของเรามีกระบวนการมากกว่าปกติประมาณ 10 เท่า จึงไปถึงจุดที่เกิดปัญหาได้ง่ายกว่ามาก ทรัพยากรด้านเครือข่ายจึงอาจเต็มขีดความสามารถ แม้ปริมาณข้อมูลที่รับส่งในภาวะปกติจะดูไม่ได้สูงจนต้องเตรียมทรัพยากรไว้มากขนาดนั้นก็ตาม
เรายังใช้ Envoy เพื่อรวมการเชื่อมต่อให้ได้มากที่สุด โดยใช้ Envoy เปลี่ยนการเชื่อมต่อ HTTP/1 ของ Python เป็น HTTP/2 เพื่อให้สามารถรับส่งข้อมูลหลายรายการพร้อมกันผ่านการเชื่อมต่อเดียว จากนั้นจึงจัดการการเชื่อมต่อเหล่านั้นเป็นกลุ่มและเปิดการเชื่อมต่อไว้ให้นานขึ้น นอกจากนี้ Envoy ยังเป็นจุดศูนย์กลางที่ช่วยให้เรากำหนดลิมิตการใช้งานและกลไกตัดการเชื่อมต่อเมื่อระบบมีปัญหาได้ ซึ่งจะมีประสิทธิภาพน้อยกว่าหากนำไปใช้แยกกันในแต่ละกระบวนการ Python
ภาพ 05 · การรวมการเชื่อมต่อ
คำขอเท่าเดิม การเชื่อมต่อน้อยลง
การจัดการกลุ่มการเชื่อมต่อและการส่งข้อมูลหลายชุดผ่านการเชื่อมต่อ HTTP/2 เดียวกัน ช่วยลดภาระด้านการเชื่อมต่อของระบบปลายทาง
ปัจจัยหนึ่งที่ช่วยให้เราขยายบริการ Python ได้ถึงระดับนี้คือ API ของ Habitat ถูกออกแบบให้มีขอบเขตจำกัด จึงควบคุมและคาดการณ์ทรัพยากรที่แต่ละคำขอต้องใช้ได้ แทนที่จะอนุญาตให้ไคลเอ็นต์เขียนคำสั่ง SQL แบบใดก็ได้ ซึ่งอาจนำไปสู่การอ่านข้อมูลจำนวนมหาศาลจากทั้งตารางหรือการเชื่อมข้อมูลจากหลายตาราง Habitat ให้ใช้ NoSQL API ที่เรียบง่ายแทน การจำกัดความสามารถของ API เช่นนี้เป็นสิ่งที่เราตั้งใจแลกเพื่อให้ Habitat มีคุณสมบัติที่ต้องการ
เราออกแบบระบบโดยให้ความสำคัญกับคำขอที่เรียบง่าย คาดการณ์ได้ และใช้ปริมาณงานในการประมวลผลใกล้เคียงกันทุกครั้ง จากประสบการณ์ของเรา ระบบลักษณะนี้ขยายได้ง่ายกว่ามาก อีกทั้งยังลดโอกาสที่จะเกิดข้อผิดพลาดหรือถูกนำไปใช้อย่างไม่เหมาะสม ส่วนคำขอที่กระจายงานออกไปในจำนวนที่คาดเดาไม่ได้ถือเป็นความเสี่ยงต่อการปฏิบัติงาน เพราะทำให้การแยกส่วนระบบและการกระจายโหลดซับซ้อนขึ้น อีกทั้งยังอาจทำให้เวลาแฝงพุ่งสูงขึ้นอย่างฉับพลันจนรองรับการขยายขนาดได้ยาก ทั้งสำหรับตัวบริการและไคลเอ็นต์
ก่อนที่เราจะย้ายมาใช้ Habitat และ Azure Cosmos DB ข้อมูลออนไลน์ส่วนใหญ่ของ OpenAI จัดเก็บอยู่ใน Postgres ในตอนนั้นเรายังสามารถตรวจสอบการเปลี่ยนแปลงของคำสั่งค้นข้อมูลและโครงสร้างฐานข้อมูลทั้งหมดได้ไม่ยาก เพื่อให้แน่ใจว่าทำงานได้อย่างมีประสิทธิภาพและเรียกใช้กับข้อมูลที่มีดัชนีก่อนนำขึ้นใช้งานจริง แต่เมื่อทีมและผลิตภัณฑ์เติบโตขึ้น การตรวจสอบในลักษณะนี้ก็เกินกว่าจะจัดการได้อย่างรวดเร็ว และกลายเป็นสาเหตุที่ทำให้ระบบล่มอยู่บ่อยครั้ง เพราะเพียงมีคำสั่งค้นข้อมูลใหม่ที่ใช้ทรัพยากรสูงหนึ่งรายการอยู่ในเส้นทางการทำงานที่มีการเรียกใช้บ่อย ก็สามารถทำให้ฐานข้อมูลล่มได้
ปัญหาในที่นี้คือความไม่สมดุลของต้นทุน เพราะการเขียนคำสั่ง SQL ที่ใช้ทรัพยากรมากและประมวลผลได้ยากนั้นกลับทำได้ง่ายและมีต้นทุนต่ำ ใน Habitat เราหลีกเลี่ยงปัญหานี้ด้วยการทำให้ไคลเอ็นต์เห็นได้อย่างชัดเจนว่าคำขอใดมีต้นทุนในการประมวลผลสูง Habitat ไม่อนุญาตให้มีคำขอที่ไม่จำกัดขอบเขตจนทำให้ระบบรับภาระหนักเกินไป ส่วนการเชื่อมข้อมูลที่ซับซ้อนและการไล่สำรวจกราฟ ทีมผลิตภัณฑ์จะต้องรับภาระในการประมวลผลบางส่วนเอง ซึ่งโดยรวมแล้วช่วยผลักดันให้ทีมออกแบบระบบได้อย่างมีประสิทธิภาพมากขึ้น
Habitat มี NoSQL API ที่ออกแบบโดยอิงตามประเภทออบเจ็กต์และ Edge ที่ไคลเอ็นต์กำหนด โดยได้รับแรงบันดาลใจจาก TAO(เปิดในหน้าต่างใหม่) ไคลเอ็นต์จะกำหนดออบเจ็กต์และ Edge รวมถึงความสัมพันธ์ระหว่างกันไว้ล่วงหน้า แต่ไม่ต้องกำหนดเนื้อหาภายในของแต่ละประเภท ความสัมพันธ์ที่ได้จึงมีลักษณะคล้ายกราฟ แต่ตัว Habitat เองไม่รองรับคำขอสำหรับการไล่สำรวจกราฟแบบทั่วไป นอกเหนือจากการค้นหา Edge ที่เชื่อมต่อโดยตรงกับออบเจ็กต์ใดออบเจ็กต์หนึ่ง
เราแบ่งข้อมูลแบบกราฟนี้ออกเป็นส่วนๆ โดยจัดเก็บแต่ละออบเจ็กต์ไว้ในส่วนเดียวกับข้อมูลความสัมพันธ์ที่เชื่อมออกจากออบเจ็กต์นั้นในระดับระบบจัดเก็บข้อมูล แต่ในระดับฐานข้อมูล เราไม่ได้พยายามเป็นพิเศษที่จะจัดเก็บออบเจ็กต์ที่เชื่อมโยงถึงกันไว้ในตำแหน่งเดียวกัน ผลคือเราสามารถแบ่งข้อมูลของโมเดลออกเป็นส่วนๆ เพื่อรองรับการขยายระบบในแนวนอนได้ง่าย แต่การไล่ตามความสัมพันธ์ในข้อมูลแบบกราฟกลับไม่มีประสิทธิภาพนัก เพราะการไล่จากออบเจ็กต์หนึ่งไปยังอีกออบเจ็กต์หนึ่งในแต่ละขั้น อาจต้องดึงข้อมูลจากบัญชี Azure Cosmos DB สองบัญชีที่แยกจากกันโดยสิ้นเชิงและอยู่คนละภูมิภาค
สำหรับไคลเอ็นต์ที่ต้องการค้นหาข้อมูลซับซ้อนขึ้น เรามีมุมมองรองแบบออฟไลน์ของ Habitat ให้ใช้ผ่าน Rockset เราใช้การจับการเปลี่ยนแปลงข้อมูล (CDC) เพื่อสตรีมการเปลี่ยนแปลงจากพื้นที่จัดเก็บออนไลน์ไปยังอินสแตนซ์ Rockset ที่แยกจากกันแบบเกือบเรียลไทม์ ทีมไคลเอ็นต์แต่ละทีมมีหน้าที่ขยายอินสแตนซ์ Rockset ของตนเองให้รองรับความต้องการค้นหาที่ซับซ้อน
แม้การจัดเตรียม Rockset จะเพิ่มขั้นตอนและความยุ่งยากให้กับไคลเอ็นต์ของเรา แต่เรามองว่าเป็นสิ่งที่คุ้มค่าที่จะแลกในเวลานี้ เรากำหนดให้การค้นข้อมูลแบบเรียบง่ายเป็นตัวเลือกหลัก พร้อมเปิดทางให้ผู้ที่ต้องการค้นข้อมูลที่ซับซ้อนสามารถใช้วิธีอื่นได้ การออกแบบนี้ช่วยไม่ให้งานวิเคราะห์และงานค้นหาที่ต้องอ่านข้อมูลจำนวนมากเข้ามาสร้างภาระให้ระบบจัดเก็บข้อมูลออนไลน์
การเลื่อนการเขียนระบบใหม่เพื่อย้ายออกจาก Python ออกไปหนึ่งปี ทำให้เรามีเวลาไปจัดการกับความท้าทายที่เร่งด่วนและสร้างผลกระทบได้มากกว่าในช่วงที่เราเติบโตอย่างก้าวกระโดด แต่เมื่อแพลตฟอร์มเริ่มมีความพร้อมมากขึ้น การเติบโตยังคงเร่งตัว และบริการนี้กลายเป็นบริการที่มีจำนวนคอร์มากเป็นอันดับสองของ OpenAI รวมถึงมีขนาดการใช้งาน Envoy ใหญ่เป็นอันดับสี่ ในที่สุดก็ถึงเวลาที่เราต้องก้าวต่อจาก Python ในช่วงที่มีปริมาณงานสูงสุด Python ช่วยให้เรารองรับคำขอได้มากกว่า 20 ล้านครั้งต่อวินาที
ในไตรมาสที่ 2 ของ พ.ศ. 2569 เราใช้วิศวกรเพียง 2 คน ร่วมกับ Codex และ GPT‑5.5 ก็สามารถเขียนบริการทั้งหมดขึ้นใหม่ด้วย Rust ได้ ปัจจุบันบริการใหม่ที่พัฒนาด้วย Rust รองรับคำขอในระบบจริงของเราแล้ว 95% และเรามีแผนจะยุติการใช้งาน Python ทั้งหมดภายในไม่กี่สัปดาห์ข้างหน้า ข้อมูลของเราแสดงให้เห็นว่าบริการ Rust ใช้ CPU ได้อย่างมีประสิทธิภาพมากกว่าเวอร์ชัน Python ถึง 6 เท่า และใช้หน่วยความจำได้อย่างมีประสิทธิภาพมากกว่าถึง 15 เท่า อีกทั้งยังมีทั้งเวลาแฝงเฉลี่ยและเวลาแฝงส่วนปลายต่ำลงอย่างมาก เรามีแผนจะแบ่งปันสิ่งที่ได้เรียนรู้เพิ่มเติมในบล็อกครั้งต่อไป
บริการที่เคยใช้ Python และปัจจุบันใช้ Rust เป็นเพียงองค์ประกอบหนึ่งของ Habitat เท่านั้น ในตอนที่ 2 ของบทความชุดนี้ ซึ่งจะอธิบายว่าเราขยายระบบพื้นที่จัดเก็บออนไลน์อย่างรวดเร็วเพื่อรองรับผู้ใช้ ChatGPT มากกว่า 1 พันล้านคนได้อย่างไร เราจะเจาะลึกถึงเลเยอร์พื้นที่จัดเก็บ รวมถึงวิธีที่ Habitat รองรับข้อมูลมากกว่า 500 เพตะไบต์และคำขอมากกว่า 70 ล้านครั้งต่อวินาที
หากคุณอยากทำงานกับระบบ OLTP ในระดับแนวหน้าของวงการและสนใจงานวิศวกรรมลักษณะนี้ ลองดูตำแหน่งที่กำลังเปิดรับสมัครในทีมของเรา


