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

(ที่มักถูกอ้างถึงว่า: หากเป้าหมายสูงสุดของกัปตันคือการรักษาเรือไว้ เขาก็จอดเรือไว้ที่ท่าเรือ ตลอดไป)

St. Thomas Aquinas, Summa Theologica (1265–1274)

ใน บทที่ 2 เราได้พูดถึงเป้าหมายของการสร้างแอปพลิเคชันและระบบที่มี ความน่าเชื่อถือ , ความสามารถในการขยายขนาด , และ ความสามารถในการบำรุงรักษา . ธีมเหล่านี้ได้สอดแทรกอยู่ในทุกบท—ตัวอย่างเช่น เราได้พูดถึงอัลกอริธึมการทนต่อความผิดพลาด (fault-tolerance) มากมายที่ ช่วยปรับปรุงความน่าเชื่อถือ, การแยกส่วน (sharding) เพื่อเพิ่มความสามารถในการขยายขนาด, และกลไกสำหรับการวิวัฒนาการและนามธรรมที่ ช่วยปรับปรุงความสามารถในการบำรุงรักษา.

ในบทนี้เราจะรวบรวมแนวคิดทั้งหมดเหล่านี้เข้าด้วยกันและต่อยอดจากแนวคิดเกี่ยวกับสถาปัตยกรรมสตรีมมิ่ง/event-driven จาก บทที่ 12 โดยเฉพาะเพื่อพัฒนาปรัชญาของการพัฒนาแอปพลิเคชันที่ตอบสนองเป้าหมายเหล่านั้น บทนี้มีความเป็น ความคิดเห็นส่วนตัวมากกว่าบทก่อนหน้า โดยนำเสนอการเจาะลึกในปรัชญาเฉพาะด้านเดียวแทนที่จะเปรียบเทียบ หลายแนวทาง

Data Integration (การรวมข้อมูล)

ธีมที่เกิดขึ้นซ้ำๆ ในหนังสือเล่มนี้คือ สำหรับปัญหาใดๆ ก็ตาม มีหลายวิธีแก้ไข แต่ละวิธี มีข้อดี ข้อเสีย และ trade-offs ของตัวเอง ตัวอย่างเช่น เมื่อพูดถึง storage engines ใน บทที่ 4 เราได้ตรวจสอบ log-structured storage, B-trees, และ column-oriented storage เมื่อพูดถึงการจำลองข้อมูลใน บทที่ 6 เราได้พิจารณาแนวทาง single-leader, multi-leader, และ leaderless

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

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

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

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

Combining Specialized Tools by Deriving Data (การรวมเครื่องมือเฉพาะทางโดยการ Derive ข้อมูล)

เพื่อยกตัวอย่างหนึ่ง เรามักจะต้องรวม OLTP database เข้ากับ full-text search index เพื่อจัดการกับ queries สำหรับคำสำคัญที่กำหนดเอง แม้ว่าฐานข้อมูลบางตัว (เช่น PostgreSQL) จะมี ฟีเจอร์ full-text indexing ซึ่งอาจเพียงพอสำหรับแอปพลิเคชันง่ายๆ [ 1 ] แต่สิ่งอำนวยความสะดวกในการค้นหาที่ซับซ้อนกว่านั้นต้องการเครื่องมือ information-retrieval ที่เชี่ยวชาญเฉพาะ ในทางกลับกัน search indexes โดยทั่วไปไม่เหมาะที่จะเป็นระบบบันทึกข้อมูลหลัก (system of record) ที่ทนทาน ดังนั้น แอปพลิเคชันจำนวนมากจึงต้อง รวมสองเครื่องมือเข้าด้วยกันเพื่อตอบสนองความต้องการทั้งหมดของพวกเขา

เราได้กล่าวถึงประเด็นของการรวมระบบข้อมูลใน "การทำให้ระบบอยู่ใน Sync กัน" . เมื่อจำนวนการแทนค่าของข้อมูลเพิ่มขึ้น ปัญหาการรวมก็จะยากขึ้น นอกเหนือจาก ฐานข้อมูลและ search index แล้ว คุณอาจต้องเก็บสำเนาของข้อมูลในระบบวิเคราะห์ (data warehouses หรือระบบ batch และ stream processing); รักษา caches หรือ denormalized versions ของ objects ที่ derive มาจากข้อมูลต้นฉบับ; ส่งข้อมูลผ่าน machine learning, classification, ranking, หรือ recommendation systems; หรือส่งการแจ้งเตือนตามการเปลี่ยนแปลงของข้อมูล

Reasoning about dataflows (การใช้เหตุผลเกี่ยวกับ dataflows)

เมื่อจำเป็นต้องรักษาสำเนาของข้อมูลเดียวกันไว้ในระบบจัดเก็บข้อมูลหลายระบบเพื่อตอบสนอง รูปแบบการเข้าถึงที่หลากหลาย คุณต้องเข้าใจ inputs และ outputs อย่างชัดเจน ข้อมูลถูกเขียนที่ไหนเป็นอันดับแรก และการแทนค่าใดบ้างที่ derive จากแหล่งใด คุณจะนำข้อมูลไปยัง ที่ที่ถูกต้องทั้งหมดในรูปแบบที่ถูกต้องได้อย่างไร

ตัวอย่างเช่น คุณอาจจัดให้ข้อมูลถูกเขียนไปยังระบบบันทึกข้อมูลหลัก (system-of-record database) ก่อน หลังจากนั้นการเปลี่ยนแปลงที่ทำกับฐานข้อมูลนั้นจะถูกจับ (capture) (ดู "Change Data Capture" ) และนำไปใช้กับ search index ในลำดับเดียวกัน ถ้า CDC เป็นวิธีเดียวในการอัปเดต index คุณ มั่นใจได้ว่า index ถูก derive มาจาก system of record ทั้งหมด ดังนั้นจึงสอดคล้อง กับมัน (ยกเว้น bugs ในซอฟต์แวร์) การเขียนไปยังฐานข้อมูลเป็นวิธีเดียวในการป้อน input ใหม่ เข้าสู่ระบบนี้

การให้แอปพลิเคชันเขียนโดยตรงไปยังทั้ง search index และฐานข้อมูล ทำให้เกิดปัญหาที่แสดงใน รูปที่ 12-4 ซึ่งไคลเอนต์สองตัวส่งการเขียนที่ขัดแย้งกันพร้อมกัน และระบบจัดเก็บข้อมูลทั้งสองประมวลผล ในลำดับที่ต่างกัน ในกรณีนี้ ทั้งฐานข้อมูลและ search index ไม่ได้เป็น "ผู้รับผิดชอบ" ในการกำหนดลำดับของการเขียน ดังนั้นพวกเขาอาจตัดสินใจที่ขัดแย้งกันและกลายเป็น ไม่สอดคล้องกันอย่างถาวร

ถ้าคุณสามารถส่ง input ของผู้ใช้ทั้งหมดผ่านระบบเดียวที่ตัดสินใจลำดับ สำหรับการเขียนทั้งหมด มันจะง่ายขึ้นมากในการ derive การแทนค่าอื่นๆ ของข้อมูลโดยการประมวลผล การเขียนในลำดับเดียวกัน นี่เป็นการประยุกต์ใช้แนวทาง state machine replication ที่เราเห็น ใน "Consensus ในทางปฏิบัติ" . ไม่ว่าคุณจะใช้ CDC หรือ event sourcing log ก็มีความสำคัญน้อยกว่าหลักการของการตัดสินใจ total order

การอัปเดตระบบข้อมูลที่ derive (derived data system) โดยใช้ event log มักจะทำให้เป็น deterministic และ idempotent (ดู "Idempotence" ) ซึ่งทำให้การกู้คืนจากความผิดพลาดทำได้ง่าย

Derived data versus distributed transactions (Derived data กับ distributed transactions)

แนวทางคลาสสิกสำหรับการทำให้ระบบข้อมูลสอดคล้องกันเกี่ยวข้องกับ distributed transactions ดังที่ได้กล่าวถึงใน "Two-Phase Commit" . การใช้ derived data systems เปรียบเทียบกับ distributed transactions เป็นอย่างไร

ในระดับนามธรรม พวกเขาบรรลุเป้าหมายที่คล้ายกันด้วยวิธีที่แตกต่างกัน Distributed transactions ใช้ atomic commit protocol เพื่อให้แน่ใจว่าการเปลี่ยนแปลงถูกนำไปใช้แบบ atomic ในขณะที่ระบบที่ใช้ log บรรลุความถูกต้องผ่าน deterministic retry และ idempotence

ความแตกต่างที่ใหญ่ที่สุดคือระบบ transactions มักจะรับประกันว่าหลังจากเขียนค่าแล้ว คุณสามารถอ่านค่าที่เป็นปัจจุบันได้ทันที (ดู "การอ่าน writes ของตัวเอง" ). ในทางกลับกัน ระบบ derived data มักถูกอัปเดตแบบ async ดังนั้นโดยค่าเริ่มต้นพวกเขา ไม่ได้รับประกันว่าการอ่านจะเป็นข้อมูลล่าสุด

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

ในกรณีที่ไม่มีการสนับสนุนอย่างแพร่หลายสำหรับ good distributed transaction protocol ข้อมูลที่ derive จาก log เป็นแนวทางที่มีแนวโน้มมากที่สุดสำหรับการรวมระบบข้อมูลต่างๆ การรับประกันเช่นการอ่าน writes ของตัวเองนั้นมีประโยชน์ อย่างไรก็ตาม และมันไม่เกิดประโยชน์ที่จะบอกทุกคนว่า "eventual consistency เป็นสิ่งที่หลีกเลี่ยง—จงทำใจและเรียนรู้ที่จะจัดการกับมัน" (อย่างน้อยก็ไม่มีคำแนะนำที่ดีเกี่ยวกับ วิธีการ จัดการกับมัน)

ต่อมาในบทนี้เราจะพูดถึงแนวทางบางอย่างสำหรับการ implement การรับประกันที่แข็งแกร่งขึ้นบน ระบบที่ derive แบบ async และหาจุดกึ่งกลางระหว่าง distributed transactions และระบบที่ใช้ log แบบ async

The limits of total ordering (ข้อจำกัดของการจัดลำดับแบบ total order)

สำหรับระบบที่มีขนาดเล็กพอ การสร้าง totally ordered event log เป็นไปได้อย่างสมบูรณ์ (ดังที่ แสดงให้เห็นโดยความนิยมของฐานข้อมูลที่มี single-leader replication ซึ่งสร้าง log ดังกล่าวอย่างแม่นยำ) อย่างไรก็ตาม เมื่อระบบถูกขยายขนาดไปสู่ workloads ที่ใหญ่ขึ้นและซับซ้อนมากขึ้น ข้อจำกัด ก็เริ่มปรากฏให้เห็น:

  • ในกรณีส่วนใหญ่ การสร้าง totally ordered log ต้องการให้ events ทั้งหมดผ่าน single leader node ที่ตัดสินใจเกี่ยวกับลำดับ ถ้า throughput ของ events มากกว่าที่เครื่องเดียว จะจัดการได้ คุณต้องแยกส่วน (shard) log ออกเป็นหลายเครื่อง ลำดับของ events ในสอง shards จะคลุมเครือ

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

  • เมื่อแอปพลิเคชันถูก deploy เป็น microservices ทางเลือกการออกแบบทั่วไปคือการ deploy แต่ละ service และ state ที่ทนทานของมันเป็นหน่วยอิสระ โดยไม่มี state ที่ทนทานที่ใช้ร่วมกันระหว่าง services เมื่อสอง events มาจาก services ที่แตกต่างกัน events เหล่านั้นไม่มีลำดับที่กำหนด

  • แอปพลิเคชันบางตัวรักษา client-side state ที่ถูกอัปเดตทันทีเมื่อมี user input (โดยไม่ต้อง รอการยืนยันจากเซิร์ฟเวอร์) และแม้แต่ยังคงทำงานแบบออฟไลน์ ด้วย แอปพลิเคชันดังกล่าว ไคลเอนต์และเซิร์ฟเวอร์มีแนวโน้มสูงที่จะเห็น events ในลำดับที่ต่างกัน

ในทางทฤษฎี การตัดสินใจ total order ของ events เรียกว่า total order broadcast ซึ่งดังที่เราเห็นใน "The Many Faces of Consensus" นั้นเทียบเท่ากับ consensus อัลกอริธึม consensus ส่วนใหญ่ถูกออกแบบมาสำหรับสถานการณ์ที่ throughput ของโหนดเดียวเพียงพอที่จะประมวลผลสตรีม events ทั้งหมด และอัลกอริธึมเหล่านี้ ไม่ได้มีกลไกให้หลายโหนดแบ่งปันงานในการจัดลำดับ events

Ordering events to capture causality (การจัดลำดับ events เพื่อจับความสัมพันธ์เชิงสาเหตุ)

ถ้าไม่มีความเชื่อมโยงเชิงสาเหตุระหว่าง events การไม่มี total order ก็ไม่ใช่ปัญหาใหญ่ เนื่องจาก events ที่เกิดขึ้นพร้อมกันสามารถถูกจัดลำดับตามอำเภอใจได้ บางกรณีอื่นๆ จัดการได้ง่าย— ตัวอย่างเช่น การอัปเดตหลายครั้งของ object เดียวกันสามารถถูกจัดลำดับแบบ total order โดยการ routing การอัปเดตทั้งหมดสำหรับ object ID เฉพาะไปยัง shard ของ log เดียวกัน อย่างไรก็ตาม การพึ่งพาเชิงสาเหตุบางครั้งเกิดขึ้นในวิธีที่ละเอียดอ่อนกว่า

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

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

ในตัวอย่างนี้ การแจ้งเตือนเป็นเหมือนการ join ระหว่างข้อความและรายการเพื่อน ทำให้เกี่ยวข้องกับปัญหาเรื่องเวลาของ joins ที่เราได้พูดถึงก่อนหน้านี้ (ดู "Time dependence of joins" ). น่าเสียดายที่ปัญหานี้ดูเหมือนจะไม่มีคำตอบง่ายๆ [ 2 , 3 ]. จุดเริ่มต้นรวมถึงสิ่งต่อไปนี้:

  • Logical timestamps สามารถให้ total ordering ได้โดยไม่ต้องมีการประสานงาน (ดู "ID Generators และ Logical Clocks" ) ดังนั้นพวกมันอาจช่วยได้เมื่อ total order broadcast เป็นไปไม่ได้ อย่างไรก็ตาม พวกมันยังคงต้องการให้ ผู้รับจัดการ events ที่ถูกส่งไม่เรียงลำดับ และพวกมันต้องการ metadata เพิ่มเติม ที่ต้องถูกส่งต่อ

  • ถ้าคุณสามารถ log event เพื่อบันทึกสถานะของระบบที่ผู้ใช้เห็นก่อนตัดสินใจ และกำหนด identifier ที่ไม่ซ้ำให้กับ event นั้น event ใดๆ ในภายหลังสามารถอ้างอิงถึง identifier ของ event นั้นเพื่อบันทึกการพึ่งพาเชิงสาเหตุ [ 4 ]

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

รูปแบบสำหรับการพัฒนาแอปพลิเคชันอาจเกิดขึ้นในอนาคตที่ช่วยให้การพึ่งพาเชิงสาเหตุ ถูกจับได้อย่างมีประสิทธิภาพ และ state ที่ derive ไว้ถูกรักษาไว้อย่างถูกต้อง โดยไม่ต้องบังคับให้ events ทั้งหมด ผ่านคอขวดของ total order broadcast

Batch and Stream Processing (Batch และ Stream Processing)

เป้าหมายของการรวมข้อมูลคือการทำให้แน่ใจว่าข้อมูลอยู่ในรูปแบบที่ถูกต้องในที่ที่ถูกต้องทั้งหมด การทำเช่นนี้ต้องอาศัยการ consume inputs, การแปลง, การ join, การกรอง, การรวม, การฝึกโมเดล, การประเมินผล, และในที่สุดการเขียนไปยัง outputs ที่เหมาะสม Batch และ stream processors เป็นเครื่องมือ สำหรับบรรลุเป้าหมายนี้ outputs ของ batch และ stream processes คือ datasets ที่ derive ไว้ เช่น search indexes, materialized views, คำแนะนำที่จะแสดงให้ผู้ใช้, ตัววัดรวม, และอื่นๆ

ดังที่เราเห็นในบท 11 และ 12 , batch และ stream processing มีหลักการร่วมกันหลายอย่าง ความแตกต่างพื้นฐานหลักคือ stream processors ดำเนินการกับ datasets ที่ไม่มีขอบเขต (unbounded) ในขณะที่ batch process inputs มีขนาดที่ทราบและจำกัด

Maintaining derived state (การรักษา state ที่ derive ไว้)

Batch processing มีลักษณะ functional ที่ค่อนข้างชัดเจน (แม้ว่าโค้ดจะไม่ได้เขียนด้วยภาษา functional programming) มันส่งเสริม deterministic, pure functions ที่ output ขึ้นอยู่กับ input เท่านั้นและไม่มี side effects นอกเหนือจาก outputs ที่ชัดเจน โดยปฏิบัติต่อ inputs เป็น immutable และ outputs เป็น append-only Stream processing ก็คล้ายกัน แต่มันขยาย operators เพื่อให้มี state ที่จัดการได้และทนต่อความผิดพลาด

หลักการของ deterministic functions ที่มี inputs และ outputs ที่กำหนดไว้อย่างดีนั้นไม่เพียงดี สำหรับการทนต่อความผิดพลาด แต่ยังทำให้การใช้เหตุผลเกี่ยวกับ dataflows ในองค์กรง่ายขึ้น [ 5 ]. ไม่ว่าข้อมูลที่ derive ไว้จะเป็น search index, statistical model, หรือ cache มันก็ มีประโยชน์ในการคิดในแง่ของ data pipelines ที่ derive สิ่งหนึ่งจากอีกสิ่งหนึ่ง ผลักดันการเปลี่ยนแปลงสถานะ ในระบบหนึ่งผ่าน functional application code และนำผลลัพธ์ไปใช้กับระบบที่ derive ไว้

ในหลักการแล้ว ระบบข้อมูลที่ derive ไว้สามารถรักษาได้แบบ synchronous เช่นเดียวกับที่ relational database อัปเดต secondary indexes แบบ synchronous ภายใน transaction เดียวกับ writes ไปยังตารางที่ถูก index อย่างไรก็ตาม asynchrony คือสิ่งที่ทำให้ระบบที่ใช้ event logs มีความทนทาน มันช่วยให้ความผิดพลาด ในส่วนหนึ่งของระบบถูกจำกัดอยู่เฉพาะที่ ในขณะที่ distributed transactions จะ abort ถ้ามี participant ใดล้มเหลว ดังนั้นพวกมันมีแนวโน้มที่จะขยายความผิดพลาดโดยแพร่กระจายไปยังส่วนอื่นของระบบ

