1 คะแนน โดย GN⁺ 2025-12-09 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Jepsen ทดสอบความทนทานและความสอดคล้องของระบบการส่งข้อความแบบกระจาย NATS JetStream ในสภาวะความล้มเหลวที่หลากหลาย
  • ผลการทดสอบพบว่ามีการสูญเสียข้อมูลและเกิดปรากฏการณ์ split-brain จาก ความเสียหายของไฟล์ (.blk, snapshot) และการ จำลองเหตุการณ์ไฟฟ้าดับ
  • JetStream ทำ fsync โดยค่าเริ่มต้นเพียงทุก 2 นาที ทำให้ข้อความที่ได้รับการอนุมัติล่าสุดอาจยังไม่ถูกเขียนลงดิสก์
  • แม้เพียง การ crash ของ OS โหนดเดียว ก็สามารถทำให้เกิดการสูญเสียข้อมูลและความไม่สอดคล้องของ replica ได้
  • Jepsen แนะนำให้เปลี่ยนค่าเริ่มต้นของ NATS เป็น fsync=always หรือทำการ ระบุความเสี่ยงการสูญเสียข้อมูลไว้ในเอกสารอย่างชัดเจน

1. ภูมิหลัง

  • NATS เป็นระบบสตรีมมิ่งยอดนิยมที่ใช้ในการ publish/subscribe ข้อความแบบ stream
    • JetStream ใช้อัลกอริทึมฉันทามติ Raft เพื่อทำซ้ำข้อมูล และรับประกันการส่งอย่างน้อยหนึ่งครั้ง (at-least-once)
  • JetStream ระบุในเอกสารว่ามีความสอดคล้องแบบ Linearizable และ ความพร้อมใช้งานเสมอ แต่ตาม กฎ CAP ไม่สามารถทำได้ทั้งสองเงื่อนไขพร้อมกัน
  • ตามเอกสารของ NATS สตรีม 3 โหนดสามารถทนต่อการสูญเสียเซิร์ฟเวอร์ได้ 1 ตัว และสตรีม 5 โหนดสามารถทนต่อการสูญเสียได้ 2 ตัว
  • ข้อความถูกถือว่า “บันทึกสำเร็จ” เมื่อเซิร์ฟเวอร์ได้ acknowledge คำขอ publish แล้ว
  • การรักษาความสอดคล้องข้อมูลต้องการโหนด ส่วนใหญ่ (quorum) และในคลัสเตอร์ 5 โหนด ต้องมีอย่างน้อย 3 โหนดทำงานเพื่อบันทึกข้อความใหม่ได้

2. การออกแบบการทดสอบ

  • Jepsen ทำการทดสอบโดยใช้ไคลเอนต์ JNATS 2.24.0 และคอนเทนเนอร์ Debian 12 LXC
    • บางการทดสอบใช้ Docker image อย่างเป็นทางการของ NATS บนสภาพแวดล้อม Antithesis
  • สร้าง JetStream stream เดี่ยว (replication 5) และใส่ความล้มเหลว เช่น การหยุด/ครัชกระบวนการ, network partition, packet loss, ความเสียหายไฟล์ ฯลฯ
  • ใช้ระบบไฟล์ LazyFS เพื่อจำลองเหตุการณ์ไฟฟ้าดับที่ทำให้การเขียนที่ยังไม่ถูก fsync สูญหาย
  • แต่ละ process จะ publish ข้อความเฉพาะตัวเอง และหลังการทดสอบเสร็จ จะตรวจสอบการมีอยู่ของข้อความที่ได้รับการ acknowledge ในทุกโหนด
  • ถ้าข้อความมีอยู่ในบางโหนดเท่านั้น จะถูกจัดว่าเป็น divergence (ความไม่ตรงกันของการทำซ้ำ)

3. ผลลัพธ์หลัก

