NewSQL = NoSQL+กรด

NewSQL = NoSQL+กรด
จนกระทั่งเมื่อไม่นานมานี้ Odnoklassniki จัดเก็บข้อมูลประมาณ 50 TB ที่ประมวลผลแบบเรียลไทม์ใน SQL Server สำหรับปริมาณดังกล่าว แทบจะเป็นไปไม่ได้เลยที่จะมอบการเข้าถึงที่รวดเร็วและเชื่อถือได้ และแม้แต่การเข้าถึงศูนย์ข้อมูลแบบทนทานต่อความล้มเหลวโดยใช้ SQL DBMS โดยทั่วไป ในกรณีเช่นนี้ พื้นที่จัดเก็บ NoSQL อันใดอันหนึ่งจะถูกใช้งาน แต่ไม่ใช่ทุกสิ่งที่สามารถถ่ายโอนไปยัง NoSQL ได้: หน่วยงานบางแห่งจำเป็นต้องมีการรับประกันธุรกรรม ACID

สิ่งนี้นำเราไปสู่การใช้พื้นที่จัดเก็บ NewSQL ซึ่งก็คือ DBMS ที่ให้ความทนทานต่อข้อผิดพลาด ความสามารถในการปรับขนาด และประสิทธิภาพของระบบ NoSQL แต่ในขณะเดียวกันการรักษา ACID ก็รับประกันความคุ้นเคยกับระบบคลาสสิก มีระบบอุตสาหกรรมที่ใช้งานได้ไม่กี่ระบบในประเภทใหม่นี้ ดังนั้นเราจึงนำระบบดังกล่าวมาใช้เองและนำไปปฏิบัติเชิงพาณิชย์

มันทำงานอย่างไรและเกิดอะไรขึ้น - อ่านแบบตัดตอน

ปัจจุบัน ผู้ชม Odnoklassniki ต่อเดือนมีผู้เยี่ยมชมมากกว่า 70 ล้านคน เรา เราอยู่ในห้าอันดับแรก เครือข่ายโซเชียลที่ใหญ่ที่สุดในโลก และเป็นหนึ่งในยี่สิบไซต์ที่ผู้ใช้ใช้เวลามากที่สุด โครงสร้างพื้นฐาน OK รองรับโหลดที่สูงมาก: คำขอ HTTP มากกว่าหนึ่งล้านคำขอ/วินาทีต่อส่วนหน้า บางส่วนของกลุ่มเซิร์ฟเวอร์มากกว่า 8000 ชิ้นตั้งอยู่ใกล้กัน - ในศูนย์ข้อมูลสี่แห่งในมอสโก ซึ่งช่วยให้มีเวลาแฝงของเครือข่ายระหว่างกันน้อยกว่า 1 มิลลิวินาที

เราใช้ Cassandra มาตั้งแต่ปี 2010 โดยเริ่มจากเวอร์ชัน 0.6 วันนี้มีคลัสเตอร์หลายสิบกลุ่มที่ดำเนินการอยู่ คลัสเตอร์ที่เร็วที่สุดประมวลผลมากกว่า 4 ล้านการดำเนินการต่อวินาที และจัดเก็บที่ใหญ่ที่สุด 260 TB

อย่างไรก็ตาม สิ่งเหล่านี้ล้วนเป็นคลัสเตอร์ NoSQL ธรรมดาที่ใช้สำหรับจัดเก็บข้อมูล มีการประสานงานที่อ่อนแอ ข้อมูล. เราต้องการแทนที่พื้นที่จัดเก็บข้อมูลหลักที่สอดคล้องกัน นั่นคือ Microsoft SQL Server ซึ่งใช้มาตั้งแต่ก่อตั้ง Odnoklassniki ที่เก็บข้อมูลประกอบด้วยเครื่อง SQL Server Standard Edition มากกว่า 300 เครื่องซึ่งมีข้อมูล 50 TB - องค์กรธุรกิจ ข้อมูลนี้ได้รับการแก้ไขโดยเป็นส่วนหนึ่งของธุรกรรม ACID และจำเป็นต้องมี ความสม่ำเสมอสูง.

ในการกระจายข้อมูลไปยังโหนด SQL Server เราใช้ทั้งแนวตั้งและแนวนอน การแบ่งพาร์ติชัน (การแบ่งส่วน) ในอดีต เราใช้รูปแบบการแบ่งส่วนข้อมูลแบบง่ายๆ: แต่ละเอนทิตีเชื่อมโยงกับโทเค็น ซึ่งเป็นฟังก์ชันของ ID เอนทิตี เอนทิตีที่มีโทเค็นเดียวกันถูกวางไว้บนเซิร์ฟเวอร์ SQL เดียวกัน ความสัมพันธ์หลัก-รายละเอียดถูกนำมาใช้เพื่อให้โทเค็นของบันทึกหลักและบันทึกย่อยตรงกันเสมอและอยู่บนเซิร์ฟเวอร์เดียวกัน ในโซเชียลเน็ตเวิร์ก บันทึกเกือบทั้งหมดถูกสร้างขึ้นในนามของผู้ใช้ ซึ่งหมายความว่าข้อมูลผู้ใช้ทั้งหมดภายในระบบย่อยที่ใช้งานได้หนึ่งจะถูกจัดเก็บไว้ในเซิร์ฟเวอร์เดียว นั่นคือธุรกรรมทางธุรกิจมักจะเกี่ยวข้องกับตารางจากเซิร์ฟเวอร์ SQL หนึ่งเครื่อง ซึ่งทำให้สามารถตรวจสอบความสอดคล้องของข้อมูลโดยใช้ธุรกรรม ACID ในเครื่องโดยไม่จำเป็นต้องใช้ ช้าและไม่น่าเชื่อถือ ธุรกรรม ACID แบบกระจาย