เราเห็นใน "Sharding และ Secondary Indexes" ว่า secondary indexes มักข้ามขอบเขตของ shards ระบบที่ถูก shard พร้อม secondary indexes ต้อง ส่ง writes ไปยังหลาย shards (ถ้า index ถูก partitioned แบบ term) หรือส่ง reads ไปยังทุก shards (ถ้า index ถูก partitioned แบบ document) การสื่อสารข้าม shard ดังกล่าวก็มีความน่าเชื่อถือและ สามารถขยายขนาดได้มากที่สุดถ้า index ถูกรักษาแบบ asynchronous [ 6 ]

Reprocessing data for application evolution (การประมวลผลข้อมูลซ้ำเพื่อวิวัฒนาการของแอปพลิเคชัน)

เมื่อรักษาข้อมูลที่ derive ไว้ batch และ stream processing ต่างก็มีประโยชน์ Stream processing ช่วยให้ การเปลี่ยนแปลงใน input สะท้อนใน derived views ด้วยความล่าช้าต่ำ ในขณะที่ batch processing ช่วยให้ ข้อมูลประวัติที่สะสมเป็นจำนวนมากถูกประมวลผลซ้ำเพื่อ derive views ใหม่บน dataset ที่มีอยู่

โดยเฉพาะอย่างยิ่ง การประมวลผลข้อมูลที่มีอยู่ซ้ำเป็นกลไกที่ดีสำหรับการบำรุงรักษาระบบ การพัฒนาให้รองรับฟีเจอร์ใหม่และความต้องการที่เปลี่ยนไป หากไม่มีการประมวลผลซ้ำ วิวัฒนาการของ schema จะถูกจำกัดอยู่ที่การเปลี่ยนแปลงง่ายๆ เช่นการเพิ่มฟิลด์ optional ใหม่ให้กับ record หรือเพิ่ม record ชนิดใหม่ ในทางกลับกัน ด้วยการประมวลผลซ้ำ มันเป็นไปได้ที่จะปรับโครงสร้าง dataset ให้เป็นโมเดลที่แตกต่างอย่างสิ้นเชิงเพื่อรองรับความต้องการใหม่ได้ดีขึ้น

Schema Migrations on Railways (การโยกย้าย Schema บนรางรถไฟ)

"การโยกย้าย schema" ขนาดใหญ่เกิดขึ้นในระบบที่ไม่ใช่คอมพิวเตอร์เช่นกัน ตัวอย่างเช่น ในช่วงต้น ของการสร้างทางรถไฟในอังกฤษศตวรรษที่ 19 มีมาตรฐานที่แข่งขันกันหลายมาตรฐานสำหรับ gauge (ระยะห่างระหว่างรางสองเส้น) รถไฟที่สร้างสำหรับ gauge หนึ่งไม่สามารถวิ่งบนรางของ อีก gauge หนึ่ง ซึ่งจำกัดการเชื่อมต่อที่อาจเป็นไปได้ในเครือข่ายรถไฟ [ 7 ]

หลังจากที่มาตรฐาน gauge เดียวถูกตัดสินใจในปี 1846 รางที่มี gauge อื่นต้องถูก เปลี่ยน—แต่จะทำอย่างไรโดยไม่ต้องปิดเส้นทางรถไฟเป็นเวลาหลายเดือนหรือหลายปี? วิธีแก้ไขคือการแปลงรางเป็น dual gauge หรือ mixed gauge โดยการเพิ่มรางที่สาม การแปลงนี้สามารถทำได้ทีละน้อย และเมื่อเสร็จสิ้น รถไฟของทั้งสอง gauges สามารถวิ่งบนเส้นทาง โดยใช้สองในสามราง ในที่สุด เมื่อรถไฟทั้งหมดถูก แปลงเป็น standard gauge แล้ว รางที่ให้ nonstandard gauge ก็สามารถถูกเอาออกได้

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

Derived views อนุญาตให้มีการวิวัฒนาการแบบ ค่อยเป็นค่อยไป . ถ้าคุณต้องการปรับโครงสร้าง dataset คุณไม่จำเป็นต้องทำการโยกย้ายเป็นการสลับอย่างกระทันหัน แต่คุณสามารถรักษา schema เก่าและ schema ใหม่ไว้เคียงข้างกันเป็นสอง views ที่ derive อย่างอิสระจากข้อมูลพื้นฐานเดียวกัน จากนั้นคุณสามารถเริ่มย้ายผู้ใช้จำนวนเล็กน้อยไปยัง view ใหม่เพื่อทดสอบประสิทธิภาพและค้นหา bugs ในขณะที่ผู้ใช้ส่วนใหญ่ยังคงถูก route ไปยัง view เก่า คุณสามารถค่อยๆ เพิ่มสัดส่วนของผู้ใช้ที่เข้าถึง view ใหม่ และในที่สุดคุณก็สามารถละทิ้ง view เก่าได้ [ 8 , 9 ]

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

Unifying batch and stream processing (การรวม batch และ stream processing)

ข้อเสนอแรกเริ่มสำหรับการรวม batch และ stream processing คือ lambda architecture [ 11 ] ซึ่งมีปัญหาหลายประการ [ 12 ] และเลิกใช้ไปแล้ว ระบบที่ใหม่กว่าช่วยให้ batch computations (การประมวลผลข้อมูลประวัติซ้ำ) และ stream computations (การประมวลผล events เมื่อมาถึง) ถูก implement ในระบบเดียวกัน [ 13 ]—แนวทางที่บางครั้งรู้จักกันในชื่อ kappa architecture [ 12 ]

การรวม batch และ stream processing ในระบบเดียวต้องการคุณสมบัติต่อไปนี้:

  • ความสามารถในการเล่นซ้ำ events ประวัติผ่าน processing engine เดียวกับที่จัดการสตรีม ของ events ล่าสุด ตัวอย่างเช่น log-based message brokers มีความสามารถในการเล่นซ้ำ messages และ stream processors บางตัวสามารถอ่าน input จาก distributed filesystem หรือ object storage

  • Exactly-once semantics สำหรับ stream processors—นั่นคือ การรับประกันว่า output จะเหมือนกับ ถ้าไม่มีความผิดพลาดเกิดขึ้น แม้ว่าความผิดพลาดจะเกิดขึ้นจริงก็ตาม เช่นเดียวกับ batch processing สิ่งนี้ต้องการ การละทิ้ง partial outputs ของ failed tasks ใดๆ

  • เครื่องมือสำหรับการ windowing โดย event time ไม่ใช่ processing time เนื่องจาก processing time ไม่มีความหมาย เมื่อประมวลผล events ประวัติซ้ำ ตัวอย่างเช่น Apache Beam มี API สำหรับการแสดง การคำนวณดังกล่าว ซึ่งสามารถรันได้โดยใช้ Apache Flink หรือ Google Cloud Dataflow

Unbundling Databases (การแยกส่วนประกอบฐานข้อมูล)

ในระดับนามธรรม ฐานข้อมูล, batch/stream processors, และระบบปฏิบัติการล้วนทำหน้าที่ เดียวกัน: พวกมันเก็บข้อมูลบางอย่าง และพวกมันอนุญาตให้คุณประมวลผลและสอบถามข้อมูลนั้น [ 14 , 15 ] ฐานข้อมูลเก็บข้อมูลใน records ของ data model (แถวในตาราง, documents, vertices ใน graph, ฯลฯ) ในขณะที่ filesystem ของระบบปฏิบัติการเก็บข้อมูลในไฟล์—แต่ในแกนกลางแล้ว ทั้งคู่เป็นระบบ "การจัดการข้อมูล" [ 16 ] ดังที่เราเห็นใน บทที่ 11 , batch processors ก็เหมือน distributed version ของ Unix

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

Unix และ relational databases ได้เข้าถึงปัญหาการจัดการข้อมูลด้วยปรัชญาที่แตกต่างกันมาก Unix มองว่าจุดประสงค์ของมันคือการนำเสนอ abstraction ฮาร์ดแวร์ที่มีเหตุผลแต่ค่อนข้างระดับต่ำให้กับโปรแกรมเมอร์ ในขณะที่ relational databases ต้องการให้ abstraction ระดับสูงแก่โปรแกรมเมอร์แอปพลิเคชันที่จะ ซ่อนความซับซ้อนของโครงสร้างข้อมูลบนดิสก์, concurrency, การกู้คืนจาก crash, และอื่นๆ Unix พัฒนา pipes และ files ที่เป็นแค่ลำดับของไบต์ ในขณะที่ฐานข้อมูลพัฒนา SQL และ transactions

แนวทางไหนดีกว่ากัน? มันขึ้นอยู่กับสิ่งที่คุณต้องการ Unix "ง่ายกว่า" ในแง่ที่ว่ามันเป็น wrapper ที่ค่อนข้างบาง รอบทรัพยากรฮาร์ดแวร์; relational databases "ง่ายกว่า" ในแง่ที่ว่า short declarative query สามารถใช้โครงสร้างพื้นฐานที่ทรงพลังมากมาย (query optimization, indexes, join methods, concurrency control, replication, ฯลฯ) โดยที่ผู้เขียน query ไม่จำเป็นต้องเข้าใจรายละเอียดการ implement

ความตึงเครียดระหว่างปรัชญาเหล่านี้ดำรงอยู่มานานหลายทศวรรษ (ทั้ง Unix และ relational model เกิดขึ้นใน ช่วงต้นทศวรรษ 1970) และยังไม่ได้รับการแก้ไข ตัวอย่างเช่น การเคลื่อนไหว NoSQL อาจถูกตีความได้ว่าต้องการ ใช้แนวทางแบบ Unix ของ low-level abstractions ในโดเมนของการจัดเก็บข้อมูล OLTP แบบกระจาย

ส่วนนี้พยายามที่จะประนีประนอมทั้งสองแนวทาง ด้วยความหวังว่าเราจะสามารถรวมสิ่งที่ดีที่สุดของทั้งสอง โลกเข้าด้วยกัน

Composing Data Storage Technologies (การประกอบเทคโนโลยีการจัดเก็บข้อมูล)

ตลอดทั้งเล่มนี้เราได้พูดถึงคุณสมบัติต่างๆ ที่ฐานข้อมูลมีให้และวิธีการทำงาน รวมถึงสิ่งเหล่านี้:

  • Secondary indexes ซึ่งช่วยให้คุณค้นหา records อย่างมีประสิทธิภาพตามค่าของฟิลด์

  • Materialized views ซึ่งเป็น cache ของผลลัพธ์ query ที่คำนวณล่วงหน้า

  • Replication logs ซึ่งทำให้สำเนาของข้อมูลบนโหนดอื่นเป็นปัจจุบัน

  • Full-text search indexes ซึ่งช่วยให้ค้นหาคำสำคัญในข้อความและถูกสร้างใน relational databases บางตัว [ 1 ]

ในบท 11 และ 12 , ธีมที่คล้ายกันเกิดขึ้น; เราได้พูดถึงการสร้าง full-text search indexes, การบำรุงรักษา materialized view, และการจำลองการเปลี่ยนแปลงจากฐานข้อมูลไปยังระบบข้อมูลที่ derive โดยใช้ CDC

ดูเหมือนว่ามีความคล้ายคลึงระหว่างคุณสมบัติที่สร้างไว้ในฐานข้อมูลและระบบข้อมูลที่ derive ที่ผู้คนสร้างด้วย batch และ stream processors

Creating an index (การสร้าง Index)

ลองคิดถึงสิ่งที่เกิดขึ้นเมื่อคุณรัน CREATE INDEX เพื่อสร้าง index ใหม่ใน relational database ฐานข้อมูลต้องสแกน snapshot ที่สอดคล้อง ของตาราง, เลือกค่าฟิลด์ทั้งหมดที่ถูก index, เรียงลำดับ, และเขียน index ออกมา จากนั้นมันต้อง ประมวลผล backlog ของ writes ที่เกิดขึ้นตั้งแต่ snapshot ที่สอดคล้องถูกถ่าย (สมมติว่า ตารางไม่ได้ถูกล็อคขณะสร้าง index ดังนั้น writes สามารถดำเนินต่อไปได้) เมื่อเสร็จแล้ว ฐานข้อมูลต้องยังคงทำให้ index เป็นปัจจุบันทุกครั้งที่ transaction เขียนไปยังตาราง

กระบวนการนี้คล้ายคลึงอย่างน่าทึ่งกับการตั้งค่า follower replica ใหม่ (ดู "การตั้งค่า Followers ใหม่" ) และยังคล้ายกับการบูทสแตรป CDC ในระบบสตรีมมิ่ง (ดู "Initial snapshot" )

ทุกครั้งที่คุณรัน CREATE INDEX , ฐานข้อมูลจะประมวลผล dataset ที่มีอยู่ซ้ำและ derive index เป็น view ใหม่บน ข้อมูลที่มีอยู่ ข้อมูลที่มีอยู่อาจเป็น snapshot ของสถานะแทนที่จะเป็น log ของการเปลี่ยนแปลงทั้งหมดที่ เคยเกิดขึ้น แต่ทั้งสองอย่างมีความเกี่ยวข้องกันอย่างใกล้ชิด

The meta-database of everything (เมทาฐานข้อมูลของทุกสรรพสิ่ง)

ในแง่นี้ dataflow ทั่วทั้งองค์กรเริ่มดูเหมือนฐานข้อมูลขนาดใหญ่หนึ่งแห่ง [ 5 ]. ทุกครั้งที่ batch, stream, หรือ ETL process ขนส่งข้อมูลจากที่หนึ่งและรูปแบบหนึ่งไปยังอีกที่หนึ่ง และอีกรูปแบบหนึ่ง มันทำหน้าที่เหมือน subsystem ของฐานข้อมูลที่ทำให้ indexes หรือ materialized views เป็นปัจจุบัน

เมื่อมองเช่นนี้ batch และ stream processors ก็เหมือนกับการ implement triggers, stored procedures, และ materialized view maintenance algorithms อย่างละเอียด ระบบข้อมูลที่ derive ที่พวกมันรักษา ก็เหมือน index types ที่แตกต่างกัน ตัวอย่างเช่น relational database อาจรองรับ B-tree indexes, hash indexes, spatial indexes, และ index types อื่นๆ ในสถาปัตยกรรมที่กำลังเกิดขึ้นของระบบข้อมูลที่ derive แทนที่จะ implement สิ่งอำนวยความสะดวกเหล่านั้นเป็นคุณสมบัติของผลิตภัณฑ์ฐานข้อมูลแบบบูรณาการเดียว พวกมันถูกจัดหาโดยซอฟต์แวร์ต่างๆ ที่ทำงานบนเครื่องต่างๆ และบริหารโดยทีมต่างๆ

การพัฒนาเหล่านี้จะพาเราไปที่ไหนในอนาคต? ถ้าเราเริ่มต้นจาก premise ที่ว่าไม่มี data model หรือรูปแบบการจัดเก็บเดียวที่เหมาะกับทุกรูปแบบการเข้าถึง มีสองเส้นทางที่เครื่องมือจัดเก็บ และประมวลผลที่แตกต่างกันสามารถถูกประกอบเป็นระบบที่สอดคล้องกันได้:

Federated databases (การรวม reads)

มันเป็นไปได้ที่จะมี unified query interface สำหรับ storage engines และวิธีการประมวลผลที่หลากหลาย —แนวทางที่เรียกว่า federated database หรือ polystore [ 17 , 18 ] ตัวอย่างเช่น foreign data wrapper ของ PostgreSQL และ federated query engines เช่น Trino, Hoptimator, และ Xorq แอปพลิเคชันที่ต้องการ data model หรือ query interface เฉพาะยังสามารถเข้าถึง storage engines ที่อยู่เบื้องหลังได้โดยตรง ในขณะที่ผู้ใช้ที่ต้องการรวมข้อมูลจากแหล่งต่างๆ สามารถทำได้ ง่ายๆ ผ่าน federated interface

Federated query interface ดำเนินตามประเพณี relational ของระบบบูรณาการเดียวที่มี ภาษา query ระดับสูงและ semantics ที่สวยงาม แต่มีการ implement ที่ซับซ้อน

Unbundled databases (การรวม writes)

ในขณะที่ federation จัดการกับการ query แบบอ่านอย่างเดียวข้ามหลายระบบ มันไม่มี คำตอบที่ดีสำหรับการทำให้ writes ซิงค์กันข้ามระบบเหล่านั้น เรากล่าวว่าภายในฐานข้อมูลเดียว การสร้าง index ที่สอดคล้องเป็นคุณสมบัติที่สร้างไว้ เมื่อเราประกอบระบบจัดเก็บข้อมูลหลายระบบ เราก็จำเป็นต้องทำให้แน่ใจว่าการเปลี่ยนแปลงข้อมูลทั้งหมดไปสิ้นสุดในที่ที่ถูกต้องทั้งหมด แม้ในกรณีที่เกิดความผิดพลาด การทำให้ง่ายต่อการเชื่อมต่อระบบจัดเก็บข้อมูลอย่างน่าเชื่อถือ (เช่นผ่าน CDC และ event logs) ก็เหมือน unbundling คุณสมบัติการบำรุงรักษา index ของฐานข้อมูลในแบบที่สามารถซิงค์ writes ข้ามเทคโนโลยีที่แตกต่างกัน [ 5 , 19 ]

แนวทาง unbundled ดำเนินตามประเพณี Unix ของเครื่องมือขนาดเล็กที่ทำสิ่งหนึ่งได้ดี [ 20 ], สื่อสารผ่าน uniform low-level API (pipes), และสามารถประกอบกันโดยใช้ ภาษาระดับสูง (shell) [ 14 ]

Making unbundling work (ทำให้ unbundling ใช้งานได้)

Federation และ unbundling เป็นสองด้านของเหรียญเดียวกัน: การประกอบระบบที่เชื่อถือได้ ขยายขนาดได้ และบำรุงรักษาได้จากส่วนประกอบที่หลากหลาย Federated read-only querying ต้องการการแมป data model หนึ่งไปยังอีกอันหนึ่ง ซึ่งต้องใช้ความคิด แต่ท้ายที่สุดแล้วเป็นปัญหาที่จัดการได้ การทำให้ writes ซิงค์กันในระบบจัดเก็บข้อมูลหลายระบบเป็นปัญหาทางวิศวกรรมที่ยากกว่า ดังนั้นเราจะเน้นที่ นั้นตรงนี้

แนวทางดั้งเดิมในการทำให้ writes ซิงค์กันต้องใช้ distributed transactions ข้ามระบบจัดเก็บข้อมูลที่ต่างกัน [ 17 ] ซึ่งมีปัญหาดังที่ได้กล่าวไว้ก่อนหน้านี้ ธุรกรรมภายในระบบจัดเก็บหรือสตรีม processing ระบบเดียวนั้นเป็นไปได้ แต่เมื่อข้อมูลข้ามพรมแดนระหว่างเทคโนโลยีที่แตกต่างกัน asynchronous event log ที่มี idempotent writes เป็นแนวทางที่ทนทานกว่าและปฏิบัติได้จริงมากกว่า

