1 คะแนน โดย GN⁺ 2024-05-17 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในการทดสอบ Datomic Pro 1.0.7075 ดูเหมือนจะให้ความปลอดภัยระหว่างทรานแซ็กชันที่แข็งแกร่งกว่าที่เอกสารอ้างไว้ แต่ ความหมายเชิงทรานแซ็กชันภายใน แตกต่างจากโมเดลการทำงานแบบลำดับอนุกรมทั่วไปอย่างมาก
  • ประวัติการทดสอบทั้งหมดดูเป็นแบบ Serializable และเซสชัน peer เดี่ยวให้คุณสมบัติใกล้เคียง Strong Session Serializable ส่วนการเขียนและการอ่านด้วย d/sync ใกล้เคียง Strong Serializable
  • การ add, retract และ transaction function ของ Datomic ไม่ได้ทำงานแบบสะสมตามลำดับภายในทรานแซ็กชัน แต่แต่ละฟังก์ชันทำงานโดยมองเห็นเฉพาะ สถานะ DB ณ จุดเริ่มต้น เท่านั้น
  • หากนำ transaction function ที่ปลอดภัยเมื่อแยกกัน เช่น approve และ deny มารวมกันในทรานแซ็กชันเดียว ผลลัพธ์จากการประกอบกันอาจก่อให้เกิด การละเมิดอินวาเรียนต์ ได้
  • เมื่อใส่ transaction function หลายตัวไว้ในทรานแซ็กชันเดียว ควรตรวจสอบความสัมพันธ์ระหว่าง read set และ write set และใช้ ข้อจำกัดแบบชัดเจน เช่น entity predicate, attribute predicate และ entity spec ร่วมด้วย

โมเดลและสถาปัตยกรรมของ Datomic Pro

  • Datomic เป็น ฐานข้อมูล OLTP แบบ Entity-Attribute-Value ที่สร้างแบบจำลองแนวคิดเรื่องเวลาอย่างชัดเจน
    • สถานะของ DB ณ ช่วงเวลาหนึ่งแสดงเป็นเซตของ datom ในรูป [entity, attribute, value]
    • ในแต่ละ datom จะเก็บด้วยว่าทรานแซ็กชันใดเป็นผู้เพิ่มหรือถอนข้อมูลนั้น
    • datom ทั้งหมดอยู่ในรูป 5-ทูเพิล [entity, attribute, value, transaction, asserted-or-retracted?]
  • Datomic เป็น temporal database จึงสามารถขอ snapshot โดยอิงทั้งเวลาตรรกะในอดีตหรือเวลา wall-clock ได้ ไม่ใช่เฉพาะปัจจุบัน
    • ยังสามารถคิวรีผ่านมุมมองประวัติทั้งหมดเพื่อตรวจสอบได้ว่าข้อเท็จจริงบางอย่างเคยมีอยู่ในอดีตหรือไม่
    • รองรับการคิวรีผ่าน API สไตล์ Datalog, API สำหรับไล่กราฟ และชนิด Entity แบบสไตล์ ODM
  • Datomic Pro เป็นเวอร์ชันที่ผู้ใช้ดูแลระบบเองได้ ส่วน Datomic Cloud ทำงานบน AWS และมีสถาปัตยกรรมบางส่วนต่างออกไป
  • Datomic Pro มีโครงสร้างที่หลายองค์ประกอบทำงานร่วมกัน
    • Transactor รับผิดชอบการรันทรานแซ็กชันเขียน การดูแลดัชนี และการบันทึกลงสตอเรจ
    • Peer เป็น thick client ที่ฝังไลบรารี JVM ทำหน้าที่ส่งทรานแซ็กชัน คิวรีอ่านไปยังสตอเรจ และจัดการแคช
    • สำหรับแอปพลิเคชันภาษาอื่น ยังมีโมเดล client-server ที่ใช้ thin client และ peer server ให้ด้วย
  • ภายใน Datomic จะ append แต่ละทรานแซ็กชันลงใน log ตามลำดับเวลา และดูแล 4 ดัชนีที่เรียงตามการผสมกันของ entity, attribute, value และ time
    • log และดัชนีถูกเก็บเป็นต้นไม้แบบคงอยู่ถาวรและไม่เปลี่ยนแปลงบนสตอเรจอย่าง Cassandra หรือ DynamoDB
    • เนื่องจากโหนดของต้นไม้ไม่เปลี่ยนแปลง backing storage จึงจำเป็นต้องรับประกันเพียง eventual consistency
    • ตอน commit ตัว transactor จะบันทึกโหนดต้นไม้แบบ immutable ชุดใหม่ แล้วเลื่อน root pointer ด้วย compare-and-set (CaS) ซึ่ง CaS นี้ต้องการ Sequential consistency
  • Sequential CaS รับประกันลำดับแบบโกลบอลของทรานแซ็กชัน แต่ก็ผูก throughput ฝั่งเขียนไว้กับความเร็วของ transactor ตัวเดียว
    • โดยทั่วไป Datomic จะมี active transactor เพียงตัวเดียวในแต่ละเวลา และกระจายหลาย transactor เพื่อรองรับความทนทานต่อความขัดข้อง
    • peer จะเชื่อมต่อกับทั้งสตอเรจและ transactor โดยตรง และแต่ละตัวเก็บสำเนา root pointer ที่เพิ่มขึ้นแบบโมโนโทนิกของตนเอง
    • การอ่านสามารถแคชโหนดต้นไม้แบบ immutable ได้ ทำให้เมื่อเพิ่มจำนวน peer ก็ขยายสมรรถนะการอ่านได้เกือบเป็นเชิงเส้น