ขอบคุณการแบ่งส่วนและเร่งความเร็ว SQL:

  • เราไม่ใช้ข้อจำกัดของคีย์ต่างประเทศ เนื่องจากเมื่อแบ่งรหัสเอนทิตีอาจอยู่บนเซิร์ฟเวอร์อื่น
  • เราไม่ใช้ขั้นตอนการจัดเก็บและทริกเกอร์เนื่องจากมีภาระเพิ่มเติมบน DBMS CPU
  • เราไม่ใช้ JOIN เนื่องจากทั้งหมดที่กล่าวมาข้างต้นและมีการอ่านแบบสุ่มจากดิสก์จำนวนมาก
  • ภายนอกธุรกรรม เราใช้ระดับการแยกการอ่านที่ไม่มีข้อผูกมัดเพื่อลดการหยุดชะงัก
  • เราทำธุรกรรมระยะสั้นเท่านั้น (โดยเฉลี่ยแล้วสั้นกว่า 100 มิลลิวินาที)
  • เราไม่ใช้ UPDATE และ DELETE แบบหลายแถวเนื่องจากการหยุดชะงักจำนวนมาก - เราอัปเดตเพียงครั้งละหนึ่งระเบียนเท่านั้น
  • เราดำเนินการสืบค้นเฉพาะบนดัชนีเท่านั้น - การสืบค้นที่มีแผนการสแกนตารางแบบเต็มสำหรับเราหมายถึงการใช้งานฐานข้อมูลมากเกินไปและทำให้ฐานข้อมูลล้มเหลว

ขั้นตอนเหล่านี้ทำให้เราสามารถบีบประสิทธิภาพสูงสุดออกจากเซิร์ฟเวอร์ SQL ได้เกือบหมด แต่ปัญหาก็เพิ่มมากขึ้นเรื่อยๆ มาดูพวกเขากันดีกว่า

ปัญหาเกี่ยวกับ SQL

  • เนื่องจากเราใช้การแบ่งส่วนย่อยที่เขียนขึ้นเอง ผู้ดูแลระบบจึงเพิ่มส่วนใหม่ด้วยตนเอง ตลอดเวลานี้ แบบจำลองข้อมูลที่ปรับขนาดได้ไม่ได้ให้บริการตามคำขอ
  • เมื่อจำนวนบันทึกในตารางเพิ่มขึ้น ความเร็วของการแทรกและการแก้ไขจะลดลง เมื่อเพิ่มดัชนีลงในตารางที่มีอยู่ ความเร็วจะลดลงตามปัจจัย การสร้างและการสร้างดัชนีใหม่จะเกิดขึ้นพร้อมกับการหยุดทำงาน
  • การมี Windows สำหรับ SQL Server จำนวนเล็กน้อยในการผลิตทำให้การจัดการโครงสร้างพื้นฐานทำได้ยาก

แต่ปัญหาหลักคือ

ความอดทนต่อความผิดพลาด

เซิร์ฟเวอร์ SQL แบบคลาสสิกมีความทนทานต่อข้อผิดพลาดต่ำ สมมติว่าคุณมีเซิร์ฟเวอร์ฐานข้อมูลเพียงเซิร์ฟเวอร์เดียว และเซิร์ฟเวอร์จะล้มเหลวทุกๆ สามปี ในระหว่างนี้ไซต์จะปิดให้บริการเป็นเวลา 20 นาที ซึ่งถือว่ายอมรับได้ หากคุณมีเซิร์ฟเวอร์ 64 เครื่อง ไซต์จะหยุดทำงานทุกๆ สามสัปดาห์ และถ้าคุณมีเซิร์ฟเวอร์ 200 เครื่อง ไซต์จะไม่ทำงานทุกสัปดาห์ นี่เป็นปัญหา

สิ่งที่สามารถทำได้เพื่อปรับปรุงความทนทานต่อข้อผิดพลาดของเซิร์ฟเวอร์ SQL? วิกิพีเดียเชิญชวนให้เราสร้าง คลัสเตอร์ที่มีความพร้อมใช้งานสูง: โดยในกรณีที่ส่วนประกอบใดชำรุดจะมีการสำรองข้อมูล

สิ่งนี้ต้องใช้อุปกรณ์ราคาแพงจำนวนมาก: การทำซ้ำจำนวนมาก ใยแก้วนำแสง พื้นที่จัดเก็บข้อมูลที่ใช้ร่วมกัน และการรวมการสำรองไม่สามารถทำงานได้อย่างน่าเชื่อถือ: ประมาณ 10% ของการสลับจบลงด้วยความล้มเหลวของโหนดสำรอง เช่น รถไฟที่อยู่ด้านหลังโหนดหลัก

แต่ข้อเสียเปรียบหลักของคลัสเตอร์ที่มีความพร้อมใช้งานสูงคือความพร้อมใช้งานเป็นศูนย์หากศูนย์ข้อมูลที่คลัสเตอร์ตั้งอยู่ล้มเหลว Odnoklassniki มีศูนย์ข้อมูลสี่แห่ง และเราจำเป็นต้องรับประกันการดำเนินงานในกรณีที่เกิดความล้มเหลวโดยสิ้นเชิงในหนึ่งในนั้น

สำหรับสิ่งนี้เราสามารถใช้ มัลติมาสเตอร์ การจำลองแบบที่สร้างไว้ใน SQL Server โซลูชันนี้มีราคาแพงกว่ามากเนื่องจากต้นทุนของซอฟต์แวร์และประสบปัญหาที่รู้จักกันดีเกี่ยวกับการจำลองแบบ - ความล่าช้าในการทำธุรกรรมที่คาดเดาไม่ได้ด้วยการจำลองแบบซิงโครนัสและความล่าช้าในการใช้การจำลองแบบ (และเป็นผลให้สูญเสียการแก้ไข) ด้วยการจำลองแบบอะซิงโครนัส โดยนัย การแก้ไขข้อขัดแย้งด้วยตนเอง ทำให้ตัวเลือกนี้ไม่สามารถใช้ได้กับเราโดยสิ้นเชิง

ปัญหาทั้งหมดนี้จำเป็นต้องมีวิธีแก้ไขที่รุนแรง และเราเริ่มวิเคราะห์อย่างละเอียด ที่นี่เราจำเป็นต้องทำความคุ้นเคยกับสิ่งที่ SQL Server ทำเป็นหลักนั่นคือธุรกรรม