ตัวอย่างเช่น distributed transactions ถูกใช้ใน stream processors บางตัวเพื่อให้ได้ exactly-once semantics และสิ่งนี้สามารถทำงานได้ดี อย่างไรก็ตาม เมื่อ transaction จะต้องเกี่ยวข้องกับระบบ ที่เขียนโดยคนกลุ่มต่างๆ (เช่น เมื่อข้อมูลถูกเขียนจาก stream processor ไปยัง distributed key-value store หรือ search index) การขาด standardized transaction protocol ทำให้การรวมเข้ากันยากขึ้นมาก ordered log ของ events ที่มี idempotent consumers เป็น abstraction ที่ง่ายกว่ามากและเป็นไปได้มากกว่าในการ implement ข้าม ระบบ ที่ต่างกัน [ 5 ]

ข้อได้เปรียบใหญ่ของการรวมแบบใช้ log คือ loose coupling ระหว่างส่วนประกอบต่างๆ ซึ่งแสดงออกในสองทาง:

  • ในระดับระบบ, asynchronous event streams ทำให้ระบบโดยรวมทนทานต่อการหยุดทำงานหรือ การเสื่อมประสิทธิภาพของส่วนประกอบแต่ละชิ้นมากขึ้น ถ้า consumer ทำงานช้าหรือล้มเหลว event log สามารถ buffer messages ไว้ ทำให้ producer และ consumers อื่นๆ ทำงานต่อไปได้ โดยไม่ได้รับผลกระทบ consumer ที่เสียสามารถตามทันเมื่อได้รับการแก้ไข ดังนั้นมัน ไม่พลาดข้อมูลใดๆ และความผิดพลาดถูกจำกัดไว้ ในทางตรงกันข้าม การโต้ตอบแบบ synchronous ของ distributed transactions มีแนวโน้มที่จะยกระดับความผิดพลาดในพื้นที่ไปสู่ความล้มเหลวขนาดใหญ่

  • ในระดับมนุษย์, การ unbundling ระบบข้อมูลช่วยให้ส่วนประกอบซอฟต์แวร์และ services ได้รับการพัฒนา ปรับปรุง และบำรุงรักษาอย่างอิสระจากกันโดยทีมต่างๆ ความเชี่ยวชาญเฉพาะทางช่วยให้ แต่ละทีมมุ่งเน้นการทำสิ่งหนึ่งได้ดี ด้วย interfaces ที่กำหนดไว้อย่างดีกับระบบของทีมอื่น Event logs ให้ interface ที่ทรงพลังพอที่จะจับคุณสมบัติความสอดคล้องที่ค่อนข้างแข็งแกร่ง (เนื่องจากความทนทานและการจัดลำดับของ events) แต่ก็ทั่วไปพอที่จะ ใช้ได้กับข้อมูลแทบทุกชนิด

Unbundled versus integrated systems (ระบบ unbundled เทียบกับระบบบูรณาการ)

ถ้า unbundling กลายเป็นแนวทางแห่งอนาคตจริงๆ มันจะไม่แทนที่ฐานข้อมูลในรูปแบบปัจจุบัน ของพวกเขา พวกมันยังคงจำเป็นมากเท่าที่เคยเป็นมา สำหรับการรักษาสถานะใน stream processors และเพื่อ ให้บริการ queries สำหรับ output ของ batch และ stream processors Specialized query engines ก็จะ ยังคงมีความสำคัญสำหรับ workloads เฉพาะ—ตัวอย่างเช่น query engines ใน data warehouses ถูกปรับให้เหมาะสมสำหรับ exploratory analytical queries และจัดการกับ workload ประเภทนี้ได้ดีมาก

ความซับซ้อนของการรันโครงสร้างพื้นฐานหลายชิ้นอาจเป็นปัญหา ซอฟต์แวร์แต่ละชิ้น มี learning curve และปัญหา configuration และลักษณะการดำเนินงานของตัวเอง ดังนั้นมันคุ้มค่าที่จะ deploy ส่วนประกอบที่เคลื่อนไหวให้น้อยที่สุดเท่าที่จะเป็นไปได้ ผลิตภัณฑ์ซอฟต์แวร์บูรณาการเดียวอาจสามารถบรรลุ ประสิทธิภาพที่ดีกว่าและคาดเดาได้มากกว่าสำหรับ workloads ประเภทที่มันถูกออกแบบมา เมื่อเทียบกับระบบที่ประกอบด้วยเครื่องมือหลายอย่างที่คุณประกอบเข้ากับ application code [ 21 ] การสร้างเพื่อ scale ที่คุณไม่ต้องการคือความพยายามที่สูญเปล่าและอาจล็อกคุณเข้าสู่การออกแบบที่ยืดหยุ่นไม่ได้ โดยเนื้อแท้แล้ว มันเป็นรูปแบบหนึ่งของการ optimization ก่อนเวลาอันควร

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

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

เครื่องมือสำหรับการประกอบระบบข้อมูลกำลังดีขึ้น Debezium สามารถ extract change streams จากฐานข้อมูล มากมาย, protocol ของ Kafka กำลังกลายเป็นมาตรฐาน de facto สำหรับ event streams, และ incremental view maintenance engines (ดู "Incremental View Maintenance" ) ทำให้สามารถคำนวณและอัปเดต caches ของ complex queries ล่วงหน้าได้

Designing Applications Around Dataflow (การออกแบบแอปพลิเคชันรอบ Dataflow)

แนวคิดทั่วไปของการอัปเดตข้อมูลที่ derive เมื่อข้อมูลต้นทางเปลี่ยนแปลงนั้นไม่ใช่เรื่องใหม่ ตัวอย่างเช่น สเปรดชีตมีขีดความสามารถในการเขียนโปรแกรม dataflow ที่ทรงพลัง [ 22 ]: คุณสามารถใส่สูตรในเซลล์เดียว (เช่น ผลรวมของเซลล์ในคอลัมน์อื่น) และเมื่อ input ใดๆ ของ สูตรเปลี่ยนแปลง ผลลัพธ์ของสูตรจะถูกคำนวณใหม่โดยอัตโนมัติ นี่คือสิ่งที่เราต้องการ ในระดับระบบข้อมูล เมื่อ record ในฐานข้อมูลเปลี่ยนแปลง เราต้องการให้ index ใดๆ สำหรับ record นั้นถูก อัปเดตโดยอัตโนมัติ และ cached views หรือ aggregations ที่ขึ้นอยู่กับ record นั้นถูก รีเฟรชโดยอัตโนมัติ เราไม่ควรต้องกังวลเกี่ยวกับรายละเอียดทางเทคนิคของการรีเฟรชนี้ และควร แค่ไว้วางใจว่ามันทำงานได้อย่างถูกต้อง

ดังนั้น ระบบข้อมูลส่วนใหญ่ยังคงมีสิ่งที่ต้องเรียนรู้จากคุณสมบัติที่ VisiCalc มีอยู่แล้วในปี 1979 [ 23 ] ความแตกต่างจากสเปรดชีตคือระบบข้อมูลในปัจจุบันต้องทนต่อความผิดพลาด ขยายขนาดได้ และสามารถเก็บข้อมูลได้อย่างทนทาน พวกเขายังต้องสามารถรวมเทคโนโลยีที่แตกต่างกันซึ่งเขียน โดยคนกลุ่มต่างๆ ตลอดเวลา และใช้ libraries และ services ที่มีอยู่ มันไม่สมจริงที่จะ คาดหวังให้ซอฟต์แวร์ทั้งหมดถูกพัฒนาโดยใช้ภาษา framework หรือเครื่องมือใดเครื่องมือหนึ่งโดยเฉพาะ

ในส่วนนี้เราจะขยายแนวคิดเหล่านี้และสำรวจวิธีการสร้างแอปพลิเคชัน รอบ unbundled databases และ dataflow

Application code as a derivation function (Application code ในฐานะฟังก์ชัน derive)

เมื่อ dataset หนึ่งถูก derive จากอีก dataset หนึ่ง มันผ่านฟังก์ชันการแปลงบางประเภท ตัวอย่างเช่น:

  • Secondary index เป็น dataset ที่ derive ชนิดหนึ่งที่มีฟังก์ชันการแปลงที่ตรงไปตรงมา— สำหรับแต่ละแถวหรือ document ในตารางฐาน มันเลือกค่าที่ต้องการ index และเรียงลำดับตามค่าเหล่านั้น (สมมติว่าเป็น SSTable หรือ B-tree index ซึ่งเรียงตาม key)

  • Full-text search index ถูกสร้างขึ้นโดยการใช้ฟังก์ชัน natural language processing ต่างๆ เช่น การตรวจจับภาษา, การแบ่งคำ, stemming หรือ lemmatization, การแก้ไขการสะกด, และการระบุคำพ้องความหมาย ตามด้วยการสร้างโครงสร้างข้อมูลสำหรับการค้นหาที่มีประสิทธิภาพ (เช่น inverted index)

  • ในระบบ ML เราสามารถพิจารณาโมเดลว่า derive จาก training data โดยการใช้ ฟังก์ชัน feature extraction และ statistical analysis ต่างๆ เมื่อโมเดลถูกใช้กับ input ข้อมูลใหม่ output ของมันจะ derive จาก input นั้นและพารามิเตอร์ที่เรียนรู้ (และด้วยเหตุนี้ โดยอ้อม จาก training data)

  • Cache มักประกอบด้วยการรวมข้อมูลในรูปแบบที่จะแสดง ใน UI การ populate cache จึงต้องมีความรู้ว่าฟิลด์ใดบ้างที่ถูกอ้างอิงใน UI; การเปลี่ยนแปลงใน UI อาจต้องการการอัปเดตคำจำกัดความของวิธีการ populate cache และการสร้าง cache ขึ้นใหม่

ฟังก์ชัน derive สำหรับ secondary index เป็นที่ต้องการทั่วไปมากจนถูกสร้างในฐานข้อมูล จำนวนมากเป็นคุณสมบัติหลัก และคุณสามารถเรียกใช้มันได้โดยการรัน CREATE INDEX . สำหรับ full-text indexing คุณสมบัติทางภาษาพื้นฐานสำหรับภาษาทั่วไปอาจถูกสร้างในฐานข้อมูล แต่คุณสมบัติที่ซับซ้อนกว่ามักต้องการการปรับแต่งเฉพาะโดเมน ใน machine learning feature engineering เป็นที่รู้กันว่าขึ้นอยู่กับแอปพลิเคชันอย่างมากและมักต้องรวมความรู้โดยละเอียดเกี่ยวกับ ปฏิสัมพันธ์ของผู้ใช้และการ deploy แอปพลิเคชัน [ 24 ]

เมื่อฟังก์ชันที่สร้าง dataset ที่ derive ไม่ใช่ฟังก์ชันมาตรฐานเหมือน การสร้าง secondary index จำเป็นต้องใช้โค้ดที่กำหนดเองเพื่อจัดการกับแง่มุมเฉพาะแอปพลิเคชัน โค้ดกำหนดเองนี้คือจุดที่ฐานข้อมูลหลายตัวมีปัญหา แม้ว่า relational databases โดยทั่วไปจะรองรับ triggers, stored procedures, และ user-defined functions ซึ่งสามารถใช้เพื่อรัน application code ภายในฐานข้อมูล พวกมันกลับเป็นเหมือนสิ่งที่ถูกเพิ่มเข้ามาทีหลังในการออกแบบฐานข้อมูล

Separation of application code and state (การแยก application code และ state)

ในทางทฤษฎี ฐานข้อมูลสามารถเป็นสภาพแวดล้อมการ deploy สำหรับ application code ตามอำเภอใจ เช่นระบบปฏิบัติการ อย่างไรก็ตาม ในทางปฏิบัติมันกลับกลายเป็นว่าไม่เหมาะสมสำหรับจุดประสงค์นี้ พวกมันไม่เข้ากันดีกับความต้องการของการพัฒนาแอปพลิเคชันสมัยใหม่ เช่นการจัดการ dependency และ package, version control, rolling upgrades, ความสามารถในการวิวัฒนาการ, การตรวจสอบ, metrics, การเรียก network services, และการรวมกับระบบภายนอก

ในทางกลับกัน เครื่องมือ deploy และ cluster management เช่น Kubernetes, Docker, Mesos, YARN, และอื่นๆ ถูกออกแบบมาเฉพาะสำหรับการรัน application code ด้วยการมุ่งเน้นที่ การทำสิ่งหนึ่งได้ดี พวกมันสามารถทำได้ดีกว่าฐานข้อมูลที่ให้การทำงานของ user-defined functions เป็นหนึ่งในคุณสมบัติมากมายของมัน

แอปพลิเคชันเว็บส่วนใหญ่ในปัจจุบันถูก deploy เป็น stateless services ซึ่งคำขอของผู้ใช้ใดๆ สามารถถูก route ไปยัง application server ใดก็ได้ และเซิร์ฟเวอร์ลืมทุกอย่างเกี่ยวกับคำขอหลังจาก ส่งการตอบกลับ ด้วยรูปแบบการ deploy นี้ เซิร์ฟเวอร์สามารถถูกเพิ่มและลบได้ตามต้องการ ซึ่งสะดวก—แต่ state ต้องไปอยู่ที่ไหนสักแห่ง (โดยทั่วไปคือฐานข้อมูล) แนวโน้มคือการเก็บ stateless application logic แยกจากการจัดการ state (ฐานข้อมูล): ไม่ใส่ application logic ในฐานข้อมูลและไม่เก็บ persistent state ในแอปพลิเคชัน [ 25 ] ดังที่คนในชุมชน functional programming ชอบพูดติดตลก "เราเชื่อในการแยก Church และ State" [ 26 ]

Note (หมายเหตุ)

การอธิบายมุกตลกมักจะเสียอรรถรส แต่นี่คือคำอธิบายเพื่อไม่ให้ใครรู้สึกถูกทิ้งไว้ข้างหลัง Church หมายถึงนักคณิตศาสตร์ Alonzo Church ผู้สร้าง lambda calculus ซึ่งเป็นรูปแบบแรกเริ่มของการคำนวณที่เป็นพื้นฐานของภาษา functional programming ส่วนใหญ่ lambda calculus ไม่มี mutable state (เช่นไม่มีตัวแปรที่สามารถถูกเขียนทับ) ดังนั้นอาจกล่าวได้ว่า mutable state แยกจากงานของ Church

ในโมเดลแอปพลิเคชันเว็บทั่วไปนี้ ฐานข้อมูลทำหน้าที่เป็นตัวแปรร่วมที่เปลี่ยนแปลงได้ (mutable shared variable) ที่สามารถเข้าถึงแบบ synchronous ผ่านเครือข่าย แอปพลิเคชันสามารถอ่านและอัปเดตตัวแปร และฐานข้อมูลดูแลให้มันทนทาน ให้ concurrency control และ fault tolerance บางอย่าง

อย่างไรก็ตาม ในภาษาโปรแกรมส่วนใหญ่ คุณไม่สามารถสมัครรับการเปลี่ยนแปลง (subscribe) ของ mutable variable— คุณสามารถอ่านมันเป็นระยะเท่านั้น ต่างจากในสเปรดชีต ผู้อ่านตัวแปรไม่ได้รับการแจ้งเตือน ถ้าค่าของตัวแปรเปลี่ยนแปลง (คุณสามารถ implement การแจ้งเตือนดังกล่าวในโค้ดของคุณเอง— ซึ่งเรียกว่า observer pattern —แต่ภาษาส่วนใหญ่ไม่มี pattern นี้เป็นคุณสมบัติ built-in)

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

Dataflow: Interplay between state changes and application code (Dataflow: ปฏิสัมพันธ์ระหว่างการเปลี่ยนแปลง state และ application code)

การคิดเกี่ยวกับแอปพลิเคชันในแง่ของ dataflow หมายถึงการเจรจาต่อรองความสัมพันธ์ระหว่าง application code และการจัดการ state ใหม่ แทนที่จะปฏิบัติต่อฐานข้อมูลเป็นตัวแปรเชิงรับ ที่ถูกจัดการโดยแอปพลิเคชัน เราคิดมากขึ้นเกี่ยวกับปฏิสัมพันธ์และการทำงานร่วมกันระหว่าง state, การเปลี่ยนแปลง state, และโค้ดที่ประมวลผลมัน Application code ตอบสนองต่อการเปลี่ยนแปลง state ในที่หนึ่งโดยการ trigger การเปลี่ยนแปลง state ในอีกที่หนึ่ง

เราได้เห็นแนวคิดนี้แล้วใน CDC, ใน actor model, ใน triggers, และใน incremental view maintenance การ unbundling ฐานข้อมูลหมายถึงการนำไปใช้กับการสร้าง datasets ที่ derive นอกฐานข้อมูลหลัก: caches, full-text search indexes, machine learning, หรือระบบวิเคราะห์ เราสามารถใช้ stream processing และ messaging systems เพื่อจุดประสงค์นี้

การรักษาข้อมูลที่ derive ไว้ต้องการคุณสมบัติต่อไปนี้ ซึ่ง log-based message brokers สามารถให้ได้:

  • เมื่อรักษาข้อมูลที่ derive ไว้ ลำดับของการเปลี่ยนแปลง state มักจะสำคัญ (ถ้า views หลายตัว ถูก derive จาก event log พวกเขาต้องประมวลผล events ในลำดับเดียวกันเพื่อให้ สอดคล้องกัน)

  • การทนต่อความผิดพลาดเป็นสิ่งจำเป็น—การสูญเสียเพียง message เดียวทำให้ dataset ที่ derive ไว้ไม่ซิงค์กับแหล่งข้อมูลอย่างถาวร ทั้ง message delivery และการอัปเดตสถานะที่ derive ต้องเชื่อถือได้

การจัดลำดับ messages ที่เสถียรและการประมวลผล messages ที่ทนต่อความผิดพลาดเป็นความต้องการที่ค่อนข้างเข้มงวด แต่พวกมันมีราคาถูกกว่าและมีความทนทานในการดำเนินงานมากกว่า distributed transactions Stream processors สมัยใหม่สามารถให้การรับประกันการจัดลำดับและความน่าเชื่อถือเหล่านี้ใน scale และพวกมันอนุญาตให้ application code ถูกเรียกใช้เป็น stream operators

Application code นี้สามารถทำการประมวลผลตามอำเภอใจที่ฟังก์ชัน derive built-in ในฐานข้อมูล โดยทั่วไปไม่สามารถให้ได้ เช่นเดียวกับเครื่องมือ Unix ที่ต่อกันด้วย pipes, stream operators สามารถถูกประกอบ เพื่อสร้างระบบขนาดใหญ่รอบ dataflow แต่ละ operator รับ streams ของการเปลี่ยนแปลง state เป็น input และผลิต streams ของการเปลี่ยนแปลง state อื่นเป็น output