โมเดลทรานแซ็กชันของ Datomic

  • Datomic ไม่ได้ให้ interactive transaction แบบฐานข้อมูล OLTP ทั่วไป
    • ไม่ใช่โมเดลที่เริ่มทรานแซ็กชัน รับผลของการดำเนินการ จากนั้นส่งการดำเนินการถัดไป และค่อย commit ตอนท้าย
    • แม้จะมี transaction function คล้าย stored procedure แต่ก็ไม่สามารถคืนค่าตามอำเภอใจให้ผู้เรียกได้
  • เส้นทางการอ่านและการเขียนถูกแยกออกจากกันอย่างชัดเจน
    • db จะคืนค่าสถานะ DB ล่าสุดที่ peer รับรู้อยู่
    • d/sync จะซิงก์กับ transactor เพื่อให้ได้สถานะล่าสุดเมื่อเทียบกับทุก peer หรือสถานะหลังจากเวลาที่กำหนด
    • d/as-of ใช้ดึงสถานะ DB ณ เวลาในอดีต
    • เนื่องจากสถานะ DB เป็น immutable การคิวรีหลายครั้งบนสถานะเดียวกันจึงทำงานบนเวลาตรรกะเดียวกันอย่างตรงกันทุกประการ
  • ทรานแซ็กชันเขียนแสดงเป็น operation แบบ ordered list
    • ตัวอย่างเช่น :db/add, :db/retract, db/cas และการเรียก transaction function ที่ผู้ใช้กำหนดเอง
    • transaction function รับสถานะ DB ณ จุดเริ่มต้นของทรานแซ็กชันพร้อมอาร์กิวเมนต์ แล้วคืนค่าเป็นเซตของ operation ใหม่
    • การเรียกฟังก์ชันจะถูกขยายแบบเรียกซ้ำไปเรื่อย ๆ จนเหลือเพียง assertion และ retraction
  • transaction function สามารถอ่านข้อมูลภายในเพื่อกำหนดการเขียนแบบมีเงื่อนไขได้ แต่จะไม่คืนผลการอ่านหรือข้อมูลตามอำเภอใจกลับไปยังผู้เรียก transact โดยตรง
    • transact จะคืนค่าสถานะ DB ก่อนทรานแซ็กชัน สถานะ DB หลังทรานแซ็กชัน และเซตของ datom ที่ถูกขยายแล้ว
    • ผู้เรียกสามารถใช้ pre-state และ post-state เพื่อตรวจสอบได้ว่ามีการเขียนแบบมีเงื่อนไขเกิดขึ้นหรือไม่
  • Datomic เป็นโมเดลที่ออกแบบมาเพื่อแก้ปัญหาโดยยึด snapshot ของ DB ที่สร้างได้ต้นทุนต่ำและส่งต่อได้เป็นศูนย์กลาง
    • Nubank เป็นผู้พัฒนาปัจจุบันของ Datomic โดยให้บริการทางการเงินแก่ผู้ใช้ราว 94 ล้านราย และประมวลผลธุรกรรมของผู้ใช้เฉลี่ยวันละ 2.3 พันล้านรายการ
    • ผลิตภัณฑ์เกือบทั้งหมดของ Nubank ใช้ Datomic เป็น system of record

ข้ออ้างเรื่องความสอดคล้องและการออกแบบการทดสอบ

  • เอกสารของ Datomic ระบุว่าเป็นธุรกรรมแบบ ACID และมองว่าธุรกรรมจะถูกบันทึกลงสตอเรจเป็น atomic write เดียว และ peer แต่ละตัวจะสังเกตเห็นธุรกรรมทั้งหมดที่เสร็จสมบูรณ์แล้วจนถึงช่วงเวลาหนึ่งในลำดับรวม
    • ก่อนที่ไคลเอนต์จะได้รับ acknowledgement ธุรกรรมจะถูก flush ลง durable storage
  • เอกสารในช่วงเริ่มต้นการวิเคราะห์เมื่อต้นเดือนมกราคม 2024 อ้างอย่างไม่เป็นทางการว่าธุรกรรมเขียนเป็นแบบ Serializable
    • เอกสารยังอธิบาย Datomic ว่าเป็นระบบ “single-writer” แต่ Jepsen มองว่าคำอธิบายนี้ไม่แม่นยำด้วยเหตุผลสองประการ
    • เพื่อรองรับความทนทานต่อความขัดข้อง สามารถรัน transactor ได้หลายตัว และเนื่องจากตัวตรวจจับความขัดข้องไม่อาจสมบูรณ์แบบ จึงอาจเกิดช่วงเวลาที่ตัดสินว่า transactor หลายตัว active อยู่พร้อมกัน
    • แม้จะมี transactor เพียงตัวเดียว หากเครือข่ายหน่วง ข้อความไปยังสตอเรจก็อาจ interleave กับข้อความจาก transactor ตัวอื่นได้
  • ความปลอดภัยของ Datomic ถูกประเมินว่ามาจาก Sequential consistency ของ storage CaS operation ไม่ใช่ตรรกะ “single-writer”
    • แม้จะมี transactor หลายตัวทำงานพร้อมกัน CaS ก็ต้องเป็นตัวให้ความปลอดภัย
  • การทดสอบใช้ Datomic test suite ที่เขียนด้วย Jepsen testing library
    • ติดตั้ง Datomic Pro 1.0.7075 บนคลัสเตอร์โหนด Debian Bookworm
    • ใช้ตาราง AWS DynamoDB เป็นสตอเรจ
    • สองโหนดรัน transactor และโหนดที่เหลือรัน peer
  • peer เป็นโปรแกรม Clojure ขนาดเล็กที่ใช้ Datomic peer library และเปิด HTTP API สำหรับการทดสอบ
    • การทดสอบรันทั้งโหมดที่อาจเกิด stale read ได้ด้วย d/db และโหมดที่รับประกันความใหม่ด้วย d/sync
  • fault injection ถูกใช้กับทั้ง transactor และ peer
    • มีการฉีด process pause, crash และ clock error
    • มีการสร้าง network partition ระหว่าง transactor·peer และระหว่างโหนด·สตอเรจ
    • มีการร้องขอ Datomic garbage collection ด้วย
  • transactor จะปิดตัวเองหากไม่สามารถรักษาการเชื่อมต่อที่เสถียรกับสตอเรจได้
    • เมื่อใช้ timeout เริ่มต้น 5 วินาทีกับโหนดนอก AWS แม้ความผันผวนของเครือข่ายตามปกติก็ทำให้ปิดตัวลงทุกไม่กี่นาที
    • แม้ในสภาพแวดล้อมทดสอบบน EC2 หากใช้ timeout 1 วินาที ก็ยังปิดตัวลงทุก 10~20 นาที
    • Datomic แนะนำให้ operator รีสตาร์ต transactor ด้วย supervisor daemon และการทดสอบนี้ใช้ systemd service ที่ตั้งค่า Restart=on-failure