การทำธุรกรรมง่ายๆ

พิจารณาธุรกรรมที่ง่ายที่สุดจากมุมมองของโปรแกรมเมอร์ SQL ที่ใช้งาน: การเพิ่มรูปภาพลงในอัลบั้ม อัลบั้มและรูปถ่ายจะถูกจัดเก็บไว้ในจานที่แตกต่างกัน อัลบั้มนี้มีเคาน์เตอร์รูปภาพสาธารณะ จากนั้นธุรกรรมดังกล่าวจะแบ่งออกเป็นขั้นตอนดังต่อไปนี้:

  1. เราล็อคอัลบั้มด้วยกุญแจ
  2. สร้างรายการในตารางรูปภาพ
  3. หากรูปภาพมีสถานะสาธารณะ ให้เพิ่มตัวนับรูปภาพสาธารณะในอัลบั้ม อัปเดตบันทึกและทำธุรกรรม

หรือในรหัสเทียม:

TX.start("Albums", id);
Album album = albums.lock(id);
Photo photo = photos.create(…);

if (photo.status == PUBLIC ) {
    album.incPublicPhotosCount();
}
album.update();

TX.commit();

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

เมื่อดำเนินการธุรกรรม อาจเกิดการแก้ไขข้อมูลเดียวกันจากระบบอื่นพร้อมกันได้ ตัวอย่างเช่น แอนตี้สแปมอาจตัดสินว่าผู้ใช้น่าสงสัย ดังนั้นรูปภาพของผู้ใช้ทั้งหมดจึงไม่ควรเปิดเผยต่อสาธารณะอีกต่อไป จึงต้องส่งรูปภาพเหล่านั้นเพื่อการกลั่นกรอง ซึ่งหมายถึงการเปลี่ยน photo.status เป็นค่าอื่นและปิดตัวนับที่เกี่ยวข้อง แน่นอนว่าหากการดำเนินการนี้เกิดขึ้นโดยไม่มีการรับประกันความเป็นอะตอมมิกของการใช้งานและการแยกการดัดแปลงที่แข่งขันกัน ดังเช่นใน กรดผลลัพธ์จะไม่ใช่สิ่งที่จำเป็น - ตัวนับภาพถ่ายจะแสดงค่าที่ไม่ถูกต้องหรือภาพถ่ายทั้งหมดจะไม่ถูกส่งเพื่อการกลั่นกรอง

รหัสที่คล้ายกันจำนวนมากซึ่งจัดการองค์กรธุรกิจต่างๆ ภายในธุรกรรมเดียวได้ถูกเขียนขึ้นตลอดการดำรงอยู่ของ Odnoklassniki จากประสบการณ์การโยกย้ายไปยัง NoSQL ความสอดคล้องในที่สุด เรารู้ว่าความท้าทายที่ยิ่งใหญ่ที่สุด (และการลงทุนด้านเวลา) มาจากการพัฒนาโค้ดเพื่อรักษาความสอดคล้องของข้อมูล ดังนั้นเราจึงพิจารณาข้อกำหนดหลักสำหรับพื้นที่จัดเก็บใหม่เพื่อจัดเตรียมธุรกรรม ACID จริงสำหรับตรรกะของแอปพลิเคชัน

ข้อกำหนดอื่น ๆ ที่สำคัญไม่น้อยคือ:

  • หากศูนย์ข้อมูลล้มเหลว จะต้องมีทั้งการอ่านและการเขียนไปยังที่จัดเก็บข้อมูลใหม่
  • รักษาความเร็วการพัฒนาในปัจจุบัน นั่นคือเมื่อทำงานกับที่เก็บใหม่ จำนวนโค้ดควรจะเท่ากันโดยประมาณ ไม่จำเป็นต้องเพิ่มสิ่งใด ๆ ลงในที่เก็บ พัฒนาอัลกอริธึมสำหรับการแก้ไขข้อขัดแย้ง การบำรุงรักษาดัชนีรอง ฯลฯ
  • ความเร็วของการจัดเก็บข้อมูลใหม่จะต้องค่อนข้างสูงทั้งเมื่ออ่านข้อมูลและเมื่อประมวลผลธุรกรรม ซึ่งหมายความว่าไม่สามารถใช้วิธีแก้ปัญหาที่เข้มงวดทางวิชาการ เป็นสากล แต่ช้า เช่น เป็นต้น กระทำสองเฟส.
  • การปรับขนาดอัตโนมัติทันที
  • ใช้เซิร์ฟเวอร์ราคาถูกทั่วไปโดยไม่จำเป็นต้องซื้อฮาร์ดแวร์แปลกใหม่
  • ความเป็นไปได้ในการพัฒนาพื้นที่จัดเก็บข้อมูลโดยนักพัฒนาของบริษัท กล่าวอีกนัยหนึ่ง มีการให้ความสำคัญกับโซลูชันที่เป็นกรรมสิทธิ์หรือโอเพ่นซอร์ส โดยเฉพาะอย่างยิ่งใน Java

การตัดสินใจการตัดสินใจ

จากการวิเคราะห์วิธีแก้ปัญหาที่เป็นไปได้ เรามาถึงตัวเลือกสถาปัตยกรรมที่เป็นไปได้สองแบบ:

ประการแรกคือการใช้เซิร์ฟเวอร์ SQL ใดๆ และใช้ความทนทานต่อข้อผิดพลาดที่จำเป็น กลไกการปรับขนาด คลัสเตอร์เมื่อเกิดข้อผิดพลาด การแก้ไขข้อขัดแย้ง และธุรกรรม ACID แบบกระจาย เชื่อถือได้ และรวดเร็ว เราให้คะแนนตัวเลือกนี้ว่าไม่สำคัญและใช้แรงงานมาก