Stream processors and services (Stream processors และ services)

รูปแบบการพัฒนาแอปพลิเคชันที่โดดเด่นในปัจจุบันแบ่งฟังก์ชันการทำงานออกเป็นชุดของ services ที่สื่อสารผ่าน synchronous network requests เช่น REST APIs ข้อดีของ service-oriented architecture แบบนี้เหนือ monolithic application เดียวคือหลักๆ แล้วคือ organizational scalability ผ่าน loose coupling ทีมต่างๆ สามารถทำงานบน services ที่แตกต่างกัน ซึ่งลดความพยายามในการประสานงานระหว่างทีม (ตราบใดที่ services สามารถ deploy และอัปเดตได้อย่างอิสระ)

การประกอบ stream operators เป็น dataflow systems มีลักษณะคล้ายคลึงกับ แนวทาง microservices หลายอย่าง [ 27 , 28 ] อย่างไรก็ตาม กลไกการสื่อสารพื้นฐานแตกต่างกันมาก: การสตรีมข้อความแบบทิศทางเดียว และ asynchronous แทนที่จะเป็นการโต้ตอบแบบ request/response แบบ synchronous

นอกเหนือจากข้อดีที่ระบุไว้ใน "Event-Driven Architectures" , เช่นการทนต่อความผิดพลาดที่ดีขึ้น ระบบ dataflow ยังสามารถบรรลุประสิทธิภาพที่ดีกว่า REST APIs หรือ RPC แบบดั้งเดิม ตัวอย่างเช่น สมมติว่าลูกค้ากำลังซื้อสินค้าที่มีราคาใน สกุลเงินหนึ่งแต่ชำระในอีกสกุลเงินหนึ่ง ในการแปลงสกุลเงิน คุณต้องทราบ อัตราแลกเปลี่ยนปัจจุบัน การดำเนินการนี้สามารถ implement ได้สองวิธี [ 27 , 29 ]:

  • ในแนวทาง microservices โค้ดที่ประมวลผลการซื้ออาจจะ query exchange rate service หรือฐานข้อมูลเพื่อรับอัตราปัจจุบันสำหรับสกุลเงินนั้น

  • ในแนวทาง dataflow โค้ดที่ประมวลผลการซื้อจะสมัครรับสตรีม ของการอัปเดตอัตราแลกเปลี่ยนล่วงหน้าและบันทึกอัตราปัจจุบันในฐานข้อมูลท้องถิ่นเมื่อ มีการเปลี่ยนแปลง เมื่อถึงเวลาประมวลผลการซื้อ โค้ดประมวลผลสามารถ query ฐานข้อมูลท้องถิ่นนี้ได้โดยตรง

แนวทางที่สองแทนที่ synchronous network request ไปยัง service อื่นด้วย query ไปยังฐานข้อมูลท้องถิ่น (ซึ่งอาจอยู่บนเครื่องเดียวกัน หรือแม้แต่ในกระบวนการเดียวกัน) ในแนวทาง microservices คุณสามารถหลีกเลี่ยง synchronous network request ได้โดยการ cache อัตราแลกเปลี่ยนใน service ที่ประมวลผลการซื้อ อย่างไรก็ตาม เพื่อให้ cache นั้นสดใหม่ คุณต้อง poll หาอัตราแลกเปลี่ยนที่อัปเดตเป็นระยะหรือสมัครรับสตรีม ของการเปลี่ยนแปลง—ซึ่งคือสิ่งที่เกิดขึ้นในแนวทาง dataflow นั่นเอง

แนวทาง dataflow ไม่เพียงเร็วกว่า แต่ยังทนทานต่อความล้มเหลวของ service อื่นมากกว่า network request ที่เร็วและเชื่อถือได้มากที่สุดคือไม่มี network request เลย! แทนที่จะเป็น RPC ตอนนี้เรามี stream join ระหว่าง purchase events และ exchange rate update events

การ join ขึ้นอยู่กับเวลา: ถ้า purchase events ถูกประมวลผลซ้ำในเวลาที่หลัง อัตราแลกเปลี่ยนจะเปลี่ยนไป ถ้าคุณต้องการสร้าง output ดั้งเดิมขึ้นมาใหม่ คุณจะต้อง ได้รับอัตราแลกเปลี่ยนในอดีต ณ เวลาที่ซื้อเดิม ไม่ว่าคุณจะ query service หรือสมัครรับสตรีมของการอัปเดตอัตราแลกเปลี่ยน คุณจะต้องจัดการกับการพึ่งพาเวลา นี้ (ดู "Time dependence of joins" )

การสมัครรับสตรีมของการเปลี่ยนแปลง แทนที่จะ query สถานะปัจจุบันเมื่อจำเป็น ทำให้เราเข้าใกล้โมเดลการคำนวณแบบสเปรดชีตมากขึ้น เมื่อข้อมูลชิ้นหนึ่งเปลี่ยนแปลง ข้อมูลที่ derive ที่ขึ้นอยู่กับมันสามารถถูกอัปเดตได้อย่างรวดเร็ว ยังมีคำถามที่เปิดอยู่อีกมาก— ตัวอย่างเช่น เกี่ยวกับประเด็นเช่น time-dependent joins—แต่การสร้างแอปพลิเคชัน รอบแนวคิด dataflow เป็นทิศทางที่มีแนวโน้มในการสำรวจ

Observing Derived State (การสังเกต State ที่ Derive ไว้)

ในระดับนามธรรม ระบบ dataflow ที่กล่าวถึงในส่วนก่อนหน้าให้กระบวนการสำหรับ การสร้าง datasets ที่ derive (เช่น search indexes, materialized views, และ predictive models) และรักษา พวกมันให้เป็นปัจจุบัน เรียกกระบวนการนั้นว่า write path . เมื่อใดก็ตามที่ข้อมูลถูกเขียนไปยังระบบ มันอาจผ่านหลายขั้นตอนของ batch และ stream processing และในที่สุดทุก dataset ที่ derive จะถูกอัปเดตเพื่อรวมข้อมูลที่ถูก เขียน รูปที่ 13-1 แสดงตัวอย่างการอัปเดต search index

Diagram illustrating the write and read paths in a search index, showing document updates processed through linguistic analysis to update the index, and queries accessing the index for search results.

Figure 13-1. ใน search index, writes (การอัปเดตเอกสาร) พบกับ reads (queries)

แต่ทำไมคุณถึงสร้าง dataset ที่ derive ตั้งแต่แรก? ส่วนใหญ่แล้วเพราะคุณต้องการ query มันอีกครั้งในภายหลัง นี่คือ read path : เมื่อให้บริการคำขอของผู้ใช้ คุณอ่านจาก dataset ที่ derive อาจประมวลผลเพิ่มเติม กับผลลัพธ์ และสร้างการตอบกลับไปยังผู้ใช้

เมื่อรวมกันแล้ว write path และ read path ครอบคลุมการเดินทางทั้งหมดของข้อมูล จากจุดที่มันถูกรวบรวมไปยังจุดที่มันถูกบริโภค (อาจโดยมนุษย์อีกคน) Write path เป็นส่วน ของการเดินทางที่ถูกคำนวณล่วงหน้า; มันทำอย่าง eager ทันทีที่ข้อมูลเข้ามา ไม่ว่ามีคนขอเห็นมันหรือไม่ Read path เป็นส่วนของการเดินทางที่เกิดขึ้นเมื่อ มีคนขอเท่านั้น ถ้าคุณคุ้นเคยกับภาษา functional programming คุณอาจสังเกตว่า write path คล้ายกับ eager evaluation และ read path คล้ายกับ lazy evaluation

Dataset ที่ derive คือจุดที่ write path และ read path พบกัน ดังที่แสดงใน รูปที่ 13-1 . มันแทน trade-off ระหว่างปริมาณงานที่ต้องทำ ณ เวลาเขียน กับปริมาณ ที่ต้องทำ ณ เวลาอ่าน

Materialized views and caching (Materialized views และ caching)

Full-text search index เป็นตัวอย่างที่ดี: write path อัปเดต index และ read path ค้นหา index สำหรับคำสำคัญ ทั้ง reads และ writes ต้องทำงานบางส่วน Writes ต้องอัปเดต index entries สำหรับคำทั้งหมดที่ปรากฏในเอกสาร Reads ต้องค้นหาแต่ละคำใน query และใช้ Boolean logic เพื่อค้นหาเอกสารที่มี ทั้งหมด คำใน query (operator AND ) หรือ ใดๆ คำพ้องของแต่ละคำ (operator OR )

ถ้าคุณไม่มี index search query จะต้องสแกนเอกสารทั้งหมด (เหมือน grep ) ซึ่งจะแพงมากถ้าคุณมีเอกสารจำนวนมาก การไม่มี index หมายถึงงานน้อยลงบน write path (ไม่มี index ที่ต้องอัปเดต) แต่งานมากขึ้นบน read path

ในทางกลับกัน คุณสามารถจินตนาการถึงการคำนวณผลลัพธ์การค้นหาล่วงหน้าสำหรับทุก queries ที่เป็นไปได้ ในกรณีนั้น คุณจะมีงานน้อยลงบน read path: ไม่ต้องใช้ Boolean logic, แค่หาผลลัพธ์สำหรับ query ของคุณและส่งคืน อย่างไรก็ตาม write path จะแพงกว่ามาก ชุดของ queries การค้นหาที่เป็นไปได้ไม่มีที่สิ้นสุด (หรืออย่างน้อยก็ exponential ในจำนวนคำในคลังข้อมูล) ดังนั้นการคำนวณผลลัพธ์การค้นหาทั้งหมดล่วงหน้าจึงไม่เป็นไปได้

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

จากตัวอย่างนี้ เราสามารถเห็นว่า index ไม่ใช่ขอบเขตเดียวที่เป็นไปได้ระหว่าง write path และ read path การ cache ผลลัพธ์การค้นหาทั่วไปเป็นไปได้ และ การสแกนแบบ grep โดยไม่มี index ก็เป็นไปได้กับเอกสารจำนวนน้อย เมื่อมองเช่นนี้ บทบาทของ caches, indexes, และ materialized views นั้นง่าย: พวกมันเลื่อนขอบเขตระหว่าง read path และ write path พวกมันอนุญาตให้เราทำงานมากขึ้นบน write path โดยการคำนวณผลลัพธ์ล่วงหน้า เพื่อประหยัดความพยายามบน read path

การเลื่อนขอบเขตระหว่างงานที่ทำบน write path และ read path เป็นหัวข้อของ ตัวอย่างเครือข่ายสังคมใน "กรณีศึกษา: Home Timelines ของเครือข่ายสังคม" . ในตัวอย่างนั้น เราเห็นว่าขอบเขตระหว่าง write path และ read path อาจถูกวาด แตกต่างกันสำหรับคนดังเมื่อเทียบกับผู้ใช้ทั่วไป หลังจาก 500 หน้า เรากลับมาที่จุดเริ่มต้น!

Stateful, offline-capable clients (Clients ที่มี state และทำงานออฟไลน์ได้)

แนวคิดของขอบเขตระหว่าง write และ read paths นั้นน่าสนใจเพราะเราสามารถพูดคุยเกี่ยวกับการเลื่อน ขอบเขตนั้นและสำรวจว่าการเลื่อนนั้นหมายถึงอะไรในทางปฏิบัติ ลองพิจารณามันในบริบทที่ แตกต่าง

ในอดีต เว็บเบราว์เซอร์เป็น stateless clients ที่สามารถทำสิ่งที่มีประโยชน์ได้เมื่อคุณมี การเชื่อมต่ออินเทอร์เน็ตเท่านั้น (สิ่งเดียวที่คุณทำได้แบบออฟไลน์คือการเลื่อนขึ้นลงในหน้า ที่คุณโหลดไว้ก่อนหน้านี้ขณะออนไลน์) อย่างไรก็ตาม แอปเว็บ JavaScript หน้าเดียวในปัจจุบันมี ขีดความสามารถที่มี state มากมาย รวมถึงการโต้ตอบ UI ฝั่ง client และพื้นที่จัดเก็บในเครื่องแบบถาวร ในเว็บเบราว์เซอร์ แอปมือถือสามารถเก็บ state จำนวนมากบนอุปกรณ์และ ไม่ต้องเดินทางไปกลับเซิร์ฟเวอร์สำหรับการโต้ตอบผู้ใช้ส่วนใหญ่

ใน "Sync Engines และ Local-First Software" เราเห็นว่า persistent local state ช่วยให้คลาสของแอปพลิเคชันที่ผู้ใช้สามารถทำงานออฟไลน์ โดยไม่ต้องเชื่อมต่ออินเทอร์เน็ต และซิงค์กับเซิร์ฟเวอร์ระยะไกลในพื้นหลังเมื่อ การเชื่อมต่อเครือข่ายพร้อมใช้งาน [ 30 ] เนื่องจากอุปกรณ์มือถือบางครั้งมีการเชื่อมต่ออินเทอร์เน็ตเซลลูลาร์ที่ช้าและไม่น่าเชื่อถือ มันเป็นข้อได้เปรียบใหญ่สำหรับผู้ใช้ถ้า UI ไม่ต้องรอ synchronous network requests และแอปส่วนใหญ่ทำงานออฟไลน์

เมื่อเราเคลื่อนออกจากสมมติฐานของ stateless clients ที่พูดกับฐานข้อมูลกลางและไปสู่ state ที่ถูกรักษาบนอุปกรณ์ผู้ใช้ โลกของโอกาสใหม่ๆ ก็เปิดขึ้น โดยเฉพาะอย่างยิ่ง เราสามารถคิดถึง state บนอุปกรณ์เป็น cache ของ state บนเซิร์ฟเวอร์ . พิกเซลบนหน้าจอเป็น materialized view ของ model objects ใน client app; model objects เป็น replica ท้องถิ่นของ state ใน datacenter ระยะไกล [ 31 ]

Pushing state changes to clients (การส่ง state changes ไปยัง clients)

ถ้าคุณโหลดหน้าเว็บทั่วไปในเว็บเบราว์เซอร์ และข้อมูลเปลี่ยนแปลงบนเซิร์ฟเวอร์ในภายหลัง เบราว์เซอร์จะไม่ทราบเกี่ยวกับการเปลี่ยนแปลงจนกว่าคุณจะโหลดหน้าใหม่ เบราว์เซอร์อ่านข้อมูล ณ จุดเวลาเดียว สมมติว่ามันเป็น static; เบราว์เซอร์ไม่ได้สมัครรับการอัปเดตจาก เซิร์ฟเวอร์ ดังนั้น state ในเบราว์เซอร์เป็น cache ที่เก่าที่ไม่ถูกอัปเดต เว้นแต่คุณจะ poll หาการเปลี่ยนแปลงอย่างชัดเจน (โปรโตคอลการสมัครรับฟีดที่ใช้ HTTP เช่น RSS จริงๆ แล้วเป็นรูปแบบพื้นฐานของการ polling)

โปรโตคอลที่ใหม่กว่าได้ก้าวไปไกลกว่ารูปแบบ request/response พื้นฐานของ HTTP Server-sent events (EventSource API) และ WebSockets ให้ช่องทางการสื่อสารที่เว็บเบราว์เซอร์ สามารถรักษา TCP connection ที่เปิดอยู่กับเซิร์ฟเวอร์ และเซิร์ฟเวอร์สามารถส่ง messages ไปยังเบราว์เซอร์ได้อย่างแข็งขันตราบเท่าที่มันยังคงเชื่อมต่ออยู่ สิ่งนี้ให้โอกาส สำหรับเซิร์ฟเวอร์ในการแจ้ง client ผู้ใช้ปลายทางเกี่ยวกับการเปลี่ยนแปลงใดๆ ต่อ state ที่มันเก็บไว้ในเครื่อง ซึ่งช่วยลดความเก่าของ client-side state

ในแง่ของโมเดล write path และ read path ของเรา การผลักการเปลี่ยนแปลง state ไปยังอุปกรณ์ client อย่างแข็งขันหมายถึงการขยาย write path ไปจนถึงผู้ใช้ปลายทาง เมื่อ client เริ่มต้นครั้งแรก มันยังคงต้องใช้ read path เพื่อรับ state เริ่มต้น แต่หลังจากนั้นมันสามารถพึ่งพาสตรีมของการเปลี่ยนแปลง state ที่ส่งโดยเซิร์ฟเวอร์ แนวคิดที่เราพูดถึงเกี่ยวกับ stream processing และ messaging จึงไม่ถูกจำกัด ให้ทำงานใน datacenter; เราสามารถนำมันไปต่อและขยายมันไปจนถึงอุปกรณ์ผู้ใช้ปลายทาง [ 32 ]

อุปกรณ์จะออฟไลน์เป็นบางครั้ง และพวกมันจะไม่สามารถรับการแจ้งเตือนใดๆ เกี่ยวกับการเปลี่ยนแปลง state จากเซิร์ฟเวอร์ในช่วงเวลานั้น แต่เราแก้ปัญหานั้นแล้ว; ใน "Consumer offsets" เราได้พูดถึงว่า consumer ของ log-based message broker สามารถ reconnect ได้หลังจากล้มเหลว หรือถูกตัดการเชื่อมต่อ และทำให้แน่ใจว่ามันไม่พลาด messages ใดๆ ที่มาถึงขณะที่ มันถูกตัดการเชื่อมต่อ เทคนิคเดียวกันนี้ใช้ได้กับผู้ใช้แต่ละคน โดยที่แต่ละอุปกรณ์ เป็นสมาชิกรายย่อยของสตรีม events ขนาดเล็ก

End-to-end event streams (Event streams แบบ end-to-end)

เครื่องมือสำหรับการพัฒนา stateful clients และ UIs เช่น React และ Elm [ 33 ] มีความสามารถในการอัปเดต UI ที่แสดงผลเพื่อตอบสนองต่อการเปลี่ยนแปลง ใน state ที่อยู่เบื้องหลัง มันคงเป็นธรรมชาติมากที่จะขยายโมเดลการเขียนโปรแกรมนี้ เพื่อให้เซิร์ฟเวอร์สามารถผลัก event การเปลี่ยนแปลง state ไปยัง client-side event pipeline นี้

การเปลี่ยนแปลง state สามารถไหลผ่าน write path แบบ end-to-end: จากการโต้ตอบบน อุปกรณ์หนึ่งที่ trigger การเปลี่ยนแปลง, ผ่าน event logs และระบบข้อมูลที่ derive ต่างๆ และ stream processors, ไปจนถึง user interface บนอุปกรณ์อื่น การเปลี่ยนแปลง state เหล่านี้สามารถ ถูกแพร่กระจายด้วยความล่าช้าค่อนข้างต่ำ—เช่น ต่ำกว่าหนึ่งวินาทีแบบ end to end