เวิร์กโหลดสี่แบบ

  • List Append

    • เวิร์กโหลด List Append ใช้ร่วมกับ Elle transaction checker
    • ในเชิงตรรกะ มันจัดการรายการของสมาชิกจำนวนเต็ม โดยแต่ละรายการระบุด้วย primary key แบบจำนวนเต็ม
    • ไคลเอนต์ทำธุรกรรมแบบสุ่มที่ประกอบด้วยการอ่านรายการหรือ append สมาชิกที่ไม่ซ้ำ
    • Elle ตรวจสอบ aborted read, intermediate read, การละเมิด internal consistency, ความไม่ตรงกันของลำดับสมาชิก และตัดสินการละเมิดโมเดลความสอดคล้องด้วยการหา cycle ใน dependency graph
    • ใน Datomic รายการถูกเข้ารหัสเป็น entity หนึ่งตัวและแอตทริบิวต์สองตัว
    • append/key ทำหน้าที่เป็น primary key
    • append/elements เป็นแอตทริบิวต์หลายค่า ใช้เก็บสมาชิกจำนวนเต็มของรายการ
    • เนื่องจากแอตทริบิวต์หลายค่าเป็น set ที่ไม่มีลำดับ Jepsen จึงจัดเรียงสมาชิกตาม transaction timestamp ของแต่ละ datom เพื่อให้ได้ลำดับที่ Elle ต้องการ
    • ข้อจำกัดที่ว่าไม่มี mixed read-write transaction ถูกหลบเลี่ยงด้วย transaction function และการคำนวณ pre-state
    • การเขียนทำผ่าน transaction function
    • ใช้ pre-state ที่ transact ส่งกลับมาเพื่อคำนวณว่าการอ่านภายในธุรกรรมน่าจะเห็นอะไร
    • ฟังก์ชันเดียวกันนี้ถูกรันหนึ่งครั้งใน transact และอีกรอบหนึ่งใน peer เพื่อเสริมการอ่านตาม pre-state
  • List Append with CaS

    • เวิร์กโหลด List Append with CaS ใช้แพตเทิร์น db/cas
    • ผู้ใช้สามารถอ่านสถานะปัจจุบันด้วย d/db แล้วส่ง [:db/cas 123 :counter/value 4 5] เพื่อเปลี่ยนค่าเป็น 5 เฉพาะเมื่อค่าปัจจุบันเป็น 4 เป็นต้น
    • หากใช้ db/cas กับทุกการเขียน ก็สามารถสร้าง Snapshot Isolation แบบ ad hoc ทับบน logical “user transaction” ได้
    • เวิร์กโหลดนี้เก็บรายการเป็นสตริงคั่นด้วย comma แบบ single-valued แทนที่จะเป็นหลายค่า
    • มันจะอ่านตอนเริ่มธุรกรรม จากนั้นใช้การอ่านและการเขียนในเครื่อง แล้วประกอบเป็นธุรกรรม CaS ที่รับประกันว่าสถานะที่อ่านมายังไม่เปลี่ยนหลังจากนั้น
  • Internal

    • เวิร์กโหลด Internal วัดความสอดคล้องภายในธุรกรรมโดยตรง
    • กรณี assert ค่า 1 ให้กับ entity attribute เดียวกัน แล้วตามด้วย assert ค่า 2
    • กรณี assert fact แล้ว retract fact นั้นในธุรกรรมเดียวกัน
    • กรณี assert ค่าแล้วพยายามเปลี่ยนด้วย CaS
    • กรณีทำ CaS หลายครั้ง เช่น 1→2, 2→3
    • กรณีสร้าง entity แล้วแก้ไขด้วย lookup ref
    • รวมถึงกรณีพยายาม increment ค่าสองครั้งด้วย transaction function
  • Grant

    • เวิร์กโหลด Grant ตรวจสอบว่า transaction function รักษา function invariant ได้หรือไม่
    • grant ถูกเข้ารหัสเป็น entity เดี่ยวที่มีแอตทริบิวต์สามตัวคือ created-at, approved-at, denied-at
    • grant ต้องไม่อยู่ในสถานะ approved และ denied พร้อมกัน
    • ฟังก์ชัน approve และ deny จะตรวจสอบก่อนว่า grant ถูก approved หรือ denied ไปแล้วหรือไม่ และจะ abort หากจำเป็น
    • มีการตรวจสอบว่า grant จะกลายเป็นทั้ง approved และ denied พร้อมกันหรือไม่ ภายใต้ชุดขอบเขตธุรกรรมหลายแบบ

ผลการทดสอบ: ความปลอดภัยระหว่างธุรกรรมดูแข็งแกร่ง

  • Jepsen ไม่พบพฤติกรรมที่ขัดกับข้ออ้างด้านความปลอดภัยหลักของ Datomic
    • ธุรกรรมดูเสมือนถูกนำไปใช้ตามลำดับรวม
    • ลำดับนั้นสอดคล้องกับลำดับการปฏิบัติการในเครื่องของแต่ละ peer
  • ประวัติที่จำกัดเฉพาะธุรกรรมเขียน และประวัติการอ่านที่ใช้ (d/sync conn) สอดคล้องกับ real-time order
    • Jepsen มองว่าสิ่งนี้ดูเป็น Strict Serializable
  • หากตีความโดยผูก session กับ peer เดียว Datomic ดูเหมือนจะรับประกัน Strong Session Serializability
    • ประวัติธุรกรรมแยกไม่ออกจากประวัติที่รันด้วย total order ใด total order หนึ่ง
    • และลำดับนั้นตรงกับลำดับที่สังเกตได้จากแต่ละ peer
  • d/db ส่งคืนสำเนา DB ที่อัปเดตแบบอะซิงโครนัส จึงอาจเกิด stale read ได้
    • เอกสารของ Datomic ก็ระบุชัดว่า peer read อาจมองไม่เห็นบางธุรกรรมที่เพิ่ง commit ไปไม่นาน
    • d/sync จะซิงก์กับ transactor เพื่อป้องกัน stale read
  • การยืนยันเชิงทดลองยังมีข้อจำกัดอยู่
    • เราพิสูจน์การมีอยู่ของบั๊กได้ แต่พิสูจน์การไม่มีอยู่ไม่ได้
    • correctness error ของระบบสตอเรจที่ Datomic พึ่งพาอาจนำไปสู่การละเมิดการรับประกันของ Datomic ได้
    • Datomic บน DynamoDB ปลอดภัยได้เท่ากับ compare-and-set operation ของ DynamoDB