ตัวเลือกที่สองคือการใช้พื้นที่จัดเก็บ NoSQL สำเร็จรูปที่มีการปรับขนาดที่ใช้งาน คลัสเตอร์ล้มเหลว การแก้ไขข้อขัดแย้ง และปรับใช้ธุรกรรมและ SQL ด้วยตัวคุณเอง เมื่อมองแวบแรก แม้แต่งานนำ SQL ไปใช้ ไม่ต้องพูดถึงธุรกรรม ACID ก็ดูเหมือนเป็นงานที่ต้องใช้เวลาหลายปี แต่แล้วเราก็ตระหนักว่าชุดคุณลักษณะ SQL ที่เราใช้ในทางปฏิบัตินั้นยังห่างไกลจาก ANSI SQL เลย แคสแซนดรา ซีคิวแอล ห่างไกลจาก ANSI SQL เมื่อพิจารณา CQL ให้ละเอียดยิ่งขึ้น เราพบว่า CQL ค่อนข้างใกล้เคียงกับสิ่งที่เราต้องการ

แคสแซนดราและซีคิวแอล

แล้ว Cassandra มีความสามารถอะไรบ้างที่น่าสนใจ?

ประการแรก คุณสามารถสร้างตารางที่รองรับข้อมูลประเภทต่างๆ ได้ที่นี่ คุณสามารถทำ SELECT หรือ UPDATE บนคีย์หลักได้

CREATE TABLE photos (id bigint KEY, owner bigint,…);
SELECT * FROM photos WHERE id=?;
UPDATE photos SET … WHERE id=?;

เพื่อให้แน่ใจว่าข้อมูลจำลองมีความสอดคล้องกัน Cassandra จึงใช้ แนวทางองค์ประชุม. ในกรณีที่ง่ายที่สุด หมายความว่าเมื่อมีการวางเรพลิกาสามเรพลิกาของแถวเดียวกันบนโหนดที่แตกต่างกันของคลัสเตอร์ การเขียนจะถือว่าสำเร็จหากโหนดส่วนใหญ่ (นั่นคือ สองในสาม) ยืนยันความสำเร็จของการดำเนินการเขียนนี้ . ข้อมูลแถวจะถือว่าสอดคล้องกันหากโหนดส่วนใหญ่ได้รับการสำรวจและยืนยันเมื่ออ่าน ดังนั้นด้วยการจำลองสามรายการ จึงรับประกันความสอดคล้องของข้อมูลที่สมบูรณ์และรวดเร็วหากโหนดหนึ่งล้มเหลว แนวทางนี้ทำให้เราสามารถนำโครงร่างที่เชื่อถือได้มากยิ่งขึ้น กล่าวคือ ส่งคำขอไปยังแบบจำลองทั้งสามเสมอ รอการตอบกลับจากแบบจำลองที่เร็วที่สุดทั้งสองรายการ การตอบสนองล่าช้าของแบบจำลองที่สามจะถูกยกเลิกในกรณีนี้ โหนดที่ตอบสนองช้าอาจมีปัญหาร้ายแรง - เบรก, การรวบรวมขยะใน JVM, การเรียกคืนหน่วยความจำโดยตรงในเคอร์เนล Linux, ความล้มเหลวของฮาร์ดแวร์, การตัดการเชื่อมต่อจากเครือข่าย อย่างไรก็ตาม สิ่งนี้จะไม่ส่งผลกระทบต่อการดำเนินงานหรือข้อมูลของลูกค้าแต่อย่างใด

แนวทางเมื่อเราติดต่อสามโหนดและรับการตอบกลับจากสองเรียกว่า การเก็งกำไร: คำขอแบบจำลองเพิ่มเติมจะถูกส่งก่อนที่มันจะ "หลุด" เสียอีก

ข้อดีอีกประการหนึ่งของ Cassandra ก็คือ Batchlog ซึ่งเป็นกลไกที่ทำให้แน่ใจว่าชุดการเปลี่ยนแปลงที่คุณทำถูกนำไปใช้อย่างสมบูรณ์หรือไม่ใช้เลย สิ่งนี้ช่วยให้เราสามารถแก้ A ในกรด - อะตอมมิกซิตีได้ทันที

สิ่งที่ใกล้เคียงที่สุดกับการทำธุรกรรมใน Cassandra คือสิ่งที่เรียกว่า “ธุรกรรมที่มีน้ำหนักเบา". แต่พวกเขายังห่างไกลจากธุรกรรม ACID "ของจริง": อันที่จริงนี่เป็นโอกาสที่จะทำ CAS กับข้อมูลจากบันทึกเดียว โดยใช้ฉันทามติโดยใช้โปรโตคอล Paxos รุ่นเฮฟวี่เวท ดังนั้นความเร็วของการทำธุรกรรมดังกล่าวจึงต่ำ

สิ่งที่เราขาดหายไปในคาสซานดรา

ดังนั้นเราจึงต้องใช้ธุรกรรม ACID จริงใน Cassandra การใช้สิ่งที่เราสามารถนำคุณสมบัติที่สะดวกสบายอื่น ๆ ของ DBMS แบบคลาสสิกไปใช้ได้อย่างง่ายดาย: ดัชนีที่รวดเร็วสม่ำเสมอ ซึ่งจะช่วยให้เราทำการเลือกข้อมูลไม่เพียงแต่ด้วยคีย์หลักเท่านั้น และสร้าง ID ที่เพิ่มขึ้นอัตโนมัติแบบโมโนโทนิกเป็นประจำ

ซี*วัน

จึงมี DBMS ใหม่เกิดขึ้น ซี*วันประกอบด้วยโหนดเซิร์ฟเวอร์สามประเภท:

  • พื้นที่เก็บข้อมูล – (เกือบ) เซิร์ฟเวอร์ Cassandra มาตรฐานที่รับผิดชอบในการจัดเก็บข้อมูลบนดิสก์ในเครื่อง เมื่อปริมาณข้อมูลและปริมาณข้อมูลเพิ่มมากขึ้น ปริมาณของข้อมูลก็สามารถปรับขนาดเป็นสิบหรือร้อยได้อย่างง่ายดาย
  • ผู้ประสานงานธุรกรรม - ตรวจสอบการทำธุรกรรม
  • ลูกค้าคือเซิร์ฟเวอร์แอปพลิเคชันที่ใช้การดำเนินธุรกิจและเริ่มต้นธุรกรรม ลูกค้าดังกล่าวอาจมีได้หลายพันราย