แอปพลิเคชันบางอย่าง เช่น instant messaging และเกมออนไลน์ มีสถาปัตยกรรม "real-time" ดังกล่าวอยู่แล้ว (ในแง่ของการโต้ตอบที่มีความล่าช้าต่ำ ไม่ใช่ในแง่ของการรับประกัน เวลาตอบสนอง) ทำไมเราไม่สร้างแอปพลิเคชันทั้งหมดด้วยวิธีนี้ล่ะ?

ความท้าทายคือสมมติฐานของ stateless clients และการโต้ตอบแบบ request/response ถูกฝังลึกอยู่ในฐานข้อมูล, libraries, frameworks, และโปรโตคอลของเรา ที่เก็บข้อมูลจำนวนมาก รองรับ read และ write operations ที่ request ส่งคืน response เดียว แต่น้อยกว่ามากที่รองรับ operations ที่ request ส่งคืนสตรีมของ responses เมื่อเวลาผ่านไป (เช่น ความสามารถในการสมัครรับการเปลี่ยนแปลง)

เพื่อขยาย write path ไปจนถึงผู้ใช้ปลายทาง เราจะต้องคิดใหม่โดยพื้นฐาน วิธีที่เราสร้างระบบเหล่านี้หลายระบบ ย้ายออกจากการโต้ตอบแบบ request/response และไปสู่ publish/subscribe dataflow [ 31 ] สิ่งนี้ต้องใช้ความพยายาม แต่มันจะมีข้อดีในการทำให้ UIs ตอบสนองมากขึ้น และให้การสนับสนุนออฟไลน์ที่ดีขึ้น

Reads are events too (Reads ก็เป็น events เช่นกัน)

เราได้พูดคุยว่าเมื่อ stream processor เขียนข้อมูลที่ derive ไปยัง store (database, cache, หรือ index) และ store นั้นถูก query, store ทำหน้าที่เป็นขอบเขตระหว่าง write path และ read path มันอนุญาตให้ random-access read queries กับข้อมูลที่มิฉะนั้นจะต้องสแกน event log ทั้งหมด

ในหลายกรณี datastore ถูกแยกจากระบบสตรีมมิ่ง อย่างไรก็ตาม จำไว้ว่า stream processors ยังต้องรักษาสถานะเพื่อดำเนินการ aggregations และ joins สถานะนี้โดยปกติ ถูกซ่อนอยู่ภายใน stream processor แต่บาง frameworks อนุญาตให้มันถูก query โดย clients ภายนอก [ 34 ] ทำให้ stream processor กลายเป็นฐานข้อมูลอย่างง่ายชนิดหนึ่ง

มาขยายแนวคิดนั้นกัน ในโมเดลที่เราได้พูดถึงจนถึงตอนนี้ writes ไปยัง store ผ่าน event log ในขณะที่ reads เป็น transient network requests ที่ไปยังโหนดที่เก็บ ข้อมูลที่ถูก query โดยตรง นี่คือการออกแบบที่สมเหตุสมผล แต่ไม่ใช่สิ่งเดียวที่เป็นไปได้ มันยังเป็นไปได้ที่จะแทน read requests เป็น streams ของ events และส่งทั้ง read events และ write events ผ่าน stream processor ตัวประมวลผลตอบสนองต่อ read events โดยการ emit ผลลัพธ์ของการอ่านไปยัง output stream [ 35 ]

เมื่อทั้ง writes และ reads ถูกแทนเป็น events และถูก route ไปยัง stream operator เดียวกัน เรากำลังทำ stream-table join ระหว่างสตรีมของ read queries และฐานข้อมูล แต่ละ read event ต้องถูกส่งไปยัง database shard ที่เก็บข้อมูลที่เกี่ยวข้อง เช่นเดียวกับ batch และ stream processors ที่ copartition inputs ด้วย key เดียวกันเมื่อทำ joins

ความสัมพันธ์ระหว่างการให้บริการ requests และการทำ joins นี้ค่อนข้างพื้นฐาน [ 36 ]. read request แบบครั้งเดียวผ่าน join operator ซึ่งจะลืม request ทันที; subscribe request เป็น persistent join กับ events ในอดีตและอนาคต ที่อีกด้านหนึ่งของ join

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

การเขียน read requests ไปยัง storage ที่ทนทานจึงช่วยให้การติดตามความสัมพันธ์เชิงสาเหตุ ดีขึ้น แต่มันทำให้เกิดต้นทุน storage และ I/O เพิ่มเติม การปรับระบบดังกล่าว เพื่อลด overhead ยังคงเป็นปัญหาการวิจัยที่เปิดอยู่ [ 2 ] แต่ถ้าคุณ log read requests เพื่อวัตถุประสงค์ในการดำเนินงาน เป็นผลข้างเคียงของการประมวลผล request มันก็ไม่ใช่การเปลี่ยนแปลงใหญ่ที่จะทำให้ log นั้น เป็นแหล่งของ requests แทน

Multishard data processing (การประมวลผลข้อมูลข้ามหลาย shards)

สำหรับ queries ที่สัมผัสเพียง shard เดียว ความพยายามในการส่งพวกมันผ่านสตรีมและรวบรวม สตรีมของ responses อาจมากเกินไป อย่างไรก็ตาม แนวคิดนี้เปิดโอกาสของการดำเนินการแบบกระจาย ของ complex queries ที่ต้องรวมข้อมูลจากหลาย shards โดยใช้ประโยชน์จาก โครงสร้างพื้นฐานสำหรับ message routing, sharding, และ joining ที่มีอยู่แล้วใน stream processors

ฟีเจอร์ distributed RPC ของ Storm รองรับรูปแบบการใช้งานนี้ ตัวอย่างเช่น มันถูกใช้ในการคำนวณ จำนวนคนที่เห็น URL บนเครือข่ายสังคม—นั่นคือ การรวมของ follower sets ของทุกคนที่โพสต์ URL นั้น [ 37 ] เมื่อชุดผู้ใช้ถูก shard การคำนวณนี้ต้องการการรวมผลลัพธ์จากหลาย shards

อีกตัวอย่างของ pattern นี้เกิดขึ้นในการป้องกันการฉ้อโกง เพื่อประเมินความเสี่ยงว่า purchase event เฉพาะเป็นการฉ้อโกงหรือไม่ คุณสามารถตรวจสอบคะแนนชื่อเสียงของ ที่อยู่ IP, ที่อยู่อีเมล, ที่อยู่สำหรับเรียกเก็บเงิน, ที่อยู่จัดส่ง และอื่นๆ ของผู้ใช้ ฐานข้อมูลชื่อเสียงแต่ละแห่งถูก shard ดังนั้นการรวบรวมคะแนนสำหรับ purchase event เฉพาะต้องใช้ลำดับของ joins กับ datasets ที่ถูก shard ต่างกัน [ 38 ]

กราฟการดำเนินการ query ภายในของ query engines ใน data warehouses มีลักษณะคล้ายกัน ถ้าคุณจำเป็นต้องทำ multishard join แบบนี้ มันอาจง่ายกว่าที่จะใช้ฐานข้อมูล ที่มีฟีเจอร์นี้แทนที่จะ implement มันโดยใช้ stream processor อย่างไรก็ตาม การปฏิบัติต่อ queries เป็น streams ให้ทางเลือกสำหรับการ implement แอปพลิเคชันขนาดใหญ่ที่ทำงาน กับข้อจำกัดของโซลูชันสำเร็จรูปทั่วไป

Aiming for Correctness (การมุ่งสู่ความถูกต้อง)

ด้วย stateless services ที่อ่านข้อมูลอย่างเดียว มันไม่ใช่เรื่องใหญ่ถ้ามีอะไรผิดพลาด; คุณสามารถแก้ไข bug และรีสตาร์ท service แล้วทุกอย่างก็กลับมาเป็นปกติ ระบบที่มี state เช่นฐานข้อมูลนั้นไม่ ง่ายนัก พวกมันถูกออกแบบมาให้จดจำสิ่งต่างๆ ตลอดไป (ไม่มากก็น้อย) ดังนั้นถ้ามีอะไรผิดพลาด ผลกระทบ ก็อาจคงอยู่ตลอดไปเช่นกัน—ซึ่งหมายความว่าพวกมันต้องการการคิดที่รอบคอบมากขึ้น [ 39 ]

เราต้องการสร้างแอปพลิเคชันที่เชื่อถือได้และ ถูกต้อง (เช่น โปรแกรมที่มี semantics ที่กำหนดไว้อย่างดีและเข้าใจได้ แม้ในกรณีที่เกิดความผิดพลาดต่างๆ) เป็นเวลาประมาณสี่ทศวรรษที่คุณสมบัติ transactions อย่าง atomicity, isolation, และ durability เป็นเครื่องมือที่ถูกเลือกใช้สำหรับการสร้างแอปพลิเคชันที่ถูกต้อง อย่างไรก็ตาม รากฐานเหล่านั้นอ่อนแอกว่าที่คิด: ตัวอย่างเช่น ความสับสนของ weak isolation levels (ดู "Weak Isolation Levels" )

ในบางพื้นที่ transactions ถูกยกเลิกโดยสิ้นเชิงและแทนที่ด้วยโมเดลที่ให้ประสิทธิภาพ และ scalability ที่ดีกว่าแต่ semantics ที่ยุ่งเหยิงกว่า Consistency มักถูกพูดถึงแต่ถูกนิยามไม่ดี บางคนยืนยันว่าเราควร "ยอมรับ weak consistency" เพื่อ ความพร้อมใช้งานที่ดีขึ้น ขณะที่ขาดความคิดที่ชัดเจนว่ามันหมายถึงอะไรในทางปฏิบัติ

สำหรับหัวข้อที่สำคัญขนาดนี้ ความเข้าใจและวิธีการทางวิศวกรรมของเรากลับไม่แน่นอน อย่างน่าประหลาดใจ ตัวอย่างเช่น มันยากมากที่จะพิจารณาว่าปลอดภัยหรือไม่ที่จะรันแอปพลิเคชัน เฉพาะโดยใช้ transaction isolation level หรือ replication configuration ระดับใดระดับหนึ่ง [ 40 , 41 ] บ่อยครั้งที่โซลูชันง่ายๆ ดูเหมือนทำงานได้ถูกต้องเมื่อ concurrency ต่ำและไม่มีข้อผิดพลาด แต่กลับกลายเป็นว่ามี bugs ที่ละเอียดอ่อนมากมายในสถานการณ์ที่ท้าทายกว่า

ตัวอย่างเช่น การทดลอง Jepsen ของ Kyle Kingsbury [ 42 ] ได้เน้นย้ำถึงความแตกต่างอย่างมากระหว่างการรับประกันความปลอดภัยที่อ้างของผลิตภัณฑ์บางตัว กับพฤติกรรมจริงเมื่อมีปัญหาเครือข่ายและ crashes แม้ว่าผลิตภัณฑ์โครงสร้างพื้นฐานเช่นฐานข้อมูล จะปราศจากปัญหา โค้ดแอปพลิเคชันก็ยังต้องใช้ฟีเจอร์ที่พวกมันให้อย่างถูกต้อง ซึ่งมีแนวโน้มที่จะเกิดข้อผิดพลาดถ้าการตั้งค่ายากที่จะเข้าใจ (เช่นในกรณีของ weak isolation levels, quorum configurations และอื่นๆ)

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

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

The End-to-End Argument for Databases (ข้อโต้แย้งแบบ End-to-End สำหรับฐานข้อมูล)

เพียงเพราะแอปพลิเคชันใช้ระบบข้อมูลที่ให้คุณสมบัติความปลอดภัยที่ค่อนข้างแข็งแกร่ง เช่น serializable transactions ก็ไม่ได้หมายความว่าแอปพลิเคชันจะปลอดภัยจากการสูญเสียหรือ การปนเปื้อนของข้อมูล ตัวอย่างเช่น ถ้าแอปพลิเคชันมี bug ที่ทำให้มันเขียนข้อมูลที่ไม่ถูกต้องหรือลบ ข้อมูลจากฐานข้อมูล serializable transactions จะไม่ช่วยคุณ นี่คือข้อโต้แย้งที่สนับสนุน ข้อมูลแบบ immutable และ append-only เพราะมันง่ายกว่าที่จะกู้คืนจากความผิดพลาดดังกล่าว ถ้าคุณลบความสามารถของโค้ดที่ผิดพลาดในการทำลายข้อมูลที่ดี

แม้ว่า immutability จะมีประโยชน์ แต่ก็ไม่ใช่ยาวิเศษด้วยตัวมันเอง มาดูตัวอย่างที่ละเอียดอ่อนกว่า ของวิธีการปนเปื้อนข้อมูลที่สามารถเกิดขึ้นได้

Exactly-once execution of an operation (การดำเนินการแบบ Exactly-once)

ใน "Distributed Transactions ข้ามระบบที่แตกต่างกัน" เราได้แนะนำแนวคิดของ exactly-once (หรือ effectively-once ) semantics ในบริบทของการประมวลผล message แนวคิดคือ: ถ้ามีอะไรผิดพลาดระหว่าง การประมวลผล message คุณสามารถยอมแพ้ (โดยการทิ้ง message และทำให้เกิดการสูญเสียข้อมูล) หรือลองอีกครั้ง ถ้าคุณลองอีกครั้ง มีความเสี่ยงที่การประมวลผลสำเร็จในครั้งแรกแล้ว และคุณแค่ไม่ได้รับการยืนยัน ดังนั้น message จะถูกประมวลผลสองครั้ง

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

หนึ่งในแนวทางที่มีประสิทธิภาพมากที่สุดคือการทำให้การดำเนินการ idempotent —นั่นคือ ทำให้แน่ใจว่ามันมีผลเหมือนกัน ไม่ว่าจะถูกดำเนินการครั้งเดียวหรือหลายครั้ง อย่างไรก็ตาม การทำเช่นนี้สำหรับการดำเนินการที่ไม่ได้เป็น idempotent โดยธรรมชาติต้องใช้ ความพยายามและความระมัดระวัง คุณอาจต้องรักษา metadata เพิ่มเติม (เช่นชุดของ operation IDs ที่อัปเดตค่า) และทำให้แน่ใจว่ามี fencing เมื่อ fail over จากโหนดหนึ่งไปยังอีกโหนดหนึ่ง (ดู "Distributed Locks และ Leases" )

Duplicate suppression (การระงับข้อมูลซ้ำ)

รูปแบบเดียวกันของความจำเป็นในการระงับ duplicates เกิดขึ้นในที่อื่นๆ มากมาย นอกเหนือจาก stream processing ตัวอย่างเช่น TCP ใช้ sequence numbers บน packets เพื่อจัดลำดับที่ถูกต้องที่ผู้รับและเพื่อตรวจสอบว่า packets ใดสูญหายหรือซ้ำซ้อนบนเครือข่าย packets ที่สูญหายจะถูกส่งซ้ำ และ duplicates จะถูกลบออกโดย TCP stack ก่อนที่มันจะส่ง ข้อมูลให้แอปพลิเคชัน

อย่างไรก็ตาม การระงับ duplicates นี้ทำงานเฉพาะในบริบทของ TCP connection เดียว ลองนึกภาพว่า TCP connection คือการเชื่อมต่อของ client ไปยังฐานข้อมูล และกำลังดำเนินการ transaction ใน ตัวอย่างที่ 13-1 . ในฐานข้อมูลหลายตัว transaction ผูกกับ client connection (ถ้า client ส่ง queries หลายตัว ฐานข้อมูลรู้ว่าพวกมันอยู่ใน transaction เดียวกัน เพราะพวกมันถูกส่งบน TCP connection เดียวกัน) ถ้า client ประสบปัญหา network interruption และ connection timeout หลังจากส่ง COMMIT แต่ก่อนที่จะได้รับการตอบกลับจากเซิร์ฟเวอร์ฐานข้อมูล มันไม่รู้ว่า transaction ถูก commit หรือ abort แล้ว (เราเห็นสถานการณ์นี้ใน รูปที่ 9-1 )

Example 13-1. การโอนเงินแบบไม่ idempotent จากบัญชีหนึ่งไปยังอีกบัญชีหนึ่ง
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

Client สามารถ reconnect ไปยังฐานข้อมูลและลองทำ transaction อีกครั้ง แต่ตอนนี้มันอยู่นอก ขอบเขตของการระงับ duplicates ของ TCP เนื่องจาก transaction ใน ตัวอย่างที่ 13-1 ไม่ได้เป็น idempotent จึงอาจมีการโอน $22 แทนที่จะเป็น $11 ที่ต้องการ ดังนั้น แม้ว่าโค้ดแบบนี้จะเป็นตัวอย่างมาตรฐานสำหรับ atomicity ของ transaction มันก็ไม่ถูกต้อง และธนาคารจริงไม่ได้ทำงานแบบนี้ [ 3 ]

โปรโตคอล 2PC (ดู "Two-Phase Commit" ) ทำลายการแมปแบบ one-to-one ระหว่าง TCP connection และ transaction เพราะพวกมันต้องอนุญาตให้ transaction coordinator reconnect ไปยังฐานข้อมูล หลังจาก network fault และบอกมันว่าจะ commit หรือ abort transaction ที่ค้างอยู่ เพียงพอหรือไม่ที่จะรับประกันว่า transaction จะถูกดำเนินการเพียงครั้งเดียว? น่าเสียดายที่ ไม่

แม้ว่าเราจะสามารถระงับ duplicate transactions ระหว่าง database client และ server เรายังต้องกังวลเกี่ยวกับเครือข่ายระหว่างอุปกรณ์ผู้ใช้ปลายทางและ application server ตัวอย่างเช่น ถ้า client ผู้ใช้ปลายทางเป็นเว็บเบราว์เซอร์ มันอาจใช้ HTTP POST request เพื่อส่งคำสั่งไปยังเซิร์ฟเวอร์ บางทีผู้ใช้อาจมีการเชื่อมต่อข้อมูลเซลลูลาร์ ที่อ่อน และพวกเขาส่ง POST request สำเร็จ แต่พวกเขาสูญเสียสัญญาณก่อนที่จะได้รับการตอบกลับจากเซิร์ฟเวอร์

ในกรณีนี้ ผู้ใช้จะเห็นข้อความแสดงข้อผิดพลาดและอาจลองอีกครั้งด้วยตนเอง เว็บเบราว์เซอร์เตือน "คุณแน่ใจหรือว่าต้องการส่งฟอร์มนี้อีกครั้ง?"—และผู้ใช้ตอบว่าใช่ เพราะพวกเขาต้องการให้การดำเนินการ เกิดขึ้น (รูปแบบ Post/Redirect/Get [ 43 ] หลีกเลี่ยงข้อความเตือนนี้ในการทำงานปกติ แต่มันไม่ช่วยถ้า POST request timeout) จากมุมมองของเว็บเซิร์ฟเวอร์ การลองใหม่เป็น request แยกต่างหาก และจากมุมมองของฐานข้อมูล มันเป็น transaction แยกต่างหาก กลไกการ deduplication ปกติไม่ช่วย