ความหมายเชิงภายในของทรานแซกชัน: ไม่ใช่ลำดับ แต่เป็นความพร้อมกัน

  • ฐานข้อมูลส่วนใหญ่และการนิยามเชิงรูปแบบหลักของ isolation ของทรานแซกชัน ให้ความหมายแบบ serial execution ภายในทรานแซกชัน
    • หากเป็น set x = 1; read x; โดยทั่วไป read จะเห็นค่า 1
    • งานนิยามเชิงรูปแบบของ Adya, Cerone·Bernardi·Gotsman, Crooks·Alvisi·Pu·Clement เป็นต้น ระบุทั้งลำดับของ operation ภายในทรานแซกชัน และคุณสมบัติว่า “read ที่ตามมาจะสังเกตเห็น write ก่อนหน้า”
  • transaction request ของ Datomic เป็น ordered list แต่การทำงานจริงไม่ได้คงลำดับนั้นไว้
    • add, retract และ transaction function ทำงานราวกับถูกรันพร้อมกัน
    • transaction function จะสังเกตเห็นสถานะ DB ณ จุดเริ่มต้นของทรานแซกชันเสมอ
    • มันจะไม่เห็นผลของ assertion, retraction หรือ transaction function ก่อนหน้า
  • หากใส่ CaS เดียวกันสองครั้งให้กับ entity ที่มีค่าปัจจุบันเป็น 0 ใน Datomic ทั้งสองจะเห็นสถานะเริ่มต้นเป็น 0 และสำเร็จทั้งคู่
[[:db/cas 123 :internal/value 0 1]
 [:db/cas 123 :internal/value 0 1]]
  • ในโมเดลแบบลำดับ CaS ตัวแรกควรเปลี่ยนค่าเป็น 1 และ CaS ตัวที่สองควรล้มเหลว
    • แต่ใน Datomic CaS ทั้งสองจะสร้าง assertion ที่ซ้ำกัน และค่าท้ายสุดจะเป็น 1
  • transaction function สำหรับ increment สองครั้งก็ให้ผลต่างจากโมเดลแบบลำดับเช่นกัน