NewSQL = NoSQL+กรด

เซิร์ฟเวอร์ทุกประเภทเป็นส่วนหนึ่งของคลัสเตอร์ทั่วไป ใช้โปรโตคอลข้อความ Cassandra ภายในเพื่อสื่อสารระหว่างกัน และ ซุบซิบ เพื่อแลกเปลี่ยนข้อมูลคลัสเตอร์ ด้วย Heartbeat เซิร์ฟเวอร์จะเรียนรู้เกี่ยวกับความล้มเหลวร่วมกัน รักษาสคีมาข้อมูลเดียว เช่น ตาราง โครงสร้าง และการจำลองข้อมูล รูปแบบการแบ่งพาร์ติชัน โทโพโลยีคลัสเตอร์ ฯลฯ

ลูกค้า

NewSQL = NoSQL+กรด

แทนที่จะใช้ไดรเวอร์มาตรฐาน โหมด Fat Client จะถูกใช้ โหนดดังกล่าวไม่ได้จัดเก็บข้อมูล แต่สามารถทำหน้าที่เป็นผู้ประสานงานในการดำเนินการตามคำขอได้ กล่าวคือ ลูกค้าเองทำหน้าที่เป็นผู้ประสานงานคำขอของตน โดยจะสอบถามแบบจำลองพื้นที่เก็บข้อมูลและแก้ไขข้อขัดแย้ง นี่ไม่เพียงแต่เชื่อถือได้และเร็วกว่าไดรเวอร์มาตรฐานซึ่งต้องมีการสื่อสารกับผู้ประสานงานระยะไกล แต่ยังช่วยให้คุณควบคุมการส่งคำขอได้อีกด้วย ภายนอกธุรกรรมที่เปิดบนไคลเอ็นต์ คำขอจะถูกส่งไปยังที่เก็บข้อมูล หากลูกค้าได้เปิดธุรกรรมแล้ว คำขอทั้งหมดภายในธุรกรรมจะถูกส่งไปยังผู้ประสานงานธุรกรรม
NewSQL = NoSQL+กรด

C*One ผู้ประสานงานธุรกรรม

ผู้ประสานงานคือสิ่งที่เรานำไปใช้กับ C*One ตั้งแต่เริ่มต้น มีหน้าที่ในการจัดการธุรกรรม การล็อค และลำดับการใช้ธุรกรรม

สำหรับธุรกรรมที่ได้รับบริการแต่ละรายการ ผู้ประสานงานจะสร้างการประทับเวลา: แต่ละธุรกรรมที่ตามมาจะมากกว่าธุรกรรมก่อนหน้า เนื่องจากระบบการแก้ไขข้อขัดแย้งของ Cassandra ขึ้นอยู่กับการประทับเวลา (ของบันทึกสองรายการที่ขัดแย้งกัน รายการที่มีการประทับเวลาล่าสุดถือเป็นปัจจุบัน) ข้อขัดแย้งจะได้รับการแก้ไขเสมอเพื่อสนับสนุนธุรกรรมที่ตามมา ดังนั้นเราจึงดำเนินการ นาฬิกาแลมพอร์ต - วิธีที่ประหยัดในการแก้ไขข้อขัดแย้งในระบบแบบกระจาย

ล็อค

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

การใช้การล็อคดังกล่าวทำได้ง่ายในสภาพแวดล้อมที่ไม่กระจาย ในระบบแบบกระจาย มีสองตัวเลือกหลัก: ใช้การล็อกแบบกระจายบนคลัสเตอร์ หรือกระจายธุรกรรมเพื่อให้ธุรกรรมที่เกี่ยวข้องกับบันทึกเดียวกันได้รับการบริการโดยผู้ประสานงานคนเดียวกันเสมอ

เนื่องจากในกรณีของเราข้อมูลถูกกระจายไปยังกลุ่มของธุรกรรมท้องถิ่นใน SQL แล้ว จึงตัดสินใจมอบหมายกลุ่มธุรกรรมท้องถิ่นให้กับผู้ประสานงาน: ผู้ประสานงานคนหนึ่งทำธุรกรรมทั้งหมดด้วยโทเค็นตั้งแต่ 0 ถึง 9 คนที่สอง - ด้วยโทเค็นตั้งแต่ 10 ถึง 19 และอื่น ๆ เป็นผลให้แต่ละอินสแตนซ์ของผู้ประสานงานกลายเป็นต้นแบบของกลุ่มธุรกรรม

จากนั้นการล็อคสามารถนำไปใช้ในรูปแบบของ HashMap ซ้ำ ๆ ในหน่วยความจำของผู้ประสานงาน

ผู้ประสานงานล้มเหลว

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

ศูนย์ข้อมูลแต่ละแห่งมีโหนดผู้ประสานงานอย่างน้อยสองโหนด ผู้ประสานงานแต่ละคนจะส่งข้อความฮาร์ทบีทไปยังผู้ประสานงานอื่นๆ เป็นระยะๆ และแจ้งให้ทราบเกี่ยวกับการทำงานของผู้ประสานงาน รวมถึงข้อความฮาร์ทบีทใดที่ได้รับจากผู้ประสานงานในคลัสเตอร์ครั้งล่าสุด

NewSQL = NoSQL+กรด