Uniquely identifying requests (การระบุ requests ด้วย identifier ที่ไม่ซ้ำ)

เพื่อทำให้ request idempotent ผ่านหลาย hops ของการสื่อสารเครือข่าย การพึ่งพา กลไก transaction ที่ฐานข้อมูลให้มานั้นไม่เพียงพอ คุณต้องพิจารณาการไหล end-to-end ของ request

ตัวอย่างเช่น คุณสามารถสร้าง unique identifier สำหรับแต่ละ request (เช่น UUID) และรวมมันเป็นฟิลด์ฟอร์มที่ซ่อนอยู่ใน client application หรือคำนวณ hash ของฟิลด์ฟอร์มที่เกี่ยวข้องทั้งหมดเพื่อ derive request ID [ 3 ] ถ้าเว็บเบราว์เซอร์ส่ง POST request สองครั้ง request ทั้งสองจะมี request ID เดียวกัน คุณสามารถส่ง request ID นั้นไปจนถึงฐานข้อมูล และตรวจสอบว่าคุณ ดำเนินการเพียง request เดียวด้วย ID ที่กำหนด ดังที่แสดงใน ตัวอย่างที่ 13-2 .

Example 13-2. การระงับ request ซ้ำโดยใช้ unique ID
ALTER TABLE requests ADD UNIQUE (request_id);

BEGIN TRANSACTION;

INSERT INTO requests
  (request_id, from_account, to_account, amount)
  VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00);

UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;

COMMIT;

โค้ดนี้พึ่งพา uniqueness constraint บนคอลัมน์ request_id . ถ้า transaction พยายาม insert ID ที่มีอยู่แล้ว INSERT จะล้มเหลวและ transaction จะถูก abort ป้องกันไม่ให้มีผลสองครั้ง Relational databases โดยทั่วไปสามารถรักษา uniqueness constraint ได้อย่างถูกต้อง แม้ใน weak isolation levels (ในขณะที่การ check-then-insert ในระดับแอปพลิเคชันอาจล้มเหลวภายใต้ nonserializable isolation ดังที่กล่าวถึงใน "Write Skew และ Phantoms" )

นอกจากการระงับ request ซ้ำแล้ว ตาราง requests ใน ตัวอย่างที่ 13-2 ยังทำหน้าที่เป็น event log ชนิดหนึ่ง ซึ่งมีประโยชน์สำหรับ event sourcing หรือ CDC การอัปเดตยอดคงเหลือในบัญชีไม่จำเป็นต้องเกิดขึ้นใน transaction เดียวกับการแทรก event เนื่องจากพวกมันซ้ำซ้อนและสามารถ derive จาก request event ได้ใน consumer ปลายทาง—ตราบใดที่ event ถูกประมวลผล exactly once ซึ่งสามารถบังคับใช้อีกครั้ง โดยใช้ request ID

The end-to-end argument (ข้อโต้แย้งแบบ end-to-end)

สถานการณ์ของการระงับ duplicate transactions นี้เป็นเพียงตัวอย่างหนึ่งของหลักการทั่วไปที่ เรียกว่า end-to-end argument ซึ่งถูกอธิบายโดย Saltzer, Reed, และ Clark ในปี 1984 [ 44 ]:

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

ในตัวอย่างของเรา ฟังก์ชันที่เป็นปัญหา คือการระงับ duplicates เราเห็นว่า TCP ระงับ duplicate packets ในระดับ TCP connection และ stream processors บางตัวให้สิ่งที่เรียกว่า exactly-once semantics ในระดับการประมวลผล message แต่นั่นไม่เพียงพอที่จะป้องกันผู้ใช้จากการส่ง duplicate request ถ้า request แรก timeout โดยตัวของมันเอง TCP, database transactions, และ stream processors ไม่สามารถกำจัด duplicates เหล่านี้ได้อย่างสมบูรณ์ การแก้ปัญหาต้องใช้โซลูชันแบบ end-to-end: transaction identifier ที่ถูกส่งจาก client ผู้ใช้ปลายทางไปจนถึงฐานข้อมูล

ข้อโต้แย้งแบบ end-to-end ยังใช้กับการตรวจสอบความสมบูรณ์ของข้อมูล Checksums ที่สร้างใน Ethernet, TCP, และ TLS สามารถตรวจจับการปนเปื้อนของ packets ในเครือข่าย แต่พวกมันไม่สามารถตรวจจับการปนเปื้อนเนื่องจาก bugs ในซอฟต์แวร์ที่ปลายส่งและปลายรับ ของการเชื่อมต่อเครือข่าย หรือการปนเปื้อนบนดิสก์ที่เก็บข้อมูล ถ้าคุณต้องการจับ แหล่งที่มาของการปนเปื้อนข้อมูลทั้งหมดที่เป็นไปได้ คุณต้องมี end-to-end checksums

ข้อโต้แย้งที่คล้ายกันใช้กับการเข้ารหัส [ 44 ] รหัสผ่านบนเครือข่าย WiFi ในบ้านของคุณป้องกันไม่ให้คนดักฟัง การรับส่งข้อมูล WiFi ของคุณ แต่ไม่ใช่ผู้โจมตีที่อื่นบนอินเทอร์เน็ต; TLS/SSL ระหว่าง client และเซิร์ฟเวอร์ของคุณป้องกันผู้โจมตีเครือข่าย แต่ไม่ใช่การบุกรุกเซิร์ฟเวอร์ มีเพียง end-to-end encryption และ authentication เท่านั้นที่สามารถป้องกันสิ่งเหล่านี้ทั้งหมดได้

แม้ว่าคุณสมบัติระดับต่ำ (TCP duplicate suppression, Ethernet checksums, WiFi encryption) ไม่สามารถให้คุณสมบัติ end-to-end ที่ต้องการได้ด้วยตัวเอง พวกมันยังคงมีประโยชน์ เพราะพวกมันลดความน่าจะเป็นของปัญหาในระดับที่สูงกว่า ตัวอย่างเช่น HTTP requests มักจะเสียหายถ้าเราไม่มี TCP ที่จัด packets กลับเข้าลำดับที่ถูกต้อง เราแค่ต้องจำไว้ว่า คุณสมบัติความน่าเชื่อถือระดับต่ำนั้นไม่เพียงพอด้วยตัวเองในการรับประกันความถูกต้องแบบ end-to-end

Applying end-to-end thinking in data systems (การใช้ความคิดแบบ end-to-end ในระบบข้อมูล)

สิ่งนี้พาเรากลับไปยังวิทยานิพนธ์ดั้งเดิม: เพียงเพราะแอปพลิเคชันใช้ระบบข้อมูลที่ให้ คุณสมบัติความปลอดภัยที่ค่อนข้างแข็งแกร่ง เช่น serializable transactions ก็ไม่ได้หมายความว่า แอปพลิเคชันจะปลอดภัยจากการสูญเสียหรือการปนเปื้อนของข้อมูล แอปพลิเคชันเองต้อง ดำเนินมาตรการ end-to-end เช่นการระงับ duplicates ด้วย

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

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

Transactions มีราคาแพง โดยเฉพาะเมื่อพวกมันเกี่ยวข้องกับเทคโนโลยีการจัดเก็บที่ต่างกัน (ดู "Distributed Transactions ข้ามระบบที่แตกต่างกัน" ) เมื่อเราปฏิเสธที่จะใช้ distributed transactions เพราะพวกมันแพงเกินไป เราก็ต้อง implement กลไกการทนต่อความผิดพลาดใน application code ขึ้นมาใหม่ ดังที่ตัวอย่างมากมายตลอดทั้งเล่มนี้ได้แสดงให้เห็น การใช้เหตุผลเกี่ยวกับ concurrency และ partial failure นั้นยากและขัดกับ intuition ดังนั้นกลไกระดับแอปพลิเคชันส่วนใหญ่ จึงไม่ทำงานอย่างถูกต้อง ผลที่ตามมาคือข้อมูลที่สูญหายหรือปนเปื้อน

ด้วยเหตุผลเหล่านี้ มันคุ้มค่าที่จะสำรวจ abstractions การทนต่อความผิดพลาดที่ทำให้ง่าย ในการให้คุณสมบัติความถูกต้องแบบ end-to-end ที่เฉพาะกับแอปพลิเคชัน แต่ยังรักษาประสิทธิภาพที่ดีและลักษณะการดำเนินงานในสภาพแวดล้อมแบบกระจายขนาดใหญ่

Enforcing Constraints (การบังคับใช้ข้อจำกัด)

มาคิดถึงความถูกต้องในบริบทของแนวคิดเกี่ยวกับ unbundling databases เราเห็นว่า การระงับ duplicate แบบ end-to-end สามารถทำได้ด้วย request ID ที่ถูกส่งจาก client ไปยังฐานข้อมูลที่บันทึกการเขียน แล้วข้อจำกัดประเภทอื่นๆ ล่ะ?

โดยเฉพาะอย่างยิ่ง มาสนใจ uniqueness constraints เช่นที่เราพึ่งพาใน ตัวอย่างที่ 13-2 . ใน "ข้อจำกัดและการรับประกันความเป็นเอกลักษณ์" เราเห็นตัวอย่างอื่นๆ ของฟีเจอร์แอปพลิเคชันที่ต้องบังคับใช้ความเป็นเอกลักษณ์: ชื่อผู้ใช้หรือที่อยู่อีเมลต้องระบุผู้ใช้ได้อย่างไม่ซ้ำ, บริการจัดเก็บไฟล์ไม่สามารถมีไฟล์ ที่มีชื่อเดียวกันมากกว่าหนึ่งไฟล์, และคนสองคนไม่สามารถจองที่นั่งเดียวกันบนเที่ยวบินหรือในโรงละคร

ข้อจำกัดประเภทอื่นก็คล้ายกันมาก—ตัวอย่างเช่น การรับประกันว่ายอดคงเหลือในบัญชี ไม่มีวันติดลบ, การไม่ขายสินค้ามากกว่าที่มีในสต็อกในคลังสินค้า, หรือห้องประชุม ไม่มีการจองที่ทับซ้อนกัน เทคนิคที่บังคับใช้ความเป็นเอกลักษณ์มักสามารถใช้ สำหรับข้อจำกัดประเภทนี้ได้เช่นกัน

Uniqueness constraints require consensus (Uniqueness constraints ต้องการ consensus)

ใน บทที่ 10 เราเห็นว่าในสภาพแวดล้อมแบบกระจาย การบังคับใช้ uniqueness constraint ต้องใช้ consensus ถ้า requests ที่เกิดขึ้นพร้อมกันหลายตัวมีค่าเดียวกัน ระบบต้องตัดสินใจ ว่าการดำเนินการที่ขัดแย้งกันตัวใดถูกยอมรับ และปฏิเสธตัวอื่นว่าเป็นการละเมิดข้อจำกัด

วิธีที่พบบ่อยที่สุดในการบรรลุ consensus นี้คือการทำให้โหนดเดียวเป็น leader และรับผิดชอบในการตัดสินใจทั้งหมด นั่นใช้ได้ดีตราบใดที่คุณไม่รังเกียจที่จะส่ง requests ทั้งหมดผ่านโหนดเดียว (แม้ว่า client จะอยู่อีกซีกโลก) และตราบใดที่โหนดนั้น ไม่ล้มเหลว อัลกอริธึม consensus เช่น Raft จัดการปัญหาของการเลือก leader ใหม่อย่างปลอดภัยถ้า leader ปัจจุบันล้มเหลว (หรือเชื่อว่าล้มเหลวเนื่องจากปัญหาเครือข่าย) และป้องกัน split brain

การตรวจสอบความเป็นเอกลักษณ์สามารถขยายขนาดได้โดยการ sharding ตามค่าที่ต้องไม่ซ้ำ ตัวอย่างเช่น ถ้าคุณต้องรับประกันความเป็นเอกลักษณ์ตาม request ID ดังใน ตัวอย่างที่ 13-2 , คุณสามารถทำให้แน่ใจว่า requests ทั้งหมดที่มี request ID เดียวกันถูก route ไปยัง shard เดียวกัน ถ้าคุณต้องการให้ชื่อผู้ใช้ไม่ซ้ำ คุณสามารถ shard โดย hash ของชื่อผู้ใช้

อย่างไรก็ตาม asynchronous multi-leader replication ไม่สามารถใช้ได้ เพราะ leader ที่แตกต่างกันอาจยอมรับ writes ที่ขัดแย้งกันพร้อมกัน และทำให้ค่าไม่ซ้ำอีกต่อไป ถ้าคุณต้องการปฏิเสธ writes ที่ละเมิดข้อจำกัด ทันที การประสานงานแบบ synchronous เป็นสิ่งที่หลีกเลี่ยงไม่ได้ [ 45 ]

Uniqueness in log-based messaging (ความเป็นเอกลักษณ์ใน messaging ที่ใช้ log)

log ที่ใช้ร่วมกันทำให้แน่ใจว่า consumers ทั้งหมดเห็น messages ในลำดับเดียวกัน (การรับประกัน total order broadcast ซึ่งดังที่เราได้กำหนดใน "The Many Faces of Consensus" , เทียบเท่ากับ consensus) ในแนวทาง unbundled database ที่ใช้ log-based messaging เราสามารถใช้แนวทางที่คล้ายกันมากในการบังคับใช้ uniqueness constraints

Stream processor consume messages ทั้งหมดใน log shard ตามลำดับบนเธรดเดียว ดังนั้น ถ้า log ถูก shard ตามค่าที่ต้องไม่ซ้ำ stream processor สามารถตัดสินใจอย่าง unambiguous และ deterministic ว่าการดำเนินการที่ขัดแย้งกันตัวใด มาก่อนใน log ตัวอย่างเช่น ในกรณีของผู้ใช้หลายคนพยายามขอชื่อผู้ใช้เดียวกัน [ 46 ]:

  1. ทุก request สำหรับชื่อผู้ใช้จะถูกเข้ารหัสเป็น message และต่อท้าย shard ที่กำหนดโดย hash ของชื่อผู้ใช้

  2. Stream processor อ่าน requests ใน log ตามลำดับ โดยใช้ฐานข้อมูลท้องถิ่น เพื่อติดตามว่าชื่อผู้ใช้ใดถูกใช้ไปแล้ว สำหรับทุก request สำหรับชื่อผู้ใช้ที่ว่าง มันบันทึกชื่อว่าถูกใช้และ emit message ความสำเร็จไปยัง output stream สำหรับทุก request สำหรับชื่อผู้ใช้ที่ถูกใช้แล้ว มัน emit message การปฏิเสธ ไปยัง output stream

  3. Client ที่ขอชื่อผู้ใช้จะดู output stream และรอ message ความสำเร็จ หรือการปฏิเสธที่สอดคล้องกับ request ของมัน

อัลกอริธึมนี้เหมือนกับการสร้างเพื่อให้ได้ consensus โดยใช้ shared log ที่เราเห็นใน บทที่ 10 . มันปรับขนาดได้ง่ายสำหรับปริมาณ request ที่มากโดยการเพิ่มจำนวน shards เนื่องจากแต่ละ shard สามารถประมวลผลได้อย่างอิสระ

แนวทางนี้ใช้ได้ไม่เฉพาะกับ uniqueness constraints แต่ยังรวมถึงข้อจำกัดประเภทอื่นๆ อีกมากมาย หลักการพื้นฐานของมันคือ writes ที่อาจขัดแย้งกันจะถูก route ไปยัง shard เดียวกันและประมวลผลตามลำดับ คำจำกัดความของความขัดแย้งอาจขึ้นอยู่กับแอปพลิเคชัน แต่ stream processor สามารถใช้ logic ตามอำเภอใจในการตรวจสอบความถูกต้องของ request

Multishard request processing (การประมวลผล request ข้ามหลาย shards)

การรับประกันว่าการดำเนินการถูกดำเนินการแบบ atomic ในขณะที่ยังคงเป็นไปตามข้อจำกัด จะน่าสนใจมากขึ้นเมื่อมีหลาย shards เข้ามาเกี่ยวข้อง ใน ตัวอย่างที่ 13-2 , มีสาม shards ที่เป็นไปได้: อันหนึ่งมี request ID, อันหนึ่งมีบัญชีผู้รับ, และอันหนึ่งมีบัญชีผู้จ่าย ไม่มีเหตุผลที่สิ่งเหล่านี้ต้องอยู่ใน shard เดียวกัน เนื่องจากพวกมันเป็นอิสระจากกัน

ในแนวทางดั้งเดิมของฐานข้อมูล การดำเนิน transaction นี้จะต้องใช้ atomic commit ข้ามทั้งสาม shards ซึ่งโดยเนื้อแท้แล้วบังคับมันให้อยู่ใน total order เมื่อเทียบกับ transactions อื่นๆ ทั้งหมดบน shards เหล่านั้น เนื่องจาก ตอนนี้มีการประสานงานข้าม shard shards ที่แตกต่างกันจึงไม่สามารถประมวลผลได้อย่างอิสระ อีกต่อไป ดังนั้น throughput มักจะลดลง

อย่างไรก็ตาม ความถูกต้องที่เทียบเท่าสามารถทำได้โดยไม่ต้องมี cross-shard transactions โดยใช้ sharded logs และ stream processors รูปที่ 13-2 แสดงตัวอย่างของ payment transaction ที่ต้องตรวจสอบว่าบัญชีต้นทาง มีเงินเพียงพอ และถ้าใช่ ให้โอนจำนวนเงินไปยังบัญชีปลายทางแบบ atomic พร้อมหักค่าธรรมเนียม

Diagram illustrating the process of transferring money from a source account to a destination and fees account using event logs and stream processors.

Figure 13-2. การตรวจสอบว่าบัญชีต้นทางมีเงินเพียงพอและการโอนเงินแบบ atomic ไปยัง บัญชีปลายทางและบัญชีค่าธรรมเนียม โดยใช้ event logs และ stream processors