[['internal/increment "x"]
 ['internal/increment "x"]]
  • ถ้าค่าเริ่มต้นเป็น 0 ผลลัพธ์ในโมเดลแบบลำดับคือ 2
  • แต่ใน Datomic ฟังก์ชันทั้งสองต่างเห็นสถานะเริ่มต้นเป็น 0 และค่าท้ายสุดเป็น 1
  • transaction function มองไม่เห็น assertion ที่มาก่อนหน้าด้วย
[[:db/add id-of-x :internal/value 1]
 ['internal/increment "x"]]
  • ใน Datomic ค่าท้ายสุดจะเป็น 1 ไม่ใช่ 2
  • lookup ref ก็ใช้สถานะ DB ณ จุดเริ่มต้นของทรานแซกชันเช่นกัน
    • จึงไม่สามารถเพิ่ม entity แล้วอ้างถึง entity นั้นด้วย lookup ref ภายในทรานแซกชันเดียวกันได้
    • ในกรณีนี้จะ abort พร้อมข้อผิดพลาด Unable to resolve entity

การตรวจจับความขัดแย้งและ pseudo write skew

  • Datomic จะ abort ด้วย :db.error/datoms-conflict หากมีการ assert ค่าที่ต่างกันให้กับแอตทริบิวต์ cardinality เดี่ยวภายในทรานแซกชันเดียวกัน
    • หาก assert ค่า 2 บนค่าเริ่มต้น 0 และพร้อมกันนั้น increment function สร้าง assertion ของค่า 1 ก็จะเกิด conflict
    • การตรวจจับ conflict นี้อาจช่วยป้องกันผลลัพธ์น่าประหลาดใจจำนวนมากที่เกิดจากการประกอบ transaction function แบบผิดพลาด
  • ถ้า write set เป็นคนละคู่ [entity, attribute] การตรวจจับ conflict เพียงอย่างเดียวจะรักษา invariant ได้ยาก
    • เวิร์กโหลด grant แสดงสถานการณ์นี้
    • approve และ deny ต่างตรวจสอบก่อนว่า grant นั้นยังไม่ได้ approved หรือ denied จากนั้นจึงเพิ่มคนละแอตทริบิวต์
  • เมื่อเรียก approve และ deny จากคนละทรานแซกชัน ทรานแซกชันแบบ Serializable ของ Datomic จะคง invariant ไว้ได้
    • แต่ถ้าเรียกทั้งสองร่วมกันภายในทรานแซกชันเดียว ทั้งคู่จะเห็นสถานะเริ่มต้นและสำเร็จทั้งคู่
[['grant/approve id]
 ['grant/deny id]]
  • ผลลัพธ์คือ grant จะมีทั้ง approved-at และ denied-at
    • ทำให้ invariant ที่ว่า “grant ต้องไม่เป็น approved และ denied พร้อมกัน” ถูกทำลาย
    • ตัวตรวจสอบ conflict ภายในทรานแซกชันของ Datomic ไม่สามารถกันสิ่งนี้ได้ เพราะฟังก์ชันทั้งสองสร้าง assertion บนคนละแอตทริบิวต์
  • ปรากฏการณ์นี้คล้ายกับ Write Skew ของ Berenson และคณะ
    • เพราะฟังก์ชันทั้งสองมองไม่เห็นผลของกันและกัน จึงเกิด read-write anti-dependency cycle
    • หากมอง transaction function เป็นทรานแซกชัน ก็จะคล้ายกับ G2-item anomaly ที่ Repeatable Read และ Serializability ห้ามไว้
  • Datomic และ Nubank มองพฤติกรรมนี้ว่าไม่ใช่บั๊ก แต่เป็นพฤติกรรมที่คาดไว้ของ Datomic
    • Nubank มีแผนจะคง semantics แบบ concurrent intra-transaction ของ Datomic ต่อไป

เสริมความแข็งแรงของ invariant ด้วย entity predicate

  • Datomic มีกลไกข้อจำกัดอย่าง type, uniqueness, predicate ของแอตทริบิวต์บางตัว และ entity predicate
    • entity predicate รับสถานะ DB ตัวเลือกที่มีการใช้ผลของทรานแซกชันทั้งหมดแล้ว พร้อม entity ID แล้วคืนค่า true หรือ false ว่าอนุญาตให้ commit หรือไม่
    • แม้ชื่อจะเป็น entity predicate แต่สามารถเข้าถึงสถานะ DB ทั้งหมดได้ จึงแสดงข้อจำกัดระดับ global ที่เกินกว่า entity เดียวได้ด้วย
  • ในตัวอย่าง grant สามารถใช้ predicate valid-grant? เพื่อไม่ให้ approved-at และ denied-at อยู่พร้อมกันได้
(defn valid-grant?
  [db eid]
  (let [{:grant/keys [approved-at denied-at]}
        (d/pull db '[:grant/approved-at
                     :grant/denied-at]
                   eid)]
   (not (and approved-at denied-at))))
  • ใน schema จะเพิ่ม entity spec เพื่ออ้างถึง predicate นี้
    • entity predicate ที่ผูกกับ entity spec จะไม่ถูกใช้กับทุกทรานแซกชันโดยอัตโนมัติ
    • Datomic มองว่าการใช้ entity spec เป็นการตัดสินใจระดับโดเมน และแต่ละทรานแซกชันต้องร้องขออย่างชัดเจน
  • ฟังก์ชัน approve และ deny สามารถร้องขอให้ใช้ entity spec หลังเพิ่มแอตทริบิวต์แล้ว ผ่าน :db/ensure virtual datom
(defn approve
  [db id]
  [[:db/add id :grant/approved-at (Date.)]
   [:db/add id :db/ensure :grant/valid?]])
  • เมื่อใช้ entity spec นี้ การพยายามทำ approve และ deny พร้อมกันในทรานแซกชันเดียวกันจะทำให้เกิดข้อผิดพลาดของ entity predicate และ invariant จะถูกรักษาไว้
    • ข้อผิดพลาดจะมี :db.error/entity-pred, :db.error/pred-return false รวมอยู่ด้วย

การเปลี่ยนแปลงเอกสารและคำแนะนำสำหรับผู้ใช้

  • Datomic ปรับแก้เอกสารครั้งใหญ่หลังทำงานร่วมกับ Jepsen
    • transaction safety documentation สะท้อนระดับความปลอดภัยที่เข้มงวดกว่าซึ่ง Datomic เห็นว่าระบบมอบให้จริง
    • ระบุอย่างชัดเจนถึง Serializability แบบทั่วทั้งระบบ, monotonicity ต่อ peer, และ Strict Serializability สำหรับการเขียนหรือการอ่านที่ใช้ sync
    • คำอธิบายแบบ “single-writer” ถูกนำออกจากเอกสารด้านความปลอดภัย
  • เอกสาร transaction syntax and semantics ครอบคลุมโครงสร้างของ transaction request, กฎการขยายของ map form และ transaction function, รวมถึงกระบวนการนำ transaction ไปใช้โดยละเอียด
  • เอกสาร transaction functions ก็ได้รับการปรับแก้เช่นกัน
    • อธิบายกลไกหลายอย่างที่รับประกัน consistency, การสร้างและการเรียกใช้ฟังก์ชัน, และพฤติกรรมของฟังก์ชันที่มีมาให้ในตัว
    • ถ้อยคำที่ระบุว่า transaction function สามารถ “atomically analyze and transform database values” หรือรับประกัน “atomic read-modify-write processing” ถูกนำออก
  • Datomic ตั้งใจจะเรียกโครงสร้างข้อมูลที่ส่งให้ d/transact ต่อไปว่า transaction request แทนที่จะเรียกว่า “transaction”
    • องค์ประกอบภายในจะเรียกว่า “data” แทน “statements” หรือ “operations”
    • [:db/add ...] และ [:db/retract ...] คือ assertion request และ retraction request ตามลำดับ
    • สิ่งนี้ช่วยแยกความต่างระหว่าง assertion datom จริง กับ assertion request ที่ยังไม่สมบูรณ์ภายใน transaction request
  • ข้อควรระวังที่ผู้ใช้ต้องรู้มีความชัดเจน
    • Serializability ระหว่างทรานแซกชันของ Datomic เชื่อถือได้
    • แต่ concurrent execution semantics ภายในทรานแซกชันเป็นทางเลือกที่พบไม่บ่อย จึงต้องระวังเมื่อเรียก transaction function หลายตัวในทรานแซกชันเดียวกัน
    • โดยเฉพาะกรณีที่ read set ทับซ้อนกันแต่ write set แยกจากกัน
    • การ increment หลายครั้งอาจถูกรวมเงียบ ๆ จนเหลือเป็นการอัปเดตครั้งเดียว
    • ใช้ attribute predicate และ entity spec ได้ แต่ entity spec ต้องร้องขออย่างชัดเจนในทุกทรานแซกชันที่ต้องการ
  • ในแง่การปฏิบัติการ ยังต้องคำนึงถึงการรีสตาร์ต transactor และความผันผวนของเครือข่ายด้วย
    • Datomic transactor จะปิดตัวเองหากสื่อสารกับ storage ไม่ได้เป็นเวลาหลายนาที
    • Jepsen แนะนำให้เพิ่ม retry loop ให้ transactor เพื่อให้ทนต่อความผันผวนของเครือข่ายได้ดีขึ้น

ข้อจำกัดและคำถามวิจัยในอนาคต

  • การทดสอบครั้งนี้ยังมีรายการที่อยู่นอกขอบเขตการประเมิน
    • ยังไม่ได้ประเมิน excision และ historical query
    • แม้จะยังไม่ได้ตรวจสอบ Datomic client library แต่ Jepsen มองว่าพฤติกรรมของมันน่าจะคล้ายกับ peer ที่ใช้ทดสอบ
    • ใช้ storage engine เพียงตัวเดียวคือ DynamoDB
    • Datomic Cloud ก็ไม่ได้ถูกประเมิน และ Datomic Cloud ใช้สถาปัตยกรรมที่ต่างออกไปเล็กน้อย
  • Jepsen ระบุว่าแทบไม่รู้จักระบบหรือการทำ formalization ที่ให้ทั้ง Serializability ระหว่างทรานแซกชันและ concurrent semantics ภายในทรานแซกชันพร้อมกัน
  • โมเดลของ Datomic ก่อให้เกิดคำถามวิจัยหลายข้อ
    • จะมอง Datomic transaction เป็น dual ของ transaction แบบดั้งเดิม หรือเป็นโมเดล “co-transaction” ได้หรือไม่
    • จุดแข็งและจุดอ่อนของโมเดลนี้จะบรรเทาได้ด้วย static analysis, runtime check หรือ API extension หรือไม่
    • ผู้ใช้จริงมีแนวโน้มมากแค่ไหนที่จะเขียน transaction ที่ทำลาย invariant
  • มีการกล่าวถึงโครงการวิจัย temporal Datalog ที่ใช้เปรียบเทียบ ได้แก่ Alvaro’s Dedalus และ Fauna
    • Dedalus ก็ทำให้ transaction เกิดขึ้น “all at once” เช่นเดียวกับ Datomic
    • Fauna เป็น temporal database ที่รองรับ Strong Serializability ด้วย และต่างจาก Datomic ตรงที่ดูเหมือนจะให้ทั้ง serial execution และ incremental side effect ภายในทรานแซกชัน
  • ความคล้ายคลึงกันระหว่าง end-of-transaction conflict checker ของ Datomic กับกฎ first-committer-wins ของ Snapshot Isolation ก็ยังเป็นโอกาสสำหรับการวิจัย
    • มีส่วนใดของงานวิจัยด้าน Snapshot Isolation ที่สามารถนำมาใช้กับ Datomic ได้บ้าง
    • cycle ภายใน Datomic transaction จะแสดงด้วย anti-dependency edge แบบใด
    • ยังมีคำถามต่อว่ามี analogue ในเชิงความหมายภายในสำหรับปรากฏการณ์อย่าง lost update, Fractured Read, read-only transaction anomaly และ Long Fork หรือไม่
  • ความเชื่อมโยงกับ CALM theorem ก็ยังสามารถศึกษาเพิ่มเติมได้
    • transaction function ที่มีความเป็น monotonic ในเชิงตรรกะ สามารถประกอบรวมกันได้อย่างปลอดภัยภายใน Datomic transaction เดียวกันหรือไม่
    • สามารถศึกษาต่อได้ว่าโปรแกรม Datalog ที่ไม่มี negation จะยังปลอดภัยภายใต้โมเดลการรันนี้หรือไม่

1 ความคิดเห็น

 
GN⁺ 2024-05-17
ความคิดเห็นบน Hacker News
  • ผมได้เฝ้าดูงานนี้ดำเนินไปอยู่ข้าง ๆ และการได้เห็นกระบวนการถกเถียงนั้นน่าสนใจมากจริง ๆ
    ก็น่าประหลาดใจที่ Jepsen ไม่พบเบั๊กร้ายแรง และแค่การทำให้เอกสารกับพฤติกรรมพิเศษที่ตั้งใจออกแบบไว้ชัดเจนขึ้น ก็ถือเป็นผลลัพธ์ที่มีประโยชน์มากแล้ว
    เมื่อคิดว่าเรากำลัง ใช้ Datomic ดำเนินงานธนาคาร อยู่ นี่ก็คุ้มค่ามากในฐานะการฝึกสร้างความเชื่อมั่น

    • ถ้าคิดว่านี่เป็นฐานข้อมูลที่ Rich Hickey ออกแบบ ผลลัพธ์ก็ไม่ได้แปลกใจเท่าไร
      เป็นบทความที่ยอดเยี่ยมจริง ๆ และทุกครั้งที่รู้สึกว่าตัวเองค่อนข้างฉลาด การอ่าน บทวิเคราะห์ Jepsen ก็ช่วยให้ถ่อมตัวลงได้ดี
    • ขอถามได้ไหมครับว่าเป็นธนาคารไหน?
    • เกี่ยวกับส่วนที่ว่า “ก็น่าประหลาดใจที่ Jepsen ไม่พบเบั๊กร้ายแรง” ในรายงานระบุไว้ว่า “เราพิสูจน์การมีอยู่ของบั๊กได้ แต่พิสูจน์ การไม่มีอยู่ของบั๊ก ไม่ได้”
    • ก่อนจะนำไปใช้ดำเนินงานธนาคาร ไม่ได้ทำการตรวจสอบแบบนี้เองมาก่อนหรือครับ?
  • ครั้งนี้เป็นครั้งแรกที่ผมอ่านรายงาน Jepsen อย่างลึกซึ้ง และผมชอบส่วนที่อธิบาย การทำงานภายในของทรานแซกชันใน Datomic อย่างชัดเจน
    ผมยังได้ตระหนักด้วยว่าตัวเองไม่เข้าใจความแตกต่างระหว่างทรานแซกชันของ Datomic กับทรานแซกชันของฐานข้อมูล SQL มากแค่ไหน
    โดยเฉพาะย่อหน้าที่ว่า “ก่อนหน้านี้ Datomic เรียกโครงสร้างข้อมูลที่ส่งให้ d/transact ว่า ‘transaction’ และเรียกองค์ประกอบของมันว่า ‘statements’ หรือ ‘operations’ ต่อไปนี้ตั้งใจจะเรียกโครงสร้างนี้ว่า ‘transaction request’ และเรียกองค์ประกอบของมันว่า ‘data’” นั้นสะดุดตามาก
    สงสัยว่านี่มีความหมายอย่างไรต่อ d/transact-async ในเนมสเปซ datomic.api และฟังก์ชันที่เกี่ยวข้อง
    ผมไม่ได้ใช้ Datomic มาเกือบ 1 ปีแล้ว ดูเหมือนหลายอย่างจะเปลี่ยนไปมาก

    • จากผลการทดสอบของ Jepsen ไม่จำเป็นต้องเปลี่ยนแปลง ซอฟต์แวร์ Datomic
      ฟีเจอร์ทั้งหมดของ datomic.api ยังคงเหมือนเดิม
  • เป็นรายงานที่ยอดเยี่ยมและละเอียดเกี่ยวกับฐานข้อมูลที่ดีจริง ๆ
    ดีใจมากเช่นกันที่เอกสารถูกทำให้ชัดเจนและอัปเดต
    เพิ่มเติมคือ ถ้า Apple ช่วยออกค่าใช้จ่ายในการวิเคราะห์ Jepsen สำหรับ FoundationDB ก็คงดีมากจริง ๆ
    ผมรู้ว่า Aphyr เคยบอกว่า “การทดสอบของพวกเขาน่าจะดีกว่า” แต่ถ้า Jepsen ไม่พบปัญหาใน FoundationDB จริง ๆ นั่นก็จะเป็นหลักฐานที่แข็งแรงอีกอย่างหนึ่งว่าเป็นฐานข้อมูลที่ยอดเยี่ยม

    • ผมไม่ได้อยากแย่งอาชีพของ Aphyr เลย แต่สงสัยว่ามีเหตุผลอะไรเป็นพิเศษที่ทำให้ผู้ร่วมพัฒนาที่มีแรงจูงใจเพียงพอไม่สามารถสร้าง การทดสอบ Jepsen เองได้ หรือว่ามันแพงจนแม้แต่ “วิธีคล้าย ๆ GoFundMe” ก็ยังรับไม่ไหว
      ผมไม่ได้เชี่ยวชาญด้านนี้ แต่พอเห็นคำว่า “ถ้า $foo ช่วยออกค่าใช้จ่ายให้ก็ดี” ก็ทำให้หูผึ่ง
      เงินทุนมีอยู่เหลือเฟือ แต่จากประสบการณ์ของผม การรอให้ Apple ทำอะไรสักอย่างมักใช้เวลานาน
  • Jepsen พบสถานการณ์ที่ชัดเจนซึ่งนำไปสู่ การละเมิด invariant แต่สิ่งที่ Datomic ตอบสนองดูเหมือนจะมีแค่การทำให้เอกสารชัดเจนขึ้น จึงน่าประทับใจอยู่
    สุดท้ายแล้วหมายความว่าทีม Datomic ยอมรับว่าการละเมิดแบบนี้เกิดขึ้นได้ แต่ไม่ใส่ใจหรือเปล่า?
    ในบทความบอกว่า “จากมุมมองของ Datomic การละเมิด invariant ในเวิร์กโหลด grant เป็นข้อผิดพลาดของผู้ใช้ ฟังก์ชันทรานแซกชันไม่ได้ถูกรันแบบ atomic ตามลำดับ หาก operation อื่นในทรานแซกชันสามารถทำให้ precondition นั้นใช้ไม่ได้ การตรวจสอบ precondition ในฟังก์ชันทรานแซกชันก็ไม่ปลอดภัย”

    • ตามที่ Jepsen ตรวจสอบแล้ว กลไกบังคับใช้ invariant ของ Datomic ทำงานตามที่ออกแบบไว้
      หากต้องการดูว่าสิ่งนี้หมายความว่าอย่างไรจากมุมมองผู้ใช้ ลองพิจารณาข้อมูลทรานแซกชันแบบกึ่งโค้ดดังนี้
      [ [Stu favorite-number 41] ;; maybe more stuff [Stu favorite-number 42] ]
      ถ้าอ่านแบบเชิงปฏิบัติการ จะดูเหมือนว่าช่วงต้นของทรานแซกชันผมชอบเลข 41 และต่อมาจึงชอบเลข 42
      หลังทรานแซกชันจบ ผู้สังเกตควรคาดหวังว่าจะเห็นว่าผมชอบแค่ 42 และต้องกังวลว่าในเงื่อนไขใดบ้างที่จะเห็น 41 ได้
      การตีความเชิงปฏิบัติการของความหมายภายในทรานแซกชันแบบนี้พบได้ทั่วไปในหลายฐานข้อมูล แต่ตั้งสมมติฐานว่ามีหลายช่วงเวลาอยู่ภายในทรานแซกชัน
      ใน Datomic ไม่มีช่วงเวลาแบบนั้น และก็ไม่ต้องการให้มี อีกทั้งชอบตรงที่ไม่ต้องกังวลว่าเกิดอะไรขึ้น “ระหว่างกลางทรานแซกชัน”
      ใน Datomic ข้อเท็จจริงทั้งหมดของทรานแซกชันเกิดขึ้น ณ เวลาเดียวกัน ดังนั้นทรานแซกชันนี้จึงบอกว่าผมเริ่มชอบตัวเลขทั้งสองพร้อมกัน
      ถ้าอ่านทรานแซกชันของ Datomic ผิดว่าเป็นการประกอบกันของหลาย operation ก็ย่อมหา “ความผิดปกติของ invariant” ได้สารพัด
      ในทางกลับกัน หากเอาโมเดลของ Datomic ไปครอบ SQL transaction ผิด ๆ ก็จะหา “ความผิดปกติของ invariant” ได้เช่นกัน
      เพราะมีโอกาสเข้าใจผิดแบบนี้ จึงจำเป็นต้องมีเอกสารที่ดี และเราได้ปรับปรุงเอกสารร่วมกับ Jepsen [1] เพื่อขัดเกลาถ้อยคำที่ชวนให้เผลอเข้าใจผิดและลดความเข้าใจผิด
      เรายังเพิ่ม technical note ที่พูดถึงความเข้าใจผิดเฉพาะเรื่องนี้โดยตรงด้วย [2]
      [1] https://docs.datomic.com/transactions/transactions.html#tran...
      [2] https://docs.datomic.com/tech-notes/comparison-with-updating...
    • สรุปคือใกล้เคียงกับว่า “เป็นกับดักที่อาจเกิดขึ้นได้ แต่สอดคล้องกับเอกสาร และทำงานตามที่ออกแบบไว้”
      จะสำคัญจริงหรือไม่นั้นขึ้นอยู่กับว่าผู้ใช้พยายามรักษา invariant แบบใดด้วยการเขียนฟังก์ชันทรานแซกชัน และฟังก์ชันนั้นจะรักษา invariant ได้เฉพาะเมื่อรันแบบ ตามลำดับ ไม่ใช่พร้อมกันหรือไม่
      จุดยืนของ Datomic หรือถ้าฝั่ง Datomic เข้ามาชี้แจงเองก็คงดี คือผู้ใช้ไม่ได้เขียนฟังก์ชันทรานแซกชันแบบนั้นบ่อยนัก
      จุดยืนนี้พอปกป้องได้ เพราะในเอกสารระบุไว้ชัดว่าฟังก์ชันทรานแซกชันไม่ได้สังเกตกันเอง แต่สังเกต สถานะตอนเริ่มทรานแซกชัน
      ในทางกลับกัน เอกสารก็มีถ้อยคำที่ชวนให้คิดว่าฟังก์ชันทรานแซกชันสามารถใช้เพื่อรักษา invariant ได้: “[txn fns] can atomically analyze and transform database values. You can use them to ensure atomic read-modify-update processing, and integrity constraints...”
      เพราะถ้อยคำนี้ และเพราะฐานข้อมูลแทบทั้งหมดที่เป็น serializable ใช้ความหมายแบบลำดับภายในทรานแซกชัน รายงานจึงให้พื้นที่กับประเด็นนี้ค่อนข้างมาก
      เป็นคำถามที่ซับซ้อน จึงไม่มีคำตอบที่ชัดเจน และอยากฟังว่าชุมชนฐานข้อมูลทั่วไป โดยเฉพาะผู้ใช้ Datomic รับความหมายแบบนี้อย่างไร
    • ผมว่าท้ายส่วน 3.1 ของบทความก็ตอบไว้แล้ว: “พฤติกรรมนี้อาจน่าประหลาดใจ แต่โดยรวมแล้วสอดคล้องกับเอกสารของ Datomic Nubank ไม่มีแผนจะเปลี่ยนพฤติกรรมนี้ และเราไม่ถือว่านี่เป็นบั๊ก”
      ถ้าพูดว่า “สถานการณ์ที่นำไปสู่การละเมิด invariant” จะฟังเหมือนเป็นบั๊กของ Datomic แต่เรื่องนี้ไม่ใช่แบบนั้น
      ต้องเข้าใจ วิธีที่ Datomic ประมวลผลทรานแซกชัน แล้วเขียนโค้ดให้สอดคล้อง
      ไม่เกี่ยวกับ Nubank แต่ผมไม่เคยเจอสถานการณ์ที่เรื่องนี้เป็นปัญหาในการใช้ Datomic เป็นฐานข้อมูลทั่วไป
    • ฟังดูคล้ายกับสถานการณ์ในฐานข้อมูลเชิงสัมพันธ์บางตัว ที่ต้องรู้ว่าถ้าจะอัปเดตโดยอาศัยค่าที่เพิ่งเลือกมา ต้องใช้ SELECT ... FOR UPDATE
  • เสริมสำหรับคนที่ไม่รู้ ชื่อ Jepsen เป็นมุกเล่นคำจาก Carly Rae Jepsen ผู้ร้องเพลง “call me maybe”
    ผมว่ามันเป็นชื่อที่สมบูรณ์แบบสำหรับโปรเจกต์วิจัยระบบกระจายศูนย์

    • ผมคิดว่าโปรเจกต์นี้เก่าแล้วก็จริง แต่ไม่รู้ว่าเพลงนั้นเก่าขนาดนั้น
      commit แรกคือปี 2013
      https://github.com/jepsen-io/jepsen/tree/4b112e7046a20efa80a...
      เพลงออกปี 2011 เวลาผ่านไปเร็วจริง ๆ
  • ผมยังไม่ได้ใช้ Datomic ในงานจริงมานานนัก แต่มันแปลกเฉพาะตัวมากจนสงสัยว่ามีอะไรในนี้ที่ควรน่าประหลาดใจหรือเปล่า
    ทรานแซกชันของ Datomic โดยพื้นฐานแล้วใกล้เคียงกับ batch และผมคิดมาตลอดว่ามันเป็น single-thread เสมอ ดังนั้นการไม่มี race condition มากนักจึงดูสมเหตุสมผล
    ตามการออกแบบแล้วมันอยู่ฝั่งช้าและปลอดภัย

    • ตัวอย่างที่ว่า “ถ้าเพิ่ม x สองครั้งในทรานแซกชันเดียวกัน ผลจะเป็น x+1 ไม่ใช่ x+2” ดูค่อนข้างสำคัญ
      คงต้องระวังมากทีเดียว
    • Datomic มีการบีบอัด transaction log ในสักวันหนึ่งไหม?
  • ขอบคุณ Kyle
    ชัดเจนว่าเอกสารของเรายังไม่เพียงพอ
    ผมพยายามเขียนเอกสารเกี่ยวกับ โมเดลทรานแซกชัน ของ Datomic ให้ชัดเจนและครอบคลุมยิ่งขึ้นร่วมกับ Rich
    หวังว่าจะช่วยป้องกันความเข้าใจผิดที่พบบ่อยได้ล่วงหน้า และยินดีรับฟีดแบ็กทั้งหมด
    https://docs.datomic.com/transactions/model.html

  • โมเดลข้อมูลของ Datomic ค่อนข้างเข้าใจง่ายหากคุ้นเคยกับ triple store หรือ RDF
    แต่ในเอกสารหรือการถกเถียงออนไลน์ มักไม่ค่อยมีการพูดถึงความคล้ายคลึงนี้
    ผมสงสัยว่าเป็นเพราะผู้คนไม่คุ้นเคยกับแนวคิดแบบนั้น หรือเพราะมองว่าการเชื่อมโยงไปถึง semantic web เป็นสิ่งรบกวน หรือว่ามีความแตกต่างพื้นฐานบางอย่างที่ผมพลาดไป

  • ผมรอบทวิเคราะห์นี้มานานจริง ๆ
    ช่วงนี้กำลังสร้าง ที่เก็บข้อมูลคล้าย Datomic ด้วยตัวเองอยู่ เลยน่าจะมีประโยชน์ และตอนนี้กำลังอ่านอยู่
    บทวิเคราะห์ MongoDB ก็สนุกดี และบทวิเคราะห์อื่น ๆ อย่าง Redis, RethinkDB ก็ควรค่าแก่การอ่านมาก
    หวังว่าสักวันจะมีบทวิเคราะห์เกี่ยวกับ rqlite/dqlite หรือ turso/libsql ด้วย