สวัสดีทุกคน. วลาดิสลาฟ โรดิน ติดต่อมา ปัจจุบันฉันสอนหลักสูตรเกี่ยวกับสถาปัตยกรรมซอฟต์แวร์และสถาปัตยกรรมซอฟต์แวร์ความเครียดสูงที่ OTUS ในความคาดหมายของการเริ่มสตรีมหลักสูตรใหม่
การแนะนำ
เนื่องจาก HDD สามารถทำงานได้ประมาณ 400-700 การดำเนินการต่อวินาทีเท่านั้น (ซึ่งเทียบไม่ได้กับ rps ทั่วไปบนระบบที่มีโหลดสูง) ฐานข้อมูลดิสก์แบบคลาสสิกจึงเป็นคอขวดของสถาปัตยกรรม ดังนั้นจึงจำเป็นต้องให้ความสนใจเป็นพิเศษกับรูปแบบการปรับขนาดของการจัดเก็บข้อมูลนี้
ปัจจุบันมีรูปแบบการปรับขนาดฐานข้อมูลอยู่ 2 รูปแบบ: การจำลองแบบและการแบ่งส่วน Sharding ช่วยให้คุณสามารถปรับขนาดการดำเนินการเขียน และลด rps ต่อการเขียนต่อเซิร์ฟเวอร์ในคลัสเตอร์ของคุณ การจำลองแบบช่วยให้คุณทำสิ่งเดียวกันได้ แต่ด้วยการดำเนินการอ่าน บทความนี้เน้นรูปแบบนี้
การจำลองแบบ
หากคุณดูการจำลองแบบในระดับที่สูงมาก มันเป็นเรื่องง่าย: คุณมีเซิร์ฟเวอร์เดียว มีข้อมูลอยู่บนนั้น และเซิร์ฟเวอร์นี้ก็ไม่สามารถรับมือกับภาระในการอ่านข้อมูลนี้ได้อีกต่อไป คุณเพิ่มเซิร์ฟเวอร์อีกสองสามเซิร์ฟเวอร์ ซิงโครไนซ์ข้อมูลระหว่างเซิร์ฟเวอร์ทั้งหมด และผู้ใช้สามารถอ่านจากเซิร์ฟเวอร์ใดก็ได้ในคลัสเตอร์ของคุณ
แม้จะมีความเรียบง่ายที่ชัดเจน แต่ก็มีหลายตัวเลือกในการจำแนกการใช้งานต่างๆ ของโครงร่างนี้:
- ตามบทบาทในคลัสเตอร์ (master-master หรือ master-slave)
- ตามวัตถุที่ส่ง (ตามแถว ตามคำสั่ง หรือผสม)
- ตามกลไกการซิงโครไนซ์โหนด
วันนี้เราจะมาพูดถึงประเด็นที่ 3
การทำธุรกรรมเกิดขึ้นได้อย่างไร?
หัวข้อนี้ไม่เกี่ยวข้องโดยตรงกับการจำลองแบบ สามารถเขียนบทความแยกต่างหากได้ แต่เนื่องจากการอ่านเพิ่มเติมไม่มีประโยชน์หากไม่เข้าใจกลไกการทำธุรกรรม ฉันขอเตือนคุณถึงสิ่งพื้นฐานที่สุด การทำธุรกรรมจะเกิดขึ้นใน 3 ขั้นตอน:
- การบันทึกธุรกรรมลงในบันทึกฐานข้อมูล
- การใช้ธุรกรรมในกลไกฐานข้อมูล
- ส่งคืนคำยืนยันให้กับลูกค้าว่าธุรกรรมได้นำไปใช้สำเร็จแล้ว
ในฐานข้อมูลที่แตกต่างกัน อัลกอริธึมนี้อาจมีความแตกต่าง: ตัวอย่างเช่น ในกลไก InnoDB ของฐานข้อมูล MySQL จะมีบันทึก 2 รายการ: รายการหนึ่งสำหรับการจำลอง (บันทึกไบนารี) และอีกรายการหนึ่งสำหรับการรักษา ACID (บันทึกเลิกทำ/ทำซ้ำ) ในขณะที่อยู่ใน PostgreSQL มีหนึ่งบันทึกที่ทำหน้าที่ทั้งสองฟังก์ชัน (บันทึกล่วงหน้า = WAL) แต่สิ่งที่นำเสนอข้างต้นนั้นเป็นแนวคิดทั่วไปอย่างแม่นยำซึ่งช่วยให้ไม่ต้องคำนึงถึงความแตกต่างดังกล่าว
การจำลองแบบซิงโครนัส (ซิงค์)
มาเพิ่มตรรกะเพื่อจำลองการเปลี่ยนแปลงที่ได้รับไปยังอัลกอริทึมการส่งธุรกรรม:
- การบันทึกธุรกรรมลงในบันทึกฐานข้อมูล
- การใช้ธุรกรรมในกลไกฐานข้อมูล
- การส่งข้อมูลไปยังแบบจำลองทั้งหมด
- ได้รับการยืนยันจากแบบจำลองทั้งหมดว่าธุรกรรมเสร็จสมบูรณ์แล้ว
- ส่งคืนคำยืนยันให้กับลูกค้าว่าธุรกรรมได้นำไปใช้สำเร็จแล้ว
ด้วยวิธีนี้เราได้รับข้อเสียหลายประการ:
- ไคลเอนต์รอการเปลี่ยนแปลงที่จะนำไปใช้กับแบบจำลองทั้งหมด
- เมื่อจำนวนโหนดในคลัสเตอร์เพิ่มขึ้น เราจะลดโอกาสที่การดำเนินการเขียนจะสำเร็จ
หากทุกอย่างชัดเจนมากหรือน้อยในประเด็นที่ 1 สาเหตุของประเด็นที่ 2 ก็คุ้มค่าที่จะอธิบาย หากในระหว่างการจำลองแบบซิงโครนัส เราไม่ได้รับการตอบกลับจากโหนดอย่างน้อยหนึ่งโหนด เราจะย้อนกลับธุรกรรม ดังนั้น โดยการเพิ่มจำนวนโหนดในคลัสเตอร์ คุณจะเพิ่มโอกาสที่การดำเนินการเขียนจะล้มเหลว
เราสามารถรอการยืนยันจากโหนดเพียงไม่กี่เปอร์เซ็นต์ เช่น จาก 51% (องค์ประชุม) ได้หรือไม่ ใช่ เราทำได้ แต่ในเวอร์ชันคลาสสิก จำเป็นต้องมีการยืนยันจากโหนดทั้งหมด เนื่องจากนี่คือวิธีที่เราสามารถรับประกันความสอดคล้องของข้อมูลที่สมบูรณ์ในคลัสเตอร์ ซึ่งเป็นข้อได้เปรียบที่ไม่ต้องสงสัยของการจำลองประเภทนี้
การจำลองแบบอะซิงโครนัส (async)
มาแก้ไขอัลกอริธึมก่อนหน้ากัน เราจะส่งข้อมูลไปยังแบบจำลอง "ในภายหลัง" และ "ในภายหลัง" การเปลี่ยนแปลงจะมีผลกับแบบจำลอง:
- การบันทึกธุรกรรมลงในบันทึกฐานข้อมูล
- การใช้ธุรกรรมในกลไกฐานข้อมูล
- ส่งคืนคำยืนยันให้กับลูกค้าว่าธุรกรรมได้นำไปใช้สำเร็จแล้ว
- การส่งข้อมูลไปยังเรพลิกาและนำการเปลี่ยนแปลงไปใช้กับเรพลิกา
แนวทางนี้นำไปสู่ความจริงที่ว่าคลัสเตอร์ทำงานได้อย่างรวดเร็ว เนื่องจากเราไม่ปล่อยให้ไคลเอ็นต์รอให้ข้อมูลเข้าถึงแบบจำลองและแม้แต่ถูกคอมมิตด้วยซ้ำ
แต่เงื่อนไขของการดัมพ์ข้อมูลลงบนเรพลิกา “ในภายหลัง” อาจนำไปสู่การสูญเสียธุรกรรม และการสูญเสียธุรกรรมที่ได้รับการยืนยันจากผู้ใช้ เพราะหากข้อมูลไม่มีเวลาสำหรับการจำลอง การยืนยันกับลูกค้า เกี่ยวกับความสำเร็จของการดำเนินการที่ถูกส่งไปและโหนดที่การเปลี่ยนแปลงมาถึงนั้นทำให้ HDD เสียหาย เราสูญเสียธุรกรรมซึ่งอาจนำไปสู่ผลลัพธ์ที่ไม่พึงประสงค์อย่างมาก
การจำลองแบบกึ่งซิงค์
ในที่สุดเราก็มาถึงการจำลองแบบกึ่งซิงโครนัส การจำลองแบบประเภทนี้ไม่เป็นที่รู้จักมากนักหรือแพร่หลายมากนัก แต่เป็นที่สนใจอย่างมาก เนื่องจากสามารถรวมข้อดีของการจำลองแบบซิงโครนัสและอะซิงโครนัสเข้าด้วยกันได้
ลองรวม 2 วิธีก่อนหน้านี้เข้าด้วยกัน เราจะไม่รักษาลูกค้าไว้นาน แต่เราจำเป็นต้องจำลองข้อมูล:
- การบันทึกธุรกรรมลงในบันทึกฐานข้อมูล
- การใช้ธุรกรรมในกลไกฐานข้อมูล
- การส่งข้อมูลไปยังแบบจำลอง
- ได้รับการยืนยันจากแบบจำลองว่าได้รับการเปลี่ยนแปลงแล้ว (จะมีการนำไปใช้ "ในภายหลัง")
- ส่งคืนคำยืนยันให้กับลูกค้าว่าธุรกรรมได้นำไปใช้สำเร็จแล้ว
โปรดทราบว่าด้วยอัลกอริทึมนี้ การสูญเสียธุรกรรมจะเกิดขึ้นก็ต่อเมื่อทั้งโหนดได้รับการเปลี่ยนแปลงและโหนดแบบจำลองล้มเหลว ความน่าจะเป็นของความล้มเหลวดังกล่าวถือว่าต่ำและยอมรับความเสี่ยงเหล่านี้
แต่ด้วยวิธีนี้อาจมีความเสี่ยงที่จะเกิดการอ่าน Phantom ลองจินตนาการถึงสถานการณ์ต่อไปนี้: ในขั้นตอนที่ 4 เราไม่ได้รับการยืนยันจากแบบจำลองใดๆ เราต้องย้อนกลับธุรกรรมนี้และไม่ส่งคืนการยืนยันให้กับลูกค้า เนื่องจากข้อมูลถูกนำไปใช้ในขั้นตอนที่ 2 จึงมีช่องว่างเวลาระหว่างจุดสิ้นสุดของขั้นตอนที่ 2 และการย้อนกลับของธุรกรรม ในระหว่างที่ธุรกรรมแบบขนานสามารถดูการเปลี่ยนแปลงที่ไม่ควรอยู่ในฐานข้อมูล
การจำลองแบบกึ่งซิงค์ที่สูญเสียน้อยลง
หากคุณคิดสักนิด คุณก็สามารถย้อนกลับขั้นตอนของอัลกอริทึมและแก้ไขปัญหาการอ่าน Phantom ในสถานการณ์นี้ได้:
- การบันทึกธุรกรรมลงในบันทึกฐานข้อมูล
- การส่งข้อมูลจำลอง
- ได้รับการยืนยันจากแบบจำลองว่าได้รับการเปลี่ยนแปลงแล้ว (จะมีการนำไปใช้ "ในภายหลัง")
- การใช้ธุรกรรมในกลไกฐานข้อมูล
- ส่งคืนคำยืนยันให้กับลูกค้าว่าธุรกรรมได้นำไปใช้สำเร็จแล้ว
ตอนนี้เรายอมรับการเปลี่ยนแปลงเฉพาะเมื่อมีการจำลองแบบเท่านั้น
เอาท์พุต
เช่นเคย ไม่มีวิธีแก้ปัญหาในอุดมคติ มีชุดของวิธีแก้ปัญหา ซึ่งแต่ละวิธีก็มีข้อดีและข้อเสียของตัวเอง และเหมาะสำหรับการแก้ปัญหาประเภทต่างๆ นี่เป็นเรื่องจริงสำหรับการเลือกกลไกในการซิงโครไนซ์ข้อมูลในฐานข้อมูลที่ถูกจำลองแบบ ชุดข้อดีที่การจำลองแบบกึ่งซิงโครนัสมีความแข็งแกร่งและน่าสนใจเพียงพอจนถือว่าคุ้มค่าแก่ความสนใจ แม้ว่าจะมีความแพร่หลายต่ำก็ตาม