กระบวนการนี้ทำงานดังต่อไปนี้ [ 47 ]:

  1. Request ที่จะโอนเงินจากบัญชีต้นทางไปยังบัญชีปลายทางจะได้รับ unique request ID จาก client ของผู้ใช้และต่อท้าย log shard ตาม ID บัญชีต้นทาง

  2. Stream processor อ่าน log ของ requests และรักษาฐานข้อมูลที่มีสถานะของ บัญชีต้นทางและ IDs ของ requests ที่มันได้ประมวลผลแล้ว เนื้อหาของฐานข้อมูลนี้ derive จาก log ทั้งหมด เมื่อ stream processor พบ request ที่มี ID ที่มันไม่เคยเห็นมาก่อน มันตรวจสอบในฐานข้อมูลท้องถิ่นว่าบัญชีต้นทางมีเงินเพียงพอ สำหรับการโอน

    ถ้าใช่ มันอัปเดตฐานข้อมูลท้องถิ่นเพื่อสำรองจำนวนเงินที่ชำระในบัญชีต้นทาง และ emit events ไปยัง logs อื่นๆ หลายตัว: outgoing payment event ไปยัง log shard สำหรับบัญชีต้นทาง (input log ของตัวเอง), incoming payment event ไปยัง log shard สำหรับบัญชีปลายทาง, และ incoming payment event ไปยัง log shard สำหรับบัญชีค่าธรรมเนียม request ID ดั้งเดิมถูกรวมอยู่ใน events ที่ emit

  3. ในที่สุด outgoing payment event ถูกส่งกลับไปยัง source account processor (ซึ่งอาจได้รับ events ที่ไม่เกี่ยวข้องในระหว่างนั้น) stream processor จดจำตาม request ID ว่ามันเป็น payment ที่มันสำรองไว้ก่อนหน้านี้ และดำเนินการ payment โดยอัปเดตสถานะท้องถิ่นสำหรับบัญชีต้นทาง มันละเว้น duplicates ตาม request ID

  4. Log shards สำหรับบัญชีปลายทางและค่าธรรมเนียมถูก consume โดย stream processing tasks อิสระ เมื่อพวกเขาได้รับ incoming payment event พวกเขาอัปเดตสถานะท้องถิ่นเพื่อสะท้อนการ payment และ deduplicate events ตาม request ID

รูปที่ 13-2 แสดงสามบัญชีในสาม shards แยกกัน แต่พวกมันสามารถอยู่ใน shard เดียวกันได้—มันไม่สำคัญ ข้อกำหนดเดียวคือ events สำหรับบัญชีใดๆ ถูกประมวลผล ตามลำดับ log อย่างเคร่งครัดด้วย at-least-once semantics และ stream processors เป็น deterministic

ตัวอย่างเช่น พิจารณาสิ่งที่เกิดขึ้นถ้า source account processor crash ขณะประมวลผล payment request messages output อาจถูก emit หรือไม่ก่อนที่ crash จะเกิดขึ้น หลังจากกู้คืนจาก crash processor จะประมวลผล request เดียวกันอีกครั้ง (เนื่องจาก at-least-once semantics) และมันจะตัดสินใจ เหมือนกันว่าจะอนุญาต payment หรือไม่ (เนื่องจากมันเป็น deterministic) มันจะ emit messages output เดียวกันด้วย request ID เดียวกันไปยัง outgoing, incoming, และ fees account shards ถ้า messages ซ้ำกัน consumers ปลายทางจะละเว้นพวกมันตาม request ID

Atomicity ในระบบนี้ไม่ได้มาจาก transactions แต่มาจากข้อเท็จจริงที่ การเขียน event request เริ่มต้นไปยัง source account log เป็น atomic action เมื่อ event หนึ่งอยู่ใน log events ปลายทางทั้งหมดจะถูกเขียน ในที่สุด—อาจหลังจาก stream processors กู้คืนจาก crashes และอาจมี duplicates แต่พวกมันจะปรากฏในที่สุด

ด้วย exactly-once semantics ตัวอย่างนี้จะง่ายต่อการ implement เนื่องจาก semantics รับประกันว่าสถานะท้องถิ่นของ stream processor สอดคล้องกับชุดของ messages ที่มันประมวลผล ดังนั้นถ้ามัน crash และประมวลผล messages บางส่วนซ้ำ สถานะท้องถิ่นของมันก็จะถูกรีเซ็ตเป็น สิ่งที่มันเป็นก่อนที่ messages เหล่านั้นจะถูกประมวลผล

ถ้าผู้ใช้ใน รูปที่ 13-2 ต้องการทราบว่าการโอนของพวกเขาถูกอนุมัติหรือไม่ พวกเขาสามารถสมัครรับ source account log shard และรอ outgoing payment event เพื่อแจ้งให้ผู้ใช้ ทราบอย่างชัดเจนถ้ายอดคงเหลือไม่เพียงพอ stream processor สามารถ emit event "ชำระเงินถูกปฏิเสธ" ไปยัง log shard นั้น

โดยการแบ่ง multishard transaction ออกเป็นหลายขั้นตอนที่ถูก shard ต่างกัน และใช้ end-to-end request ID เราบรรลุคุณสมบัติความถูกต้องเดียวกัน (ทุก request ถูกใช้ exactly once กับทั้งบัญชีผู้จ่ายและผู้รับ) แม้ในกรณีที่เกิดความผิดพลาด โดยไม่ต้องใช้ atomic commit protocol

Timeliness and Integrity (ความทันเวลาและความสมบูรณ์)

คุณสมบัติที่สะดวกของระบบ transactional หลายระบบคือทันทีที่ transaction commit writes ของมันจะปรากฏให้ transactions อื่นเห็นทันที คุณสมบัตินี้ถูกทำให้เป็นทางการเป็น strict serializability (กล่าวถึงใน "Linearizability กับ Serializability" )

นี่ไม่ใช่กรณีเมื่อ unbundling การดำเนินการข้ามหลายขั้นตอนของ stream processors Consumers ของ log เป็น asynchronous โดยการออกแบบ ดังนั้น producer ไม่ได้รอจนกว่า message ของมันจะถูกประมวลผลโดย consumers อย่างไรก็ตาม มันเป็นไปได้สำหรับ client ที่จะรอให้ message ปรากฏบน output stream เช่นผู้ใช้ที่รอ outgoing payment หรือ payment declined event ใน รูปที่ 13-2 ซึ่งขึ้นอยู่กับว่ามีเงินเพียงพอในบัญชีต้นทางหรือไม่

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

โดยทั่วไป คำว่า consistency รวมข้อกำหนดสองอย่างที่ควรพิจารณาแยกกัน:

Timeliness (ความทันเวลา)

Timeliness หมายถึงการทำให้แน่ใจว่าผู้ใช้สังเกตระบบในสถานะที่ทันสมัย เราเห็นก่อนหน้านี้ว่า ถ้าผู้ใช้อ่านจากสำเนาข้อมูลที่เก่า พวกเขาอาจสังเกตมันในสถานะที่ไม่สอดคล้องกัน (ดู "ปัญหากับ Replication Lag" ) อย่างไรก็ตาม ความไม่สอดคล้องนั้นเป็นชั่วคราว และในที่สุดมันจะถูกแก้ไขโดยการรอและลองอีกครั้ง

CAP theorem ใช้ "consistency" ในความหมายของ linearizability ซึ่งเป็นวิธีที่แข็งแกร่ง ในการบรรลุ timeliness คุณสมบัติ timeliness ที่อ่อนแอกว่า เช่น read-after-write consistency ก็มีประโยชน์เช่นกัน

Integrity (ความสมบูรณ์)

Integrity หมายถึงการไม่มีการปนเปื้อน—ไม่มีการสูญเสียข้อมูล และไม่มีข้อมูลที่ขัดแย้งหรือเท็จ โดยเฉพาะอย่างยิ่ง ถ้า dataset ที่ derive ถูกรักษาเป็น view ของข้อมูลต้นทาง การ derive ต้องถูกต้อง ตัวอย่างเช่น database index ต้องสะท้อนเนื้อหาของฐานข้อมูลอย่างถูกต้อง—index ที่มี records หายไปนั้นไม่มีประโยชน์มาก

ถ้า integrity ถูกละเมิด ความไม่สอดคล้องนั้นเป็นถาวร; การรอและลองอีกครั้ง จะไม่แก้ไขการปนเปื้อนของฐานข้อมูลในกรณีส่วนใหญ่ แต่ต้องมีการตรวจสอบและซ่อมแซมอย่างชัดแจ้ง ในบริบทของ ACID transactions "consistency" มักถูกเข้าใจว่าเป็นแนวคิด เรื่องความสมบูรณ์ที่เฉพาะกับแอปพลิเคชัน Atomicity และ durability เป็นเครื่องมือสำคัญ สำหรับการรักษา integrity

ในรูปแบบสโลแกน: การละเมิด timeliness อนุญาตภายใต้ eventual consistency ในขณะที่การละเมิด integrity ส่งผลให้เกิดความไม่สอดคล้องถาวร

ในแอปพลิเคชันส่วนใหญ่ integrity สำคัญกว่า timeliness มาก การละเมิด timeliness อาจน่ารำคาญและทำให้สับสน แต่การละเมิด integrity อาจเป็นหายนะ

ตัวอย่างเช่น ในใบแจ้งยอดบัตรเครดิตของคุณ มันไม่น่าแปลกใจถ้า transaction ที่คุณทำภายใน 24 ชั่วโมงที่ผ่านมายังไม่ปรากฏ เป็นเรื่องปกติที่ระบบเหล่านี้ มีความล่าช้าอยู่บ้าง เรารู้ว่าธนาคาร reconcile และ settle transactions แบบ asynchronous และ timeliness ไม่สำคัญมากที่นี่ [ 3 ] อย่างไรก็ตาม มันจะแย่มากถ้ายอดคงเหลือในใบแจ้งยอด ไม่เท่ากับผลรวมของ transactions บวกยอดคงเหลือในใบแจ้งยอดก่อนหน้า (ข้อผิดพลาดในการรวม) หรือถ้า transaction ถูกเรียกเก็บจากคุณแต่ไม่ได้ จ่ายให้กับร้านค้า (เงินหายไป) ปัญหาดังกล่าวจะเป็นการละเมิด integrity ของระบบ

Correctness of dataflow systems (ความถูกต้องของระบบ dataflow)

ACID transactions โดยทั่วไปให้ทั้ง timeliness (เช่น linearizability) และ integrity (เช่น atomic commit) ดังนั้นถ้าคุณเข้าใกล้ ความถูกต้องของแอปพลิเคชันจากมุมมองของ ACID transactions ความแตกต่างระหว่าง timeliness และ integrity ก็ไม่ค่อยมีความสำคัญ

ในทางกลับกัน คุณสมบัติที่น่าสนใจของระบบ dataflow ที่ใช้ event ที่เราได้พูดถึงในบทนี้คือพวกมันแยก timeliness และ integrity ออกจากกัน เมื่อประมวลผล event streams แบบ asynchronous ไม่มีการรับประกัน timeliness เว้นแต่คุณจะสร้าง consumers ที่รอให้ message มาถึงก่อนที่จะส่งคืน ตัวอย่างเช่น ผู้ใช้สามารถขอ payment และจากนั้นอ่านสถานะของบัญชี ก่อนที่ stream processor จะดำเนินการ request; ผู้ใช้จะไม่เห็น payment ที่พวกเขาเพิ่งขอ

อย่างไรก็ตาม integrity เป็นศูนย์กลางของระบบสตรีมมิ่ง ดังที่เราได้เห็น exactly-once หรือ effectively-once semantics เป็นกลไกสำหรับการรักษา integrity ถ้า event สูญหายหรือมีผลสองครั้ง integrity ของระบบข้อมูล อาจถูกละเมิด ดังนั้น การส่ง messages ที่ทนต่อความผิดพลาด และการระงับ duplicates (เช่น idempotent operations) จึงมีความสำคัญ สำหรับการรักษา integrity ของระบบข้อมูลในกรณีที่เกิดความผิดพลาด

ดังที่เราเห็นในส่วนก่อนหน้า ระบบ stream processing ที่เชื่อถือได้สามารถรักษา integrity ได้โดยไม่ต้องใช้ distributed transactions และ atomic commit protocol ซึ่งหมายความว่าพวกมันสามารถบรรลุความถูกต้องที่เทียบเคียงได้ ด้วยประสิทธิภาพและความทนทานในการดำเนินงานที่ดีกว่ามาก เราบรรลุ integrity นี้ผ่านการรวมกันของกลไก:

  • การแทนเนื้อหาของ write operation เป็น message เดียว ซึ่งสามารถ เขียนแบบ atomic ได้ง่าย—แนวทางที่เข้ากันได้ดีมากกับ event sourcing

  • การ derive การอัปเดตสถานะอื่นๆ ทั้งหมดจาก message เดียวผ่าน deterministic derivation functions คล้ายกับ stored procedures

  • การส่ง request ID ที่สร้างโดย client ผ่านกระบวนการทุกระดับ ช่วยให้การระงับ duplicate และ idempotence แบบ end-to-end

  • การทำให้ messages เป็น immutable และอนุญาตให้ข้อมูลที่ derive ถูกประมวลผลซ้ำเป็นครั้งคราว ซึ่งทำให้ง่ายต่อการกู้คืนจาก bugs

Loosely interpreted constraints (ข้อจำกัดแบบหลวม)

ดังที่ได้กล่าวไว้ก่อนหน้านี้ การบังคับใช้ uniqueness constraint ต้องใช้ consensus ซึ่งโดยทั่วไป implement โดยการส่ง events ทั้งหมดใน shard เฉพาะ ผ่านโหนดเดียว ข้อจำกัดนี้หลีกเลี่ยงไม่ได้ถ้าเราต้องการรูปแบบดั้งเดิมของ uniqueness constraint และ stream processing ไม่สามารถหลีกเลี่ยงมันได้

อย่างไรก็ตาม แอปพลิเคชันจริงจำนวนมากมีความต้องการทางธุรกิจที่อนุญาต การละเมิดสิ่งที่คุณอาจคิดว่าเป็น hard constraints:

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

  • ในทำนองเดียวกัน สายการบินหลายแห่งขายตั๋วเกิน (overbook) โดยคาดหวังว่าผู้โดยสารบางคนจะพลาดเที่ยวบิน และโรงแรมหลายแห่งจองห้องเกิน โดยคาดหวังว่าแขกบางคนจะยกเลิก ในกรณีเหล่านี้ ข้อจำกัดของ "หนึ่งคนต่อหนึ่งที่นั่ง" ถูกละเมิดโดยเจตนาเพื่อเหตุผลทางธุรกิจ และกระบวนการชดเชย (การคืนเงิน, การอัปเกรด, การจัดหาห้องฟรีที่โรงแรมใกล้เคียง) ถูกวางไว้เพื่อจัดการกับความต้องการที่เกินอุปทาน แม้ว่าจะไม่มีการ overbooking ก็ตาม กระบวนการขอโทษและชดเชยก็ยังจำเป็นสำหรับการจัดการกับเหตุการณ์ เช่นเที่ยวบินที่ถูกยกเลิกเนื่องจากสภาพอากาศเลวร้ายหรือพนักงานนัดหยุดงาน —การกู้คืนจากปัญหาดังกล่าวเป็นเพียงส่วนปกติของธุรกิจ [ 3 ]

  • ถ้ามีคนถอนเงินมากกว่าที่มีในบัญชี ธนาคารสามารถเรียกเก็บ ค่าธรรมเนียม overdraft และขอให้พวกเขาชำระเงินที่ค้างชำระ โดยการจำกัดการถอนรวมต่อวัน ความเสี่ยงต่อธนาคารถูกจำกัด

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

ในบริบททางธุรกิจหลายแห่ง ดังนั้นจึงยอมรับได้ที่จะละเมิดข้อจำกัดชั่วคราว และแก้ไขมันทีหลังโดยการขอโทษ การเปลี่ยนแปลงเพื่อแก้ไขข้อผิดพลาดนี้เรียกว่า compensating transaction [ 48 , 49 ] ต้นทุนของการขอโทษ (ในแง่ของเงินหรือชื่อเสียง) แตกต่างกันไป แต่มักจะค่อนข้างต่ำ คุณไม่สามารถยกเลิกการส่งอีเมลได้ แต่คุณสามารถส่งอีเมลติดตามผล พร้อมการแก้ไขได้ ถ้าคุณเรียกเก็บเงินบัตรเครดิตสองครั้งโดยไม่ได้ตั้งใจ คุณสามารถคืนเงินหนึ่งในรายการ และต้นทุนสำหรับคุณก็แค่ค่าธรรมเนียมการดำเนินการ และอาจเป็นการร้องเรียนของลูกค้า เมื่อเงินถูกจ่ายออกจากตู้ ATM คุณไม่สามารถเอามันคืนได้โดยตรง แม้ว่าในหลักการคุณสามารถส่งเจ้าหนี้ เพื่อเรียกเก็บเงินคืนถ้าบัญชีถูก overdraft และลูกค้าไม่ยอมคืน

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

แอปพลิเคชันเหล่านี้ ต้องการ integrity คุณไม่ต้องการสูญเสียการจองหรือให้เงินหายไป เพราะเครดิตและเดบิตที่ไม่ตรงกัน แต่พวกเขา ไม่ต้องการ timeliness ในการบังคับใช้ข้อจำกัด ถ้าคุณขายสินค้ามากกว่าที่คุณมี ในคลังสินค้า คุณสามารถแก้ไขปัญหาได้ในภายหลัง การทำเช่นนี้คล้ายกับ แนวทางการแก้ไขความขัดแย้งที่เราได้พูดถึงใน "การจัดการกับ Writes ที่ขัดแย้งกัน" .

Coordination-avoiding data systems (ระบบข้อมูลที่หลีกเลี่ยงการประสานงาน)

ตอนนี้เราได้สังเกตสองสิ่งที่น่าสนใจ:

  • ระบบ dataflow สามารถรักษาการรับประกัน integrity บนข้อมูลที่ derive โดยไม่ต้องมี atomic commit, linearizability, หรือ synchronous cross-shard coordination

  • แม้ว่า strict uniqueness constraints ต้องการ timeliness และ coordination แอปพลิเคชันจำนวนมากก็โอเคกับ loose constraints ที่อาจถูกละเมิดชั่วคราว และแก้ไขภายหลัง ตราบใดที่ integrity ถูกรักษาไว้ตลอด

เมื่อนำมารวมกัน การสังเกตเหล่านี้หมายความว่าระบบ dataflow สามารถให้บริการ การจัดการข้อมูลสำหรับแอปพลิเคชันจำนวนมากโดยไม่ต้องใช้ coordination ในขณะที่ยังให้การรับประกัน integrity ที่แข็งแกร่ง ระบบข้อมูลที่ หลีกเลี่ยงการประสานงาน ดังกล่าวมีความน่าสนใจมาก; พวกมันสามารถบรรลุประสิทธิภาพที่ดีกว่า และการทนต่อความผิดพลาดมากกว่าระบบที่ต้องทำ synchronous coordination [ 45 ]

ตัวอย่างเช่น ระบบดังกล่าวสามารถทำงานแบบกระจายข้ามหลาย datacenters ใน multi-leader configuration โดยการจำลองแบบ asynchronous ระหว่างภูมิภาค datacenter ใดๆ สามารถทำงานได้อย่างอิสระจากที่อื่น เพราะไม่ต้องการ synchronous cross-region coordination ระบบดังกล่าว จะมีการรับประกัน timeliness ที่อ่อนแอ—มันไม่สามารถเป็น linearizable โดยไม่ต้องใช้ coordination—แต่มันยังคงมีการรับประกัน integrity ที่แข็งแกร่ง