เมื่อได้รับข้อมูลที่คล้ายกันจากผู้อื่นโดยเป็นส่วนหนึ่งของข้อความฮาร์ทบีท ผู้ประสานงานแต่ละคนจะตัดสินใจด้วยตัวเองว่าโหนดคลัสเตอร์ใดกำลังทำงานอยู่ และโหนดใดไม่ทำงาน โดยได้รับคำแนะนำจากหลักการองค์ประชุม: หากโหนด X ได้รับข้อมูลจากโหนดส่วนใหญ่ในคลัสเตอร์เกี่ยวกับโหนดปกติ การรับข้อความจากโหนด Y ดังนั้น Y จะทำงาน และในทางกลับกัน ทันทีที่คนส่วนใหญ่รายงานข้อความที่หายไปจากโหนด Y แล้ว Y ก็ปฏิเสธ เป็นที่สงสัยว่าหากองค์ประชุมแจ้งโหนด X ว่าไม่ได้รับข้อความจากมันอีกต่อไป โหนด X เองก็จะถือว่าตัวเองล้มเหลว

ข้อความฮาร์ทบีทจะถูกส่งด้วยความถี่สูงประมาณ 20 ครั้งต่อวินาที โดยมีระยะเวลา 50 มิลลิวินาที ใน Java เป็นการยากที่จะรับประกันการตอบสนองของแอปพลิเคชันภายใน 50 มิลลิวินาที เนื่องจากการหยุดชั่วคราวที่เกิดจากตัวรวบรวมขยะที่เทียบเคียงได้ เราสามารถบรรลุเวลาตอบสนองนี้ได้โดยใช้ตัวรวบรวมขยะ G1 ซึ่งช่วยให้เราสามารถระบุเป้าหมายในช่วงระยะเวลาที่ GC หยุดชั่วคราวได้ อย่างไรก็ตาม ในบางครั้งตัวรวบรวมหยุดชั่วคราวเกิน 50 ms ซึ่งอาจนำไปสู่การตรวจพบข้อผิดพลาดที่ผิดพลาดได้ เพื่อป้องกันไม่ให้สิ่งนี้เกิดขึ้น ผู้ประสานงานจะไม่รายงานความล้มเหลวของโหนดระยะไกลเมื่อข้อความฮาร์ทบีทแรกหายไป เฉพาะในกรณีที่หลาย ๆ หายไปติดต่อกัน นี่คือวิธีที่เราจัดการเพื่อตรวจจับความล้มเหลวของโหนดผู้ประสานงานในปี 200 นางสาว.

แต่การทำความเข้าใจอย่างรวดเร็วว่าโหนดใดหยุดทำงานนั้นไม่เพียงพอ เราจำเป็นต้องทำอะไรบางอย่างเกี่ยวกับเรื่องนี้

การจอง

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

NewSQL = NoSQL+กรด

สมมติว่าเราต้องการทำธุรกรรมในกลุ่ม 50 เรามาพิจารณาล่วงหน้าเกี่ยวกับแผนการทดแทนนั่นคือโหนดใดที่จะดำเนินการธุรกรรมในกลุ่ม 50 ในกรณีที่ผู้ประสานงานหลักล้มเหลว เป้าหมายของเราคือการรักษาฟังก์ชันการทำงานของระบบในกรณีที่ศูนย์ข้อมูลล้มเหลว ลองพิจารณาว่าการสำรองครั้งแรกจะเป็นโหนดจากศูนย์ข้อมูลอื่น และการสำรองที่สองจะเป็นโหนดจากศูนย์ข้อมูลที่สาม รูปแบบนี้ถูกเลือกเพียงครั้งเดียวและไม่เปลี่ยนแปลงจนกว่าโทโพโลยีของคลัสเตอร์จะเปลี่ยนนั่นคือจนกว่าโหนดใหม่จะเข้ามา (ซึ่งเกิดขึ้นน้อยมาก) ขั้นตอนในการเลือกต้นแบบที่ใช้งานอยู่ใหม่ หากอันเก่าล้มเหลวจะเป็นดังนี้เสมอ: กำลังสำรองแรกจะกลายเป็นต้นแบบที่ใช้งานอยู่ และหากหยุดทำงาน กำลังสำรองที่สองจะกลายเป็นต้นแบบที่ใช้งานอยู่

โครงการนี้มีความน่าเชื่อถือมากกว่าอัลกอริธึมสากลเนื่องจากเพื่อเปิดใช้งานต้นแบบใหม่ก็เพียงพอแล้วที่จะระบุความล้มเหลวของอันเก่า

แต่ลูกค้าจะเข้าใจได้อย่างไรว่าอาจารย์คนไหนทำงานอยู่ตอนนี้? ไม่สามารถส่งข้อมูลไปยังลูกค้านับพันได้ภายใน 50 มิลลิวินาที สถานการณ์อาจเกิดขึ้นได้เมื่อลูกค้าส่งคำขอเพื่อเปิดธุรกรรมโดยไม่รู้ว่าข้อมูลหลักนี้ไม่ทำงานอีกต่อไป และคำขอจะหมดเวลา เพื่อป้องกันไม่ให้สิ่งนี้เกิดขึ้น ลูกค้าคาดการณ์ว่าจะส่งคำขอเพื่อเปิดธุรกรรมไปยังกลุ่มหลักและทุนสำรองทั้งสองของเขาในคราวเดียว แต่เฉพาะผู้ที่เป็นมาสเตอร์ที่ใช้งานอยู่ในขณะนี้เท่านั้นที่จะตอบสนองต่อคำขอนี้ ลูกค้าจะทำการสื่อสารในภายหลังทั้งหมดภายในธุรกรรมเฉพาะกับต้นแบบที่ใช้งานอยู่เท่านั้น

ต้นแบบการสำรองข้อมูลจะวางคำขอที่ได้รับสำหรับธุรกรรมที่ไม่ได้อยู่ในคิวของธุรกรรมที่ยังไม่เกิด ซึ่งจะถูกเก็บไว้เป็นระยะเวลาหนึ่ง หากต้นแบบที่ใช้งานอยู่ตาย ต้นแบบใหม่จะร้องขอให้เปิดธุรกรรมจากคิวและตอบสนองต่อลูกค้า หากลูกค้าได้เปิดธุรกรรมกับต้นแบบเก่าแล้ว การตอบกลับครั้งที่สองจะถูกละเว้น (และแน่นอนว่าธุรกรรมดังกล่าวจะไม่สมบูรณ์และลูกค้าจะทำซ้ำ)