3.1 การสูญเสียข้อมูลทั้งหมดใน NATS 2.10.22 (#6888)

  • พบปรากฏการณ์ที่ stream ของ JetStream หายไปทั้งหมด แม้มีเพียงการ crash process อย่างเดียว
  • หลังเกิดข้อผิดพลาด No matching streams for subject แล้วไม่สามารถกู้คืนได้เป็นเวลาหลายชั่วโมง
  • สาเหตุเกิดจาก การกลับด้าน snapshot ของ leader, การลบสถานะ Raft เป็นต้น และได้รับการแก้ไขในเวอร์ชัน 2.10.23

3.2 การสูญเสียข้อมูลเมื่อไฟล์ .blk เสียหาย (#7549)

  • เมื่อไฟล์ .blk ของ JetStream มี ข้อผิดพลาดเพียง 1 บิตหรือการตัดทอน (truncation) จะสูญเสียการเขียนที่ได้รับการอนุมัติได้เป็นจำนวนแสนรายการ
    • ตัวอย่าง: สูญหาย 679,153 จาก 1,367,069 รายการ
  • แม้ความเสียหายเกิดในโหนดบางตัวเท่านั้นก็เกิดการสูญเสียข้อมูลขนาดใหญ่และ split-brain ได้
    • ตัวอย่าง: โหนด n1, n3, n5 สูญเสียข้อความสูงสุดถึง 78%
  • NATS กำลังดำเนินการตรวจสอบประเด็นนี้

3.3 การลบข้อมูลทั้งหมดเมื่อไฟล์ snapshot เสียหาย (#7556)

  • เมื่อไฟล์ snapshot ใน data/jetstream/$SYS/_js_/ เสียหาย โหนดจะมองว่า stream เป็น orphaned และ ลบข้อมูลทั้งหมด
  • แม้เพียงโหนดบางส่วนเสียหาย ก็อาจทำให้คลัสเตอร์ไม่ถึง quorum และ stream ใช้งานไม่ได้ถาวร
  • ตัวอย่าง: โหนด n3, n5 เสียหาย → n3 ถูกเลือกเป็น leader และลบ jepsen-stream ทั้งหมด
  • Jepsen ชี้ให้เห็นความเสี่ยงว่าในช่วงการเลือก leader โหนดที่เสียหายอาจถูกเลือกเป็น leader ได้

3.4 การสูญเสียข้อมูลจากการตั้งค่า fsync เริ่มต้น (#7564)

  • JetStream ทำ fsync ทุก 2 นาทีเป็นค่าเริ่มต้น และยืนยันข้อความทันทีหลังรับข้อมูล
    • ส่งผลให้ข้อความที่ได้รับการอนุมัติเมื่อเร็ว ๆ นี้อาจยังไม่ถูกเขียนลงดิสก์
  • เมื่อเกิด การสูญหายไฟฟ้าหรือ kernel crash อาจทำให้การสูญเสียข้อความที่ได้รับการอนุมัติได้หลายสิบวินาที
    • ตัวอย่าง: สูญหาย 131,418 จาก 930,005 รายการ
  • การล้มเหลวของโหนดเดี่ยวต่อเนื่องกันก็อาจนำไปสู่การลบ stream ทั้งหมดได้
  • เอกสารแทบไม่กล่าวถึงพฤติกรรมนี้อย่างชัดเจน
  • Jepsen แนะนำให้เปลี่ยนเป็น fsync=always โดยค่าเริ่มต้น หรือมี คำเตือนชัดเจนเกี่ยวกับความเสี่ยงการสูญเสียข้อมูล

3.5 split-brain จาก OS crash ของโหนดเดียว (#7567)

  • การเกิด การดับไฟฟ้าหรือ kernel crash ของโหนดเดียวก็สามารถทำให้เกิด การสูญเสียข้อมูลและความไม่สอดคล้องกันของการทำซ้ำ ได้
  • ในโครงสร้าง leader-follower เมื่อตัวอย่างบางโหนด commit ไว้ในหน่วยความจำเท่านั้นแล้วล้มเหลว
    หลายโหนดจึงสูญเสียการเขียนนั้นและดำเนินไปยังสถานะแบบใหม่
  • ในการทดสอบ หลังไฟดับโหนดเดียวเกิดขึ้นพบ split-brain ที่คงอยู่นาน
    • พบร่องรอยการสูญเสียข้อความที่ได้รับการอนุมัติในช่วงเวลาแตกต่างกันระหว่างโหนดต่าง ๆ
  • Jepsen อ้างถึงกรณีใน Kafka ที่คล้ายกัน และเน้นว่า ความเสี่ยงเดียวกันยังคงมีในระบบฐาน Raft

4. การอภิปรายและข้อสรุป

  • ปัญหาการสูญเสียข้อมูลทั้งหมดใน 2.10.22 ได้รับการแก้ใน 2.10.23 แล้ว
  • ใน 2.12.1 ยังคงเกิด การสูญเสียข้อมูลและ split-brain จาก ความเสียหายไฟล์และ OS crash อยู่
  • เมื่อไฟล์ .blk และ snapshot เสียหาย พบการขาดหายข้อความที่บางโหนดหรือการลบ stream ทั้งหมด
  • เนื่องจากช่วงเวลารัน fsync เริ่มต้นยาว การสูญเสียข้อมูลที่ได้รับการยืนยันอาจเกิดเมื่อมีการล้มเหลวโหนดหลายตัวพร้อมกัน
  • Jepsen เสนอให้ตั้ง fsync=always หรือระบุ คำเตือนความเสี่ยงอย่างชัดแจ้งในเอกสาร
  • ข้ออ้างว่า JetStream “พร้อมใช้งานตลอดเวลา” ขัดกับ กฎ CAP จึงต้องแก้ไขเอกสาร
  • Jepsen ระบุว่า การพิสูจน์การมี bug เป็นเรื่องที่ทำได้ แต่ไม่สามารถพิสูจน์การขาดความปลอดภัยได้

4.1 บทบาทของ LazyFS

  • ใช้ LazyFS เพื่อจำลองการสูญเสียการเขียนที่ยังไม่ผ่าน fsync
  • การเกิดไฟฟ้าดับสามารถจำลองข้อผิดพลาด storage หลายแบบ เช่น torn write และความเสียหายอื่น ๆ ได้
  • งานวิจัยที่เกี่ยวข้อง When Amnesia Strikes (VLDB 2024) รายงานข้อบกพร่องแบบคล้ายกันใน PostgreSQL, Redis, ZooKeeper และระบบอื่น ๆ

4.2 ภารกิจต่อไป

  • การสูญเสียข้อความในระดับ consumer รายตัว, ลำดับข้อความ, และการยืนยัน Linearizable/Serializable ยังไม่ได้ถูกทดสอบ
  • การรับประกันการส่ง exactly-once (หนึ่งครั้งอย่างแม่นยำ) ยังเป็นหัวข้อวิจัยในอนาคต
  • พบข้อผิดพลาดเอกสารและ ขั้นตอน health check สำคัญที่ขาดหาย ในการเพิ่มหรือลบโหนด (#7545)
  • แนวทางการเปลี่ยนแปลงโครงสร้างคลัสเตอร์ให้ปลอดภัยยังไม่ชัดเจน

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

 
GN⁺ 2025-12-09
ความเห็นจาก Hacker News
  • ทุกครั้งที่มีใครสักคนข้าม ทฤษฎีที่ซับซ้อน แล้วสร้างระบบแบบนี้ขึ้นมา ก็จะได้เห็น aphyr มาถล่มมันพัง
    ตอนนี้เลยเริ่มสงสัยว่า AI อาจจะอ่านเอกสารโปรเจกต์แล้วทำนาย ความเป็นไปได้ของการสูญหายของข้อมูล จากแค่วลีการตลาดได้หรือเปล่า
    • ให้ความรู้สึกเหมือนกำลังลูบเครายาวแล้วพยักหน้าเห็นด้วย
      คนชอบพูดกันเสมอว่า “ทฤษฎีนั้นถูกให้ความสำคัญเกินไป” หรือ “การแฮ็กดีกว่าการเรียนในโรงเรียน” แต่สุดท้ายก็มักสะดุดขาตัวเองใน problem space ที่ถูกบันทึกไว้เป็นเอกสาร
    • ผมก็เคยให้ LLM ทำอะไรคล้าย ๆ กันเหมือนกัน แล้วผลลัพธ์ก็ค่อนข้าง มีประโยชน์
  • ให้ความรู้สึกว่า NATS ไม่สนใจทฤษฎี CAP
    • ดูเหมือนเป็นคำพูดที่ถูกประเมินค่าต่ำไป
  • ผมใช้ NATS สำหรับงาน in-memory pub/sub มาโดยตลอด และในส่วนนั้นมันยอดเยี่ยมมาก
    มันจัดการ รายละเอียดการสเกลที่ละเอียดอ่อน ได้ดีด้วย
    แต่ไม่เคยใช้ persistence มาก่อน เลยไม่คิดว่ามันจะเปราะบางได้ถึงขนาดนี้
    ตกใจที่มันอ่อนแอแม้กระทั่งต่อความเสียหายของไฟล์แบบบิตเดียว
  • มีข้อมูลที่เกี่ยวข้องคือ Jepsen กับ Antithesis เพิ่งเผยแพร่ อภิธานศัพท์ระบบกระจาย เมื่อไม่นานมานี้
    เป็นแหล่งอ้างอิงที่ดีมาก → Jepsen Glossary
  • เคยสงสัยเรื่อง ความแตกต่างของเนื้อหา ระหว่าง aphyr.com/tags/jepsen กับ jepsen.io/analyses
    เพิ่งเจอ aphyr.com ไม่นานนี้ เลยคาดหวังว่าจะมีข้อมูลเชิงลึกเยอะมาก
    • เดิมที Jepsen เริ่มต้นจาก ซีรีส์บล็อกส่วนตัว
      หลังจากนั้น jepsen.io ก็พัฒนาเป็น โปรเจกต์แบบมืออาชีพ และเริ่มดำเนินการอย่างจริงจังราว 10 ปีก่อน
  • สงสัยว่าทำไมถึงมีการตั้งค่า “Lazy fsync by Default” อยู่
    เป็นเพราะต้องการเพิ่มผล benchmark ให้สูงขึ้นหรือเปล่า? ในคลัสเตอร์เล็ก ๆ การตั้งค่าแบบนี้มักจะ เป็นต้นตอของปัญหา
    • มันไม่ได้เพิ่มแค่ latency แต่ยังเพิ่ม throughput ด้วย
      แอปพลิเคชันจำนวนมากไม่ได้ต้องการความทนทานแบบเต็มรูปแบบ ดังนั้น lazy fsync อาจมีประโยชน์ได้
      แต่การตั้งให้เป็นค่าเริ่มต้นยังเป็นเรื่องที่ถกเถียงกันได้
    • ผมสงสัยมาตลอดว่าทำไม fsync ถึงต้องถูกหน่วงไว้แบบนี้เสมอ
      ดูเหมือนว่าน่าจะแก้ด้วยการทำ batch แบบเดียวกับ TCP corking ได้
    • นี่เป็นหนึ่งในสิ่งที่ทำได้เพราะมันเป็นระบบกระจาย
      เนื่องจากความล้มเหลวจาก lazy fsync มักจะไม่เกิดขึ้นพร้อมกันในทุกโหนด
    • ใช่ เป็นทางเลือกเพื่อเพิ่มประสิทธิภาพ
    • ต้องการทั้งความทนทานจาก การทำสำเนาและการกระจาย และ throughput ที่ได้มาจาก lazy fsync
  • ขอแนะนำ s2.dev เป็น ทางเลือกแบบ serverless สำหรับ JetStream
    ข้อดี: รองรับ สตรีมไม่จำกัด พร้อมความทนทานระดับ object storage
    ข้อเสีย: ยังไม่มีฟีเจอร์ consumer group
    • สงสัยว่าเคยรันการทดสอบ Jepsen บ้างหรือยัง
  • ปัญหาคือ NATS โดยพื้นฐานแล้วจะทำ fsync ทุก 2 นาทีเท่านั้น และส่ง ack กลับทันที
    ถ้ามีหลายโหนดล้มพร้อมกัน ก็อาจเกิด การสูญหายของข้อมูลที่ถูก commit แล้ว ได้
    ทำให้นึกถึงการตลาด “เว็บสเกล” ในยุคแรก ๆ ของ MongoDB
    ผมคิดว่าค่าเริ่มต้นควรเป็น ตัวเลือกที่ปลอดภัยที่สุด เสมอ
    • NATS ระบุไว้อย่างชัดเจนว่ามัน รับประกันแค่ความพร้อมใช้งานของคลัสเตอร์
      ซึ่งผมกลับชอบจุดนั้น เพราะสามารถออกแบบระบบต่อยอดบนมันได้
      ตอนที่ใช้ในปี 2018 มันทั้งเร็วและดูแลง่าย
    • DB สมัยใหม่ส่วนใหญ่ก็ไม่ได้มีค่าเริ่มต้นที่ปลอดภัยสมบูรณ์
      เช่น ระดับ transaction isolation เริ่มต้นของ PostgreSQL คือ read committed
      Redis เองก็ทำ fsync ทุก 1 วินาทีเป็นค่าเริ่มต้น
    • Redis cluster จะส่ง ack กลับก็ต่อเมื่อทำสำเนาไปยังหลายโหนดแล้วเท่านั้น
      แม้ใน standalone Redis ก็สามารถตั้งให้ ack หลัง fsync ได้ แต่เพราะ OS buffering จึงยังรับประกันเต็มที่ได้ยาก
      สุดท้ายแล้วสิ่งสำคัญคือการเข้าใจ ความหมายของ ack ให้ชัดเจน
    • ระบบส่วนใหญ่เลือก จุดแลกเปลี่ยนระหว่างความเร็วกับความทนทาน
      ถ้ายึดแต่ค่าเริ่มต้นที่ปลอดภัยอย่างเดียว ประสิทธิภาพก็จะตกลงมาก และภาระในการจูนเองก็จะไปอยู่ที่ผู้ใช้
      เช่น ระดับ isolation เริ่มต้นของ Postgres ก็อ่อนพอที่จะทำให้เกิด race condition ได้
      อ้างอิง: บทความทดสอบ Hermitage
    • ปัญหาคือ fsync มีแต่ ทางเลือกที่สุดโต่ง ให้ใช้
      ในยุค SSD ขั้นกลางอย่าง group-commit หายไปแล้ว และตอนนี้ ต้นทุนการสลับ syscall กลายเป็นคอขวด
      2 นาทีถือว่านานเกินไป (และควรพิจารณาความต่างระหว่าง fdatasync กับ fsync ด้วย)
  • พูดตรง ๆ ว่าพอเดาได้ แต่ไม่คิดว่าจะหนักถึงขนาดนี้
    น่าจะใช้ Redpanda ไปเลยดีกว่า
  • เคยสงสัยว่าน่าจะปรับปรุงคำเตือนเรื่องประสิทธิภาพ fsync ของ NATS ได้หรือไม่
    ถ้าทำ batch flush เป็นช่วง ๆ latency อาจเพิ่มขึ้น แต่ throughput น่าจะยังรักษาไว้ได้
    • ไม่จำเป็นต้องใช้ช่วงเวลาคงที่ แค่ คิวคำขอเขียนไว้ระหว่างที่ fsync กำลังทำงาน แล้วค่อยจัดการรวมใน batch ถัดไปก็พอ
      วิธีนี้คล้ายกับการรวม รอบของ Paxos เข้าด้วยกัน
      ควรประมวลผลโดยพอรอบหนึ่งจบก็เริ่ม batch ถัดไปทันที