ในบริบทนี้ serializable transactions ยังคงมีประโยชน์เป็นส่วนหนึ่งของการรักษา state ที่ derive แต่พวกมันสามารถรันในขอบเขตเล็กที่พวกมันทำงานได้ดี [ 6 ] Heterogeneous distributed transactions เช่น XA transactions ไม่จำเป็น การประสานงานแบบ synchronous ยังคงสามารถถูกนำมาใช้ในที่ที่จำเป็น (ตัวอย่างเช่น เพื่อบังคับใช้ข้อจำกัดที่เข้มงวดก่อนการดำเนินการที่ไม่สามารถ กู้คืนได้) แต่ไม่จำเป็นที่ทุกอย่างจะต้องจ่ายต้นทุนของ coordination ถ้ามีเพียงส่วนเล็กของแอปพลิเคชันที่ต้องการมัน [ 32 ]

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

Trust, but Verify (เชื่อใจแต่ตรวจสอบ)

การพูดคุยทั้งหมดของเราเกี่ยวกับความถูกต้อง, integrity, และ fault tolerance อยู่ภายใต้สมมติฐานว่าบางสิ่งอาจผิดพลาดได้ แต่สิ่งอื่นจะไม่ผิดพลาด เราเรียกสมมติฐานดังกล่าวว่า system model (ดู "System Model และ Reality" ) ตัวอย่างเช่น เราควรสมมติว่า processes สามารถ crash, เครื่องสามารถเสียพลังงานกะทันหัน, และเครือข่ายสามารถหน่วงหรือทิ้ง messages โดยพลการ เราอาจสมมติว่าข้อมูลที่เขียนลงดิสก์ จะไม่สูญหายหลังจาก fsync , ข้อมูลในหน่วยความจำจะไม่เสียหาย, และคำสั่งคูณของ CPU ของเราจะ คืนผลลัพธ์ที่ถูกต้องเสมอ

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

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

Maintaining integrity in the face of software bugs (การรักษา integrity ท่ามกลาง bugs ซอฟต์แวร์)

นอกจากปัญหาฮาร์ดแวร์ดังกล่าวแล้ว ยังมีความเสี่ยงของ bugs ซอฟต์แวร์เสมอ ซึ่งจะไม่ถูกจับโดย checksums ระดับต่ำของเครือข่าย, หน่วยความจำ, หรือ filesystem แม้แต่ซอฟต์แวร์ฐานข้อมูลที่ใช้อย่างแพร่หลายก็มี bugs—ตัวอย่างเช่น MySQL เวอร์ชันก่อนหน้ามีปัญหาในการรักษา uniqueness constraints อย่างถูกต้อง [ 50 ], และ serializable isolation level ของ PostgreSQL มี write skew anomalies ในอดีต [ 51 ] แม้ว่า MySQL และ PostgreSQL จะเป็นฐานข้อมูลที่ทนทานและได้รับการยอมรับ ที่ผ่านการทดสอบการใช้งานโดยหลายคนเป็นเวลาหลายปี ในซอฟต์แวร์ที่โตน้อยกว่า สถานการณ์ก็มีแนวโน้มที่จะแย่กว่ามาก

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

เมื่อมันมาถึง application code เราต้องสมมติ bugs มากขึ้น เนื่องจากแอปพลิเคชันส่วนใหญ่ไม่ได้รับการตรวจสอบและทดสอบมากเท่ากับ โค้ดฐานข้อมูล แอปพลิเคชันจำนวนมากไม่ได้ใช้ฟีเจอร์ที่ฐานข้อมูลมีให้สำหรับ การรักษา integrity อย่างถูกต้องด้วยซ้ำ เช่น foreign-key หรือ uniqueness constraints [ 25 ]

Consistency ในความหมายของ ACID ขึ้นอยู่กับแนวคิดที่ว่าฐานข้อมูลเริ่มต้น ในสถานะที่สอดคล้อง และ transaction แปลงมันจากสถานะที่สอดคล้องหนึ่งไปยัง อีกสถานะที่สอดคล้อง ดังนั้นเราคาดหวังให้ฐานข้อมูลอยู่ในสถานะที่สอดคล้องเสมอ อย่างไรก็ตาม แนวคิดนี้สมเหตุสมผลก็ต่อเมื่อเราสมมติว่า transaction ปราศจาก bugs ถ้าแอปพลิเคชันใช้ฐานข้อมูลไม่ถูกต้องในบางวิธี—ตัวอย่างเช่น การใช้ weak isolation level อย่างไม่ปลอดภัย—integrity ของฐานข้อมูล ไม่สามารถรับประกันได้

Don't just blindly trust what they promise (อย่าเชื่อสิ่งที่สัญญาไว้อย่างสุ่มสี่สุ่มห้า)

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

ดังที่ได้กล่าวถึงใน "ข้อดีของ immutable events" , auditing ไม่ใช่แค่สำหรับแอปพลิเคชันทางการเงิน อย่างไรก็ตาม ความสามารถในการตรวจสอบได้ (auditability) สำคัญมากในการเงิน เพราะทุกคนรู้ว่าความผิดพลาดเกิดขึ้น และเราทุกคนยอมรับความจำเป็น ที่จะสามารถตรวจจับและแก้ไขปัญหาได้

ระบบที่ที่เติบโตเต็มที่ก็เช่นเดียวกันมีแนวโน้มที่จะพิจารณาความเป็นไปได้ของสิ่งที่ผิดปกติ และจัดการกับความเสี่ยงนั้น ตัวอย่างเช่น ระบบจัดเก็บข้อมูลขนาดใหญ่ เช่น HDFS และ Amazon S3 ไม่ได้เชื่อถือดิสก์อย่างสมบูรณ์ ระบบเหล่านี้รัน background processes ที่อ่านไฟล์ซ้ำๆ, เปรียบเทียบกับ replicas อื่นๆ, และย้ายไฟล์จากดิสก์หนึ่งไปยังอีกดิสก์หนึ่ง เพื่อลดความเสี่ยงของการปนเปื้อนแบบเงียบ [ 52 , 53 ]

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

ระบบเช่น HDFS และ S3 ยังคงต้องสมมติว่าดิสก์ทำงานได้อย่างถูกต้อง ส่วนใหญ่ของเวลา—ซึ่งเป็นสมมติฐานที่สมเหตุสมผล แต่ไม่เหมือนกับ การสมมติว่าพวกมันทำงานได้อย่างถูกต้อง เสมอ . อย่างไรก็ตาม ระบบจำนวนไม่มากในปัจจุบันมีแนวทาง "เชื่อใจแต่ตรวจสอบ" แบบนี้ในการตรวจสอบตนเองอย่างต่อเนื่อง หลายระบบสมมติว่ารับประกันความถูกต้องแบบสัมบูรณ์และไม่ได้ จัดเตรียมสำหรับความเป็นไปได้ของการปนเปื้อนข้อมูลที่หายาก ในอนาคตเราอาจเห็นระบบ self-validating หรือ self-auditing มากขึ้นที่ตรวจสอบ integrity ของตัวเองอย่างต่อเนื่อง แทนที่จะพึ่งพาความเชื่อใจแบบ blind trust [ 54 ]

Designing for auditability (การออกแบบเพื่อความสามารถในการตรวจสอบ)

ถ้า transaction เปลี่ยนแปลง objects หลายตัวในฐานข้อมูล เหตุผลที่แท้จริงอาจยากที่จะบอกได้ในภายหลัง แม้ว่าคุณจะ จับ transaction logs การ insert, update, และ delete ในตารางต่างๆ ก็ไม่จำเป็นต้องให้ภาพที่ชัดเจนว่า ทำไม การเปลี่ยนแปลงเหล่านั้นถูกดำเนินการ การเรียกใช้ application logic ที่ตัดสินใจเกี่ยวกับการเปลี่ยนแปลงเหล่านั้นเป็นเพียงชั่วคราว และไม่สามารถทำซ้ำได้

ในทางตรงกันข้าม ระบบที่ใช้ event สามารถให้ความสามารถในการตรวจสอบได้ดีกว่า ในแนวทาง event sourcing input ของผู้ใช้ไปยังระบบจะถูกแทน เป็น immutable event เดียว และการอัปเดตสถานะที่เกิดขึ้นทั้งหมด derive จาก event นั้น การ derive สามารถทำให้เป็น deterministic และทำซ้ำได้ เพื่อให้การรัน log ของ events เดียวกันผ่านโค้ด derive เวอร์ชันเดียวกันจะให้การอัปเดตสถานะเดียวกัน

การทำให้ dataflow ชัดเจนทำให้แหล่งที่มาของข้อมูลชัดเจนขึ้น ซึ่งทำให้การตรวจสอบ integrity เป็นไปได้มากขึ้น สำหรับ event log เราสามารถใช้ hashes เพื่อตรวจสอบว่า event storage ยังไม่ถูกปนเปื้อน สำหรับ state ที่ derive ใดๆ เราสามารถรัน batch และ stream processors ที่ derive มันจาก event log ซ้ำเพื่อตรวจสอบว่าเราได้ผลลัพธ์เดียวกัน หรือแม้แต่รันการ derive ที่ซ้ำซ้อนแบบขนาน

dataflow ที่ deterministic และกำหนดไว้อย่างดียังทำให้ง่ายต่อการ debug และติดตามการดำเนินการของระบบเพื่อหาสาเหตุว่าทำไมมันถึงทำบางอย่าง [ 4 , 55 ] ถ้ามีสิ่งที่ไม่คาดคิดเกิดขึ้น มันมีค่าที่จะมีความสามารถในการวินิจฉัย เพื่อสร้างสถานการณ์ที่แน่นอนที่นำไปสู่เหตุการณ์ที่ไม่คาดคิด— ความสามารถในการ time-travel debugging

The end-to-end argument again (ข้อโต้แย้งแบบ end-to-end อีกครั้ง)

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

การตรวจสอบ integrity ของระบบข้อมูลทำได้ดีที่สุดในลักษณะ end-to-end ยิ่งเราสามารถรวมระบบใน integrity check ได้มากเท่าไร โอกาสที่การปนเปื้อนจะไม่มีใครสังเกตในบางขั้นตอนของกระบวนการก็ยิ่งน้อยลง ถ้าเราสามารถตรวจสอบว่า pipeline ข้อมูลที่ derive ทั้งหมดถูกต้องแบบ end to end แล้ว ดิสก์, เครือข่าย, services, และ algorithms ใดๆ ตามเส้นทาง ก็จะถูกรวมอยู่ในการตรวจสอบโดยปริยาย

การมี integrity checks แบบ end-to-end อย่างต่อเนื่อง ทำให้คุณมีความมั่นใจมากขึ้นเกี่ยวกับความถูกต้องของระบบของคุณ ซึ่งจะช่วยให้คุณเคลื่อนที่เร็วขึ้น [ 56 ] เช่นเดียวกับการทดสอบอัตโนมัติ auditing เพิ่มโอกาสที่ bugs จะถูกพบอย่างรวดเร็ว และลดความเสี่ยงที่การเปลี่ยนแปลงระบบ หรือเทคโนโลยีการจัดเก็บใหม่จะก่อให้เกิดความเสียหาย ถ้าคุณ ไม่กลัวที่จะทำการเปลี่ยนแปลง คุณสามารถพัฒนาแอปพลิเคชัน เพื่อตอบสนองความต้องการที่เปลี่ยนแปลงได้ดีขึ้นมาก

Tools for auditable data systems (เครื่องมือสำหรับระบบข้อมูลที่ตรวจสอบได้)

ในปัจจุบัน ระบบข้อมูลจำนวนไม่มากที่ทำให้ความสามารถในการตรวจสอบเป็น ความกังวลระดับสูง แอปพลิเคชันบางตัว implement กลไกการตรวจสอบของตัวเอง— ตัวอย่างเช่น โดยการ log การเปลี่ยนแปลงทั้งหมดไปยังตาราง audit แยกต่างหาก —แต่การรับประกัน integrity ของ audit log และสถานะฐานข้อมูล ยังคงยาก transaction log สามารถถูกทำให้กันปลอมแปลงได้ โดยการเซ็นชื่อเป็นระยะด้วยฮาร์ดแวร์รักษาความปลอดภัย แต่นั่น ไม่ได้รับประกันว่า transactions ที่ถูกต้องเข้าไปใน log ตั้งแต่แรก

Blockchains เช่น Bitcoin และ Ethereum เป็น shared append-only logs ที่มีการตรวจสอบความสอดคล้องด้วย cryptography; transactions ที่พวกมันเก็บ เป็น events, และ smart contracts ก็คือ stream processors โปรโตคอล consensus ที่พวกมันใช้รับประกันว่าโหนดทั้งหมดเห็นด้วย กับลำดับ events เดียวกัน ความแตกต่างจากโปรโตคอล consensus ใน บทที่ 10 คือ blockchains เป็น Byzantine fault-tolerant—นั่นคือ พวกมันยังทำงานได้ ถ้าโหนดที่เข้าร่วมบางส่วนมีข้อมูลที่ปนเปื้อน เนื่องจาก replicas ตรวจสอบ integrity ของกันและกันอย่างต่อเนื่อง

สำหรับแอปพลิเคชันส่วนใหญ่ blockchains มี overhead สูงเกินไป ที่จะมีประโยชน์ อย่างไรก็ตาม เครื่องมือ cryptographic บางอย่างของพวกมัน สามารถใช้ในบริบทที่เบากว่า ตัวอย่างเช่น Merkle trees [ 57 ] เป็นต้นไม้ของ hashes ที่สามารถใช้เพื่อพิสูจน์อย่างมีประสิทธิภาพ ว่า record ปรากฏใน dataset (และอีกสองสามอย่าง) Certificate transparency ใช้ append-only logs ที่ตรวจสอบด้วย cryptography และ Merkle trees เพื่อตรวจสอบความถูกต้องของ TLS/SSL certificates [ 58 , 59 ]; มันหลีกเลี่ยงความต้องการโปรโตคอล consensus โดยมี leader เดียวต่อ log

อัลกอริธึมการตรวจสอบ integrity และ auditing เช่นของ certificate transparency และ distributed ledgers อาจถูกใช้อย่างแพร่หลายมากขึ้นในระบบข้อมูลโดยทั่วไปในอนาคต จำเป็นต้องทำงานบางอย่างเพื่อทำให้พวกเขาสามารถขยายขนาดได้ เทียบเท่าระบบที่ไม่มีการ auditing ด้วย cryptography และเพื่อให้การลงโทษด้านประสิทธิภาพต่ำที่สุดเท่าที่เป็นไปได้ แต่อย่างไรก็ตาม พวกมันก็น่าสนใจ

Summary (สรุป)

ในบทนี้เราได้พูดถึงแนวทางใหม่ในการออกแบบระบบข้อมูลโดยใช้แนวคิดจาก stream processing เราเริ่มต้นด้วยการสังเกตว่าไม่มีเครื่องมือเดียวที่สามารถให้บริการทุกกรณีการใช้งานได้อย่างมีประสิทธิภาพ ดังนั้นแอปพลิเคชันต้องประกอบซอฟต์แวร์หลายชิ้นเพื่อบรรลุเป้าหมายของพวกเขา เราได้พูดถึงวิธีการแก้ปัญหา data integration นี้โดยใช้ batch processing และ event streams เพื่อให้การเปลี่ยนแปลงข้อมูลไหลระหว่างระบบ

ในแนวทางนี้ ระบบบางอย่างถูกกำหนดให้เป็น systems of record และข้อมูลอื่น derive จากพวกมันผ่านการแปลง ด้วยวิธีนี้เราสามารถรักษา indexes, materialized views, machine learning models, สรุปทางสถิติ, และอื่นๆ การทำให้ derivations และ transformations เหล่านี้เป็น asynchronous และ loosely coupled ช่วยป้องกันปัญหาในพื้นที่หนึ่งไม่ให้แพร่กระจายไปยังพื้นที่ที่ไม่เกี่ยวข้อง เพิ่มความทนทานและ fault tolerance ของระบบโดยรวม

การแสดง dataflows เป็นการแปลงจาก dataset หนึ่งไปยังอีก dataset หนึ่ง ยังช่วยให้แอปพลิเคชันพัฒนาได้ ถ้าคุณต้องการเปลี่ยนขั้นตอนการประมวลผลหนึ่ง— ตัวอย่างเช่น เพื่อเปลี่ยนแปลงโครงสร้างของ index หรือ cache—คุณ สามารถรันโค้ดการแปลงใหม่บน input dataset ทั้งหมดเพื่อ derive output ใหม่ ในทำนองเดียวกัน ถ้ามีอะไรผิดพลาด คุณสามารถแก้ไขโค้ดและประมวลผลข้อมูลซ้ำเพื่อกู้คืน

กระบวนการเหล่านี้ค่อนข้างคล้ายกับสิ่งที่ฐานข้อมูลทำภายในอยู่แล้ว ดังนั้นเรา ปรับแนวคิดของแอปพลิเคชัน dataflow เป็น unbundling ส่วนประกอบของฐานข้อมูลและการสร้างแอปพลิเคชันโดยการประกอบส่วนประกอบที่ loosely coupled เหล่านี้

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

ต่อไป เราได้พูดถึงวิธีการทำให้แน่ใจว่าการประมวลผลทั้งหมดนี้ยังคงถูกต้อง ในกรณีที่เกิดความผิดพลาด เราเห็นว่าการรับประกัน integrity ที่แข็งแกร่ง สามารถ implement ได้อย่าง scalable ด้วย asynchronous event processing โดยใช้ end-to-end request identifiers เพื่อทำให้ operations เป็น idempotent และโดยการตรวจสอบข้อจำกัดแบบ asynchronous Clients สามารถรอจนกว่าการตรวจสอบผ่าน หรือดำเนินการต่อโดยไม่ต้องรอ แต่เสี่ยงต่อการต้องขอโทษเกี่ยวกับการละเมิดข้อจำกัด แนวทางนี้ scalable และทนทานกว่ามากเมื่อเทียบกับแนวทางดั้งเดิมของการใช้ distributed transactions และเข้ากับวิธีการทำงานของกระบวนการทางธุรกิจจำนวนมากในทางปฏิบัติ

โดยการจัดโครงสร้างแอปพลิเคชันรอบ dataflow และการตรวจสอบข้อจำกัดแบบ asynchronous เราสามารถหลีกเลี่ยง coordination ส่วนใหญ่และสร้างระบบที่รักษา integrity แต่ยังคงทำงานได้ดี แม้ในสถานการณ์แบบกระจายตามภูมิศาสตร์ และในกรณีที่เกิดความผิดพลาด เพื่อสรุป เราได้พูดถึงเล็กน้อยเกี่ยวกับการใช้ audits เพื่อตรวจสอบ integrity ของข้อมูลและตรวจจับการปนเปื้อน และสังเกตว่า เทคนิคที่ใช้โดย blockchains ก็มีความคล้ายคลึงกับระบบที่ใช้ event