ความแตกต่างหลักระหว่างสิ่งที่อาจผิดพลาดกับสิ่งที่ไม่น่าจะผิดพลาดได้เลยก็คือ เมื่อสิ่งที่ไม่น่าจะผิดพลาดได้เลยเกิดผิดพลาดขึ้น มันมักจะกลายเป็นสิ่งที่เข้าถึงหรือซ่อมแซมไม่ได้
Douglas Adams, ส่วนใหญ่ก็ไม่เป็นอันตราย (1992)
Replication หมายถึงการเก็บสําเนาของข้อมูลเดียวกันบนหลายเครื่องที่เชื่อมต่อกันผ่านเครือข่าย ดังที่ได้กล่าวถึงในหัวข้อ "ระบบกระจายเทียบกับระบบโหนดเดียว" มีเหตุผลหลายประการที่คุณอาจต้องการจําลองข้อมูล ได้แก่:
-
เพื่อเก็บข้อมูลให้อยู่ใกล้ผู้ใช้ตามภูมิศาสตร์ (และลดความหน่วงในการเข้าถึง)
-
เพื่อให้ระบบสามารถทํางานต่อไปได้แม้บางส่วนล้มเหลว (และเพิ่มความพร้อมใช้งานและความคงทนของข้อมูล)
-
เพื่อขยายจํานวนเครื่องที่ให้บริการคําสั่งอ่าน (และเพิ่มปริมาณงานอ่าน)
ในบทนี้เราจะสมมติว่าชุดข้อมูลของคุณเล็กพอที่แต่ละเครื่องสามารถเก็บสําเนาทั้งหมดได้ ใน บทที่ 7 เราจะผ่อนคลายสมมตินั้นและพูดถึง sharding ( partitioning ) สําหรับชุดข้อมูลที่ใหญ่เกินไปสําหรับเครื่องเดียว ในบทต่อๆ ไปเราจะพูดถึงข้อผิดพลาดประเภทต่างๆ ที่อาจเกิดขึ้นในระบบจําลองข้อมูลและวิธีจัดการกับมัน
ถ้าข้อมูลที่คุณจําลองไม่เปลี่ยนแปลงตามกาลเวลา การจําลองก็ง่าย แค่คัดลอกข้อมูลไปยังทุก node ครั้งเดียวก็เสร็จ ความยากทั้งหมดของการจําลองข้อมูลอยู่ที่การจัดการ การเปลี่ยนแปลง ของข้อมูลที่ถูกจําลอง และนั่นคือสิ่งที่บทนี้พูดถึง เราจะพูดถึงอัลกอริทึมสามตระกูลสําหรับการจําลองการเปลี่ยนแปลงระหว่าง nodes: single-leader, multi-leader, และ leaderless replication ฐานข้อมูลแบบกระจายเกือบทั้งหมดใช้หนึ่งในสามวิธีนี้ แต่ละวิธีมีข้อดีและข้อเสียซึ่งเราจะตรวจสอบอย่างละเอียด
มีข้อแลกเปลี่ยนมากมายที่ต้องพิจารณาในการจําลองข้อมูล เช่น จะใช้ synchronous หรือ asynchronous replication และวิธีจัดการกับ replicas ที่ล้มเหลว สิ่งเหล่านี้มักเป็นตัวเลือกการกําหนดค่าในฐานข้อมูล และถึงแม้รายละเอียดจะแตกต่างกันไปตามฐานข้อมูล แต่หลักการทั่วไปก็คล้ายคลึงกันในหลายๆ การนําไปใช้ เราจะพูดถึงผลกระทบของตัวเลือกเหล่านี้ในบทนี้
การจําลองฐานข้อมูลเป็นหัวข้อเก่าแก่ หลักการไม่ได้เปลี่ยนแปลงมากนักตั้งแต่ถูกศึกษาครั้งแรกในยุค 1970 [ 1 ] เพราะข้อจํากัดพื้นฐานของเครือข่ายยังคงเหมือนเดิม อย่างไรก็ตาม แนวคิดอย่าง eventual consistency ยังคงสร้างความสับสน ในหัวข้อ "ปัญหาจาก Replication Lag" เราจะพูดถึง eventual consistency ให้ชัดเจนขึ้น และพูดถึงหลักประกันอย่าง read-your-writes และ monotonic reads
Backups and Replication (Backup และการจำลองข้อมูล)
คุณอาจสงสัยว่ายังจําเป็นต้องมี backups หรือไม่ถ้ามี replication คําตอบคือใช่ เพราะทั้งสองมีวัตถุประสงค์ต่างกัน: replicas จะสะท้อน writes จาก node หนึ่งไปยัง nodes อื่นๆ อย่างรวดเร็ว แต่ backups จะเก็บ snapshot เก่าของข้อมูลเพื่อให้คุณย้อนเวลากลับไปได้ ถ้าคุณลบข้อมูลบางอย่างโดยไม่ตั้งใจ replication ไม่ช่วยเพราะการลบจะถูกกระจายไปยัง replicas เช่นกัน คุณต้องมี backup ถ้าต้องการกู้คืนข้อมูลที่ถูกลบ
ในความเป็นจริง replication และ backups มักเสริมซึ่งกันและกัน Backups บางครั้งเป็นส่วนหนึ่งของกระบวนการตั้งค่า replication ดังที่เราจะเห็นในหัวข้อ "การตั้งค่า Follower ใหม่" ในทางกลับกัน การเก็บถาวร replication logs ก็สามารถเป็นส่วนหนึ่งของกระบวนการ backup ได้
ฐานข้อมูลบางตัวภายในจะรักษา immutable snapshots ของสถานะในอดีต ซึ่งทําหน้าที่เป็น backup ภายใน อย่างไรก็ตาม นี่หมายถึงการเก็บเวอร์ชันเก่าของข้อมูลไว้บนสื่อบันทึกข้อมูลเดียวกับสถานะปัจจุบัน ถ้าคุณมีข้อมูลจํานวนมาก การเก็บ backups ของข้อมูลเก่าไว้ใน object store ที่ถูกปรับให้เหมาะสมสําหรับข้อมูลที่เข้าถึงไม่บ่อย และเก็บเฉพาะสถานะปัจจุบันของฐานข้อมูลในพื้นที่จัดเก็บหลักอาจมีต้นทุนถูกกว่า
Single-Leader Replication (การจำลองแบบผู้นำเดียว)
แต่ละ node ที่เก็บสําเนาของฐานข้อมูลเรียกว่า replica เมื่อมีหลาย replicas คําถามก็เกิดขึ้นอย่างหลีกเลี่ยงไม่ได้: เราจะมั่นใจได้อย่างไรว่าข้อมูลทั้งหมดไปอยู่บนทุก replicas?
ทุก write ไปยังฐานข้อมูลต้องถูกประมวลผลโดยทุก replica มิฉะนั้น replicas จะไม่มีข้อมูลที่เหมือนกันอีกต่อไป วิธีแก้ปัญหาที่พบมากที่สุดเรียกว่า leader-based, primary-backup, หรือ active/passive replication มันทํางานดังนี้ (ดู Figure 6-1):
-
replica ตัวหนึ่งถูกกําหนดให้เป็น leader (หรือเรียกอีกอย่างว่า primary หรือ source [ 2 ]) เมื่อ clients ต้องการเขียนข้อมูล พวกเขาต้องส่งคําขอไปยัง leader ซึ่งจะเขียนข้อมูลใหม่ลงในพื้นที่จัดเก็บภายในของตนก่อน
-
replicas อื่นๆ เรียกว่า followers (หรือ read replicas, secondaries, หรือ hot standbys) เมื่อใดก็ตามที่ leader เขียนข้อมูลใหม่ลงในพื้นที่จัดเก็บของตน มันจะส่งการเปลี่ยนแปลงข้อมูลนั้นไปยัง followers ทั้งหมดโดยเป็นส่วนหนึ่งของ replication log หรือ change stream แต่ละ follower จะนํา log จาก leader ไปอัปเดตสําเนาท้องถิ่นของฐานข้อมูลตามลําดับ โดยประยุกต์ใช้ writes ทั้งหมดในลําดับเดียวกันกับที่ถูกประมวลผลบน leader
-
เมื่อ client ต้องการอ่านข้อมูล มันสามารถสอบถามจาก leader หรือ followers ตัวใดก็ได้ อย่างไรก็ตาม writes จะถูกรับโดย leader เท่านั้น (followers เป็นแบบอ่านอย่างเดียวจากมุมมองของ client)
Figure 6-1. การจําลองแบบ Single-Leader นํา writes ทั้งหมดไปยัง leader ที่ถูกกําหนด ซึ่งส่ง stream ของการเปลี่ยนแปลงไปยัง follower replicas
ถ้าฐานข้อมูลถูก sharded (ดู บทที่ 7) แต่ละ shard จะมี leader หนึ่งตัว shards ที่แตกต่างกันอาจมี leaders อยู่บน nodes ที่แตกต่างกัน แต่แต่ละ shard ยังคงต้องมี leader node หนึ่งตัว ในหัวข้อ "การจําลองแบบ Multi-Leader" เราจะพูดถึงโมเดลทางเลือกที่ระบบอาจมีหลาย leaders สําหรับ shard เดียวกันในเวลาเดียวกัน
Single-leader replication ถูกใช้อย่างแพร่หลายมาก เป็นฟีเจอร์ในตัวของฐานข้อมูลเชิงสัมพันธ์หลายตัว เช่น PostgreSQL, MySQL, Oracle Data Guard [ 3 ], และ SQL Server's Always On availability groups [ 4 ] มันยังใช้ในฐานข้อมูลเอกสารบางตัว (เช่น MongoDB และ DynamoDB [ 5 ]), message brokers อย่าง Kafka, replicated block devices อย่าง DRBD, และ network filesystems บางตัว อัลกอริทึม consensus หลายตัว—เช่น Raft ที่ใช้สําหรับ replication ใน CockroachDB [ 6 ], TiDB [ 7 ], etcd, และ RabbitMQ quorum queues—ก็ใช้ single leader เช่นกัน และจะเลือก leader ใหม่โดยอัตโนมัติถ้าตัวเก่าล้มเหลว (เราจะพูดถึง consensus ในรายละเอียดเพิ่มเติมใน บทที่ 10)
หมายเหตุ
ในเอกสารเก่าคุณอาจเห็นคําว่า master--slave replication ซึ่งหมายถึงสิ่งเดียวกับ leader-based replication แต่ควรหลีกเลี่ยงคํานี้เนื่องจากถูกมองว่าไม่เหมาะสมอย่างกว้างขวาง [ 8 ]
Synchronous Versus Asynchronous Replication (การจำลองแบบซิงโครนัสกับอะซิงโครนัส)
รายละเอียดที่สําคัญของระบบจําลองข้อมูลคือการจําลองเกิดขึ้นแบบ synchronous หรือ asynchronous (ในฐานข้อมูลเชิงสัมพันธ์ มักเป็นตัวเลือกที่กําหนดค่าได้ ส่วนระบบอื่นๆ มักถูกเขียนให้เป็นแบบใดแบบหนึ่งโดยเฉพาะ)
ลองคิดถึงสิ่งที่เกิดขึ้นใน Figure 6-1 ซึ่งผู้ใช้เว็บไซต์อัปเดตรูปโปรไฟล์ของตน ณ จุดเวลาหนึ่ง client ส่งคําขออัปเดตไปยัง leader หลังจากนั้นไม่นาน leader ก็ได้รับคําขอ จากนั้น leader จะส่งต่อการเปลี่ยนแปลงข้อมูลไปยัง followers และแจ้งให้ client ทราบว่าการอัปเดตสําเร็จ Figure 6-2 แสดงวิธีหนึ่งที่จังหวะเวลาอาจเป็นไปได้
Figure 6-2. การจําลองแบบ Leader-based ที่มี follower แบบซิงโครนัสหนึ่งตัวและแบบอะซิงโครนัสหนึ่งตัว
ในตัวอย่างนี้ การจําลองไปยัง follower 1 เป็นแบบ synchronous: leader จะรอจนกว่า follower 1 จะยืนยันว่าได้รับ write ก่อนที่จะรายงานความสําเร็จให้ผู้ใช้ทราบและก่อนที่จะทําให้ write นั้นปรากฏแก่ clients อื่นๆ การจําลองไปยัง follower 2 เป็นแบบ asynchronous (หรือ nonblocking): leader ส่งข้อความแต่ไม่ต้องรอการตอบกลับจาก follower
แผนภาพแสดงความล่าช้าอย่างมีนัยสําคัญก่อนที่ follower 2 จะประมวลผลข้อความ โดยปกติแล้วการจําลองค่อนข้างรวดเร็ว ระบบฐานข้อมูลส่วนใหญ่ใช้เวลาไม่ถึงวินาทีในการส่งการเปลี่ยนแปลงไปยัง followers อย่างไรก็ตาม ไม่มีการรับประกันว่าจะใช้เวลานานเท่าใด ในบางสถานการณ์ followers อาจตามหลัง leader หลายนาทีหรือมากกว่า—เช่น ถ้า follower กําลังกู้คืนจากความล้มเหลว ถ้าระบบทํางานใกล้ขีดความสามารถสูงสุด หรือถ้ามีปัญหาเครือข่ายระหว่าง nodes
ข้อดีของการจําลองแบบ synchronous คือ follower ได้รับการรับประกันว่ามีสําเนาข้อมูลที่ทันสมัยและสอดคล้องกับของ leader ถ้า leader ล้มเหลวกะทันหัน เรามั่นใจได้ว่าข้อมูลยังคงพร้อมใช้งานบน follower ข้อเสียคือถ้า synchronous follower ไม่ตอบสนอง (เพราะมันล่ม หรือเพราะมีปัญหาเครือข่าย หรือด้วยเหตุผลอื่นใด) write จะไม่สามารถประมวลผลได้ leader ต้องบล็อก writes ทั้งหมดและรอจนกว่า synchronous replica จะพร้อมใช้งานอีกครั้ง
ด้วยเหตุนี้ จึงไม่สามารถปฏิบัติได้จริงที่จะให้ followers ทั้งหมดเป็นแบบ synchronous เพราะการหยุดทํางานของ node ใด node หนึ่งจะทําให้ทั้งระบบหยุดชะงัก ในทางปฏิบัติ ถ้าฐานข้อมูลเสนอการจําลองแบบ synchronous ก็มักจะหมายความว่า หนึ่ง ใน followers เป็นแบบ synchronous และตัวอื่นๆ เป็นแบบ asynchronous ถ้า synchronous follower ไม่พร้อมใช้งานหรือช้า ตัวหนึ่งใน asynchronous followers จะถูกเปลี่ยนเป็นแบบ synchronous สิ่งนี้รับประกันว่าคุณมีสําเนาข้อมูลที่ทันสมัยบนอย่างน้อยสอง nodes: leader และ synchronous follower หนึ่งตัว การกําหนดค่านี้บางครั้งเรียกว่า semisynchronous
ในบางระบบ replicas ส่วนใหญ่ (เช่น สามในห้า รวมถึง leader) จะถูกอัปเดตแบบ synchronous และส่วนน้อยที่เหลือเป็นแบบ asynchronous นี่คือตัวอย่างของ quorum ซึ่งเราจะพูดถึงเพิ่มเติมในหัวข้อ "การใช้ Quorum สําหรับการอ่านและเขียน" Majority quorums มักใช้ในระบบ eventual consistency หรือระบบที่ใช้ consensus protocol สําหรับการเลือก leader อัตโนมัติ เราจะกลับมาที่ระบบเหล่านี้ใน บทที่ 10
บางครั้ง leader-based replication ถูกกําหนดค่าให้เป็นแบบ asynchronous โดยสมบูรณ์ ในกรณีนี้ ถ้า leader ล้มเหลวและไม่สามารถกู้คืนได้ writes ที่ยังไม่ถูกจําลองไปยัง followers จะหายไป ซึ่งหมายความว่า write ไม่ได้รับการรับประกันความคงทน แม้ว่าจะได้รับการยืนยันกับ client แล้ว อย่างไรก็ตาม การกําหนดค่าแบบ asynchronous โดยสมบูรณ์มีข้อดีคือ leader สามารถประมวลผล writes ต่อไปได้ แม้ว่า followers ทั้งหมดจะตามหลังอยู่
การลดความคงทนของข้อมูลอาจฟังดูเหมือนการแลกเปลี่ยนที่ไม่ดี แต่ asynchronous replication ก็ยังถูกใช้อย่างแพร่หลาย โดยเฉพาะถ้ามี followers จํานวนมากหรือกระจายตามภูมิศาสตร์ [ 9 ] เราจะกลับมาที่ประเด็นนี้ในหัวข้อ "ปัญหาจาก Replication Lag"
Setting Up New Followers (การตั้งค่า Follower ใหม่)
ในบางครั้ง คุณต้องตั้งค่า followers ใหม่—อาจเพื่อเพิ่มจํานวน replicas หรือแทนที่ nodes ที่ล้มเหลว คุณจะมั่นใจได้อย่างไรว่า follower ใหม่มีสําเนาข้อมูลที่ถูกต้องของ leader?
การคัดลอกไฟล์ข้อมูลจาก node หนึ่งไปยังอีก node หนึ่งโดยทั่วไปแล้วไม่เพียงพอ Clients กําลังเขียนไปยังฐานข้อมูลตลอดเวลา และข้อมูลก็เปลี่ยนแปลงอยู่เสมอ ดังนั้นการคัดลอกไฟล์แบบปกติจะเห็นส่วนต่างๆ ของฐานข้อมูล ณ จุดเวลาที่แตกต่างกัน ผลลัพธ์ที่ได้อาจไม่สมเหตุสมผล
คุณสามารถทําให้ไฟล์บนดิสก์สอดคล้องกันได้โดยการล็อกฐานข้อมูล (ทําให้ไม่พร้อมใช้งานสําหรับ writes) แต่จะขัดกับเป้าหมายของเราที่ต้องการความพร้อมใช้งานสูง โชคดีที่การตั้งค่า follower โดยปกติสามารถทําได้โดยไม่ต้องหยุดการทํางาน ในเชิงแนวคิด กระบวนการมีดังนี้:
-
ถ่าย snapshot ที่สอดคล้องกันของฐานข้อมูล leader ณ จุดเวลาใดเวลาหนึ่ง—ถ้าเป็นไปได้ โดยไม่ต้องล็อกฐานข้อมูลทั้งหมด ฐานข้อมูลส่วนใหญ่มีฟีเจอร์นี้ เพราะจําเป็นสําหรับ backups เช่นกัน ในบางกรณี จําเป็นต้องใช้เครื่องมือของบริษัทอื่น เช่น Percona XtraBackup สําหรับ MySQL
-
คัดลอก snapshot ไปยัง follower node ใหม่
-
follower เชื่อมต่อกับ leader และขอการเปลี่ยนแปลงข้อมูลทั้งหมดที่เกิดขึ้นตั้งแต่ถ่าย snapshot ซึ่งต้องให้ snapshot เชื่อมโยงกับตําแหน่งที่แน่นอนใน replication log ของ leader ตําแหน่งนั้นมีชื่อเรียกต่างๆ กัน—ตัวอย่างเช่น PostgreSQL เรียกว่า log sequence number; MySQL มีสอง กลไกคือ binlog coordinates และ global transaction identifiers (GTIDs)
-
เมื่อ follower ประมวลผลค้างของการเปลี่ยนแปลงข้อมูลตั้งแต่ snapshot เรียกว่ามัน ตามทันแล้ว ตอนนี้มันสามารถประมวลผลการเปลี่ยนแปลงข้อมูลจาก leader ต่อไปได้ตามที่เกิดขึ้น
ขั้นตอนปฏิบัติในการตั้งค่า follower แตกต่างกันอย่างมากตามฐานข้อมูล ในบางระบบกระบวนการเป็นแบบอัตโนมัติเต็มรูปแบบ ในขณะที่บางระบบอาจเป็นขั้นตอนการทํางานหลายขั้นตอนที่ค่อนข้างซับซ้อนซึ่งต้องดําเนินการโดยผู้ดูแลระบบ
คุณยังสามารถเก็บถาวร replication log ไปยัง object store พร้อมกับ snapshot ของฐานข้อมูลทั้งหมดเป็นระยะๆ นี่เป็นวิธีที่ดีในการ implement database backups และ disaster recovery และคุณสามารถทําขั้นตอนที่ 1 และ 2 ของการตั้งค่า follower ใหม่โดยดาวน์โหลดไฟล์เหล่านั้นจาก object store ตัวอย่างเช่น WAL-G ทําสิ่งนี้สําหรับ PostgreSQL, MySQL, และ SQL Server และ Litestream ทําในสิ่งที่เทียบเท่าสําหรับ SQLite
Databases Backed by Object Storage (ฐานข้อมูลที่ใช้ Object Storage)
Object storage สามารถใช้ได้มากกว่าแค่การเก็บถาวรข้อมูล ฐานข้อมูลจํานวนมากเริ่มใช้ object stores เช่น Amazon S3, Google Cloud Storage, และ Azure Blob Storage เพื่อให้บริการข้อมูลสําหรับคําสั่งค้นหาแบบเรียลไทม์ การเก็บข้อมูลฐานข้อมูลใน object storage มีประโยชน์มากมาย:
-
Object storage มีราคาถูกเมื่อเทียบกับตัวเลือกพื้นที่จัดเก็บคลาวด์อื่นๆ สิ่งนี้ช่วยให้ฐานข้อมูลบนคลาวด์สามารถเก็บข้อมูลที่ถูกสอบถามน้อยกว่าบนพื้นที่จัดเก็บที่ถูกกว่าและมีความหน่วงสูงกว่า ในขณะที่ให้บริการชุดข้อมูลทํางานจากหน่วยความจํา SSDs และ NVMe
-
Object stores ให้การจําลองแบบ multi-zone dual-region หรือ multi-region พร้อมการรับประกันความคงทนที่สูงมาก สิ่งนี้ยังช่วยให้ฐานข้อมูลสามารถหลีกเลี่ยงค่าใช้จ่ายเครือข่ายระหว่าง zone ได้
-
ฐานข้อมูลสามารถใช้ฟีเจอร์ conditional write ของ object store—ซึ่งก็คือการดําเนินการ compare-and-set (CAS)—เพื่อ implement transactions และ leadership election [ 10, 11 ]
-
การเก็บข้อมูลจากฐานข้อมูลหลายตัวใน object store เดียวกันสามารถทําให้การรวมข้อมูลง่ายขึ้น (ดูหัวข้อ "Cloud Data Warehouses") โดยเฉพาะเมื่อใช้รูปแบบเปิดเช่น Parquet และ Iceberg
ประโยชน์เหล่านี้ทําให้สถาปัตยกรรมฐานข้อมูลง่ายขึ้นอย่างมาก โดยการย้ายความรับผิดชอบของ transactions leadership election และ replication ไปยัง object storage
อย่างไรก็ตาม ระบบที่นํา object storage มาใช้สําหรับ replication ต้องเผชิญกับข้อแลกเปลี่ยน โดยเฉพาะอย่างยิ่ง object stores มีความหน่วงในการอ่านและเขียนที่สูงกว่าดิสก์ท้องถิ่นหรือ virtual block devices เช่น Amazon EBS มาก ผู้ให้บริการคลาวด์หลายรายยังคิดค่าธรรมเนียมต่อการเรียก API ซึ่งบังคับให้ระบบต้องรวม reads และ writes เป็นชุดเพื่อลดต้นทุน การรวมเป็นชุดดังกล่าวยิ่งเพิ่มความหน่วง Objects มักจะ immutable เช่นกัน ซึ่งทําให้การเขียนแบบสุ่มใน object ขนาดใหญ่เป็นการดําเนินงานที่ใช้ทรัพยากรมาก สุดท้าย object stores จํานวนมากไม่มีอินเทอร์เฟซ filesystem มาตรฐาน ซึ่งป้องกันไม่ให้ระบบที่ขาด object storage integration สามารถใช้ประโยชน์จากมันได้ อินเทอร์เฟซเช่น filesystem in userspace (FUSE) ช่วยให้ผู้ปฏิบัติงานสามารถ mount buckets ของ object store เป็น filesystems ที่แอปพลิเคชันสามารถใช้ได้โดยไม่ต้องรู้ว่าข้อมูลของตนถูกเก็บไว้บน object storage อย่างไรก็ตาม อินเทอร์เฟซ FUSE จํานวนมากยังขาดฟีเจอร์ POSIX เช่น nonsequential writes หรือ symlinks ซึ่งระบบอาจพึ่งพา
ระบบต่างๆ จัดการกับข้อแลกเปลี่ยนเหล่านี้ในหลากหลายวิธี บางระบบนําเสนอสถาปัตยกรรม tiered storage ที่วางข้อมูลที่เข้าถึงน้อยบน object storage ในขณะที่ข้อมูลใหม่หรือเข้าถึงบ่อยจะถูกเก็บไว้บนอุปกรณ์จัดเก็บที่เร็วกว่าเช่น SSDs หรือ NVMe หรือแม้แต่ในหน่วยความจํา ระบบอื่นๆ ใช้ object storage เป็นชั้นจัดเก็บหลักแต่ใช้ระบบจัดเก็บความหน่วงต่ําแยกต่างหาก (เช่น Amazon EBS หรือ Neon's Safekeepers [ 12 ]) เพื่อเก็บ WAL เมื่อเร็วๆ นี้ บางระบบไปไกลยิ่งขึ้นด้วยการนําสถาปัตยกรรม zero-disk (ZDA) มาใช้ ระบบที่ใช้ ZDA จะจัดเก็บข้อมูลทั้งหมดไปยัง object storage และใช้ดิสก์และหน่วยความจําสําหรับ caching เท่านั้น สิ่งนี้ทําให้ nodes ไม่ต้องมีสถานะถาวร ซึ่งช่วยลดความซับซ้อนในการดําเนินงาน WarpStream, Confluent Freight, Buf's Bufstream, และ Redpanda Serverless ล้วนเป็นระบบที่เข้ากันได้กับ Kafka ซึ่งสร้างด้วยสถาปัตยกรรม zero-disk คลังข้อมูลบนคลาวด์สมัยใหม่เกือบทุกตัวก็นําสถาปัตยกรรมดังกล่าวมาใช้เช่นกัน เช่นเดียวกับ Turbopuffer (vector search engine) และ SlateDB (cloud native LSM storage engine)
Handling Node Outages (การจัดการเมื่อ Node หยุดทำงาน)
node ใดๆ ในระบบสามารถหยุดทํางานได้ อาจเกิดจากความผิดพลาดโดยไม่คาดคิด หรือจากการบํารุงรักษาตามแผน (เช่น การรีบูตเครื่องเพื่อติดตั้งแพตช์ความปลอดภัยของเคอร์เนล) ความสามารถในการรีบูตแต่ละ nodes โดยไม่ต้องหยุดการทํางานเป็นข้อได้เปรียบอย่างมากสําหรับการดําเนินงานและการบํารุงรักษา ดังนั้นเป้าหมายของเราคือการทําให้ระบบโดยรวมทํางานต่อไปได้แม้แต่ละ node จะล้มเหลว และรักษาผลกระทบของการหยุดทํางานของ node ให้เล็กที่สุด
คุณจะบรรลุความพร้อมใช้งานสูงด้วย leader-based replication ได้อย่างไร?
Follower failure: Catch-up recovery (Follower ล้มเหลว: การกู้คืนแบบตามทัน)
บน ดิสก์ท้องถิ่นของตน แต่ละ follower จะเก็บ log ของการเปลี่ยนแปลงข้อมูลที่ได้รับจาก leader ถ้า follower ล่มและถูกรีสตาร์ท หรือถ้าเครือข่ายระหว่าง leader และ follower ถูกขัดจังหวะชั่วคราว follower สามารถกู้คืนได้ค่อนข้างง่าย: จาก log ของมัน มันรู้ transaction สุดท้ายที่ถูกประมวลผลก่อนที่ข้อผิดพลาดจะเกิดขึ้น ดังนั้น follower สามารถเชื่อมต่อกับ leader และขอการเปลี่ยนแปลงข้อมูลทั้งหมดที่เกิดขึ้นในช่วงเวลาที่ follower ถูกตัดการเชื่อมต่อ เมื่อมันประยุกต์ใช้การเปลี่ยนแปลงเหล่านี้แล้ว มันก็ตามทัน leader และสามารถรับ stream ของการเปลี่ยนแปลงข้อมูลต่อไปได้เช่นเดิม
แม้ว่าการกู้คืน follower จะเรียบง่ายในเชิงแนวคิด แต่อาจท้าทายในแง่ของประสิทธิภาพ ถ้าฐานข้อมูลมีปริมาณงานเขียนสูงหรือ follower ออฟไลน์เป็นเวลานาน อาจมี writes จํานวนมากที่ต้องตามให้ทัน จะมีโหลดสูงทั้งบน follower ที่กําลังกู้คืนและ leader (ซึ่งต้องส่งค้าง writes ไปยัง follower) ในระหว่างที่การตามทันนี้ดําเนินอยู่
leader สามารถลบ log ของ writes หลังจาก followers ทั้งหมดยืนยันว่าได้ประมวลผลแล้ว แต่ถ้า follower ไม่พร้อมใช้งานเป็นเวลานาน leader ต้องเผชิญกับทางเลือก: เก็บ log ไว้จนกว่า follower จะกู้คืนและตามทัน (เสี่ยงต่อการเต็มดิสก์บน leader) หรือลบ log ที่ follower ที่ไม่พร้อมใช้งานยังไม่ได้รับทราบ (ในกรณีนี้ follower จะไม่สามารถกู้คืนจาก log ได้และต้องถูกกู้คืนจาก backup เมื่อกลับมาทํางาน)
Leader failure: Failover (Leader ล้มเหลว: การ Failover)
การจัดการความล้มเหลวของ leader นั้นยุ่งยากกว่า followers ตัวหนึ่งต้องถูกเลื่อนขั้นเป็น leader ใหม่ clients ต้องถูกกําหนดค่าใหม่ให้ส่ง writes ไปยัง leader ใหม่ และ followers อื่นๆ ต้องเริ่มรับการเปลี่ยนแปลงข้อมูลจาก leader ใหม่ กระบวนการนี้เรียกว่า failover
Failover สามารถเกิดขึ้นแบบ manual (ผู้ดูแลระบบได้รับแจ้งว่า leader ล้มเหลวและดําเนินการขั้นตอนที่จําเป็นเพื่อสร้าง leader ใหม่) หรือแบบอัตโนมัติ กระบวนการ failover อัตโนมัติมักประกอบด้วยขั้นตอนต่อไปนี้:
-
การตรวจสอบว่า leader ล้มเหลว หลายสิ่งอาจผิดพลาดได้: การล่ม ไฟดับ ปัญหาเครือข่าย และอื่นๆ ไม่มีวิธีที่แน่นอนในการตรวจจับสิ่งที่เกิดขึ้น ดังนั้นระบบส่วนใหญ่จึงใช้ timeout; nodes จะส่ง messages ไปมาระหว่างกันบ่อยๆ และถ้า node ไม่ตอบสนองในช่วงเวลาหนึ่ง—เช่น 30 วินาที—ก็จะถือว่าตายแล้ว (ถ้า leader ถูกนําลงโดยตั้งใจสําหรับการบํารุงรักษาตามแผน สิ่งนี้ไม่เกี่ยวข้องเพราะ leader สามารถ trigger safe handoff ก่อนปิดเครื่องได้)
-
การเลือก leader ใหม่ สามารถทําได้ผ่านกระบวนการเลือกตั้ง (โดย leader ถูกเลือกโดยเสียงส่วนใหญ่ของ replicas ที่เหลืออยู่) หรือ leader ใหม่สามารถถูกแต่งตั้งโดย controller node ที่ถูกกําหนดไว้ก่อนหน้านี้ [ 13 ] ผู้สมัครที่ดีที่สุดสําหรับการเป็น leader มักจะเป็น replica ที่มีการเปลี่ยนแปลงข้อมูลล่าสุดจาก leader เก่ามากที่สุด (เพื่อลดการสูญเสียข้อมูล) การให้ทุก nodes เห็นพ้องต้องกันเกี่ยวกับ leader ใหม่เป็นปัญหา consensus ซึ่งกล่าวถึงในรายละเอียดใน บทที่ 10
-
การกําหนดค่าระบบใหม่ให้ใช้ leader ใหม่ clients ต้องส่ง คําขอเขียน ไปยัง leader ใหม่ (เราพูดถึงเรื่องนี้ในหัวข้อ "การกำหนดเส้นทางคำขอ") ถ้า leader เก่ากลับมา มันอาจยังเชื่อว่ามันคือ leader โดยไม่รู้ว่า replicas อื่นๆ บังคับให้มันลงจากตําแหน่ง ระบบต้องมั่นใจว่า leader เก่ากลายเป็น follower และยอมรับ leader ใหม่
Failover เต็มไปด้วยสิ่งที่อาจผิดพลาด:
-
ถ้าใช้ asynchronous replication leader ใหม่อาจไม่ได้รับ writes ทั้งหมดจาก leader เก่าก่อนที่มันจะล้มเหลว ถ้า leader คนก่อนกลับมาเข้าร่วม cluster หลังจากเลือก leader ใหม่แล้ว ควรเกิดอะไรขึ้นกับ writes เหล่านั้น? leader ใหม่อาจได้รับ writes ที่ขัดแย้งกันในระหว่างนั้น วิธีแก้ที่พบบ่อยที่สุดคือ writes ที่ไม่ได้ถูกจําลองของ leader เก่าจะถูกทิ้งไป ซึ่งหมายความว่า writes ที่คุณเชื่อว่าถูก commit แล้วนั้นไม่คงทนในที่สุด
-
การทิ้ง writes เป็นอันตรายอย่างยิ่งถ้าระบบจัดเก็บอื่นๆ นอกเหนือจากฐานข้อมูลต้องประสานงานกับเนื้อหาของฐานข้อมูล ตัวอย่างเช่น ในเหตุการณ์หนึ่งที่ GitHub [ 14 ] MySQL follower ที่ล้าสมัยถูกเลื่อนขั้นเป็น leader ฐานข้อมูลใช้ counter แบบ autoincrementing เพื่อกําหนด primary keys ให้กับแถวใหม่ แต่เนื่องจาก counter ของ leader ใหม่ตามหลังของ leader เก่า มันจึงนํา primary keys ที่เคยถูกกําหนดโดย leader เก่ามาใช้ซ้ํา primary keys เหล่านี้ยังถูกใช้ใน Redis store ด้วย ดังนั้นการนํา primary keys มาใช้ซ้ําทําให้เกิดความไม่สอดคล้องระหว่าง MySQL และ Redis ซึ่งทําให้ข้อมูลส่วนตัวบางส่วนถูกเปิดเผยแก่ผู้ใช้ที่ไม่ถูกต้อง
-
ในสถานการณ์ความผิดพลาดบางอย่าง (ดู บทที่ 9) สอง nodes อาจเชื่อว่าตนเองเป็น leader สถานการณ์นี้เรียกว่า split brain เป็นอันตราย ถ้า leaders ทั้งสองรับ writes และไม่มีกระบวนการแก้ไข conflicts (ดูหัวข้อ "การจําลองแบบ Multi-Leader") ข้อมูลอาจสูญหายหรือเสียหาย เพื่อความปลอดภัย บางระบบมีกลไกในการปิด node หนึ่งถ้าตรวจพบ leaders สองตัว อย่างไรก็ตาม ถ้ากลไกนี้ออกแบบไม่ดี คุณอาจจบลงด้วยการที่ทั้งสอง nodes ถูกปิด [ 15 ] ยิ่งกว่านั้น มีความเสี่ยงว่าเมื่อถึงเวลาที่ split brain ถูกตรวจพบและ node เก่าถูกปิด มันอาจสายเกินไปและข้อมูลเสียหายแล้ว
-
การตัดสินใจเรื่อง timeout ที่เหมาะสมก่อนที่จะประกาศว่า leader ตายนั้นอาจยุ่งยาก timeout ที่นานขึ้นหมายถึงเวลาในการกู้คืนที่นานขึ้นในกรณีที่ leader ล้มเหลว อย่างไรก็ตาม ถ้า timeout สั้นเกินไป อาจเกิด failovers ที่ไม่จําเป็น ตัวอย่างเช่น การเพิ่มขึ้นของโหลดชั่วคราวอาจทําให้เวลาในการตอบสนองของ node เพิ่มขึ้นเกิน timeout หรือปัญหาขัดข้องของเครือข่ายอาจทําให้แพ็คเก็ตล่าช้า ถ้าระบบกําลังประสบปัญหากับโหลดสูงหรือปัญหาเครือข่ายอยู่แล้ว failover ที่ไม่จําเป็นมักจะทําให้สถานการณ์แย่ลง ไม่ดีขึ้น
การป้องกัน split brain โดยการจํากัดหรือปิด leader เก่าเรียกว่า fencing; เราจะพูดถึงในรายละเอียดเพิ่มเติมในหัวข้อ "ล็อคแบบกระจายและสัญญาเช่า" อย่างไรก็ตาม ปัญหาเหล่านี้ไม่มีวิธีแก้ที่ง่าย ด้วยเหตุนี้ ทีมปฏิบัติการบางทีมจึงชอบทํา failovers ด้วยตนเอง แม้ว่าซอฟต์แวร์จะรองรับ failover อัตโนมัติ
สิ่งที่สําคัญที่สุดในการ failover คือการเลือก follower ที่ทันสมัยเป็น leader ใหม่ ถ้าใช้ synchronous หรือ semisynchronous replication นี่จะเป็น follower ที่ leader เก่ารอคอยก่อนที่จะยืนยัน writes ด้วย asynchronous replication คุณสามารถเลือก follower ที่มี log sequence number สูงที่สุด สิ่งนี้จะลดปริมาณข้อมูลที่สูญเสียระหว่าง failover; การสูญเสีย writes เพียงเสี้ยววินาทีอาจพอทนได้ แต่การเลือก follower ที่ตามหลังหลายวันอาจเป็นหายนะ
ประเด็นเหล่านี้—node failures, เครือข่ายที่ไม่น่าเชื่อถือ, และข้อแลกเปลี่ยนเกี่ยวกับ replica consistency, durability, availability, และ latency—อันที่จริงแล้วเป็นปัญหาพื้นฐานในระบบกระจาย ในบทที่ 9 และ 10 เราจะพูดถึงสิ่งเหล่านี้ในเชิงลึกมากขึ้น
Implementation of Replication Logs (การนำ Replication Logs ไปใช้)
leader-based replication ทํางานอย่างไรภายใต้ฝาครอบ? มีวิธีการจําลองหลายวิธีที่ใช้ในทางปฏิบัติ มาดูแต่ละวิธีโดยสังเขป
Statement-based replication (การจำลองแบบตามคำสั่ง)
ในกรณีที่ง่ายที่สุด leader จะบันทึกทุกคําขอเขียน ( statement ) ที่มันประมวลผลและส่ง statement log นั้นไปยัง followers สําหรับฐานข้อมูลเชิงสัมพันธ์ นี่หมายความว่าทุก INSERT, UPDATE, หรือ DELETE statement จะถูกส่งต่อไปยัง followers และแต่ละ follower จะแยกวิเคราะห์และประมวลผล SQL statement นั้นราวกับว่าได้รับจาก client
แม้ว่าวิธีการจําลองนี้อาจฟังดูสมเหตุสมผล แต่มันก็อาจล้มเหลวได้ในหลายทาง:
-
Statement ใดๆ ที่เรียกฟังก์ชัน nondeterministic เช่น
NOWเพื่อรับวันที่และเวลาปัจจุบัน หรือRANDเพื่อรับตัวเลขสุ่ม มักจะสร้างค่าที่แตกต่างกันในแต่ละ replica -
ถ้า statements ใช้คอลัมน์ autoincrementing หรือถ้าขึ้นอยู่กับข้อมูลที่มีอยู่ในฐานข้อมูล (เช่น
UPDATE...WHERE _<เงื่อนไขบางอย่าง>_) statements เหล่านั้นต้องถูกประมวลผลในลําดับที่เหมือนกันทุกประการในแต่ละ replica มิฉะนั้นอาจมีผลลัพธ์ที่แตกต่างกัน ซึ่งอาจเป็นข้อจํากัดเมื่อมีหลาย transactions ที่ทํางานพร้อมกัน -
Statements ที่มี side effects (เช่น triggers, stored procedures, user-defined functions) อาจทําให้เกิด side effects ที่แตกต่างกันในแต่ละ replica เว้นแต่ side effects จะเป็น deterministic อย่างสมบูรณ์
เป็นไปได้ที่จะแก้ไขปัญหาเหล่านั้น—ตัวอย่างเช่น leader สามารถแทนที่การเรียกฟังก์ชัน nondeterministic ด้วยค่าที่แน่นอนเมื่อบันทึก statement เพื่อให้ followers ทั้งหมดได้รับค่าเดียวกัน แนวคิดของการประมวลผล deterministic statements ในลําดับที่แน่นอนคล้ายกับโมเดล event sourcing ที่เราได้พูดถึงก่อนหน้านี้ในหัวข้อ "Event Sourcing และ CQRS" วิธีการนี้เรียกว่า state machine replication และเราจะพูดถึงทฤษฎีเบื้องหลังในหัวข้อ "การใช้ Shared Logs"
Statement-based replication ถูกใช้ใน MySQL ก่อนเวอร์ชัน 5.1 ยังคงใช้ในบางครั้งในปัจจุบันเพราะค่อนข้างกะทัดรัด แต่โดยค่าเริ่มต้น MySQL จะเปลี่ยนไปใช้ row-based replication (จะกล่าวถึงเร็วๆ นี้) ถ้ามี nondeterminism ใน statement ใดๆ VoltDB ใช้ statement-based replication และทําให้ปลอดภัยโดยกําหนดให้ transactions ต้องเป็น deterministic [ 16 ] อย่างไรก็ตาม determinism อาจยากที่จะรับประกันในทางปฏิบัติ ดังนั้นฐานข้อมูลจํานวนมากจึงเลือกใช้วิธีการจําลองอื่นๆ
Write-ahead log shipping (การส่ง Write-ahead Log)
ใน บทที่ 4 เราเห็นว่า write-ahead log จําเป็นต้องทําให้ B-tree storage engines แข็งแกร่ง การแก้ไขทุกครั้งจะถูกเขียนไปยัง WAL ก่อนเพื่อให้ tree สามารถถูกกู้คืนสู่สถานะที่สอดคล้องกันหลังจาก crash เนื่องจาก WAL ประกอบด้วยข้อมูลทั้งหมดที่จําเป็นในการกู้คืน indexes และ heap สู่สถานะที่สอดคล้องกัน เราสามารถใช้ log เดียวกันนี้เพื่อสร้าง replica บน node อื่น นอกจากจะเขียน log ไปยังดิสก์แล้ว leader ยังส่ง log ผ่านเครือข่ายไปยัง followers ของมันด้วย เมื่อ follower ประมวลผล log นี้ มันจะสร้างสําเนาของไฟล์ที่เหมือนกับที่อยู่บน leader ทุกประการ
วิธีการจําลองนี้ใช้ใน PostgreSQL และ Oracle เป็นต้น [ 17, 18 ] ข้อเสียหลักคือ log อธิบายข้อมูลในระดับที่ต่ํามาก—WAL ประกอบด้วยรายละเอียดว่าไบต์ใดถูกเปลี่ยนแปลงในบล็อกดิสก์ใด ซึ่งทําให้ replication ผูกติดแน่นกับ storage engine ถ้าฐานข้อมูลเปลี่ยนรูปแบบการจัดเก็บจากเวอร์ชันหนึ่งไปยังอีกเวอร์ชันหนึ่ง โดยทั่วไปจะไม่สามารถรันซอฟต์แวร์ฐานข้อมูลคนละเวอร์ชันบน leader และ followers ได้
นั่นอาจดูเหมือนรายละเอียดการใช้งานเล็กน้อย แต่มันอาจมีผลกระทบในการดําเนินงานอย่างมาก ถ้า replication protocol อนุญาตให้ follower ใช้ซอฟต์แวร์เวอร์ชันใหม่กว่า leader คุณสามารถอัปเกรดซอฟต์แวร์ฐานข้อมูลแบบ zero-downtime โดยอัปเกรด followers ก่อน แล้วจึงทํา failover เพื่อให้ node ที่ถูกอัปเกรดเป็น leader ใหม่ ถ้า replication protocol ไม่อนุญาตให้เวอร์ชันแตกต่างกัน อย่างที่มักเป็นกับ WAL shipping การอัปเกรดดังกล่าวต้องหยุดการทํางาน
Logical (row-based) log replication (การจำลองแบบ Logical Log แบบ Row-based)
ทางเลือกคือการใช้รูปแบบ log ที่แตกต่างกันสําหรับ replication และ storage engine ซึ่งช่วยให้ replication log ถูกแยกออกจากรายละเอียดภายในของ storage engine replication log แบบนี้เรียกว่า logical log เพื่อแยกความแตกต่างจากการแสดงข้อมูล (ทางกายภาพ) ของ storage engine
logical log สําหรับฐานข้อมูลเชิงสัมพันธ์มักเป็นลําดับของ records ที่อธิบาย writes ไปยังตารางฐานข้อมูลในระดับแถว:
-
สําหรับแถวที่ถูกแทรก log จะมีค่าของทุกคอลัมน์ใหม่
-
สําหรับแถวที่ถูกลบ log จะมีข้อมูลเพียงพอที่จะระบุแถวที่ถูกลบได้อย่างไม่ซ้ํากัน โดยปกติจะเป็น primary key แต่ถ้าไม่มี primary key ในตาราง ค่าเก่าของทุกคอลัมน์ต้องถูกบันทึก
-
สําหรับแถวที่ถูกอัปเดต log จะมีข้อมูลเพียงพอที่จะระบุแถวที่ถูกอัปเดตได้อย่างไม่ซ้ํากัน และค่าใหม่ของทุกคอลัมน์ (หรืออย่างน้อยทุกคอลัมน์ที่มีค่าเปลี่ยนแปลง)
transaction ที่แก้ไขหลายแถวจะสร้าง log records หลายรายการ ตามด้วย record ที่ระบุว่า transaction ถูก commit แล้ว เมื่อกําหนดค่าให้ใช้ row-based replication MySQL จะเก็บ logical replication log แยกต่างหาก เรียกว่า binlog นอกเหนือจาก WAL PostgreSQL implement logical replication โดยการถอดรหัส physical WAL เป็นเหตุการณ์การแทรก/อัปเดต/ลบแถว [ 19 ]
เนื่องจาก logical log ถูกแยกออกจากรายละเอียดภายในของ storage engine มันจึงสามารถรักษาความเข้ากันได้ย้อนหลังได้ง่ายขึ้น ทําให้ leader และ follower สามารถรันซอฟต์แวร์ฐานข้อมูลคนละเวอร์ชันกันได้ ซึ่งช่วยให้สามารถอัปเกรดเป็นเวอร์ชันใหม่โดยมีการหยุดทํางานน้อยที่สุด [ 20 ]
รูปแบบ logical log ยังง่ายกว่าสําหรับแอปพลิเคชันภายนอกในการแยกวิเคราะห์ แง่มุมนี้มีประโยชน์ถ้าคุณต้องการส่งเนื้อหาของฐานข้อมูลไปยังระบบภายนอก เช่น data warehouse สําหรับการวิเคราะห์แบบออฟไลน์ หรือระบบเฉพาะสําหรับการสร้าง custom indexes และ caches [ 21 ] เทคนิคนี้เรียกว่า change data capture และเราจะกลับมาที่มันใน บทที่ 12
Problems with Replication Lag (ปัญหาจาก Replication Lag)
การ สามารถทนต่อความล้มเหลวของ node เป็นเพียงเหตุผลเดียวสําหรับการต้องการ replication ดังที่กล่าวถึงในหัวข้อ "ระบบกระจายเทียบกับระบบโหนดเดียว" เหตุผลอื่นๆ ได้แก่ scalability (การประมวลผลคําขอมากกว่าเครื่องเดียวจะรับไหว) และ latency (การวาง replicas ให้ใกล้ผู้ใช้ตามภูมิศาสตร์)
Leader-based replication กําหนดให้ writes ทั้งหมดผ่าน node เดียว แต่ queries แบบอ่านอย่างเดียวสามารถไปยัง replica ใดก็ได้ สําหรับ workloads ที่ประกอบด้วยการอ่านเป็นส่วนใหญ่โดยมี writes เพียงเล็กน้อย (ซึ่งมักเป็นกรณีของบริการออนไลน์) มีตัวเลือกที่น่าสนใจ: สร้าง followers จํานวนมาก และกระจายคําขออ่านไปยัง followers เหล่านั้น สิ่งนี้ช่วยลดโหลดจาก leader และช่วยให้คําขออ่านสามารถให้บริการโดย replicas ที่อยู่ใกล้
ในสถาปัตยกรรม read-scaling นี้ คุณสามารถเพิ่มความสามารถในการให้บริการคําขอแบบอ่านอย่างเดียวโดยการเพิ่ม followers มากขึ้น อย่างไรก็ตาม วิธีการนี้ใช้ได้จริงเฉพาะกับ asynchronous replication ถ้าคุณพยายาม replicate แบบ synchronous ไปยัง followers ทั้งหมด ความล้มเหลวของ node เดียวหรือเครือข่ายขัดข้องจะทําให้ทั้งระบบไม่พร้อมใช้งานสําหรับการเขียน และยิ่งคุณมี nodes มากเท่าไร โอกาสที่ node หนึ่งจะล้มก็ยิ่งสูงขึ้น ดังนั้นการกําหนดค่าแบบ synchronous โดยสมบูรณ์จะไม่น่าเชื่อถืออย่างมาก
น่าเสียดายที่แอปพลิเคชันที่อ่านจาก asynchronous follower อาจเห็นข้อมูลที่ล้าสมัยถ้า follower ตามหลัง ซึ่งนําไปสู่ความไม่สอดคล้องที่ชัดเจนในฐานข้อมูล ถ้าคุณรัน query เดียวกันบน leader และ follower ในเวลาเดียวกัน คุณอาจได้ผลลัพธ์ที่แตกต่างกัน เพราะไม่ใช่ทุก writes ที่ถูกสะท้อนใน follower ความไม่สอดคล้องนี้เป็นสถานะชั่วคราว—ถ้าคุณหยุดเขียนไปยังฐานข้อมูลและรอสักครู่ followers จะตามทันในที่สุดและสอดคล้องกับ leader ด้วยเหตุนี้ ปรากฏการณ์นี้จึงเรียกว่า eventual consistency [ 22 ]
หมายเหตุ
คําว่า eventual consistency ถูกบัญญัติโดย Douglas Terry และคณะ [ 23 ] และถูกทําให้เป็นที่นิยมโดย Werner Vogels [ 24 ] และมันกลายเป็นคํารบเร้าของโปรเจกต์ NoSQL จํานวนมาก อย่างไรก็ตาม ไม่ใช่เฉพาะฐานข้อมูล NoSQL เท่านั้นที่เป็นแบบ eventually consistent; followers ในฐานข้อมูลเชิงสัมพันธ์ที่จําลองแบบ asynchronous ก็มีลักษณะเดียวกัน
คําว่า "eventually" จงทําให้คลุมเครือ โดยทั่วไปแล้วไม่มีขีดจํากัดว่า replica จะตามหลังได้ไกลแค่ไหน ในการทํางานปกติ ความล่าช้าระหว่างการเกิด write บน leader และการสะท้อนบน follower—_replication lag_—อาจเป็นเพียงเสี้ยววินาทีและไม่สังเกตเห็นได้ในทางปฏิบัติ อย่างไรก็ตาม ถ้าระบบทํางานใกล้ขีดความสามารถหรือเกิดปัญหาในเครือข่าย lag สามารถเพิ่มเป็นหลายวินาทีหรือหลายนาทีได้อย่างง่ายดาย
เมื่อ lag มีขนาดใหญ่ ความไม่สอดคล้องที่มันนําเข้ามาไม่ใช่แค่ปัญหาเชิงทฤษฎี แต่เป็นปัญหาจริงสําหรับแอปพลิเคชัน ในส่วนนี้เราจะเน้นตัวอย่างสามตัวอย่างของปัญหาที่มักเกิดขึ้นกับ replication lag และเราจะสรุปแนวทางบางประการในการแก้ไข
Reading your own writes (การอ่านข้อมูลที่ตัวเองเขียน)
แอปพลิเคชันจํานวนมากให้ผู้ใช้ส่งข้อมูลแล้วดูสิ่งที่พวกเขาส่ง นี่อาจเป็น record ในฐานข้อมูลลูกค้า หรือความคิดเห็นในกระทู้สนทนา หรือสิ่งอื่นๆ ในทํานองนั้น เมื่อข้อมูลใหม่ถูกส่ง มันต้องถูกส่งไปยัง leader แต่เมื่อผู้ใช้ดูข้อมูล ก็สามารถอ่านจาก follower ได้ ซึ่งเหมาะสมโดยเฉพาะถ้าข้อมูลถูกดูบ่อยแต่เขียนเป็นครั้งคราว
ด้วย asynchronous replication ปัญหาก็เกิดขึ้น ดังที่แสดงใน Figure 6-3: ถ้าผู้ใช้ดูข้อมูลหลังจากทําการเขียนไม่นาน ข้อมูลใหม่อาจยังไม่ถึง replica สําหรับผู้ใช้แล้ว มันดูเหมือนว่าข้อมูลที่พวกเขาส่งหายไป ดังนั้นพวกเขาจะไม่พอใจอย่างเข้าใจได้
Figure 6-3. ความไม่สอดคล้องสามารถเกิดขึ้นได้เมื่อผู้ใช้ทําการเขียน แล้วตามด้วยการอ่านจาก replica ที่มีข้อมูลเก่า
ในสถานการณ์นี้ เราต้องการ read-after-write consistency หรือที่เรียกว่า read-your-writes consistency [ 23 ] นี่คือหลักประกันว่าถ้าผู้ใช้โหลดหน้าเว็บซ้ํา พวกเขาจะเห็นการอัปเดตที่พวกเขาส่งเองเสมอ ไม่ได้สัญญาเกี่ยวกับผู้ใช้อื่นๆ การอัปเดตของผู้ใช้อื่นอาจไม่ปรากฏจนกว่าจะถึงเวลาต่อมา อย่างไรก็ตาม มันทําให้ผู้ใช้มั่นใจว่าข้อมูลที่ป้อนของตนถูกบันทึกอย่างถูกต้อง
เราจะ implement read-after-write consistency ในระบบที่มี leader-based replication ได้อย่างไร? มีเทคนิคต่างๆ ที่เป็นไปได้ กล่าวถึงบางส่วน:
-
เมื่ออ่านสิ่งที่ผู้ใช้อาจแก้ไข ให้อ่านจาก leader หรือ follower ที่อัปเดตแบบ synchronous มิฉะนั้นให้อ่านจาก follower ที่อัปเดตแบบ asynchronous สิ่งนี้ต้องการให้คุณมีวิธีรู้ว่าสิ่งใดอาจถูกแก้ไข โดยไม่ต้องสอบถามมัน ตัวอย่างเช่น ข้อมูลโปรไฟล์ผู้ใช้บนโซเชียลเน็ตเวิร์กโดยปกติแก้ไขได้โดยเจ้าของโปรไฟล์เท่านั้น ไม่ใช่คนอื่น ดังนั้นกฎง่ายๆ คือ: อ่านโปรไฟล์ของผู้ใช้เองจาก leader เสมอ และอ่านโปรไฟล์ของผู้ใช้อื่นจาก follower
-
ถ้าส่วนใหญ่ในแอปพลิเคชันสามารถแก้ไขได้โดยผู้ใช้ วิธีการนั้นจะไม่ได้ผล เพราะส่วนใหญ่จะต้องอ่านจาก leader (ทําให้ประโยชน์ของการ read scaling หายไป) ในกรณีนั้น อาจใช้เกณฑ์อื่นในการตัดสินใจว่าจะอ่านจาก leader หรือไม่ ตัวอย่างเช่น คุณสามารถติดตามเวลาของการอัปเดตล่าสุด และภายในหนึ่งนาทีหลังจากการอัปเดตล่าสุด ให้อ่านจาก leader ทั้งหมด [ 25 ] คุณยังสามารถติดตาม replication lag บน followers และป้องกัน queries บน follower ที่ตามหลัง leader เกินหนึ่งนาที
-
client สามารถจํา timestamp ของ write ล่าสุดของตน และระบบสามารถมั่นใจได้ว่า replica ที่ให้บริการอ่านสําหรับผู้ใช้นั้นสะท้อนการอัปเดตอย่างน้อยจนถึง timestamp นั้น ถ้า replica ไม่ทันสมัยเพียงพอ การอ่านสามารถจัดการโดย replica อื่นหรือ query สามารถรอจนกว่า replica จะตามทัน [ 26 ] timestamp อาจเป็น logical timestamp (สิ่งที่บ่งบอกลําดับของ writes เช่น log sequence number) หรือนาฬิการะบบจริง (ในกรณีนั้นการซิงค์นาฬิกากลายเป็นสิ่งสําคัญ ดูหัวข้อ "นาฬิกาที่ไม่น่าเชื่อถือ")
-
ถ้า replicas ของคุณกระจายอยู่ตามภูมิภาคต่างๆ (เพื่อความใกล้ชิดทางภูมิศาสตร์กับผู้ใช้ เพื่อความพร้อมใช้งาน หรือเพื่อความคงทน) มีความซับซ้อนเพิ่มเติม คําขอใดๆ ที่ต้องให้บริการโดย leader ต้องถูกส่งต่อไปยังภูมิภาคที่มี leader
ความซับซ้อนอีกอย่างเกิดขึ้นเมื่อผู้ใช้คนเดียวกันเข้าถึงบริการของคุณจากหลายอุปกรณ์ เช่น เว็บเบราว์เซอร์บนเดสก์ท็อปและแอปมือถือ ในกรณีนี้คุณอาจต้องการให้ cross-device read-after-write consistency: ถ้าผู้ใช้ป้อนข้อมูลบนอุปกรณ์หนึ่งแล้วดูบนอุปกรณ์อื่น พวกเขาควรเห็นข้อมูลที่เพิ่งป้อน
มีประเด็นเพิ่มเติมบางประการที่ต้องพิจารณาที่นี่:
-
วิธีการที่ต้องจํา timestamp ของการอัปเดตล่าสุดของผู้ใช้จะยากขึ้น เพราะโค้ดที่รันทํางานบนอุปกรณ์หนึ่งไม่รู้ว่ามีการอัปเดตอะไรเกิดขึ้นบนอุปกรณ์อื่น metadata นี้จะต้องถูก centralized
-
ถ้า replicas ของคุณกระจายอยู่ตามหลายภูมิภาค ไม่มีการรับประกันว่าการเชื่อมต่อจากอุปกรณ์ต่างๆ จะถูกส่งไปยังภูมิภาคเดียวกัน (ตัวอย่างเช่น ถ้าเครื่องเดสก์ท็อปของผู้ใช้ใช้การเชื่อมต่อบรอดแบนด์ที่บ้านและอุปกรณ์มือถือใช้เครือข่ายข้อมูลเซลลูลาร์ เส้นทางเครือข่ายของอุปกรณ์อาจแตกต่างกันโดยสิ้นเชิง) ถ้าวิธีการของคุณต้องการอ่านจาก leader คุณอาจต้องส่งคําขอจากทุกอุปกรณ์ของผู้ใช้ไปยังภูมิภาคเดียวกันก่อน
Regions and Availability Zones (Region และ Availability Zone)
เราใช้คําว่า region เพื่ออ้างถึง datacenter หนึ่งแห่งหรือมากกว่าในตําแหน่งทางภูมิศาสตร์เดียว ผู้ให้บริการคลาวด์วาง datacenters หลายแห่งในภูมิภาคทางภูมิศาสตร์เดียวกัน แต่ละ datacenter เรียกว่า availability zone หรือเรียกสั้นๆ ว่า zone ดังนั้น cloud region เดียวประกอบด้วยหลาย zones แต่ละ zone คือ datacenter ที่แยกจากกันซึ่งตั้งอยู่ในสถานที่ทางกายภาพที่แยกจากกัน โดยมีไฟฟ้า ระบบทําความเย็น ฯลฯ ของตัวเอง
Zones ใน region เดียวกันเชื่อมต่อกันด้วยการเชื่อมต่อเครือข่ายความเร็วสูงมาก ความหน่วงต่ําพอที่ระบบกระจายส่วนใหญ่สามารถรันโดยมี nodes กระจายอยู่ทั่วหลาย zones ใน region เดียวกันราวกับว่าอยู่ใน zone เดียว การกําหนดค่า multi-zone ช่วยให้ระบบกระจายสามารถอยู่รอดจาก zonal outages ที่ zone หนึ่งออฟไลน์ แต่ไม่ป้องกัน regional outages ที่ทุก zones ใน region ไม่พร้อมใช้งาน เพื่ออยู่รอดจาก regional outage ระบบกระจายต้องถูก deploy ข้ามหลาย regions ซึ่งอาจส่งผลให้มีความหน่วงสูงขึ้น ปริมาณงานลดลง และค่าใช้จ่ายเครือข่ายคลาวด์เพิ่มขึ้น เราจะพูดถึงข้อแลกเปลี่ยนเหล่านี้เพิ่มเติมในหัวข้อ "โทโพโลยีการจําลองแบบ Multi-Leader" สําหรับตอนนี้ แค่รู้ว่าเมื่อเราพูดถึง region เราหมายถึงกลุ่มของ zones/datacenters ในตําแหน่งทางภูมิศาสตร์เดียว
Monotonic reads (การอ่านแบบ Monotonic)
ตัวอย่างที่สองของความผิดปกติที่อาจเกิดขึ้นเมื่ออ่านจาก asynchronous followers คือผู้ใช้อาจเห็นสิ่งต่างๆ เคลื่อนย้อนกลับในเวลา
สิ่งนี้สามารถเกิดขึ้นได้ถ้าผู้ใช้ทําการอ่านหลายครั้งจาก replicas ที่แตกต่างกัน ตัวอย่างเช่น Figure 6-4 แสดงผู้ใช้ 2345 ทํา query เดียวกันสองครั้ง ครั้งแรกไปยัง follower ที่มี lag น้อย จากนั้นไปยัง follower ที่มี lag มากกว่า (สถานการณ์นี้ค่อนข้างเป็นไปได้ถ้าผู้ใช้รีเฟรชหน้าเว็บและแต่ละคําขอถูกส่งไปยังเซิร์ฟเวอร์แบบสุ่ม) query แรกส่งคืนความคิดเห็นที่ถูกเพิ่มโดยผู้ใช้ 1234 เมื่อเร็วๆ นี้ แต่ query ที่สองไม่ส่งคืนอะไรเลยเพราะ follower ที่ตามหลังยังไม่ได้รับ write นั้น ที่จริงแล้ว query ที่สองสังเกตสถานะของระบบ ณ จุดเวลาก่อนหน้า query แรก สิ่งนี้คงไม่แย่นักถ้า query แรกไม่ส่งคืนอะไรเลย เพราะผู้ใช้ 2345 คงไม่รู้ว่าผู้ใช้ 1234 เพิ่มความคิดเห็นเมื่อเร็วๆ นี้ อย่างไรก็ตาม มันสับสนมากสําหรับผู้ใช้ 2345 ถ้าพวกเขาเห็นความคิดเห็นของผู้ใช้ 1234 ปรากฏขึ้นก่อน แล้วเห็นมันหายไปอีกครั้ง
Monotonic reads [ 22 ] ให้หลักประกันว่าความผิดปกติแบบนี้จะไม่เกิดขึ้น มันเป็นหลักประกันที่น้อยกว่า strong consistency แต่แข็งแกร่งกว่า eventual consistency เมื่อคุณอ่านข้อมูล คุณอาจเห็นค่าเก่า; monotonic reads หมายความว่าถ้าผู้ใช้คนหนึ่งอ่านหลายครั้งตามลําดับ พวกเขาจะไม่เห็นเวลาย้อนกลับ (กล่าวคือ พวกเขาจะไม่อ่านข้อมูลที่เก่ากว่าหลังจากที่เคยอ่านข้อมูลที่ใหม่กว่า)
วิธีหนึ่งในการบรรลุ monotonic reads คือทําให้แน่ใจว่าผู้ใช้แต่ละคนอ่านจาก replica เดียวกันเสมอ (ผู้ใช้ที่แตกต่างกันสามารถอ่านจาก replicas ที่แตกต่างกัน) ตัวอย่างเช่น replica สามารถถูกเลือกโดยใช้ hash ของ user ID แทนที่จะสุ่ม อย่างไรก็ตาม ถ้า replica นั้นล้มเหลว queries ของผู้ใช้จะต้องถูกส่งต่อไปยัง replica อื่น
Figure 6-4. เมื่อผู้ใช้อ่านจาก replica ที่มีข้อมูลใหม่ก่อน แล้วจึงอ่านจาก replica ที่มีข้อมูลเก่า เวลาจะดูเหมือนย้อนกลับ
Consistent prefix reads (การอ่านแบบ Consistent Prefix)
ตัวอย่างที่สามของความผิดปกติจาก replication lag เกี่ยวข้องกับการละเมิดความเป็นเหตุเป็นผล ลองนึกภาพบทสนทนาสั้นๆ ระหว่างคุณพูนส์และคุณเค้ก:
คุณพูนส์: คุณมองเห็นอนาคตได้ไกลแค่ไหน คุณเค้ก?
คุณเค้ก: โดยปกติประมาณ 10 วินาทีค่ะ คุณพูนส์
มีความสัมพันธ์เชิงสาเหตุระหว่างสองประโยคนั้น: คุณเค้กได้ยินคําถามของคุณพูนส์และตอบมัน
ทีนี้ ลองนึกภาพว่ามีบุคคลที่สามกําลังฟังบทสนทนานี้ผ่าน followers สิ่งที่คุณเค้กพูดผ่าน follower ที่มี lag เล็กน้อย แต่สิ่งที่คุณพูนส์พูดมี replication lag ที่นานกว่า (ดู Figure 6-5) ผู้สังเกตการณ์นี้จะได้ยินดังนี้:
คุณเค้ก: โดยปกติประมาณ 10 วินาทีค่ะ คุณพูนส์
คุณพูนส์: คุณมองเห็นอนาคตได้ไกลแค่ไหน คุณเค้ก?
สําหรับผู้สังเกตการณ์ ฟังดูเหมือนคุณเค้กกําลังตอบคําถามก่อนที่คุณพูนส์จะถามเสียอีก พลังจิตเช่นนี้น่าประทับใจแต่สร้างความสับสนอย่างมาก [ 27 ]
Figure 6-5. ถ้า shards บางตัวถูกจําลองช้ากว่าตัวอื่น ผู้สังเกตการณ์อาจเห็นคําตอบก่อนที่จะเห็นคําถาม
การป้องกันความผิดปกติแบบนี้ต้องใช้หลักประกันอีกประเภทหนึ่ง: consistent prefix reads [ 22 ] หลักประกันนี้กล่าวว่าถ้าลําดับของ writes เกิดขึ้นในลําดับที่แน่นอน ใครก็ตามที่อ่าน writes เหล่านั้นจะเห็นมันปรากฏในลําดับเดียวกัน
นี่เป็นปัญหาเฉพาะในฐานข้อมูลแบบ sharded (partitioned) ซึ่งเราจะพูดถึงใน บทที่ 7 ถ้าฐานข้อมูลใช้ writes ในลําดับเดียวกันเสมอ การอ่านจะเห็น consistent prefix เสมอ ดังนั้นความผิดปกตินี้จะไม่เกิดขึ้น อย่างไรก็ตาม ในฐานข้อมูลแบบกระจายหลายตัว shards ที่แตกต่างกันทํางานอย่างอิสระ ดังนั้นจึงไม่มีการเรียงลําดับ writes แบบสากล เมื่อผู้ใช้อ่านจากฐานข้อมูล พวกเขาอาจเห็นบางส่วนของฐานข้อมูลในสถานะเก่าและบางส่วนในสถานะใหม่
วิธีแก้หนึ่งคือทําให้แน่ใจว่า writes ที่มีความสัมพันธ์เชิงสาเหตุต่อกันถูกเขียนไปยัง shard เดียวกัน—แต่ในบางแอปพลิเคชันไม่สามารถทําได้อย่างมีประสิทธิภาพ อัลกอริทึมบางตัวติดตามความสัมพันธ์เชิงสาเหตุอย่างชัดเจน ซึ่งเป็นหัวข้อที่เราจะกลับมาที่ในหัวข้อ "ความสัมพันธ์แบบ Happens-Before และ Concurrency"
Solutions for Replication Lag (แนวทางแก้ไข Replication Lag)
เมื่อทํางานกับระบบ eventual consistency ควรคิดว่าแอปพลิเคชันจะ behave อย่างไรถ้า replication lag เพิ่มเป็นหลายนาทีหรือหลายชั่วโมง ถ้าคําตอบคือ "ไม่มีปัญหา" ก็เยี่ยม อย่างไรก็ตาม ถ้าผลลัพธ์เป็นประสบการณ์ที่ไม่ดีสําหรับผู้ใช้ สิ่งสําคัญคือต้องออกแบบระบบให้มีหลักประกันที่แข็งแกร่งขึ้น เช่น read-after-write การแสร้งทําเป็นว่า replication เป็นแบบ synchronous ทั้งที่จริงแล้วเป็น asynchronous คือสูตรสําเร็จสําหรับปัญหาที่จะตามมา
ดังที่กล่าวไว้ก่อนหน้านี้ มีวิธีที่แอปพลิเคชันสามารถให้หลักประกันที่แข็งแกร่งกว่าฐานข้อมูลพื้นฐาน—ตัวอย่างเช่น โดยการอ่านบางประเภทบน leader หรือ follower ที่อัปเดตแบบ synchronous อย่างไรก็ตาม การจัดการปัญหาเหล่านี้ในโค้ดแอปพลิเคชันนั้นซับซ้อนและผิดพลาดได้ง่าย
โมเดลการเขียนโปรแกรมที่ง่ายที่สุดสําหรับนักพัฒนาแอปพลิเคชันคือการเลือกฐานข้อมูลที่ให้หลักประกันความสอดคล้องที่แข็งแกร่งสําหรับ replicas เช่น linearizability (ดู บทที่ 10) และรองรับ ACID transactions (ดู บทที่ 8) สิ่งนี้ช่วยให้คุณส่วนใหญ่ไม่ต้องสนใจความท้าทายที่เกิดจาก replication และปฏิบัติต่อฐานข้อมูลราวกับว่ามันมี node เดียว ในช่วงต้นทศวรรษ 2010 ขบวนการ NoSQL ส่งเสริมมุมมองว่าฟีเจอร์เหล่านี้จํากัด scalability และระบบขนาดใหญ่จะต้องยอมรับ eventual consistency
อย่างไรก็ตาม ตั้งแต่นั้นมา ฐานข้อมูลจํานวนหนึ่งเริ่มให้ strong consistency และรองรับ transaction ในขณะที่ยังคงให้ความทนทานต่อข้อผิดพลาด ความพร้อมใช้งานสูง และข้อได้เปรียบด้าน scalability ของฐานข้อมูลแบบกระจาย ดังที่กล่าวถึงในหัวข้อ "โมเดลเชิงสัมพันธ์เทียบกับโมเดลเอกสาร" แนวโน้มนี้เรียกว่า NewSQL เพื่อตรงข้ามกับ NoSQL (แม้ว่ามันจะเกี่ยวกับ SQL น้อยกว่าและเกี่ยวกับแนวทางใหม่ในการจัดการ transaction ที่ scalable มากกว่า)
แม้ว่าขณะนี้มีฐานข้อมูลแบบกระจายที่ scalable และ strongly consistent ให้ใช้แล้ว แต่ก็ยังมีเหตุผลที่ดีที่บางแอปพลิเคชันเลือกรูปแบบการจําลองที่แตกต่างกันซึ่งให้หลักประกันความสอดคล้องที่อ่อนแอกว่า โดยเฉพาะอย่างยิ่ง พวกมันสามารถให้ความยืดหยุ่นที่แข็งแกร่งกว่าเมื่อเผชิญกับการขัดจังหวะของเครือข่ายและมี overhead ต่ํากว่าเมื่อเทียบกับระบบ transactional เราจะสํารวจแนวทางดังกล่าวใน ส่วนที่เหลือของบทนี้
Multi-Leader Replication (การจำลองแบบหลายผู้นำ)
จนถึงตอนนี้ในบทนี้เราได้พิจารณาเฉพาะสถาปัตยกรรมการจําลองที่ใช้ leader เดียว แม้ว่านั่นจะเป็นแนวทางที่พบได้ทั่วไป แต่ก็มีทางเลือกที่น่าสนใจ
Single-leader replication มีข้อเสียหลักหนึ่งประการ: writes ทั้งหมดต้องผ่าน leader ตัวเดียว ถ้าคุณไม่สามารถเชื่อมต่อกับ leader ได้ด้วยเหตุผลใดก็ตาม—ตัวอย่างเช่น เพราะการขัดจังหวะของเครือข่ายระหว่างคุณกับ leader—คุณจะไม่สามารถเขียนไปยังฐานข้อมูลได้
การขยายตามธรรมชาติของโมเดล single-leader replication คือการอนุญาตให้มากกว่าหนึ่ง node สามารถรับ writes ได้ การจําลองยังเกิดขึ้นในลักษณะเดียวกัน: แต่ละ node ที่ประมวลผล write ต้องส่งต่อการเปลี่ยนแปลงข้อมูลนั้นไปยัง nodes อื่นๆ ทั้งหมด เราเรียกสิ่งนี้ว่าการกําหนดค่าแบบ multi-leader (หรือที่รู้จักในชื่อ active/active หรือ bidirectional replication) ในการตั้งค่านี้ แต่ละ leader ทําหน้าที่เป็น follower ให้กับ leaders อื่นๆ พร้อมกัน
เช่นเดียวกับ single-leader replication มีตัวเลือกระหว่างการทําให้เป็น synchronous หรือ asynchronous สมมติว่าคุณมี leaders สองตัวคือ A และ B และคุณกําลังพยายามเขียนไปยัง A ถ้า writes ถูกจําลองแบบ synchronous จาก A ไปยัง B และเครือข่ายระหว่างสอง nodes ถูกขัดจังหวะ คุณจะไม่สามารถเขียนไปยัง A ได้จนกว่าการเชื่อมต่อจะกลับคืนมา ดังนั้น synchronous multi-leader replication จึงให้โมเดลที่คล้ายกับ single-leader replication มาก เช่น คุณทําให้ B เป็น leader และ A เพียงแค่ส่งต่อคําขอเขียนใดๆ ไปยัง B เพื่อประมวลผล
ด้วยเหตุนี้ เราจะไม่ลงลึกไปใน synchronous multi-leader replication และจะถือว่ามันเทียบเท่ากับ single-leader replication ส่วนที่เหลือของส่วนนี้จะเน้นที่ asynchronous multi-leader replication ซึ่ง leader ใดๆ ก็สามารถประมวลผล writes ได้ แม้ว่าการเชื่อมต่อไปยัง leaders อื่นๆ จะถูกขัดจังหวะ
Geographically Distributed Operation (การทำงานแบบกระจายตามภูมิศาสตร์)
การใช้ multi-leader setup ภายใน region เดียวมักไม่ค่อยสมเหตุสมผล เพราะประโยชน์มักไม่คุ้มกับความซับซ้อนที่เพิ่มขึ้น อย่างไรก็ตาม ในบางสถานการณ์การกําหนดค่านี้ก็สมเหตุสมผล
ลองนึกภาพว่าคุณมีฐานข้อมูลที่มี replicas ในหลาย regions (อาจเพื่อให้ทนต่อความล้มเหลวของทั้ง region หรือเพื่อความใกล้ชิดกับผู้ใช้ของคุณ) สิ่งนี้เรียกว่าการตั้งค่าแบบ geographically distributed, geo-distributed, หรือ geo-replicated ด้วย single-leader replication leader ต้องอยู่ใน หนึ่ง ใน regions และ writes ทั้งหมดต้องผ่าน region นั้น
Figure 6-6. การจําลองแบบ Multi-Leader ข้ามหลายภูมิภาค
ในการกําหนดค่าแบบ multi-leader คุณสามารถมี leader ใน ทุก region Figure 6-6 แสดงให้เห็นว่าสถาปัตยกรรมนี้หน้าตาเป็นอย่างไร ภายในแต่ละ region จะใช้การจําลอง leader–follower ปกติ (โดย followers อาจอยู่ใน availability zone ที่แตกต่างจาก leader); ระหว่าง regions แต่ละ region's leader จะจําลองการเปลี่ยนแปลงไปยัง leaders ใน regions อื่นๆ มาเปรียบเทียบว่าการกําหนดค่าแบบ single-leader และ multi-leader ทํางานอย่างไรในการปรับใช้หลายภูมิภาค:
ประสิทธิภาพ
ในการกําหนดค่าแบบ single-leader ทุก write ต้องผ่านอินเทอร์เน็ตไปยัง region ที่มี leader ซึ่งสามารถเพิ่ม latency ให้กับ writes อย่างมีนัยสําคัญ และอาจทําให้วัตถุประสงค์ของการมีหลาย region หมดไป ในการกําหนดค่าแบบ multi-leader ทุก write สามารถประมวลผลใน region ท้องถิ่นแล้วจึงจําลองแบบ asynchronous ไปยัง regions อื่นๆ ดังนั้นความหน่วงของเครือข่ายระหว่าง region จะถูกซ่อนจากผู้ใช้ ซึ่งหมายความว่าประสิทธิภาพที่รับรู้อาจดีกว่า
ความทนทานต่อการหยุดทํางานของภูมิภาค
ในการกําหนดค่าแบบ single-leader ถ้า region ที่มี leader ไม่พร้อมใช้งาน failover สามารถเลื่อนขั้น follower ใน region อื่นเป็น leader ได้ ในการกําหนดค่าแบบ multi-leader แต่ละ region สามารถทํางานต่อไปได้อย่างอิสระจาก regions อื่นๆ และการจําลองจะตามทันเมื่อ region ที่ออฟไลน์กลับมาออนไลน์
ความทนทานต่อปัญหาเครือข่าย
แม้จะมีการเชื่อมต่อเฉพาะ ทราฟฟิกระหว่าง regions อาจน่าเชื่อถือน้อยกว่าทราฟฟิกระหว่าง zones ใน region เดียวกันหรือภายใน zone เดียว การกําหนดค่าแบบ single-leader มีความไวสูงต่อปัญหาในลิงก์ระหว่าง region นี้ เพราะเมื่อ client ใน region หนึ่งต้องการเขียนไปยัง leader ในอีก region หนึ่ง มันต้องส่งคําขอผ่านลิงก์นั้นและรอการตอบกลับก่อนที่จะเสร็จสมบูรณ์
การกําหนดค่าแบบ multi-leader ที่มี asynchronous replication สามารถทนต่อปัญหาเครือข่ายได้ดีกว่า ในระหว่างการขัดจังหวะเครือข่ายชั่วคราว leader ของแต่ละ region สามารถประมวลผล writes ต่อไปได้อย่างอิสระ
ความสอดคล้องของข้อมูล (Consistency)
ระบบ single-leader สามารถให้หลักประกันความสอดคล้องที่แข็งแกร่ง เช่น serializable transactions ซึ่งเราจะพูดถึงใน บทที่ 8 ข้อเสียที่ใหญ่ที่สุดของระบบ multi-leader คือความสอดคล้องที่พวกเขาสามารถบรรลุนั้นอ่อนแอกว่ามาก ตัวอย่างเช่น คุณไม่สามารถรับประกันได้ว่าบัญชีธนาคารจะไม่ติดลบ หรือชื่อผู้ใช้จะไม่ซ้ํากัน มันเป็นไปได้เสมอที่ leaders ที่แตกต่างกันจะประมวลผล writes ที่แต่ละอย่างก็ไม่เป็นปัญหา (การจ่ายเงินออกจากบัญชี การลงทะเบียนชื่อผู้ใช้เฉพาะ) แต่ละเมิดข้อจํากัดเมื่อรวมกับ write อีกอันบน leader อื่น
นี่เป็นเพียงข้อจํากัดพื้นฐานของระบบกระจาย [ 28 ] ถ้าคุณต้องการบังคับใช้ข้อจํากัดดังนั้นคุณจะดีกว่ากับระบบ single-leader อย่างไรก็ตาม ดังที่เราจะเห็นในหัวข้อ "การจัดการกับ Write Conflicts" ระบบ multi-leader ยังคงบรรลุคุณสมบัติความสอดคล้องที่เป็นประโยชน์ในแอปพลิเคชันหลากหลายที่ไม่ต้องการข้อจํากัดดังกล่าว
Multi-leader replication พบได้น้อยกว่า single-leader replication แต่ก็ยังรองรับโดยฐานข้อมูลหลายตัว รวมถึง MySQL, Oracle, SQL Server, และ YugabyteDB ในบางกรณีมันเป็นฟีเจอร์ add-on ภายนอก—ตัวอย่างเช่น ใน Redis Enterprise, EDB Postgres Distributed, และ pglogical [ 29 ]
เนื่องจาก multi-leader replication เป็นฟีเจอร์ที่ถูกดัดแปลงเพิ่มในฐานข้อมูลหลายตัว จึงมักมีข้อผิดพลาดในการกําหนดค่าที่ละเอียดอ่อนและการโต้ตอบที่น่าประหลาดใจกับฟีเจอร์อื่นๆ ของฐานข้อมูล ตัวอย่างเช่น autoincrementing keys, triggers, และ integrity constraints อาจเป็นปัญหา ด้วยเหตุนี้ multi-leader replication จึงมักถูกพิจารณาว่าเป็นพื้นที่อันตรายที่ควรหลีกเลี่ยงถ้าเป็นไปได้ [ 30 ]
Multi-leader replication topologies (โทโพโลยีการจำลองแบบ Multi-Leader)
replication topology อธิบายเส้นทางการสื่อสารที่ writes ถูกกระจายจาก node หนึ่งไปยังอีก node หนึ่ง ถ้าคุณมี leaders สองตัว ดังใน Figure 6-6 มีเพียง topology เดียวที่เป็นไปได้: leader 1 ต้องส่ง writes ทั้งหมดไปยัง leader 2 และในทางกลับกัน ถ้ามีมากกว่าสอง leaders ก็สามารถมี topology ต่างๆ ได้ ตัวอย่างบางส่วนแสดงใน Figure 6-7
Figure 6-7. ตัวอย่างโทโพโลยีสามแบบสําหรับการจําลองแบบ Multi-Leader
topology ที่ทั่วไปที่สุดคือ all-to-all ดังแสดงใน Figure 6-7 (c) ซึ่งทุก leader ส่ง writes ของตนไปยังทุก leader อื่น อย่างไรก็ตาม topology ที่จํากัดกว่าก็ถูกใช้เช่นกัน ตัวอย่างเช่น ใน circular topology ดังแสดงใน Figure 6-7 (a) แต่ละ node ได้รับ writes จาก node หนึ่งและส่งต่อ writes เหล่านั้น (บวก writes ของตัวเอง) ไปยังอีก node หนึ่ง star topology ดังแสดงใน Figure 6-7 (b) ก็เป็นที่นิยมเช่นกัน ที่นี่ node root ที่ถูกกําหนดหนึ่งตัวจะส่งต่อ writes ไปยัง nodes อื่นๆ ทั้งหมด star topology สามารถขยายเป็น tree ได้
หมายเหตุ
network topology แบบ star ไม่เกี่ยวข้องกับ star schema (ดูหัวข้อ "Stars and Snowflakes: Schemas for Analytics") ซึ่งอธิบายโครงสร้างของโมเดลข้อมูล
ใน circular และ star topologies write อาจต้องผ่านหลาย nodes ก่อนที่จะถึงทุก replicas ดังนั้น nodes ต้องส่งต่อการเปลี่ยนแปลงข้อมูลที่ได้รับจาก nodes อื่น เพื่อป้องกันลูปการจําลองไม่รู้จบ แต่ละ node จะได้รับ identifier ที่ไม่ซ้ํากัน และใน replication log แต่ละ write จะถูกติดแท็กด้วย identifiers ของทุก nodes ที่มันผ่าน [ 31 ] เมื่อ node ได้รับการเปลี่ยนแปลงข้อมูลที่ถูกติดแท็กด้วย identifier ของตัวเอง การเปลี่ยนแปลงข้อมูลนั้นจะถูก ignored เพราะ node รู้ว่ามันถูกประมวลผลแล้ว
Problems with different topologies (ปัญหากับโทโพโลยีที่แตกต่างกัน)
ปัญหาของ circular และ star topologies คือถ้าเพียง node เดียวล้มเหลว มันสามารถขัดขวางการไหลของ replication messages ระหว่าง nodes อื่นๆ ทําให้พวกมันไม่สามารถสื่อสารได้จนกว่า node จะถูกซ่อม topology สามารถถูกกําหนดค่าใหม่เพื่อทํางานรอบ node ที่ล้มเหลว แต่ในการปรับใช้ส่วนใหญ่การกําหนดค่าใหม่ดังกล่าวต้องทําด้วยตนเอง ความทนทานต่อข้อผิดพลาดของ topology ที่เชื่อมต่ออย่างหนาแน่นกว่า (เช่น all-to-all) ดีกว่าเพราะมันช่วยให้ messages เดินทางตามเส้นทางต่างๆ หลีกเลี่ยง single point of failure
อย่างไรก็ตาม all-to-all topologies ก็สามารถมีปัญหาได้เช่นกัน โดยเฉพาะอย่างยิ่ง ลิงก์เครือข่ายบางตัวอาจเร็วกว่าตัวอื่น (เช่น เพราะความแออัดของเครือข่าย) ส่งผลให้ replication messages บางตัว "แซง" ตัวอื่น ดังที่แสดงใน Figure 6-8
ใน Figure 6-8 client A แทรกแถวลงในตารางบน leader 1 และ client B อัปเดตแถวนั้นบน leader 3 อย่างไรก็ตาม leader 2 อาจได้รับ writes ในลําดับที่แตกต่างกัน มันอาจได้รับ update ก่อน (ซึ่งจากมุมมองของมัน เป็นการอัปเดตแถวที่ไม่มีอยู่ในฐานข้อมูล) และได้รับ insert ที่สอดคล้องกันทีหลัง (ซึ่งควรจะมาก่อน update)
Figure 6-8. ด้วยการจําลองแบบ Multi-Leader writes อาจมาถึงในลําดับที่ผิดที่บาง replicas
นี่คือปัญหาของความเป็นเหตุเป็นผล คล้ายกับที่เราเห็นในหัวข้อ "การอ่านแบบ Consistent Prefix" การอัปเดตขึ้นอยู่กับการแทรกก่อนหน้า ดังนั้นเราต้องแน่ใจว่าทุก nodes ประมวลผล insert ก่อน แล้วจึงประมวลผล update การแนบ timestamp กับทุก write อย่างเดียวนั้นไม่เพียงพอ เพราะไม่สามารถไว้วางใจนาฬิกาให้ซิงค์กันพอที่จะเรียงลําดับเหตุการณ์เหล่านี้ที่ leader 2 ได้อย่างถูกต้อง (ดู บทที่ 9)
เพื่อเรียงลําดับเหตุการณ์เหล่านี้อย่างถูกต้อง สามารถใช้เทคนิคที่เรียกว่า version vectors ซึ่งเราจะพูดถึงในหัวข้อ "การตรวจจับ Concurrent Writes" อย่างไรก็ตาม ระบบ multi-leader replication จํานวนมากไม่ได้ใช้เทคนิคที่ดีในการเรียงลําดับ updates ทําให้พวกมันอ่อนแอต่อปัญหาเช่นใน Figure 6-8 ถ้าคุณใช้ multi-leader replication ควรตระหนักถึงปัญหาเหล่านี้ อ่านเอกสารอย่างละเอียด และทดสอบฐานข้อมูลของคุณอย่างถี่ถ้วนเพื่อให้แน่ใจว่ามันให้หลักประกันที่คุณเชื่อว่ามันมีจริงๆ
Sync Engines and Local-First Software (Sync Engines และ Local-First Software)
Multi-leader replication ก็เหมาะสมเช่นกันถ้าคุณมีแอปพลิเคชันที่ต้องทํางานต่อเนื่องในขณะที่ไม่ได้เชื่อมต่ออินเทอร์เน็ต ตัวอย่างเช่น ลองนึกถึงแอปปฏิทินบนโทรศัพท์มือถือ แล็ปท็อป และอุปกรณ์อื่นๆ ของคุณ คุณต้องสามารถดูการประชุม (ทําคําขออ่าน) และเพิ่มการประชุมใหม่ (ทําคําขอเขียน) ได้ตลอดเวลา ไม่ว่าอุปกรณ์ของคุณจะเชื่อมต่ออินเทอร์เน็ตหรือไม่ก็ตาม ถ้าคุณทําการเปลี่ยนแปลงใดๆ ขณะออฟไลน์ การเปลี่ยนแปลงเหล่านั้นต้องถูกซิงค์กับเซิร์ฟเวอร์และอุปกรณ์อื่นๆ ของคุณเมื่ออุปกรณ์กลับมาออนไลน์
ในกรณีนี้ ทุกอุปกรณ์มี replica ฐานข้อมูลท้องถิ่นที่ทําหน้าที่เป็น leader (มันรับคําขอเขียน) และมีกระบวนการจําลองแบบ asynchronous multi-leader (sync) ระหว่าง replicas ของปฏิทินของคุณบนทุกอุปกรณ์ replication lag อาจเป็นชั่วโมงหรือเป็นวัน ขึ้นอยู่กับว่าคุณมีการเชื่อมต่ออินเทอร์เน็ตเมื่อใด
จากมุมมองทางสถาปัตยกรรม การตั้งค่านี้คล้ายกับ multi-leader replication ระหว่าง regions อย่างมาก เมื่อนําไปสู่ที่สุด extremes แต่ละอุปกรณ์คือ "region" และการเชื่อมต่อเครือข่ายระหว่างพวกมันไม่น่าเชื่อถืออย่างมาก
Real-time collaboration, offline-first, and local-first apps (การทำงานร่วมกันแบบ Real-time, Offline-first และ Local-first)
เว็บแอปสมัยใหม่หลายตัวมีฟีเจอร์ real-time collaboration เช่น Google Docs และ Sheets สําหรับเอกสารข้อความและสเปรดชีต Figma สําหรับกราฟิก และ Linear สําหรับการจัดการโปรเจกต์ สิ่งที่ทําให้แอปเหล่านี้ตอบสนองได้ดีคือการป้อนข้อมูลของผู้ใช้จะสะท้อนในอินเทอร์เฟซผู้ใช้ทันที โดยไม่ต้องรอการเดินทางไปกลับเครือข่ายกับเซิร์ฟเวอร์ และการแก้ไขโดยผู้ใช้คนหนึ่งจะแสดงให้ผู้ร่วมงานเห็นด้วยความหน่วงต่ํา [ 32, 33, 34 ]
สิ่งนี้ให้ผลลัพธ์เป็นสถาปัตยกรรม multi-leader อีกครั้ง: แต่ละแท็บเบราว์เซอร์ที่เปิดไฟล์ที่ใช้ร่วมกันคือ replica และการอัปเดตใดๆ ที่คุณทํากับไฟล์จะถูกจําลองแบบ asynchronous ไปยังอุปกรณ์ของผู้ใช้อื่นที่เปิดไฟล์เดียวกัน แม้ว่าแอปจะไม่อนุญาตให้คุณแก้ไขไฟล์ต่อในขณะออฟไลน์ แต่ความจริงที่ว่าผู้ใช้หลายคนสามารถแก้ไขโดยไม่ต้องรอการตอบกลับจากเซิร์ฟเวอร์ก็ทําให้มันเป็น multi-leader อยู่แล้ว
ทั้งการแก้ไขแบบออฟไลน์และการทํางานร่วมกันแบบเรียลไทม์ต้องการโครงสร้างพื้นฐานการจําลองที่คล้ายกัน แอปพลิเคชันต้องบันทึกการเปลี่ยนแปลงใดๆ ที่ผู้ใช้ทํากับไฟล์และส่งให้ผู้ร่วมงานทันที (ถ้าออนไลน์) หรือเก็บไว้ในเครื่องเพื่อส่งทีหลัง (ถ้าออฟไลน์) นอกจากนี้ แอปพลิเคชันต้องได้รับการเปลี่ยนแปลงจากผู้ร่วมงาน รวมเข้ากับสําเนาไฟล์ท้องถิ่นของผู้ใช้ และอัปเดต UI เพื่อสะท้อนเวอร์ชันล่าสุด ถ้าผู้ใช้หลายคนเปลี่ยนไฟล์พร้อมกัน อาจต้องใช้ตรรกะการแก้ไข conflict เพื่อรวมการเปลี่ยนแปลงเหล่านั้น
ไลบรารีซอฟต์แวร์ที่รองรับกระบวนการนี้เรียกว่า sync engine แม้ว่าแนวคิดนี้มีมานานแล้ว แต่คํานี้เพิ่งได้รับความสนใจเมื่อเร็วๆ นี้ [ 35, 36, 37 ] แอปพลิเคชันที่อนุญาตให้ผู้ใช้แก้ไขไฟล์ต่อในขณะออฟไลน์ (ซึ่งอาจ implement โดยใช้ sync engine) เรียกว่า offline-first [ 38 ] คําว่า local-first software หมายถึงแอปที่ทํางานร่วมกันซึ่งไม่เพียงแค่ offline-first แต่ยังออกแบบให้ทํางานต่อไปได้แม้ว่านักพัฒนาที่สร้างซอฟต์แวร์จะปิดบริการออนไลน์ทั้งหมดของพวกเขา [ 39 ] สิ่งนี้สามารถทําได้โดยใช้ sync engine ที่มีโปรโตคอล sync มาตรฐานเปิดซึ่งมีผู้ให้บริการหลายราย [ 40 ] ตัวอย่างเช่น Git เป็นระบบการทํางานร่วมกันแบบ local-first (แม้จะไม่รองรับ real-time collaboration) เนื่องจากคุณสามารถซิงค์ผ่าน GitHub, GitLab, หรือบริการโฮสต์ repository อื่นใด
Pros and cons of sync engines (ข้อดีและข้อเสียของ Sync Engines)
วิธีหลักในการสร้างเว็บแอปในปัจจุบันคือการเก็บสถานะถาวรไว้บน client น้อยมาก และพึ่งพาการส่งคําขอไปยังเซิร์ฟเวอร์เมื่อใดก็ตามที่ต้องการแสดงข้อมูลใหม่หรืออัปเดตข้อมูลบางอย่าง ในทางตรงกันข้าม เมื่อใช้ sync engine คุณมีสถานะถาวรบน client และการสื่อสารกับเซิร์ฟเวอร์จะถูกย้ายไปเป็นกระบวนการพื้นหลัง วิธีการแบบ sync engine มีข้อดีหลายประการ:
-
การมีข้อมูลในเครื่องหมายความว่า UI สามารถตอบสนองได้เร็วกว่าถ้าต้องรอการเรียกบริการเพื่อดึงข้อมูล บางแอปมีเป้าหมายที่จะตอบสนองต่อการป้อนข้อมูลของผู้ใช้ภายใน เฟรมถัดไป ของระบบกราฟิก ซึ่งหมายถึงการแสดงผลภายใน 16 ms บนจอที่มีอัตรารีเฟรช 60 Hz
-
การอนุญาตให้ผู้ใช้ทํางานต่อในขณะออฟไลน์มีคุณค่า โดยเฉพาะบนอุปกรณ์มือถือที่มีการเชื่อมต่อไม่สม่ําเสมอ ด้วย sync engine แอปไม่ต้องการโหมดออฟไลน์แยกต่างหาก: การออฟไลน์ก็เหมือนกับการมีความหน่วงเครือข่ายที่มาก
-
sync engine ทําให้โมเดลการเขียนโปรแกรมสําหรับ frontend apps ง่ายขึ้น เมื่อเทียบกับการเรียก service โดยตรงในโค้ดแอปพลิเคชัน ทุกการเรียก service ต้องการการจัดการข้อผิดพลาด ดังที่กล่าวถึงในหัวข้อ "ปัญหาของการเรียกโพรซีเยอร์ระยะไกล"; ตัวอย่างเช่น ถ้าคําขออัปเดตข้อมูลบนเซิร์ฟเวอร์ล้มเหลว อินเทอร์เฟซผู้ใช้ต้องสะท้อนข้อผิดพลาดนั้นไม่ทางใดก็ทางหนึ่ง sync engine ช่วยให้แอปอ่านและเขียนบนข้อมูลท้องถิ่น การดําเนินการเหล่านี้แทบจะไม่ล้มเหลวเลย นําไปสู่สไตล์การเขียนโปรแกรมที่ declarative มากขึ้น [ 41 ]
-
เพื่อแสดงการแก้ไขจากผู้ใช้อื่นแบบเรียลไทม์ คุณต้องได้รับการแจ้งเตือนของการแก้ไขเหล่านั้นและอัปเดต UI อย่างมีประสิทธิภาพตามนั้น sync engine ที่รวมกับโมเดล reactive programming เป็นวิธีที่ดีในการ implement สิ่งนี้ [ 42 ]
Sync engines ทํางานได้ดีที่สุดเมื่อข้อมูลทั้งหมดที่ผู้ใช้อาจต้องการถูกดาวน์โหลดล่วงหน้าและเก็บไว้ในเครื่องแบบถาวร ซึ่งหมายความว่าข้อมูลพร้อมสําหรับการเข้าถึงแบบออฟไลน์เมื่อต้องการ แต่มันก็หมายความว่า sync engines ไม่เหมาะสมถ้าผู้ใช้สามารถเข้าถึงข้อมูลจํานวนมาก ตัวอย่างเช่น การดาวน์โหลดไฟล์ทั้งหมดที่ผู้ใช้สร้างขึ้นอาจโอเค (ผู้ใช้คนหนึ่งโดยทั่วไปไม่ได้สร้างข้อมูลมากขนาดนั้น) แต่การดาวน์โหลดแคตตาล็อกทั้งหมดของเว็บไซต์ ecommerce อาจไม่สมเหตุสมผล
Sync engine ถูกบุกเบิกโดย Lotus Notes ในยุค 1980 [ 43 ] (โดยไม่ใช้คํานั้น) และ sync สําหรับแอปเฉพาะ เช่น ปฏิทิน ก็มีมานานแล้ว วันนี้เรามี sync engines แบบใช้งานทั่วไปมากมาย บางตัวใช้บริการ backend แบบ proprietary (เช่น Google Firestore, Realm, หรือ Ditto) และบางตัวมี backend แบบ open source ทําให้เหมาะสําหรับการสร้าง local-first software (เช่น PouchDB/CouchDB, Automerge, และ Yjs)
เกมวิดีโอแบบผู้เล่นหลายคนมีความต้องการคล้ายกันในการตอบสนองต่อการกระทําท้องถิ่นของผู้ใช้ทันทีและกระทบยอดกับการกระทําของผู้เล่นอื่นที่ได้รับแบบ asynchronous ผ่านเครือข่าย ในศัพท์เฉพาะของการพัฒนาเกม สิ่งที่เทียบเท่ากับ sync engine เรียกว่า netcode เทคนิคที่ใช้ใน netcode ค่อนข้างเฉพาะเจาะจงกับความต้องการของเกม [ 44 ] และไม่สามารถนําไปใช้กับซอฟต์แวร์ประเภทอื่นได้โดยตรง ดังนั้นเราจะไม่พิจารณามันเพิ่มเติมในหนังสือเล่มนี้
Dealing with Conflicting Writes (การจัดการกับ Write Conflicts)
ปัญหาที่ใหญ่ที่สุดของ multi-leader replication—ทั้งในฐานข้อมูลฝั่งเซิร์ฟเวอร์แบบ geo-distributed และ sync engine แบบ local-first บนอุปกรณ์ผู้ใช้—คือ writes ที่เกิดขึ้นพร้อมกันบน leaders ที่แตกต่างกันสามารถนําไปสู่ conflicts ที่ต้องแก้ไข
ตัวอย่างเช่น พิจารณาหน้า wiki ที่ถูกแก้ไขโดยผู้ใช้สองคนพร้อมกัน ดังแสดงใน Figure 6-9 ผู้ใช้ 1 เปลี่ยนชื่อหน้าจาก A เป็น B และผู้ใช้ 2 เปลี่ยนชื่อจาก A เป็น C อย่างอิสระ การเปลี่ยนแปลงของผู้ใช้แต่ละคนถูกนําไปใช้กับ leader ท้องถิ่นของตนสําเร็จ อย่างไรก็ตาม เมื่อการเปลี่ยนแปลงถูกจําลองแบบ asynchronous จะตรวจพบ conflict ปัญหานี้ไม่เกิดขึ้นในฐานข้อมูล single-leader
Figure 6-9. Write conflict ที่เกิดจาก leader สองตัวอัปเดต record เดียวกันพร้อมกัน
หมายเหตุ
เรากล่าวว่า writes สองครั้งใน Figure 6-9 เป็น concurrent เพราะไม่มีฝ่ายใด "รู้" เกี่ยวกับอีกฝ่ายในเวลาที่ write ถูกสร้างขึ้น มันไม่สําคัญว่า writes เกิดขึ้นพร้อมกันจริงๆ หรือไม่ ที่จริงแล้ว ถ้า writes เกิดขึ้นขณะออฟไลน์ พวกมันอาจเกิดขึ้นห่างกันเป็นระยะเวลา สิ่งที่สําคัญคือ write หนึ่งเกิดขึ้นในสถานะที่ write อีกอันมีผลแล้วหรือไม่
ในหัวข้อ "การตรวจจับ Concurrent Writes" เราจะจัดการกับคําถามว่าฐานข้อมูลสามารถระบุได้อย่างไรว่า writes สองครั้งเป็น concurrent สําหรับตอนนี้เราจะสมมติว่าเราสามารถตรวจจับ conflicts และต้องการหาวิธีที่ดีที่สุดในการแก้ไข
Conflict avoidance (การหลีกเลี่ยง Conflict)
กลยุทธ์หนึ่งสําหรับการจัดการ conflicts คือป้องกันไม่ให้เกิดขึ้นตั้งแต่แรก ตัวอย่างเช่น ถ้าแอปพลิเคชันสามารถรับประกันได้ว่า writes ทั้งหมดสําหรับ record ใด record หนึ่งผ่าน leader เดียวกัน conflicts ก็ไม่สามารถเกิดขึ้นได้ แม้ว่าฐานข้อมูลโดยรวมจะเป็น multi-leader วิธีการนี้ไม่สามารถใช้ได้สําหรับ client ของ sync engine ที่ถูกอัปเดตแบบออฟไลน์ แต่มันเป็นไปได้บางครั้งในระบบเซิร์ฟเวอร์แบบ geo-replicated [ 30 ]
ตัวอย่างเช่น ในแอปพลิเคชันที่ผู้ใช้แก้ไขได้เฉพาะข้อมูลของตนเอง คุณสามารถรับประกันว่าคําขอจากผู้ใช้เฉพาะรายจะถูกส่งไปยัง region เดียวกันเสมอและใช้ leader ใน region นั้นสําหรับการอ่านและเขียน ผู้ใช้ที่แตกต่างกันอาจมี "บ้าน" regions ที่แตกต่างกัน (อาจเลือกตามความใกล้ชิดทางภูมิศาสตร์กับผู้ใช้) แต่จากมุมมองของผู้ใช้คนใดคนหนึ่ง การกําหนดค่าก็เป็น single-leader โดยพื้นฐาน
อย่างไรก็ตาม ในบางครั้งคุณอาจต้องการเปลี่ยน leader ที่กําหนดสําหรับ record—อาจเพราะ region หนึ่งไม่พร้อมใช้งานและคุณต้องเปลี่ยนเส้นทางทราฟฟิกไปยังอีก region หรือเพราะผู้ใช้ย้ายไปยังตําแหน่งอื่นและตอนนี้ใกล้กับ region อื่นมากขึ้น ตอนนี้มีความเสี่ยงที่ผู้ใช้จะทํา write ในขณะที่การเปลี่ยน leader ที่กําหนดกําลังดําเนินอยู่ ซึ่งนําไปสู่ conflict ที่จะต้องแก้ไขโดยใช้วิธีใดวิธีหนึ่งต่อไปนี้ ดังนั้น conflict avoidance จะล้มเหลวถ้าคุณอนุญาตให้เปลี่ยน leader
สําหรับตัวอย่างอื่นของการหลีกเลี่ยง conflict ลองนึกภาพว่าคุณต้องการแทรก records ใหม่และสร้าง IDs ที่ไม่ซ้ํากันโดยใช้ counter แบบ autoincrementing ถ้าคุณมีสอง leaders คุณสามารถตั้งค่าให้ leader หนึ่งสร้างเฉพาะเลขคี่และอีกตัวสร้างเฉพาะเลขคู่ ด้วยวิธีนี้คุณมั่นใจได้ว่า leaders สองตัวจะไม่กําหนด ID เดียวกันให้กับ records ที่แตกต่างกัน เราจะพูดถึงแผนการกําหนด ID อื่นๆ ในหัวข้อ "ID Generators และ Logical Clocks"
Last write wins (discarding concurrent writes) (Last Write Wins: การทิ้ง Concurrent Writes)
ถ้าไม่สามารถหลีกเลี่ยง conflicts ได้ วิธีที่ง่ายที่สุดในการแก้ไขคือการแนบ timestamp กับแต่ละ write และใช้ค่าที่มี timestamp ล่าสุด (มากที่สุด) เสมอ ตัวอย่างเช่น ใน Figure 6-9 สมมติว่า timestamp ของ write ของผู้ใช้ 1 มากกว่า timestamp ของ write ของผู้ใช้ 2 ในกรณีนั้น leaders ทั้งสองจะตัดสินว่าชื่อใหม่ของหน้าควรเป็น B และจะทิ้ง write ที่ตั้งค่าเป็น C ถ้า writes มี timestamp เดียวกันโดยบังเอิญ สามารถเลือกผู้ชนะโดยการเปรียบเทียบค่า (เช่น สําหรับสตริง เลือกค่าที่มาก่อนตามลําดับตัวอักษร)
วิธีการนี้เรียกว่า last write wins (LWW) เพราะ write ที่มี timestamp มากที่สุดถือเป็น "ล่าสุด" อย่างไรก็ตาม คํานี้ทําให้เข้าใจผิด เพราะเมื่อ writes สองครั้งเป็น concurrent (ดังใน Figure 6-9) ซึ่งอันไหนล่าสุดนั้นไม่สามารถกําหนดได้ ดังนั้นลําดับ timestamp ของ concurrent writes จึงเป็นการสุ่มโดยพื้นฐาน
ดังนั้น ความหมายที่แท้จริงของ LWW คือ: เมื่อ record เดียวกันถูกเขียนพร้อมกันบน leaders ที่แตกต่างกัน write หนึ่งจะถูกสุ่มเลือกเป็นผู้ชนะและ writes อื่นๆ จะถูกทิ้งอย่างเงียบๆ แม้ว่าพวกมันถูกประมวลผลสําเร็จโดย leaders ของตนแล้วก็ตาม สิ่งนี้บรรลุเป้าหมายที่ว่าในที่สุดทุก replicas จะลงเอยด้วยสถานะที่สอดคล้องกัน แต่ต้องสูญเสียข้อมูล
ถ้าคุณสามารถหลีกเลี่ยง conflicts—ตัวอย่างเช่น โดยการแทรก records ด้วย key ที่ไม่ซ้ํากันเท่านั้นและไม่เคยอัปเดต—LWW ก็ไม่มีปัญหา แต่ถ้าคุณอัปเดต records ที่มีอยู่ หรือถ้า leaders ที่แตกต่างกันอาจแทรก records ด้วย key เดียวกัน คุณต้องตัดสินใจว่าการสูญเสีย updates เป็นปัญหาสําหรับแอปพลิเคชันของคุณหรือไม่ ถ้าการสูญเสีย updates ไม่สามารถยอมรับได้ คุณต้องใช้วิธีแก้ไข conflict ที่อธิบายต่อไป
ปัญหาอีกอย่างของ LWW คือถ้าใช้ real-time clock (เช่น Unix timestamp) เป็น timestamp สําหรับ writes ระบบจะไวต่อการซิงค์นาฬิกามาก ถ้า node หนึ่งมีนาฬิกาที่เร็วกว่าตัวอื่น และคุณพยายามเขียนทับค่าที่เขียนโดย node นั้น write ของคุณอาจถูก ignore เพราะมี timestamp ที่ต่ํากว่า แม้ว่ามันเกิดขึ้นทีหลังอย่างชัดเจน ปัญหานี้สามารถแก้ไขได้โดยใช้ logical clock ซึ่งเราจะพูดถึงในหัวข้อ "ID Generators และ Logical Clocks"
Manual conflict resolution (การแก้ไข Conflict ด้วยตนเอง)
ถ้าการทิ้ง writes บางส่วนของคุณแบบสุ่มไม่เป็นที่ต้องการ ตัวเลือกถัดไปคือแก้ไข conflict ด้วยตนเอง คุณอาจคุ้นเคยกับการแก้ไข conflict ด้วยตนเองจาก Git และระบบควบคุมเวอร์ชันอื่นๆ: ถ้า commits บนสอง branches แก้ไขบรรทัดเดียวกันของไฟล์เดียวกัน และคุณพยายาม merge branches เหล่านั้น คุณจะได้รับ merge conflict ที่ต้องแก้ไขก่อนการ merge จะเสร็จสมบูรณ์
ในฐานข้อมูล การให้ conflict หยุดกระบวนการจําลองทั้งหมดจนกว่ามนุษย์จะแก้ไขมันนั้นไม่สามารถปฏิบัติได้จริง แต่ฐานข้อมูลมักจะเก็บค่าที่ถูกเขียนพร้อมกันทั้งหมดสําหรับ record หนึ่ง—ตัวอย่างเช่น ทั้ง B และ C ใน Figure 6-9 ค่าเหล่านี้บางครั้งเรียกว่า siblings ครั้งถัดไปที่คุณสอบถาม record นั้น ฐานข้อมูลจะส่งคืนค่า ทั้งหมด แทนที่จะเป็นเพียงค่าล่าสุด จากนั้นคุณสามารถแก้ไขค่าเหล่านั้นด้วยวิธีใดก็ได้ ไม่ว่าจะโดยอัตโนมัติในโค้ดแอปพลิเคชัน (เช่น คุณสามารถต่อ B และ C เป็น B/C) หรือโดยสอบถามผู้ใช้ จากนั้นคุณเขียนค่ากลับไปยังฐานข้อมูลเพื่อแก้ไข conflict
วิธีการแก้ไข conflict นี้ใช้ในบางระบบ เช่น CouchDB อย่างไรก็ตาม มันก็ประสบปัญหาเหล่านี้:
-
API ของฐานข้อมูลเปลี่ยนไป—ตัวอย่างเช่น ที่ก่อนหน้านี้ชื่อของหน้า wiki เป็นแค่ string ตอนนี้มันกลายเป็น set ของ strings ที่ปกติมีหนึ่ง element แต่อาจมีหลาย elements ถ้ามี conflict ซึ่งอาจทําให้ข้อมูลทํางานยากในโค้ดแอปพลิเคชัน
-
การขอให้ผู้ใช้ merge siblings ด้วยตนเองเป็นงานที่มาก ทั้งสําหรับนักพัฒนาแอป (ที่ต้องสร้าง UI สําหรับการแก้ไข conflict) และสําหรับผู้ใช้ (ที่อาจสับสนเกี่ยวกับสิ่งที่ถูกขอให้ทําและทําไม) ในหลายกรณี การ merge โดยอัตโนมัติดีกว่ารบกวนผู้ใช้
-
การ merge siblings โดยอัตโนมัติอาจนําไปสู่พฤติกรรมที่น่าประหลาดใจถ้าไม่ได้ทําอย่างระมัดระวัง ตัวอย่างเช่น ตะกร้าสินค้าบน Amazon เคยอนุญาตให้อัปเดตพร้อมกัน ซึ่งจากนั้นถูกรวมโดยเก็บรายการตะกร้าสินค้าทั้งหมดที่ปรากฏใน siblings ใดๆ (เช่น การใช้ set union ของตะกร้า) ซึ่งหมายความว่าถ้าลูกค้าเอาสินค้าออกจากตะกร้าใน sibling หนึ่ง แต่ sibling อื่นยังมีสินค้าเก่านั้นอยู่ สินค้าที่ถูกลบจะปรากฏขึ้นอีกครั้งโดยไม่คาดคิดในตะกร้าของลูกค้า [ 45 ] ใน Figure 6-10 อุปกรณ์ 1 ลบ
Bookออกจากตะกร้าสินค้าและอุปกรณ์ 2 ลบDVDออกพร้อมกัน แต่หลังจาก merge siblings ทั้งสองรายการก็ปรากฏขึ้นอีกครั้ง -
ถ้า multiple nodes สังเกตเห็น conflict และแก้ไขมันพร้อมกัน กระบวนการแก้ไข conflict เองก็สามารถสร้าง conflict ใหม่ได้ การแก้ไขเหล่านั้นอาจไม่สอดคล้องกันด้วย—ตัวอย่างเช่น node หนึ่งอาจ merge B และ C เป็น B/C และอีก node อาจ merge เป็น C/B ถ้าคุณไม่ระมัดระวังในการเรียงลําดับอย่างสอดคล้องกัน เมื่อ conflict ระหว่าง B/C และ C/B ถูกรวม อาจส่งผลให้เกิด B/C/C/B หรือสิ่งที่คล้ายกัน
Figure 6-10. ตัวอย่างความผิดปกติของตะกร้าสินค้าของ Amazon: ถ้า conflicts ถูก merge โดยใช้ union ของ set ไอเทมที่ถูกลบอาจปรากฏขึ้นอีกครั้ง
Automatic conflict resolution (การแก้ไข Conflict อัตโนมัติ)
สําหรับแอปพลิเคชันจํานวนมาก วิธีที่ดีที่สุดในการจัดการ conflicts คือการใช้อัลกอริทึมที่ merge concurrent writes เป็นสถานะที่สอดคล้องกันโดยอัตโนมัติ การแก้ไข conflict อัตโนมัติช่วยให้แน่ใจว่า replicas ทั้งหมด ลู่เข้า สู่สถานะเดียวกัน—กล่าวคือ replicas ทั้งหมดที่ประมวลผล writes ชุดเดียวกันจะมีสถานะเดียวกัน โดยไม่ขึ้นอยู่กับลําดับที่ writes มาถึง การรวม eventual consistency เข้ากับหลักประกันการลู่เข้าเรียกว่า strong eventual consistency [ 46 ]
LWW เป็นตัวอย่างง่ายๆของอัลกอริทึมการแก้ไข conflict อัลกอริทึมการ merge ที่ซับซ้อนมากขึ้นได้รับการพัฒนาสําหรับข้อมูลประเภทต่างๆ โดยมีเป้าหมายเพื่อรักษาเอฟเฟกต์ที่ตั้งใจไว้ของการอัปเดตทั้งหมดให้มากที่สุดและหลีกเลี่ยงการสูญเสียข้อมูล:
-
ถ้าข้อมูลเป็นข้อความ (เช่น ชื่อหรือเนื้อหาของหน้า wiki) เราสามารถตรวจจับว่าอักขระใดถูกแทรกหรือลบจากเวอร์ชันหนึ่งไปยังเวอร์ชันถัดไป ผลลัพธ์ที่รวมกันจะคงการแทรกและการลบทั้งหมดที่ทําใน siblings ใดๆ ไว้ ถ้าผู้ใช้แทรกข้อความที่ตําแหน่งเดียวกันพร้อมกัน มันสามารถเรียงลําดับแบบ deterministic เพื่อให้ทุก nodes ได้ผลลัพธ์ที่รวมกันเหมือนกัน
-
ถ้าข้อมูลเป็น collection ของ items (เรียงลําดับเหมือน to-do list หรือไม่เรียงลําดับเหมือนตะกร้าสินค้า) เราสามารถ merge มันคล้ายกับข้อความโดยการติดตามการแทรกและการลบ เพื่อหลีกเลี่ยงปัญหาตะกร้าสินค้าใน Figure 6-10 อัลกอริทึมจะติดตามความจริงที่ว่า
BookและDVDถูกลบ ดังนั้นผลลัพธ์ที่รวมกันคือCart = {Soap} -
ถ้าข้อมูลเป็น integer ที่แทน counter ที่สามารถเพิ่มหรือลดได้ (เช่น จํานวนไลค์บนโพสต์โซเชียลมีเดีย) อัลกอริทึมการ merge สามารถบอกได้ว่ามีการเพิ่มและลดกี่ครั้งในแต่ละ sibling และรวมเข้าด้วยกันอย่างถูกต้องเพื่อให้ผลลัพธ์ไม่นับซ้ําและไม่ทิ้ง updates
-
ถ้าข้อมูลเป็น key-value mapping เราสามารถ merge updates ไปยัง key เดียวกันโดยใช้อัลกอริทึมการแก้ไข conflict อื่นๆกับค่าภายใต้ key นั้น Updates ไปยัง keys ที่แตกต่างกันสามารถจัดการแยกจากกัน
มีข้อจํากัดสําหรับสิ่งที่สามารถทําได้ด้วยการแก้ไข conflict ตัวอย่างเช่น ถ้าคุณต้องการบังคับว่ารายการหนึ่งมีไม่เกินห้ารายการ และผู้ใช้หลายคนเพิ่มรายการพร้อมกันจนมีมากกว่าห้ารายการรวมกัน ทางเลือกเดียวของคุณคือทิ้งบางรายการ อย่างไรก็ตาม การแก้ไข conflict อัตโนมัติก็เพียงพอที่จะสร้างแอปที่มีประโยชน์มากมาย และถ้าคุณเริ่มจากความต้องการในการสร้างแอปแบบ collaborative offline-first หรือ local-first การแก้ไข conflict ก็เป็นสิ่งที่หลีกเลี่ยงไม่ได้ และการทําให้เป็นอัตโนมัติมักเป็นวิธีที่ดีที่สุด
Conflict-free replicated datatypes and operational transformation (CRDTs และ Operational Transformation)
อัลกอริทึมสอง ตระกูลที่ใช้กันทั่วไปในการ implement การแก้ไข conflict อัตโนมัติ: conflict-free replicated datatypes (CRDTs) [ 46 ] และ operational transformation (OT) [ 47 ] พวกมันมีปรัชญาการออกแบบและลักษณะประสิทธิภาพที่แตกต่างกัน แต่ทั้งคู่สามารถทําการ merge อัตโนมัติสําหรับข้อมูลทุกประเภทที่กล่าวมาข้างต้น
Figure 6-11 แสดงตัวอย่างวิธีที่ OT และ CRDT merge updates พร้อมกันไปยังข้อความ สมมติว่าคุณมีสอง replicas ที่เริ่มต้นด้วยข้อความ ice replica หนึ่งเพิ่มตัวอักษร n ข้างหน้าเป็น nice ในขณะที่อีก replica ต่อท้ายเครื่องหมายอัศเจรีย์เป็น ice! พร้อมกัน
Figure 6-11. การแทรกข้อความสองครั้งพร้อมกันถูกรวมเข้าด้วยกันโดย OT และ CRDT ตามลําดับ
ผลลัพธ์ที่รวมกัน nice! บรรลุได้แตกต่างกันโดยอัลกอริทึมสองประเภท:
OT
เราบันทึกดัชนีที่อักขระถูกแทรกหรือลบ: n ถูกแทรกที่ดัชนี 0 และ ! ที่ดัชนี 3 ต่อมา replicas แลกเปลี่ยนการดําเนินการของพวกมัน การแทรก n ที่ดัชนี 0 สามารถนําไปใช้ได้ทันที แต่ถ้าการแทรก ! ที่ดัชนี 3 ถูกนําไปใช้กับสถานะ nice, เราจะได้ nic!e, ซึ่งไม่ถูกต้อง ดังนั้นเราจึงต้องแปลงดัชนีของการดําเนินการแต่ละครั้งเพื่อคํานึงถึงการดําเนินการพร้อมกันที่ถูกนําไปใช้แล้ว ในกรณีนี้ การแทรก ! ถูกแปลงเป็นดัชนี 4 เพื่อคํานึงถึงการแทรก n ที่ดัชนีก่อนหน้า
CRDT
CRDTs ส่วนใหญ่ให้แต่ละอักขระมี ID ที่ไม่ซ้ํากันและ immutable และใช้ ID เหล่านั้นเพื่อกําหนดตําแหน่งของการแทรก/การลบ แทนการใช้ดัชนี ตัวอย่างเช่น ใน Figure 6-11 เรากําหนด ID 1A ให้ i, ID 2A ให้ c, ฯลฯ เมื่อแทรกเครื่องหมายอัศเจรีย์ เราสร้างการดําเนินการที่มี ID ของอักขระใหม่ (4B) และ ID ของอักขระที่มีอยู่ที่เราต้องการแทรกหลังจากนั้น (3A) เพื่อแทรกที่จุดเริ่มต้นของสตริง เราให้ nil เป็น ID ของอักขระก่อนหน้า การแทรกพร้อมกันที่ตําแหน่งเดียวกันถูกเรียงลําดับโดย IDs ของอักขระ สิ่งนี้ช่วยให้ replicas ลู่เข้าโดยไม่ต้องทําการแปลงใดๆ
อัลกอริทึมจํานวนมากอิงตามรูปแบบต่างๆ ของแนวคิดเหล่านี้ Lists และ arrays สามารถรองรับได้ในลักษณะเดียวกัน โดยใช้ list elements แทนอักขระ และ datatypes อื่นๆ เช่น key-value maps ก็สามารถเพิ่มได้ค่อนข้างง่าย OTs และ CRDTs มีข้อแลกเปลี่ยนด้านประสิทธิภาพและฟังก์ชันการทํางานบางอย่าง แต่สามารถรวมข้อดีของทั้งสองไว้ในอัลกอริทึมเดียวได้ [ 48 ]
OT มักใช้สําหรับการแก้ไขข้อความแบบเรียลไทม์ร่วมกัน เช่นใน Google Docs [ 32 ] ในขณะที่ CRDTs สามารถพบได้ในฐานข้อมูลแบบกระจายเช่น Redis Enterprise, Riak, และ Azure Cosmos DB [ 49 ] Sync engines สําหรับข้อมูล JSON สามารถ implement ได้ทั้งด้วย CRDTs (เช่น Automerge หรือ Yjs) และ OT (เช่น ShareDB)
Types of conflict (ประเภทของ Conflict)
conflicts บางประเภทชัดเจน ในตัวอย่างใน Figure 6-9 writes สองครั้งแก้ไขฟิลด์เดียวกันใน record เดียวกันพร้อมกัน ตั้งค่าเป็นสองค่าที่แตกต่างกัน ไม่ต้องสงสัยเลยว่านี่คือ conflict
conflicts ประเภทอื่นอาจละเอียดอ่อนกว่าในการตรวจจับ ตัวอย่างเช่น พิจารณาระบบจองห้องประชุม: ที่ติดตามว่าห้องใดถูกจองโดยกลุ่มคนใดในเวลาใด แทนที่จะอัปเดตฟิลด์เฉพาะเมื่อจองห้องประชุม ระบบนี้จะแทรก record ใหม่ลงในฐานข้อมูลสําหรับการจองแต่ละครั้ง แอปพลิเคชันต้องแน่ใจว่าแต่ละห้องถูกจองโดยคนเพียงกลุ่มเดียวในเวลาใดเวลาหนึ่ง (กล่าวคือต้องไม่มีการจองที่ทับซ้อนกันสําหรับห้องเดียวกัน) ในกรณีนี้ conflict อาจเกิดขึ้นถ้าการจองสองครั้งถูกสร้างสําหรับห้องเดียวกันในเวลาเดียวกัน แม้ว่าแอปพลิเคชันจะตรวจสอบความพร้อมใช้งานก่อนอนุญาตให้ผู้ใช้จอง conflict เกิดขึ้นได้ถ้าการจองสองครั้งเกิดขึ้นใกล้กันจนทั้งคู่เห็นห้องว่ายังไม่ถูกจองก่อนที่จะแทรก record ใหม่
ไม่มีคําตอบสําเร็จรูปที่รวดเร็ว แต่ในบทต่อไปนี้เราจะเดินตามเส้นทางสู่ความเข้าใจที่ดีเกี่ยวกับปัญหานี้ เราจะเห็นตัวอย่าง conflicts เพิ่มเติมใน บทที่ 8 และใน บทที่ 13 เราจะพูดถึงแนวทางที่ scalable สําหรับการตรวจจับและแก้ไข conflicts ในระบบจําลอง
Leaderless Replication (การจำลองแบบไร้ผู้นำ)
วิธีการจําลองที่เราได้พูดถึงในบทนี้—single-leader และ multi-leader replication—อิงตามแนวคิดที่ว่า client ส่งคําขอเขียนไปยัง node หนึ่ง (leader) และระบบฐานข้อมูลจัดการคัดลอก write นั้นไปยัง replicas อื่น leader กําหนดลําดับที่ writes ควรถูกประมวลผล และ followers นํา writes ของ leader ไปใช้ในลําดับเดียวกัน
ระบบจัดเก็บข้อมูลบางตัวใช้แนวทางที่แตกต่าง โดยละทิ้งแนวคิดของ leader และอนุญาตให้ replica ใดๆ ก็ได้รับ writes จาก clients โดยตรง ระบบข้อมูลจําลองแรกๆ บางระบบเป็นแบบ leaderless [ 1, 50 ] แต่แนวคิดนี้ส่วนใหญ่ถูกลืมในช่วงยุคที่ฐานข้อมูลเชิงสัมพันธ์ครอบงํา มันกลับมาเป็นสถาปัตยกรรมที่ทันสมัยสําหรับฐานข้อมูลอีกครั้งหลังจาก Amazon ใช้มันสําหรับระบบ Dynamo ภายในของตนในปี 2007 [ 45 ] Riak, Cassandra, และ ScyllaDB เป็น datastores แบบ open source ที่มีโมเดล leaderless replication ที่ได้รับแรงบันดาลใจจาก Dynamo ดังนั้นฐานข้อมูลประเภทนี้จึงเรียกว่า Dynamo-style
หมายเหตุ
สถาปัตยกรรมระบบ Dynamo ดั้งเดิมถูกอธิบายไว้ในบทความ [ 45 ] แต่ไม่เคยถูกปล่อยออกนอก Amazon DynamoDB ซึ่งมีชื่อคล้ายกัน เป็นฐานข้อมูลคลาวด์รุ่นใหม่กว่าจาก Amazon มีสถาปัตยกรรมที่แตกต่างกันโดยสิ้นเชิง: มันใช้ single-leader replication ที่อิงตาม consensus algorithm แบบ Multi-Paxos [ 5, 51 ]
ในการ implement แบบ leaderless บางแบบ client ส่ง writes ของตนไปยัง replicas หลายตัวโดยตรง ในขณะที่บางแบบ node coordinator ทําสิ่งนี้แทน client อย่างไรก็ตาม ต่างจากฐานข้อมูลแบบ leader coordinator นั้นไม่ได้บังคับลําดับ writes ที่แน่นอน ดังที่เราจะเห็น ความแตกต่างในการออกแบบนี้มีผลกระทบอย่างลึกซึ้งต่อวิธีใช้งานฐานข้อมูล
Writing to the Database When a Node Is Down (การเขียนข้อมูลเมื่อ Node ไม่ทำงาน)
ลองนึกภาพ คุณมีฐานข้อมูลที่มีสาม replicas และหนึ่งใน replicas ไม่พร้อมใช้งาน—บางทีมันกําลังรีบูตเพื่อติดตั้งอัปเดตระบบ ในการกําหนดค่าแบบ single-leader ถ้าคุณต้องการประมวลผล writes ต่อไป คุณอาจต้องทํา failover (ดูหัวข้อ "การจัดการเมื่อ Node หยุดทำงาน")
ในทางกลับกัน ในการกําหนดค่าแบบ leaderless ไม่มีสิ่งที่เรียกว่า failover เพราะ replicas ทั้งหมดเท่าเทียมกันและไม่มี leaders Figure 6-12 แสดงสิ่งที่เกิดขึ้น
Figure 6-12. การเขียนไปยัง replicas ส่วนใหญ่ การอ่านจากส่วนใหญ่ และการส่งค่าล่าสุดไปยัง replica ที่ไม่พร้อมใช้งานระหว่างการเขียน
client (ผู้ใช้ 1234) ส่ง write ไปยังทั้งสาม replicas แบบขนาน และ replicas สองตัวที่พร้อมใช้งานรับ write แต่ replica ที่ไม่พร้อมใช้งานพลาดมัน สมมติว่าการได้รับ acknowledgment จากสองในสาม replicas ก็เพียงพอสําหรับ write หลังจากผู้ใช้ 1234 ได้รับการตอบกลับ OK สองครั้ง เราถือว่า write สําเร็จ client เพียงแค่ไม่สนใจความจริงที่ว่า replica หนึ่งพลาด write
ทีนี้ลองนึกภาพว่า node ที่ไม่พร้อมใช้งานกลับมาออนไลน์ และ clients เริ่มอ่านจากมัน writes ที่เกิดขึ้นขณะที่ node หยุดทํางานจะหายไปจากมัน ดังนั้นถ้าคุณอ่านจาก node นั้น คุณอาจได้รับค่า stale (ล้าสมัย) เป็นการตอบกลับ
เพื่อแก้ปัญหานั้น เมื่อ client อ่านจากฐานข้อมูล มันไม่ได้แค่ส่งคําขอไปยัง replica เดียว: คําขออ่านก็ถูกส่งไปยังหลาย nodes แบบขนานเช่นกัน client อาจได้รับการตอบกลับที่แตกต่างจาก nodes ที่แตกต่างกัน ตัวอย่างเช่น ค่าที่ทันสมัยจาก node หนึ่งและค่าที่ล้าสมัยจากอีก node
เพื่อให้ client ระบุว่าการตอบกลับใดทันสมัยและใดล้าสมัย ทุกค่าที่ถูกเขียนต้องถูกติดแท็กด้วยหมายเลขเวอร์ชันหรือ timestamp คล้ายกับที่เราเห็นในหัวข้อ "Last Write Wins: การทิ้ง Concurrent Writes" เมื่อ client ได้รับหลายค่าในการตอบกลับการอ่าน มันจะใช้ค่าที่มี timestamp มากที่สุด (แม้ว่าค่านั้นจะถูกส่งคืนโดย replica เพียงตัวเดียว และ replicas อื่นๆ หลายตัวส่งคืนค่าที่เก่ากว่า) ดูหัวข้อ "การตรวจจับ Concurrent Writes" สําหรับรายละเอียดเพิ่มเติม
Catching up on missed writes (การตามทัน writes ที่พลาดไป)
ระบบจําลองควรรับประกันว่าในที่สุดข้อมูลทั้งหมดจะถูกคัดลอกไปยังทุก replica หลังจาก node ที่ไม่พร้อมใช้งานกลับมาออนไลน์ มันจะตามทัน writes ที่มันพลาดไปได้อย่างไร? มีกลไกหลายอย่างที่ใช้ใน datastores แบบ Dynamo:
Read Repair
เมื่อ client อ่านจากหลาย nodes แบบขนาน มันสามารถตรวจจับการตอบกลับที่ล้าสมัยได้ ตัวอย่างเช่น ใน Figure 6-12 ผู้ใช้ 2345 ได้รับค่าเวอร์ชัน 6 จาก replica 3 และค่าเวอร์ชัน 7 จาก replicas 1 และ 2 client เห็นว่า replica 3 มีค่าที่ล้าสมัยและเขียนค่าที่ใหม่กว่ากลับไปยัง replica นั้น วิธีการนี้ทํางานได้ดีสําหรับค่าที่ถูกอ่านบ่อย
Hinted Handoff
ถ้า replica หนึ่งไม่พร้อมใช้งาน replica อื่นอาจเก็บ writes แทนมันในรูปแบบของ hints เมื่อ replica ที่ควรได้รับ writes เหล่านั้นกลับมา replica ที่เก็บ hints จะส่ง hints ไปยัง replica ที่กู้คืนแล้วลบ hints กระบวนการ handoff นี้ช่วยให้ replicas ทันสมัย แม้กระทั่งสําหรับค่าที่ไม่เคยถูกอ่านและดังนั้นจึงไม่ถูกจัดการโดย read repair
Anti-Entropy
นอกจากนี้ กระบวนการพื้นหลังจะมองหาความแตกต่างในข้อมูลระหว่าง replicas เป็นระยะๆ แล้วคัดลอกข้อมูลที่ขาดหายจาก replica หนึ่งไปยังอีก replica หนึ่ง ต่างจาก replication log ใน leader-based replication กระบวนการ anti-entropy นี้ไม่ได้คัดลอก writes ในลําดับใดโดยเฉพาะ และอาจมีความล่าช้าอย่างมากก่อนที่ข้อมูลจะถูกคัดลอก
Using quorums for reading and writing (การใช้ Quorum สำหรับการอ่านและเขียน)
ใน Figure 6-12 เราถือว่า write สําเร็จแม้ว่ามันถูกประมวลผลบนเพียงสองในสาม replicas จะเกิดอะไรขึ้นถ้ามีเพียงหนึ่งในสาม replicas ที่รับ write? เราสามารถผลักดันได้ไกลแค่ไหน?
ถ้าเรารู้ว่า write ที่สําเร็จทุกครั้งรับประกันว่าจะมีอยู่อย่างน้อยสองในสาม replicas นั่นหมายถึงอย่างมาก replica เดียวเท่านั้นที่อาจล้าสมัย ดังนั้นถ้าเราอ่านจากอย่างน้อยสอง replicas เรามั่นใจได้ว่าอย่างน้อยหนึ่งในสองตัวนั้นทันสมัย ถ้า replica ที่สามหยุดทํางานหรือตอบสนองช้า การอ่านก็ยังคงสามารถส่งคืนค่าที่ทันสมัยได้
โดยทั่วไป ถ้ามี n replicas ทุก write ต้องได้รับการยืนยันโดย w nodes จึงจะถือว่าสําเร็จ และเราต้องสอบถามอย่างน้อย r nodes สําหรับแต่ละอ่าน (ในตัวอย่างของเรา n = 3, w = 2, r = 2) ตราบใดที่ w + r > n เราคาดว่าจะได้รับค่าที่ทันสมัยเมื่ออ่าน เพราะอย่างน้อยหนึ่งใน r nodes ที่เรากําลังอ่านต้องทันสมัย Reads และ writes ที่ปฏิบัติตามค่า r และ w เหล่านี้เรียกว่า quorum reads และ writes [ 50 ] คุณสามารถคิดถึง r และ w เป็นจํานวนโหวตขั้นต่ําที่ต้องใช้เพื่อให้การอ่านหรือเขียนนั้นใช้ได้
ในฐานข้อมูลแบบ Dynamo พารามิเตอร์ n, w, และ r สามารถกําหนดค่าได้โดยทั่วไป ตัวเลือกทั่วไปคือทําให้ n เป็นเลขคี่ (โดยปกติ 3 หรือ 5) และตั้งค่า w = r = (n + 1) / 2 (ปัดขึ้น) อย่างไรก็ตาม คุณสามารถปรับเปลี่ยนตัวเลขตามที่เห็นสมควร ตัวอย่างเช่น workload ที่มี writes น้อยและ reads มากอาจได้ประโยชน์จากการตั้งค่า w = n และ r = 1 สิ่งนี้ทําให้อ่านเร็วขึ้นแต่มีข้อเสียคือแค่ node เดียวที่ล้มเหลวก็ทําให้ writes ทั้งหมดล้มเหลว
หมายเหตุ
อาจมีมากกว่า n nodes ใน cluster แต่ค่าใดๆ จะถูกเก็บบนเพียง n nodes ซึ่งช่วยให้ชุดข้อมูลถูก sharded รองรับชุดข้อมูลที่ใหญ่กว่าที่คุณจะใส่ใน node เดียวได้ เราจะกลับมาที่ sharding ใน บทที่ 7
เงื่อนไข quorum w + r > n ช่วยให้ระบบทนต่อ nodes ที่ไม่พร้อมใช้งานดังนี้:
-
ถ้า w < n เรายังสามารถประมวลผล writes ได้ถ้า node ไม่พร้อมใช้งาน
-
ถ้า r < n เรายังสามารถประมวลผล reads ได้ถ้า node ไม่พร้อมใช้งาน
-
ด้วย n = 3, w = 2, r = 2 เราสามารถทนต่อ node ที่ไม่พร้อมใช้งานหนึ่งตัว ดังใน Figure 6-12
-
ด้วย n = 5, w = 3, r = 3 เราสามารถทนต่อ nodes ที่ไม่พร้อมใช้งานสองตัว กรณีนี้อธิบายใน Figure 6-13
Figure 6-13. ถ้า w + r > n อย่างน้อยหนึ่งใน r replicas ที่คุณอ่านต้องเห็น write ที่สําเร็จล่าสุด
โดยปกติ reads และ writes จะถูกส่งไปยัง n replicas ทั้งหมดแบบขนานเสมอ พารามิเตอร์ w และ r กําหนดจํานวน nodes ที่เรารอ—นั่นคือ จํานวน n nodes ที่ต้องรายงานความสําเร็จก่อนที่เราจะถือว่าการอ่านหรือเขียนสําเร็จ
ถ้ามี nodes น้อยกว่าที่ต้องการ w หรือ r writes หรือ reads จะส่งคืนข้อผิดพลาด node อาจไม่พร้อมใช้งานด้วยหลายสาเหตุ: node หยุดทํางาน (เช่น crash ไฟดับ) เกิดข้อผิดพลาดขณะประมวลผลการดําเนินการ (เช่น ไม่สามารถเขียนเพราะดิสก์เต็ม) เกิดการขัดจังหวะเครือข่ายระหว่าง client และ node หรือสาเหตุอื่นๆ อีกมากมาย เราสนใจเพียงว่า node ส่งคืนการตอบสนองที่สําเร็จหรือไม่ และไม่จําเป็นต้องแยกความแตกต่างระหว่างข้อผิดพลาดประเภทต่างๆ
Understanding the limitations of quorum consistency (ข้อจำกัดของ Quorum Consistency)
ถ้าคุณมี n replicas และคุณเลือก w และ r โดยที่ w + r > n โดยทั่วไปคุณสามารถคาดหวังได้ว่าทุกอ่านจะส่งคืนค่าล่าสุดที่เขียนสําหรับ key นั้น ซึ่งเป็นกรณีนี้เพราะเซ็ตของ nodes ที่คุณเขียนและเซ็ตของ nodes ที่คุณอ่านต้องทับซ้อนกัน กล่าวคือ ในบรรดา nodes ที่คุณอ่าน ต้องมีอย่างน้อยหนึ่ง node ที่มีค่าล่าสุด (ดังที่แสดงใน Figure 6-13)
บ่อยครั้ง r และ w ถูกเลือกให้เป็นส่วนใหญ่ (มากกว่า n / 2) ของ nodes เพราะนั่นรับประกัน w + r > n ในขณะที่ยังคงทนต่อความล้มเหลวของ node ได้ถึง n / 2 (ปัดลง) แต่ quorums ไม่จําเป็นต้องเป็นส่วนใหญ่—สิ่งสําคัญคือเซ็ตของ nodes ที่ใช้โดย operation อ่านและเขียนต้องทับซ้อนกันอย่างน้อยหนึ่ง node การกําหนด quorum อื่นก็เป็นไปได้ ซึ่งช่วยให้มีความยืดหยุ่นในการออกแบบอัลกอริทึมแบบกระจาย [ 52 ]
คุณอาจตั้งค่า w และ r เป็นตัวเลขที่เล็กลง เพื่อให้ w + r ≤ n (กล่าวคือ เงื่อนไข quorum ไม่เป็นที่พอใจ) ในกรณีนี้ reads และ writes จะยังคงถูกส่งไปยัง n nodes แต่ต้องมีการตอบสนองที่สําเร็จจํานวนน้อยกว่าสําหรับการดําเนินการที่จะสําเร็จ
ด้วย w และ r ที่เล็กลง คุณมีแนวโน้มที่จะอ่านค่าที่ล้าสมัยมากขึ้น เพราะมีโอกาสมากขึ้นที่การอ่านของคุณจะไม่รวม node ที่มีค่าล่าสุด ในด้านบวก การกําหนดค่านี้ช่วยให้ latency ต่ําลง ซึ่งเป็นประโยชน์โดยเฉพาะกับ synchronous (blocking) replication การตั้งค่านี้ยังมีความพร้อมใช้งานสูงขึ้น ถ้าเกิดการขัดจังหวะเครือข่ายและ replicas จํานวนมากไม่สามารถเข้าถึงได้ มีโอกาสสูงขึ้นที่คุณจะประมวลผล reads และ writes ต่อไปได้ ต่อเมื่อจํานวน replicas ที่เข้าถึงได้ต่ํากว่า w หรือ r ฐานข้อมูลจึงจะไม่พร้อมใช้งานสําหรับการเขียนหรืออ่านตามลําดับ
อย่างไรก็ตาม แม้จะมี w + r > n คุณสมบัติความสอดคล้องก็อาจสร้างความสับสนในบางกรณีขอบ สถานการณ์บางอย่างรวมถึง:
-
ถ้า node ที่มีค่าใหม่ล้มเหลว และข้อมูลของมันถูกกู้คืนจาก replica ที่มีค่าเก่า จํานวน replicas ที่เก็บค่าใหม่อาจลดลงต่ํากว่า w ทําให้เงื่อนไข quorum เสีย
-
ในขณะที่การ rebalancing ดําเนินอยู่ ซึ่งข้อมูลบางส่วนถูกย้ายจาก node หนึ่งไปยังอีก node (ดู บทที่ 7) nodes อาจมีมุมมองที่ไม่สอดคล้องกันว่า nodes ใดควรถือ n replicas สําหรับค่าใดค่าหนึ่ง ซึ่งอาจส่งผลให้ read และ write quorums ไม่ทับซ้อนกันอีกต่อไป
-
ถ้าการอ่านเกิดขึ้นพร้อมกับการเขียน การอ่านอาจเห็นหรือไม่เห็นค่าที่เขียนพร้อมกันก็ได้ โดยเฉพาะอย่างยิ่ง เป็นไปได้ที่การอ่านหนึ่งจะเห็นค่าใหม่และการอ่านถัดไปจะเห็นค่าเก่า ดังที่เราจะเห็นในหัวข้อ "การ Implement ระบบ Linearizable"
-
ถ้า write สําเร็จบนบาง replicas แต่ล้มเหลวบนบางตัว (เช่น เพราะดิสก์บนบาง nodes เต็ม) และโดยรวมมันสําเร็จน้อยกว่า w replicas มันจะไม่ถูกย้อนกลับบน replicas ที่สําเร็จ ซึ่งหมายความว่าถ้า write ถูกรายงานว่าล้มเหลว การอ่านภายหลังอาจหรืออาจไม่ส่งคืนค่าจาก write นั้น [ 53 ]
-
ถ้าฐานข้อมูลใช้ timestamps จาก real-time clock เพื่อกําหนดว่า write ใดใหม่กว่า (เช่น Cassandra และ ScyllaDB ทํา) writes อาจถูกทิ้งอย่างเงียบๆ ถ้า node อื่นที่มีนาฬิกาเร็วกว่าได้เขียนไปยัง key เดียวกัน—ปัญหาที่เราเห็นก่อนหน้านี้ในหัวข้อ "Last Write Wins: การทิ้ง Concurrent Writes" เราจะพูดถึงรายละเอียดเพิ่มเติมในหัวข้อ "การพึ่งพา Synchronized Clocks"
-
ถ้า writes สองครั้งเกิดขึ้นพร้อมกัน write หนึ่งอาจถูกประมวลผลก่อนบน replica หนึ่ง และอีก write อาจถูกประมวลผลก่อนบนอีก replica ซึ่งนําไปสู่ conflict คล้ายกับที่เราเห็นสําหรับ multi-leader replication (ดูหัวข้อ "การจัดการกับ Write Conflicts") เราจะกลับมาที่หัวข้อนี้ในหัวข้อ "การตรวจจับ Concurrent Writes"
ดังนั้น แม้ว่า quorums ดูเหมือนจะรับประกันว่าการอ่านจะส่งคืนค่าล่าสุดที่เขียน ในทางปฏิบัติมันไม่ง่ายอย่างนั้น ฐานข้อมูลแบบ Dynamo โดยทั่วไปถูกปรับให้เหมาะสมสําหรับกรณีการใช้งานที่สามารถทนต่อ eventual consistency ได้ พารามิเตอร์ w และ r ช่วยให้คุณปรับความน่าจะเป็นของการอ่านค่าที่ล้าสมัย [ 54 ] แต่ควรระมัดระวังไม่ถือว่ามันเป็นหลักประกันที่แน่นอน
Monitoring staleness (การติดตามความเก่าของข้อมูล)
จากมุมมองการดําเนินงาน สิ่งสําคัญคือต้องติดตามว่าฐานข้อมูลของคุณกําลังส่งคืนผลลัพธ์ที่ทันสมัยหรือไม่ แม้ว่าแอปพลิเคชันของคุณจะทนต่อการอ่านค่าที่ล้าสมัยได้ คุณต้องตระหนักถึงสุขภาพของการจําลองของคุณ ถ้ามันตามหลังอย่างมีนัยสําคัญ มันควรแจ้งเตือนคุณเพื่อให้คุณตรวจสอบสาเหตุ (เช่น ปัญหาในเครือข่ายหรือ node ที่โอเวอร์โหลด)
สําหรับ leader-based replication ฐานข้อมูลโดยทั่วไปจะเปิดเผย metrics สําหรับ replication lag ซึ่งคุณสามารถป้อนเข้าสู่ระบบการติดตามได้ สิ่งนี้เป็นไปได้เพราะ writes ถูกนําไปใช้กับ leader และ followers ในลําดับเดียวกัน และแต่ละ node มีตําแหน่งใน replication log (จํานวน writes ที่มันนําไปใช้ในเครื่อง) โดยการลบตําแหน่งปัจจุบันของ follower ออกจากตําแหน่งปัจจุบันของ leader คุณสามารถวัดปริมาณ replication lag ได้
อย่างไรก็ตาม ในระบบที่มี leaderless replication ไม่มีลําดับที่แน่นอนที่ writes ถูกนําไปใช้ ซึ่งทําให้การติดตามยากขึ้น จํานวน hints ที่ replica เก็บไว้สําหรับ handoff สามารถเป็นหนึ่งในการวัดสุขภาพของระบบ แต่มันยากที่จะตีความอย่างมีประโยชน์ [ 55 ] Eventual consistency เป็นหลักประกันที่จงทําให้คลุมเครือ แต่สําหรับการดําเนินงาน สิ่งสําคัญคือต้อง สามารถวัดปริมาณ "eventual" ได้
Single-Leader Versus Leaderless Replication Performance (ประสิทธิภาพของ Single-Leader เทียบกับ Leaderless)
ระบบจําลองที่อิงตาม leader เดียวสามารถให้หลักประกันความสอดคล้องที่แข็งแกร่งซึ่งยากหรือเป็นไปไม่ได้ที่จะบรรลุในระบบ leaderless อย่างไรก็ตาม ดังที่เราเห็นในหัวข้อ "ปัญหาจาก Replication Lag" การอ่านในระบบ leader-based ก็สามารถส่งคืนค่าที่ล้าสมัยได้ถ้าคุณอ่านจาก follower ที่อัปเดตแบบ asynchronous
การอ่านจาก leader รับประกันการตอบสนองที่ทันสมัย แต่มันประสบปัญหาเกี่ยวกับประสิทธิภาพ:
-
ปริมาณงานอ่านถูกจํากัดโดยความสามารถของ leader ในการจัดการคําขอ (ตรงกันข้ามกับ read scaling ซึ่งกระจาย reads ไปยัง replicas ที่อัปเดตแบบ asynchronous ซึ่งอาจส่งคืนค่าที่ล้าสมัย)
-
ถ้า leader ล้มเหลว คุณต้องรอให้ตรวจพบข้อผิดพลาดและ failover เสร็จสมบูรณ์ก่อนจึงจะจัดการคําขอต่อได้ แม้ว่ากระบวนการ failover จะรวดเร็วมาก ผู้ใช้จะสังเกตเห็นเพราะเวลาตอบสนองที่เพิ่มขึ้นชั่วคราว ถ้า failover ใช้เวลานาน ระบบจะไม่พร้อมใช้งานตลอดระยะเวลานั้น
-
ระบบมีความไวสูงต่อปัญหาเกี่ยวกับประสิทธิภาพบน leader ถ้า leader ตอบสนองช้า (เช่น เพราะโอเวอร์โหลดหรือการแข่งขันทรัพยากร) เวลาตอบสนองที่เพิ่มขึ้นจะส่งผลกระทบต่อผู้ใช้ทันทีเช่นกัน
ข้อดีใหญ่ของสถาปัตยกรรม leaderless คือมันมีความยืดหยุ่นมากกว่าต่อปัญหาเหล่านี้ เพราะไม่มี failover และคําขอไปยัง replicas หลายตัวแบบขนานอยู่แล้ว replica หนึ่งตัวที่ช้าหรือไม่พร้อมใช้งานมีผลกระทบน้อยมากต่อเวลาตอบสนอง client เพียงแค่ใช้การตอบสนองจาก replicas อื่นที่ตอบสนองเร็วกว่า การใช้การตอบสนองที่เร็วที่สุดเรียกว่า request hedging และมันสามารถลด tail latency ได้อย่างมีนัยสําคัญ [ 56 ]
โดยแก่นแล้ว ความยืดหยุ่นของระบบ leaderless มาจากความจริงที่ว่ามันไม่แยกความแตกต่างระหว่างกรณีปกติและกรณีล้มเหลว สิ่งนี้มีประโยชน์โดยเฉพาะเมื่อจัดการกับ gray failures ซึ่ง node ไม่ได้หยุดทํางานโดยสมบูรณ์แต่ทํางานในสถานะที่ลดประสิทธิภาพซึ่งช้าผิดปกติในการจัดการคําขอ [ 57 ] หรือเมื่อ node โอเวอร์โหลด (เช่น ถ้า node ออฟไลน์มาระยะหนึ่ง การกู้คืนผ่าน hinted handoff อาจทําให้เกิดโหลดเพิ่มเติมจํานวนมาก) ระบบ leader-based ต้องตัดสินใจว่าสถานการณ์เลวร้ายพอที่จะรับประกันการ failover หรือไม่ (ซึ่งอาจทําให้เกิดการหยุดชะงักเพิ่มเติม) ในขณะที่ในระบบ leaderless คําถามนั้นไม่เกิดขึ้นเลย
อย่างไรก็ตาม ระบบ leaderless ก็สามารถมีปัญหาเกี่ยวกับประสิทธิภาพได้เช่นกัน:
-
แม้ว่าระบบไม่จําเป็นต้องทํา failover replica หนึ่งต้องตรวจจับเมื่อ replica อื่นไม่พร้อมใช้งานเพื่อให้สามารถเก็บ hints เกี่ยวกับ writes ที่ replica ที่ไม่พร้อมใช้งานพลาดไป เมื่อ replica ที่ไม่พร้อมใช้งานกลับมา กระบวนการ handoff ต้องส่ง hints เหล่านั้นไปให้มัน สิ่งนี้เพิ่มโหลดบน replicas ในเวลาที่ระบบอยู่ภายใต้ความเครียดอยู่แล้ว [ 55 ]
-
ยิ่งคุณมี replicas มากเท่าไร quorums ก็ยิ่งใหญ่ขึ้นและต้องรอการตอบสนองมากขึ้นก่อนที่คําขอจะเสร็จสมบูรณ์ แม้ว่าคุณจะรอเฉพาะ r หรือ w replicas ที่เร็วที่สุดเท่านั้น และแม้ว่าคุณจะส่งคําขอแบบขนาน r หรือ w ที่ใหญ่ขึ้นก็เพิ่มโอกาสที่คุณจะเจอ replica ที่ช้า ทําให้เวลาตอบสนองโดยรวมเพิ่มขึ้น (ดูหัวข้อ "การใช้ Metrics เวลาตอบสนอง") ในทางปฏิบัติ quorums ไม่ค่อยเกินสี่ในเจ็ด nodes หรือห้าในเก้า nodes
-
การขัดจังหวะเครือข่ายขนาดใหญ่ที่ตัดการเชื่อมต่อ client จาก replicas จํานวนมากอาจทําให้ไม่สามารถสร้าง quorum ได้ ฐานข้อมูล leaderless บางตัวเสนอตัวเลือกการกําหนดค่าที่อนุญาตให้ replica ที่เข้าถึงได้ใดๆ รับ writes แม้ว่ามันจะไม่ใช่ replicas ปกติสําหรับ key นั้น (Riak และ Dynamo เรียกว่า sloppy quorum [ 45 ]; Cassandra และ ScyllaDB เรียกว่า consistency level ANY) ไม่มีการรับประกันว่าการอ่านภายหลังจะเห็นค่าที่เขียน แต่ขึ้นอยู่กับแอปพลิเคชัน มันอาจยังดีกว่าให้ write ล้มเหลว
Multi-leader replication สามารถให้ความยืดหยุ่นต่อการขัดจังหวะเครือข่ายได้มากกว่า leaderless replication เสียอีก เนื่องจาก reads และ writes ต้องการการสื่อสารกับ leader เพียงตัวเดียว ซึ่งสามารถอยู่ร่วมตําแหน่งกับ client อย่างไรก็ตาม เนื่องจาก write บน leader หนึ่งถูกแพร่กระจายแบบ asynchronous ไปยังตัวอื่น reads อาจล้าสมัยโดยพลการ Quorum reads และ writes ให้การประนีประนอม: ความทนทานต่อข้อผิดพลาดที่ดีและความน่าจะเป็นสูงในการอ่านข้อมูลที่ทันสมัย
Multi-Region Operation (การทำงานแบบหลายภูมิภาค)
เราได้พูดถึงการจําลองข้ามภูมิภาคก่อนหน้านี้เป็นกรณีการใช้งานสําหรับ multi-leader replication (ดูหัวข้อ "การจําลองแบบ Multi-Leader") Leaderless replication ก็เหมาะสมสําหรับการทํางานแบบหลายภูมิภาคเช่นกัน เนื่องจากมันถูกออกแบบให้ทนต่อ conflicting concurrent writes, การขัดจังหวะเครือข่าย, และ latency spikes
ใน Cassandra และ ScyllaDB client ที่ต้องการทํา multi-region write จะเลือก node ในภูมิภาคท้องถิ่นก่อน เรียกว่า coordinator node และส่ง write ไปยัง node นั้น coordinator node ส่งต่อ write ไปยัง replicas ทั้งหมดในภูมิภาคของตัวเองและไปยัง replica หนึ่งในทุกภูมิภาคอื่น ซึ่งจะส่งต่อไปยัง replicas อื่นๆ ในภูมิภาคนั้น การเพิ่มประสิทธิภาพนี้หลีกเลี่ยงการทําคําขอข้ามภูมิภาคหลายครั้ง
คุณสามารถเลือกระดับความสอดคล้องที่หลากหลายซึ่งกําหนดว่าต้องการการตอบสนองกี่ครั้งสําหรับคําขอที่จะสําเร็จ ตัวอย่างเช่น คุณสามารถขอ quorum ข้าม replicas ในทุกภูมิภาค, quorum แยกในแต่ละภูมิภาค, หรือ quorum เฉพาะในภูมิภาคท้องถิ่นของ client quorum ท้องถิ่นหลีกเลี่ยงการต้องรอคําขอที่ช้าไปยังภูมิภาคอื่น แต่มันก็มีแนวโน้มที่จะส่งคืนผลลัพธ์ที่ล้าสมัยมากกว่า
Riak เก็บการสื่อสารทั้งหมดระหว่าง clients และ nodes ฐานข้อมูลในภูมิภาคเดียว ดังนั้น n อธิบายจํานวน replicas ภายในหนึ่งภูมิภาค การจําลองข้ามภูมิภาคระหว่าง clusters ฐานข้อมูลเกิดขึ้นแบบ asynchronous ในพื้นหลัง ในรูปแบบที่คล้ายกับ multi-leader replication
Detecting Concurrent Writes (การตรวจจับ Concurrent Writes)
เช่นเดียวกับ multi-leader replication ฐานข้อมูล leaderless อนุญาตให้ writes พร้อมกันไปยัง key เดียวกัน ส่งผลให้เกิด conflicts ที่ต้องแก้ไข conflicts ดังกล่าวอาจถูกตรวจจับเมื่อ writes เกิดขึ้น แต่ไม่เสมอไป: มันอาจถูกตรวจจับทีหลัง ในระหว่าง read repair, hinted handoff, หรือ anti-entropy
ปัญหาคือเหตุการณ์อาจมาถึงในลําดับที่แตกต่างกันที่ nodes ต่างๆ เนื่องจากความล่าช้าของเครือข่ายที่แปรผันและความล้มเหลวบางส่วน ตัวอย่างเช่น Figure 6-14 แสดง clients สองตัว A และ B กําลังเขียนไปยัง key X ใน datastore สาม node พร้อมกัน:
-
Node 1 ได้รับ write จาก A แต่ไม่เคยได้รับ write จาก B เนื่องจากการหยุดทํางานชั่วคราว
-
Node 2 ได้รับ write จาก A ก่อน แล้วจึงได้รับ write จาก B
-
Node 3 ได้รับ write จาก B ก่อน แล้วจึงได้รับ write จาก A
ถ้าแต่ละ node เพียงแค่เขียนทับค่าสําหรับ key เมื่อใดก็ตามที่ได้รับคําขอเขียนจาก client nodes จะกลายเป็นไม่สอดคล้องกันอย่างถาวร ดังที่แสดงโดยคําขอ get สุดท้ายใน Figure 6-14: node 2 คิดว่าค่าสุดท้ายของ X คือ B ในขณะที่ nodes อื่นคิดว่าค่าคือ A
เพื่อให้เป็น eventually consistent replicas ควรลู่เข้าหาค่าเดียวกัน สําหรับสิ่งนี้ เราสามารถใช้กลไกการแก้ไข conflict ที่เราได้พูดถึงก่อนหน้านี้ในหัวข้อ "การจัดการกับ Write Conflicts" เช่น LWW (ใช้โดย Cassandra และ ScyllaDB), การแก้ไขด้วยตนเอง, หรือ CRDTs (ใช้โดย Riak)
Figure 6-14. Concurrent writes ใน Datastore แบบ Dynamo: ไม่มีการเรียงลําดับที่ชัดเจน
LWW ง่ายต่อการ implement แต่ละ write ถูกติดแท็กด้วย timestamp และค่าที่มี timestamp สูงกว่าจะเขียนทับค่าที่มี timestamp ต่ํากว่าเสมอ อย่างไรก็ตาม timestamp ไม่ได้บอกคุณว่าสองค่าขัดแย้งกันจริงๆ (กล่าวคือ เขียนพร้อมกัน) หรือไม่ (เขียนทีละอัน) ถ้าคุณต้องการแก้ไข conflicts อย่างชัดเจน ระบบต้องใช้ความระมัดระวังมากขึ้นในการตรวจจับ concurrent writes
The happens-before relation and concurrency (ความสัมพันธ์แบบ Happens-Before และ Concurrency)
เราจะตัดสินใจได้อย่างไรว่าสองการดําเนินการเป็น concurrent? เพื่อพัฒนาความเข้าใจโดยสัญชาตญาณ มาดูตัวอย่างกัน:
-
ใน Figure 6-8 writes สองครั้งไม่ใช่ concurrent: การ insert ของ A เกิดขึ้นก่อน การ increment ของ B เพราะค่าที่ B increment คือค่าที่ A แทรก กล่าวอีกนัยหนึ่ง การดําเนินการของ B สร้างขึ้นจากการดําเนินการของ A ดังนั้นการดําเนินการของ B ต้องเกิดขึ้นทีหลัง เรายังกล่าวว่า B ขึ้นอยู่เชิงสาเหตุ กับ A
-
ในทางกลับกัน writes สองครั้งใน Figure 6-14 เป็น concurrent: เมื่อ client แต่ละตัวเริ่มการดําเนินการ มันไม่รู้ว่า client อื่นก็กําลังดําเนินการบน key เดียวกัน ดังนั้นจึงไม่มีความสัมพันธ์เชิงสาเหตุระหว่างการดําเนินการ
การดําเนินการ A เกิดขึ้นก่อน การดําเนินการ B ถ้า B รู้เกี่ยวกับ A หรือขึ้นอยู่กับ A หรือสร้างขึ้นจาก A ในทางใดทางหนึ่ง ไม่ว่าการดําเนินการหนึ่งจะเกิดขึ้นก่อนอีกการดําเนินการหรือไม่เป็นกุญแจสําคัญในการนิยามว่า concurrency หมายถึงอะไร ที่จริงแล้ว เราสามารถพูดได้ว่าสองการดําเนินการเป็น concurrent ถ้าไม่มีการดําเนินการใดเกิดขึ้นก่อนอีกการดําเนินการ [ 58 ]
ดังนั้น เมื่อใดก็ตามที่คุณมีสองการดําเนินการ A และ B มีสามความเป็นไปได้: A เกิดขึ้นก่อน B หรือ B เกิดขึ้นก่อน A หรือ A และ B เป็น concurrent สิ่งที่เราต้องการคืออัลกอริทึมที่บอกเราว่าสองการดําเนินการเป็น concurrent หรือไม่ ถ้าการดําเนินการหนึ่งเกิดขึ้นก่อนอีกการดําเนินการ การดําเนินการที่มาทีหลังควรเขียนทับการดําเนินการก่อนหน้า แต่ถ้าการดําเนินการเป็น concurrent เรามี conflict ที่ต้องแก้ไข
Concurrency, Time, and Relativity (Concurrency, เวลา, และทฤษฎีสัมพัทธภาพ)
อาจดูเหมือนว่าสองการดําเนินการควรเรียกว่า concurrent ถ้ามันเกิดขึ้น "ในเวลาเดียวกัน"—แต่ที่จริงแล้ว มันไม่สําคัญว่ามันทับซ้อนกันตามเวลาจริงหรือไม่ เพราะปัญหาของนาฬิกาในระบบกระจาย มันค่อนข้างยากที่จะบอกว่าสองสิ่งเกิดขึ้นในเวลาเดียวกันหรือไม่—เป็นปัญหาที่เราจะพูดถึงในรายละเอียดเพิ่มเติมใน บทที่ 9
สําหรับการนิยาม concurrency เวลาที่แน่นอนไม่สําคัญ เราเรียกสองการดําเนินการว่า concurrent ถ้าทั้งสองไม่รู้จักกัน โดยไม่สนใจเวลาทางกายภาพที่มันเกิดขึ้น ผู้คนบางครั้งเชื่อมโยงหลักการนี้กับทฤษฎีสัมพัทธภาพพิเศษในฟิสิกส์ [ 58 ] ซึ่งนําเสนอแนวคิดที่ว่าข้อมูลไม่สามารถเดินทางเร็วกว่าความเร็วแสง ดังนั้นเหตุการณ์สองเหตุการณ์ที่เกิดขึ้นห่างกันไม่สามารถมีผลกระทบต่อกันได้ถ้าเวลาระหว่างเหตุการณ์สั้นกว่าเวลาที่แสงใช้เดินทางระหว่างระยะทางนั้น
ในระบบคอมพิวเตอร์ สองการดําเนินการอาจเป็น concurrent แม้ว่าความเร็วแสงโดยหลักการแล้วจะอนุญาตให้การดําเนินการหนึ่งมีผลกระทบต่ออีกการดําเนินการ ตัวอย่างเช่น ถ้าเครือข่ายช้าหรือถูกขัดจังหวะในขณะนั้น สองการดําเนินการสามารถเกิดขึ้นห่างกันและยังคงเป็น concurrent เพราะปัญหาเครือข่ายป้องกันไม่ให้การดําเนินการหนึ่งรู้เกี่ยวกับอีกการดําเนินการ
Capturing the happens-before relationship (การบันทึกความสัมพันธ์แบบ Happens-Before)
มาดูอัลกอริทึมที่กําหนดว่าสองการดําเนินการเป็น concurrent หรือการดําเนินการหนึ่งเกิดขึ้นก่อนอีกการดําเนินการ เพื่อให้ง่าย เราจะเริ่มด้วยฐานข้อมูลที่มี replica เดียว เมื่อเราหาวิธีทําสิ่งนี้บน replica เดียวได้แล้ว เราสามารถขยายแนวทางไปยังฐานข้อมูล leaderless ที่มีหลาย replicas อัลกอริทึมทํางานดังนี้:
-
เซิร์ฟเวอร์รักษาหมายเลขเวอร์ชันสําหรับทุก key เพิ่มหมายเลขเวอร์ชันทุกครั้งที่ key นั้นถูกเขียน และเก็บหมายเลขเวอร์ชันใหม่พร้อมกับค่าที่เขียน
-
เมื่อ client อ่าน key เซิร์ฟเวอร์ส่งคืน siblings ทั้งหมด—ค่าทั้งหมดที่ยังไม่ถูกเขียนทับ—พร้อมกับหมายเลขเวอร์ชันล่าสุด client ต้องอ่าน key ก่อนเขียน
-
เมื่อ client เขียน key มันต้องรวมหมายเลขเวอร์ชันจากการอ่านก่อนหน้า และมันต้องรวมค่าทั้งหมดที่ได้รับในการอ่านก่อนหน้า (เช่น การใช้ CRDT ร่วมกับข้อมูลจากผู้ใช้) การตอบสนองจากคําขอเขียนยังส่งคืน siblings ทั้งหมด ซึ่งช่วยให้เราสามารถ chain writes หลายครั้ง (ดังในตัวอย่างตะกร้าสินค้าที่กล่าวถึงในหัวข้อ "การจัดการกับ Write Conflicts")
-
เมื่อเซิร์ฟเวอร์ได้รับ write ที่มีหมายเลขเวอร์ชันเฉพาะ มันสามารถเขียนทับค่าทั้งหมดที่มีหมายเลขเวอร์ชันนั้นหรือต่ํากว่า (เพราะมันรู้ว่าพวกมันถูกรวมเข้าในค่าใหม่แล้ว) แต่มันต้องเก็บค่าทั้งหมดที่มีหมายเลขเวอร์ชันสูงกว่า (เพราะค่าหม่านั้น concurrent กับ write ที่เข้ามา)
สังเกตว่าเซิร์ฟเวอร์สามารถระบุได้ว่าสองการดําเนินการเป็น concurrent หรือไม่โดยดูที่หมายเลขเวอร์ชัน เซิร์ฟเวอร์ไม่จําเป็นต้องตีความค่าด้วยตนเอง ดังนั้นค่าสามารถเป็น data structure ใดก็ได้
เมื่อ write รวมหมายเลขเวอร์ชันจากการอ่านก่อนหน้า นั่นบอกเราว่า write อิงตามสถานะก่อนหน้าใด ถ้าคุณทํา write โดยไม่รวมหมายเลขเวอร์ชัน มันจะ concurrent กับ writes อื่นๆ ทั้งหมด ดังนั้นมันจะไม่เขียนทับอะไรเลย—มันจะถูกส่งคืนเป็นหนึ่งในค่าในการอ่านครั้งต่อๆ ไป Figure 6-15 แสดงอัลกอริทึมนี้ในการทํางาน
Figure 6-15. การบันทึกความสัมพันธ์เชิงสาเหตุระหว่างไคลเอนต์สองตัวที่แก้ไขตะกร้าสินค้าพร้อมกัน
ในตัวอย่างนี้ clients สองตัวกําลังเพิ่มสินค้าไปยังตะกร้าสินค้าเดียวกันพร้อมกัน (ถ้าตัวอย่างนั้นดูไร้สาระเกินไป ลองนึกภาพเจ้าหน้าที่ควบคุมจราจรทางอากาศสองคนกําลังเพิ่มเครื่องบินไปยังภาคที่พวกเขาติดตามพร้อมกัน) เริ่มต้น ตะกร้าว่างเปล่า clients ทํา writes ห้าครั้งไปยังฐานข้อมูลระหว่างกัน:
-
Client 1 เพิ่ม
milkลงในตะกร้า นี่คือ write แรกไปยัง key นั้น ดังนั้นเซิร์ฟเวอร์เก็บมันสําเร็จและกําหนดเวอร์ชัน 1 เซิร์ฟเวอร์ยังสะท้อนค่ากลับไปยัง client พร้อมกับหมายเลขเวอร์ชัน -
Client 2 เพิ่ม
eggsลงในตะกร้า โดยไม่รู้ว่า client 1 เพิ่มmilkพร้อมกัน (client 2 คิดว่าeggsของมันเป็นสินค้าเดียวในตะกร้า) เซิร์ฟเวอร์กําหนดเวอร์ชัน 2 ให้กับ write นี้และเก็บeggsและmilkเป็นสองค่าแยกกัน (siblings) จากนั้นมันส่งคืนค่า ทั้งสอง ไปยัง client พร้อมกับหมายเลขเวอร์ชัน 2 -
Client 1 ซึ่งไม่รู้ write ของ client 2 ต้องการเพิ่ม
flourลงในตะกร้า หลังจากนั้นมันสมมติว่าเนื้อหาในตะกร้าจะเป็น[milk, flour]มันส่งค่านี้ไปยังเซิร์ฟเวอร์ พร้อมกับหมายเลขเวอร์ชันที่เซิร์ฟเวอร์ให้ไว้ก่อนหน้านี้ (1) เซิร์ฟเวอร์สามารถบอกได้จากหมายเลขเวอร์ชันว่า write ของ[milk, flour]แทนที่ค่าก่อนหน้าของ[milk]แต่มัน concurrent กับ[eggs]ดังนั้นเซิร์ฟเวอร์กําหนดเวอร์ชัน 3 ให้[milk, flour]เขียนทับค่าเวอร์ชัน 1[milk]แต่เก็บค่าเวอร์ชัน 2[eggs]และส่งคืนทั้งสองค่าที่เหลือไปยัง client -
ในขณะเดียวกัน client 2 ต้องการเพิ่ม
hamลงในตะกร้า โดยไม่รู้ว่า client 1 เพิ่งเพิ่มflourclient 2 ได้รับสองค่า[milk]และ[eggs]จากเซิร์ฟเวอร์ในการตอบกลับล่าสุด ดังนั้น client จึงรวมค่าหม่านั้นและเพิ่มhamเพื่อสร้างค่าใหม่[eggs, milk, ham]มันส่งค่านั้นไปยังเซิร์ฟเวอร์ พร้อมกับหมายเลขเวอร์ชันก่อนหน้า (2) เซิร์ฟเวอร์ตรวจจับว่าเวอร์ชัน 2 เขียนทับ[eggs]แต่ concurrent กับ[milk, flour]ดังนั้นสองค่าที่เหลือคือ[milk, flour]ด้วยเวอร์ชัน 3 และ[eggs, milk, ham]ด้วยเวอร์ชัน 4 -
สุดท้าย client 1 ต้องการเพิ่ม
baconมันได้รับ[milk, flour]และ[eggs]จากเซิร์ฟเวอร์ที่เวอร์ชัน 3 ก่อนหน้านี้ ดังนั้นมันรวมพวกมัน เพิ่มbaconและส่งค่าสุดท้าย[milk, flour, eggs, bacon]ไปยังเซิร์ฟเวอร์ พร้อมกับหมายเลขเวอร์ชัน 3 สิ่งนี้เขียนทับ[milk, flour](สังเกตว่า[eggs]ถูกเขียนทับไปแล้วในขั้นตอนที่แล้ว) แต่ concurrent กับ[eggs, milk, ham]ดังนั้นเซิร์ฟเวอร์เก็บค่าที่ concurrent ทั้งสองนั้น
การไหลของข้อมูลระหว่างการดําเนินการใน Figure 6-15 แสดงเป็นกราฟิกใน Figure 6-16 ลูกศรบ่งชี้ว่าการดําเนินการใด เกิดขึ้นก่อน การดําเนินการใด ในแง่ที่ว่าการดําเนินการที่มาทีหลัง รู้เกี่ยวกับ หรือ ขึ้นอยู่กับ การดําเนินการก่อนหน้า ในตัวอย่างนี้ clients ไม่เคยทันสมัยอย่างสมบูรณ์กับข้อมูลบนเซิร์ฟเวอร์ เนื่องจากมีการดําเนินการอื่นเกิดขึ้นพร้อมกันเสมอ แต่ค่าเวอร์ชันเก่าก็ถูกเขียนทับในที่สุด และไม่มี writes ใดสูญหาย
Figure 6-16. กราฟของความสัมพันธ์เชิงสาเหตุใน Figure 6-15
Version vectors (เวกเตอร์เวอร์ชัน)
ตัวอย่างใน Figure 6-15 ใช้ replica เดียวเท่านั้น อัลกอริทึมเปลี่ยนแปลงอย่างไรเมื่อมีหลาย replicas แต่ไม่มี leader?
Figure 6-15 ใช้หมายเลขเวอร์ชันเดียวเพื่อบันทึกความสัมพันธ์ระหว่างการดําเนินการ แต่ไม่เพียงพอเมื่อมีหลาย replicas ที่รับ writes พร้อมกัน เราจําเป็นต้องใช้หมายเลขเวอร์ชัน ต่อ replica และต่อ key แต่ละ replica เพิ่มหมายเลขเวอร์ชันของตัวเองเมื่อประมวลผล write และยังติดตามหมายเลขเวอร์ชันที่มันเห็นจาก replicas อื่นๆ ด้วย ข้อมูลนี้บ่งชี้ว่าค่าใดควรเขียนทับและค่าใดควรเก็บเป็น siblings
ชุดของหมายเลขเวอร์ชันจาก replicas ทั้งหมดเรียกว่า version vector [ 59 ] มีรูปแบบต่างๆ ของแนวคิดนี้ในการใช้งาน แต่ที่น่าสนใจที่สุดน่าจะเป็น dotted version vector [ 60, 61 ] ซึ่งใช้ใน Riak 2.0 [ 62, 63 ] เราจะไม่ลงรายละเอียด แต่วิธีการทํางานค่อนข้างคล้ายกับที่เราเห็นในตัวอย่างตะกร้าสินค้า
เช่นเดียวกับหมายเลขเวอร์ชันใน Figure 6-15 version vectors ถูกส่งจาก replicas ฐานข้อมูลไปยัง clients เมื่อค่าถูกอ่าน และพวกมันต้องถูกส่งกลับไปยังฐานข้อมูลเมื่อค่าถูกเขียนในภายหลัง (Riak เข้ารหัส version vector เป็นสตริงที่มันเรียกว่า causal context) version vector ช่วยให้ฐานข้อมูลแยกความแตกต่างระหว่างการเขียนทับและ concurrent writes
version vector ยังรับประกันว่าปลอดภัยที่จะอ่านจาก replica หนึ่งแล้วเขียนกลับไปยังอีก replica หนึ่ง การทําเช่นนี้อาจส่งผลให้เกิดการสร้าง siblings แต่ไม่มีข้อมูลสูญหายตราบใดที่ siblings ถูกรวมอย่างถูกต้อง
Version vectors and vector clocks (Version Vectors และ Vector Clocks)
version vector บางครั้งก็เรียกว่า vector clock แม้ว่าพวกมันจะไม่เหมือนกันทุกประการ ความแตกต่างนั้นละเอียดอ่อน [ 61, 64, 65 ] ดูเอกสารอ้างอิงสําหรับรายละเอียด โดยสรุป เมื่อเปรียบเทียบสถานะของ replicas version vectors เป็น data structure ที่ถูกต้องในการใช้
Summary (สรุป)
ในบทนี้เราได้ดูประเด็นของการจําลองข้อมูล การจําลองข้อมูลสามารถให้บริการวัตถุประสงค์หลายประการ:
ความพร้อมใช้งานสูง (High availability)
การทําให้ระบบทํางานต่อไปได้ แม้เมื่อเครื่องหนึ่งเครื่อง (หรือหลายเครื่อง zone หรือแม้แต่ทั้ง region) หยุดทํางาน
ความคงทนของข้อมูล (Durability)
การรับประกันว่าคุณไม่สูญเสียข้อมูล แม้ว่าเครื่องทั้งเครื่อง (หรือแม้แต่ทั้ง region) จะล้มเหลวอย่างถาวร
การทํางานแบบไม่เชื่อมต่อ (Disconnected operation)
การให้แอปพลิเคชันทํางานต่อไปได้แม้มีการขัดจังหวะเครือข่าย
ความหน่วง (Latency)
การวางข้อมูลให้ใกล้ผู้ใช้ตามภูมิศาสตร์เพื่อให้ผู้ใช้โต้ตอบกับมันได้เร็วขึ้น
ความสามารถในการขยายขนาด (Scalability)
ความสามารถในการจัดการปริมาณการอ่านที่สูงกว่าเครื่องเดียวจะรับไหว โดยการอ่านบน replicas
แม้ว่าแนวคิดจะเรียบง่าย—การเก็บสําเนาของข้อมูลเดียวกันบนหลายเครื่อง—การจําลองข้อมูลกลับกลายเป็นปัญหาที่ยุ่งยากอย่างน่าทึ่ง มันต้องการการคิดอย่างรอบคอบเกี่ยวกับ concurrency ทุกสิ่งที่อาจผิดพลาด และวิธีจัดการกับผลที่ตามมาของข้อผิดพลาดเหล่านั้น อย่างน้อยที่สุด เราต้องจัดการกับ nodes ที่ไม่พร้อมใช้งานและการขัดจังหวะเครือข่าย (และนั่นยังไม่รวมถึงข้อผิดพลาดที่ร้ายกาจกว่า เช่น การเสียหายของข้อมูลแบบเงียบเนื่องจากข้อบกพร่องของซอฟต์แวร์หรือข้อผิดพลาดของฮาร์ดแวร์)
เราพูดถึงสามแนวทางหลักในการจําลองข้อมูล:
Single-leader replication (การจําลองแบบผู้นําเดียว)
Clients ส่ง writes ทั้งหมดไปยัง node เดียว (leader) ซึ่งส่ง stream ของเหตุการณ์การเปลี่ยนแปลงข้อมูลไปยัง replicas อื่น (followers) การอ่านสามารถทําได้บน replica ใดก็ได้ แต่การอ่านจาก followers อาจล้าสมัย
Multi-leader replication (การจําลองแบบหลายผู้นํา)
Clients ส่งแต่ละ write ไปยังหนึ่งในหลาย leader nodes ซึ่งใดๆ ก็สามารถรับ writes ได้ Leaders ส่ง streams ของเหตุการณ์การเปลี่ยนแปลงข้อมูลให้กันและกันและไปยัง follower nodes
Leaderless replication (การจําลองแบบไร้ผู้นํา)
Clients ส่งแต่ละ write ไปยังหลาย nodes และอ่านจากหลาย nodes แบบขนานเพื่อตรวจจับและแก้ไข nodes ที่มีข้อมูลล้าสมัย
แต่ละแนวทางมีข้อดีและข้อเสีย Single-leader replication เป็นที่นิยมเพราะค่อนข้างเข้าใจง่ายและให้ความสอดคล้องที่แข็งแกร่ง Multi-leader และ leaderless replication สามารถแข็งแกร่งกว่าเมื่อมี nodes ที่ผิดพลาด การขัดจังหวะเครือข่าย และ latency spikes โดยมีต้นทุนคือต้องการการแก้ไข conflict และให้หลักประกันความสอดคล้องที่อ่อนแอกว่า
การจําลองข้อมูลเป็นได้ synchronous หรือ asynchronous ซึ่งมีผลกระทบอย่างลึกซึ้งต่อพฤติกรรมของระบบเมื่อเกิดข้อผิดพลาด แม้ว่า asynchronous replication จะรวดเร็วเมื่อระบบทํางานราบรื่น สิ่งสําคัญคือต้องหาว่าจะเกิดอะไรขึ้นเมื่อ replication lag เพิ่มขึ้นและเซิร์ฟเวอร์ล้มเหลว ถ้า leader ล้มเหลวและคุณเลื่อนขั้น follower ที่อัปเดตแบบ asynchronous เป็น leader ใหม่ ข้อมูลที่เพิ่ง commit อาจสูญหาย
เราดูผลกระทบที่แปลกประหลาดบางอย่างที่อาจเกิดจาก replication lag และเราพูดถึงโมเดลความสอดคล้องสองสามแบบที่เป็นประโยชน์ในการตัดสินใจว่าแอปพลิเคชันควรทํางานอย่างไรภายใต้ replication lag:
Read-after-write consistency (ความสอดคล้องแบบอ่านหลังจากเขียน)
ผู้ใช้ควรเห็นข้อมูลที่พวกเขาส่งด้วยตนเองเสมอ
Monotonic reads (การอ่านแบบ Monotonic)
หลังจากที่ผู้ใช้เห็นข้อมูล ณ จุดเวลาหนึ่งแล้ว พวกเขาไม่ควรเห็นข้อมูลจากจุดเวลาที่ก่อนหน้านั้นในภายหลัง
Consistent prefix reads (การอ่านแบบ Consistent Prefix)
ผู้ใช้ควรเห็นข้อมูลในสถานะที่สมเหตุสมผลเชิงสาเหตุ—ตัวอย่างเช่น การเห็นคําถามและคําตอบในลําดับที่ถูกต้อง
สุดท้าย เราได้พูดถึงวิธีที่ multi-leader และ leaderless replication รับประกันว่า replicas ทั้งหมดจะลู่เข้าสู่สถานะที่สอดคล้องกันในที่สุด: โดยใช้ version vector หรืออัลกอริทึมที่คล้ายกันเพื่อตรวจจับว่า writes ใดเป็น concurrent และโดยใช้อัลกอริทึมการแก้ไข conflict เช่น CRDT เพื่อรวมค่าที่เขียนพร้อมกัน LWW และการแก้ไข conflict ด้วยตนเองก็เป็นไปได้เช่นกัน
บทนี้ได้สมมติว่าทุก replica เก็บสําเนาเต็มของฐานข้อมูลทั้งหมด ซึ่งไม่สมจริงสําหรับชุดข้อมูลขนาดใหญ่ ในบทต่อไปเราจะดู sharding ซึ่งช่วยให้แต่ละเครื่องเก็บเฉพาะส่วนย่อยของข้อมูล