ธุรกรรมทำงานอย่างไร

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

NewSQL = NoSQL+กรด

เมื่อลูกค้าต้องการเปลี่ยนข้อมูลในธุรกรรม ลูกค้าจะส่งคำขอไปยังผู้ประสานงานเพื่อแก้ไขเอนทิตี และผู้ประสานงานจะวางข้อมูลใหม่ในตารางสถานะธุรกรรมในหน่วยความจำ การบันทึกจะเสร็จสิ้น - ไม่มีการบันทึกใด ๆ ในพื้นที่จัดเก็บข้อมูล

NewSQL = NoSQL+กรด

เมื่อลูกค้าร้องขอข้อมูลที่เปลี่ยนแปลงของตนเองโดยเป็นส่วนหนึ่งของธุรกรรมที่ใช้งานอยู่ ผู้ประสานงานจะดำเนินการดังต่อไปนี้:

  • หาก ID อยู่ในธุรกรรมแล้ว ข้อมูลจะถูกดึงออกจากหน่วยความจำ
  • หากไม่มี ID ในหน่วยความจำ ข้อมูลที่ขาดหายไปจะถูกอ่านจากโหนดการจัดเก็บข้อมูล รวมกับข้อมูลที่อยู่ในหน่วยความจำแล้ว และผลลัพธ์จะมอบให้กับไคลเอนต์

ดังนั้น ลูกค้าสามารถอ่านการเปลี่ยนแปลงของตนเองได้ แต่ลูกค้ารายอื่นจะไม่เห็นการเปลี่ยนแปลงเหล่านี้ เนื่องจากถูกเก็บไว้ในหน่วยความจำของผู้ประสานงานเท่านั้น แต่ยังไม่ได้อยู่ในโหนด Cassandra

NewSQL = NoSQL+กรด

เมื่อไคลเอนต์ส่งคอมมิต สถานะที่อยู่ในหน่วยความจำของบริการจะถูกบันทึกโดยผู้ประสานงานในชุดงานที่บันทึกไว้ และจะถูกส่งเป็นชุดที่บันทึกไว้ไปยังพื้นที่จัดเก็บข้อมูล Cassandra ร้านค้าทำทุกอย่างที่จำเป็นเพื่อให้แน่ใจว่าแพ็คเกจนี้ถูกนำไปใช้แบบอะตอมมิก (สมบูรณ์) และตอบกลับไปยังผู้ประสานงานที่ปลดล็อคและยืนยันความสำเร็จของการทำธุรกรรมให้กับลูกค้า

NewSQL = NoSQL+กรด

และในการย้อนกลับ ผู้ประสานงานเพียงต้องการเพิ่มหน่วยความจำที่ครอบครองโดยสถานะธุรกรรมเท่านั้น

จากการปรับปรุงข้างต้น เราได้นำหลักการ ACID มาใช้:

  • อะตอมมิกซิตี. นี่เป็นการรับประกันว่าจะไม่มีการบันทึกธุรกรรมบางส่วนในระบบ การดำเนินการย่อยทั้งหมดจะเสร็จสมบูรณ์หรือจะไม่มีการดำเนินการใดเลย เราปฏิบัติตามหลักการนี้ผ่านชุดงานที่บันทึกไว้ใน Cassandra
  • ความสม่ำเสมอ. แต่ละธุรกรรมที่ประสบความสำเร็จ ตามคำจำกัดความ จะบันทึกเฉพาะผลลัพธ์ที่ถูกต้องเท่านั้น หลังจากเปิดธุรกรรมและดำเนินการส่วนหนึ่งของการดำเนินการแล้ว หากพบว่าผลลัพธ์ไม่ถูกต้อง ระบบจะทำการย้อนกลับ
  • การแยกตัว. เมื่อทำธุรกรรม ธุรกรรมที่เกิดขึ้นพร้อมกันไม่ควรส่งผลกระทบต่อผลลัพธ์ ธุรกรรมที่แข่งขันกันจะถูกแยกออกโดยใช้การล็อคในแง่ร้ายกับผู้ประสานงาน สำหรับการอ่านนอกธุรกรรม หลักการแยกจะถูกสังเกตที่ระดับ Read Commited
  • ความมั่นคง. ไม่ว่าปัญหาในระดับล่างจะเป็นอย่างไร เช่น ระบบหยุดทำงาน ความล้มเหลวของฮาร์ดแวร์ การเปลี่ยนแปลงที่ทำโดยธุรกรรมที่เสร็จสมบูรณ์ควรยังคงอยู่เมื่อการดำเนินการกลับมาทำงานต่อ

การอ่านตามดัชนี

ลองใช้ตารางง่ายๆ:

CREATE TABLE photos (
id bigint primary key,
owner bigint,
modified timestamp,
…)

มีรหัส (คีย์หลัก) เจ้าของ และวันที่แก้ไข คุณต้องส่งคำของ่ายๆ - เลือกข้อมูลเกี่ยวกับเจ้าของพร้อมวันที่เปลี่ยนแปลง "สำหรับวันสุดท้าย"

SELECT *
WHERE owner=?
AND modified>?

เพื่อให้แบบสอบถามได้รับการประมวลผลอย่างรวดเร็ว ใน SQL DBMS แบบคลาสสิก คุณจะต้องสร้างดัชนีตามคอลัมน์ (เจ้าของ แก้ไข) เราสามารถทำได้ค่อนข้างง่าย เนื่องจากตอนนี้เรามีการรับประกันกรดแล้ว!

ดัชนีใน C*One

มีตารางต้นฉบับพร้อมรูปถ่ายซึ่งมีรหัสบันทึกเป็นคีย์หลัก

NewSQL = NoSQL+กรด

สำหรับดัชนี C*One จะสร้างตารางใหม่ซึ่งเป็นสำเนาของต้นฉบับ คีย์จะเหมือนกับนิพจน์ดัชนี และยังรวมคีย์หลักของเรกคอร์ดจากตารางต้นฉบับด้วย:

NewSQL = NoSQL+กรด

ตอนนี้แบบสอบถามสำหรับ "เจ้าของสำหรับวันสุดท้าย" สามารถเขียนใหม่เป็นตัวเลือกจากตารางอื่นได้:

SELECT * FROM i1_test
WHERE owner=?
AND modified>?

ความสอดคล้องของข้อมูลในรูปภาพตารางต้นฉบับและตารางดัชนี i1 จะได้รับการดูแลโดยอัตโนมัติโดยผู้ประสานงาน ขึ้นอยู่กับสคีมาข้อมูลเพียงอย่างเดียว เมื่อได้รับการเปลี่ยนแปลง ผู้ประสานงานจะสร้างและจัดเก็บการเปลี่ยนแปลงไม่เพียงแต่ในตารางหลักเท่านั้น แต่ยังอยู่ในสำเนาด้วย ไม่มีการดำเนินการเพิ่มเติมในตารางดัชนี บันทึกจะไม่ถูกอ่าน และไม่มีการล็อก นั่นคือการเพิ่มดัชนีแทบไม่ต้องใช้ทรัพยากรใด ๆ และแทบไม่มีผลกระทบต่อความเร็วในการปรับใช้การแก้ไข

เมื่อใช้ ACID เราสามารถใช้ดัชนีที่คล้ายกับ SQL ได้ มีความสอดคล้อง ปรับขนาดได้ รวดเร็ว เขียนได้ และมีอยู่ในภาษาคิวรี CQL ไม่จำเป็นต้องเปลี่ยนแปลงรหัสแอปพลิเคชันเพื่อรองรับดัชนี ทุกอย่างเรียบง่ายเหมือนใน SQL และที่สำคัญที่สุด ดัชนีจะไม่ส่งผลต่อความเร็วในการดำเนินการของการแก้ไขตารางธุรกรรมดั้งเดิม

เกิดอะไรขึ้น

เราได้พัฒนา C*One เมื่อสามปีที่แล้วและเปิดตัวสู่การดำเนินการเชิงพาณิชย์

ในที่สุดเราได้อะไร? มาประเมินสิ่งนี้โดยใช้ตัวอย่างระบบย่อยการประมวลผลและการจัดเก็บภาพถ่าย ซึ่งเป็นหนึ่งในประเภทข้อมูลที่สำคัญที่สุดในโซเชียลเน็ตเวิร์ก เราไม่ได้พูดถึงเนื้อความของภาพถ่าย แต่เกี่ยวกับเมตาดาต้าทุกประเภท ขณะนี้ Odnoklassniki มีบันทึกดังกล่าวประมาณ 20 พันล้านรายการ ระบบจะประมวลผลคำขออ่าน 80 รายการต่อวินาที ธุรกรรม ACID สูงสุด 8 รายการต่อวินาทีที่เกี่ยวข้องกับการแก้ไขข้อมูล

เมื่อเราใช้ SQL ที่มีปัจจัยการจำลอง = 1 (แต่ใน RAID 10) ข้อมูลเมตาของภาพถ่ายจะถูกจัดเก็บไว้ในคลัสเตอร์ที่มีความพร้อมใช้งานสูงซึ่งมีเครื่อง 32 เครื่องที่ใช้ Microsoft SQL Server (บวกกับการสำรองข้อมูล 11 รายการ) มีการจัดสรรเซิร์ฟเวอร์ 10 เครื่องเพื่อจัดเก็บข้อมูลสำรอง รถยนต์ราคาแพงทั้งหมด 50 คัน ในเวลาเดียวกัน ระบบทำงานที่พิกัดโหลดโดยไม่มีการสำรอง

หลังจากย้ายไปยังระบบใหม่ เราได้รับปัจจัยการจำลอง = 3 - สำเนาในศูนย์ข้อมูลแต่ละแห่ง ระบบประกอบด้วยโหนดจัดเก็บข้อมูล Cassandra 63 เครื่อง และเครื่องประสานงาน 6 เครื่อง รวมทั้งหมด 69 เซิร์ฟเวอร์ แต่เครื่องจักรเหล่านี้ราคาถูกกว่ามาก ต้นทุนรวมประมาณ 30% ของต้นทุนของระบบ SQL ในเวลาเดียวกัน โหลดจะถูกเก็บไว้ที่ 30%

ด้วยการเปิดตัว C*One เวลาแฝงก็ลดลงเช่นกัน: ใน SQL การดำเนินการเขียนใช้เวลาประมาณ 4,5 ms ใน C*One - ประมาณ 1,6 มิลลิวินาที ระยะเวลาการทำธุรกรรมโดยเฉลี่ยน้อยกว่า 40 ms การดำเนินการเสร็จสิ้นใน 2 ms ระยะเวลาการอ่านและเขียนโดยเฉลี่ย 2 ms เปอร์เซ็นไทล์ที่ 99 - เพียง 3-3,1 มิลลิวินาที จำนวนการหมดเวลาลดลง 100 เท่า - ทั้งหมดนี้เกิดจากการใช้การเก็งกำไรอย่างกว้างขวาง

ขณะนี้โหนด SQL Server ส่วนใหญ่ได้ถูกเลิกใช้งานแล้ว ผลิตภัณฑ์ใหม่กำลังได้รับการพัฒนาโดยใช้ C*One เท่านั้น เราปรับใช้ C*One เพื่อทำงานในคลาวด์ของเรา เมฆก้อนเดียวซึ่งช่วยให้ปรับใช้คลัสเตอร์ใหม่ได้เร็วขึ้น ลดความซับซ้อนในการกำหนดค่า และดำเนินการอัตโนมัติ หากไม่มีซอร์สโค้ด การทำเช่นนี้จะยากและยุ่งยากมากขึ้น

ตอนนี้เรากำลังดำเนินการถ่ายโอนสถานที่จัดเก็บข้อมูลอื่นๆ ของเราไปยังระบบคลาวด์ - แต่นั่นเป็นเรื่องราวที่แตกต่างไปจากเดิมอย่างสิ้นเชิง

ที่มา: will.com

เพิ่มความคิดเห็น