1 điểm bởi GN⁺ 2025-12-09 | 1 bình luận | Chia sẻ qua WhatsApp
  • Jepsen đã kiểm chứng độ bền và tính nhất quán của hệ thống nhắn tin phân tán NATS JetStream trong nhiều môi trường sự cố khác nhau
  • Kết quả thử nghiệm cho thấy xảy ra mất dữ liệu và hiện tượng split-brain trong các tình huống hỏng file (.blk, snapshot)mô phỏng sự cố mất điện
  • Theo mặc định, JetStream chỉ thực hiện fsync mỗi 2 phút một lần, nên các thông điệp đã được xác nhận gần đây có thể vẫn chưa được ghi xuống đĩa
  • Chỉ riêng sự cố crash hệ điều hành trên một nút cũng có thể gây ra mất dữ liệu và sai lệch giữa các bản sao
  • Jepsen khuyến nghị NATS đổi cấu hình mặc định sang fsync=always hoặc tài liệu hóa rõ ràng rủi ro mất dữ liệu

1. Bối cảnh

  • NATS là một hệ thống streaming phổ biến để publish/subscribe thông điệp dưới dạng stream
    • JetStream dùng thuật toán đồng thuận Raft để sao chép dữ liệu và đảm bảo phân phối ít nhất một lần (at-least-once)
  • Trong tài liệu, JetStream tuyên bố có tính nhất quán tuyến tính hóa (Linearizable)luôn sẵn sàng, nhưng theo định lý CAP thì không thể đồng thời thỏa mãn cả hai điều kiện này
  • Theo tài liệu của NATS, stream 3 nút có thể chịu mất 1 máy chủ, còn stream 5 nút có thể chịu mất 2 máy chủ
  • Một thông điệp được xem là “đã lưu thành công” vào thời điểm máy chủ acknowledge yêu cầu publish
  • Để đảm bảo tính nhất quán dữ liệu cần có đa số (quorum) nút; trong cụm 5 nút thì phải có ít nhất 3 máy hoạt động mới có thể lưu thông điệp mới

2. Thiết kế thử nghiệm

  • Jepsen tiến hành thử nghiệm với client JNATS 2.24.0 trong môi trường container Debian 12 LXC
    • Một số thử nghiệm được chạy trong môi trường Antithesis với image Docker NATS chính thức
  • Cấu hình một stream JetStream duy nhất (replication 5), rồi tiêm vào các lỗi như dừng tiến trình, crash, phân vùng mạng, mất gói, hỏng file v.v.
  • Sử dụng filesystem LazyFS để mô phỏng sự cố mất điện làm mất các lần ghi chưa được fsync
  • Mỗi tiến trình publish thông điệp riêng, và sau khi kết thúc thử nghiệm thì kiểm tra sự tồn tại của các thông điệp đã được acknowledged trên mọi nút
  • Nếu một thông điệp chỉ tồn tại trên một số nút, trường hợp đó được phân loại là divergence (sai lệch sao chép)

3. Kết quả chính

3.1 Mất toàn bộ dữ liệu trong NATS 2.10.22 (#6888)

  • Phát hiện hiện tượng toàn bộ stream JetStream biến mất chỉ với crash tiến trình đơn giản
  • Xuất hiện lỗi "No matching streams for subject" và không khôi phục được trong nhiều giờ
  • Nguyên nhân là đảo ngược snapshot của leader, xóa trạng thái Raft v.v., và đã được sửa trong phiên bản 2.10.23

3.2 Mất dữ liệu khi file .blk bị hỏng (#7549)

  • Khi file .blk của JetStream gặp lỗi một bit hoặc bị cắt ngắn (truncation), có thể làm mất hàng trăm nghìn bản ghi đã được xác nhận
    • Ví dụ: mất 679.153 trên tổng 1.367.069 bản ghi
  • Chỉ cần một số nút bị hỏng cũng có thể gây ra mất dữ liệu trên diện rộng và split-brain
    • Ví dụ: tại các nút n1, n3, n5, tỷ lệ mất thông điệp lên tới 78%
  • NATS hiện đang điều tra vấn đề này

3.3 Hỏng file snapshot làm xóa toàn bộ dữ liệu (#7556)

  • Nếu file snapshot trong data/jetstream/$SYS/_js_/ bị hỏng, nút sẽ coi stream là mồ côi (orphaned)xóa toàn bộ dữ liệu
  • Chỉ cần thiểu số nút bị hỏng cũng có thể khiến cụm không còn đa số và stream vĩnh viễn không khả dụng
  • Ví dụ: các nút n3, n5 bị hỏng → n3 được bầu làm leader và xóa toàn bộ jepsen-stream
  • Jepsen chỉ ra rủi ro nút bị hỏng trở thành leader trong quá trình bầu chọn leader

3.4 Mất dữ liệu do cấu hình fsync mặc định (#7564)

  • Theo mặc định, JetStream chỉ thực hiện fsync mỗi 2 phút, trong khi thông điệp được xác nhận ngay lập tức
    • Kết quả là các thông điệp vừa được xác nhận có thể vẫn chưa được ghi xuống đĩa
  • Khi xảy ra mất điện hoặc crash kernel, có thể mất lượng thông điệp đã được xác nhận trong hàng chục giây
    • Ví dụ: mất 131.418 trên tổng 930.005 bản ghi
  • Ngay cả chuỗi sự cố trên một nút xảy ra liên tiếp cũng có thể dẫn đến xóa toàn bộ stream
  • Tài liệu gần như không đề cập đến hành vi này
  • Jepsen khuyến nghị đổi mặc định sang fsync=always hoặc cảnh báo rõ ràng về rủi ro mất dữ liệu

3.5 Split-brain do crash OS trên một nút (#7567)

  • Chỉ riêng mất điện hoặc crash kernel trên một nút cũng có thể gây ra mất dữ liệu và sai lệch sao chép
  • Trong cấu trúc leader-follower, nếu một số nút đã acknowledge khi dữ liệu mới chỉ commit trong bộ nhớ rồi xảy ra sự cố,
    đa số nút có thể đánh mất bản ghi đó và tiếp tục vận hành với trạng thái mới
  • Trong thử nghiệm, chỉ sau một lần mất điện trên một nút đã xuất hiện split-brain kéo dài
    • Ghi nhận việc mất các đoạn thông điệp đã được xác nhận khác nhau trên từng nút
  • Jepsen viện dẫn trường hợp tương tự ở Kafka để nhấn mạnh rằng các hệ thống dựa trên Raft cũng có cùng rủi ro

4. Thảo luận và kết luận

  • Vấn đề mất toàn bộ dữ liệu trong 2.10.22 đã được khắc phục ở 2.10.23
  • Trong 2.12.1, mất dữ liệu và split-brain do hỏng file và crash OS vẫn tiếp tục xảy ra
  • Khi file .blk và file snapshot bị hỏng, có thể xảy ra thiếu thông điệp trên một số nút hoặc xóa toàn bộ stream
  • Chu kỳ fsync mặc định quá dài làm phát sinh rủi ro mất dữ liệu đã được xác nhận khi nhiều nút đồng thời gặp sự cố
  • Jepsen đề xuất dùng fsync=always hoặc đưa vào tài liệu cảnh báo rủi ro rõ ràng
  • Tuyên bố JetStream “luôn sẵn sàng” là bất khả thi theo định lý CAP, nên cần sửa tài liệu
  • Jepsen lưu ý rằng có thể chứng minh sự tồn tại của bug, nhưng không thể chứng minh sự vắng mặt của vấn đề an toàn

4.1 Vai trò của LazyFS

  • Sử dụng LazyFS để mô phỏng việc mất các lần ghi chưa được fsync
  • Có thể tái hiện nhiều lỗi lưu trữ khi mất điện, như hỏng ghi từng phần (torn write)
  • Nghiên cứu liên quan When Amnesia Strikes (VLDB 2024) cũng báo cáo các bug tương tự ở PostgreSQL, Redis, ZooKeeper v.v.

4.2 Việc cần làm tiếp theo

  • Chưa thực hiện kiểm chứng về mất thông điệp ở mức consumer đơn lẻ, thứ tự thông điệp, hay các đảm bảo Linearizable/Serializable
  • Đảm bảo phân phối chính xác một lần (exactly-once) cũng là chủ đề cho nghiên cứu trong tương lai
  • Phát hiện lỗi trong tài liệu khi thêm/xóa nút và thiếu bước health check bắt buộc (#7545)
  • Quy trình thay đổi cấu hình cụm an toàn vẫn chưa rõ ràng

1 bình luận

 
GN⁺ 2025-12-09
Ý kiến trên Hacker News
  • Mỗi lần có ai đó bỏ qua lý thuyết phức tạp để xây những hệ thống kiểu này, tôi lại thấy aphyr phá tan nó
    Giờ tôi còn tự hỏi liệu AI có thể đọc tài liệu dự án và dự đoán khả năng mất dữ liệu chỉ từ câu chữ marketing hay không
    • Cảm giác như đang vuốt bộ râu dài và gật gù đồng tình
      Mọi người lúc nào cũng nói “lý thuyết bị đánh giá quá cao” hay “hacking tốt hơn giáo dục nhà trường”, nhưng rốt cuộc lại tự vấp ngã trong một không gian vấn đề đã được ghi chép đầy đủ
    • Tôi cũng đã thử giao việc tương tự cho LLM, và kết quả khá hữu ích
  • Có cảm giác như NATS đang phớt lờ lý thuyết CAP
    • Có vẻ là một nhận xét bị đánh giá thấp
  • Tôi dùng NATS cho pub/sub in-memory, và ở phần đó nó rất tốt
    Nó cũng xử lý ổn các chi tiết scale tinh tế
    Nhưng tôi chưa từng dùng persistence, và không ngờ nó lại mong manh đến vậy
    Khá sốc khi nó còn dễ tổn thương cả trước hỏng một bit trong file
  • Về tài liệu liên quan, Jepsen và Antithesis gần đây đã công bố một bảng thuật ngữ hệ thống phân tán
    Đây là tài liệu tham khảo rất tốt → Jepsen Glossary
  • Tôi từng thắc mắc về sự khác nhau về nội dung giữa aphyr.com/tags/jepsen và jepsen.io/analyses
    Tôi mới phát hiện aphyr.com gần đây và đang kỳ vọng sẽ có nhiều góc nhìn sâu sắc
    • Jepsen ban đầu khởi đầu như một chuỗi bài blog cá nhân
      Sau đó jepsen.io phát triển thành một dự án chuyên nghiệp, và bắt đầu vận hành nghiêm túc từ khoảng 10 năm trước
  • Tôi thắc mắc vì sao lại tồn tại cấu hình “Lazy fsync by Default”
    Là để tăng điểm benchmark sao? Trong các cụm nhỏ, kiểu cấu hình này thường trở thành nguồn gốc của vấn đề
    • Không chỉ giảm độ trễ mà còn cải thiện thông lượng (throughput)
      Nhiều ứng dụng không đòi hỏi độ bền dữ liệu tuyệt đối, nên lazy fsync có thể hữu ích
      Tuy vậy, để nó làm mặc định thì vẫn còn gây tranh cãi
    • Tôi luôn thắc mắc vì sao fsync nhất định phải bị trì hoãn
      Có vẻ như có thể giải quyết bằng xử lý theo lô (batch) như TCP corking
    • Đây là một trong những điều có thể làm được trong hệ thống phân tán
      Các lỗi do lazy fsync gây ra thường không xảy ra đồng thời trên phần lớn các node
    • Đúng là một lựa chọn để tăng hiệu năng
    • Nhắm đến cả độ bền nhờ sao chép và phân tán, lẫn thông lượng có được từ lazy fsync
  • Tôi muốn giới thiệu s2.dev như một phương án serverless thay thế cho JetStream
    Ưu điểm: hỗ trợ stream không giới hạn với độ bền ở mức object storage
    Nhược điểm: vẫn chưa có tính năng consumer group
    • Không biết đã từng chạy Jepsen test cho nó chưa
  • Vấn đề là NATS mặc định chỉ fsync mỗi 2 phút, nhưng lại trả ack ngay lập tức
    Nếu nhiều node gặp sự cố cùng lúc thì có thể dẫn đến mất dữ liệu đã commit
    Nó làm tôi nhớ đến kiểu marketing “web scale” thời kỳ đầu của MongoDB
    Tôi nghĩ mặc định luôn phải là phương án an toàn nhất
    • NATS nói rõ rằng họ chỉ đảm bảo tính sẵn sàng của cluster
      Điểm đó lại là thứ tôi thấy tốt, vì có thể thiết kế hệ thống ở tầng trên dựa vào nó
      Khi dùng vào năm 2018, nó cũng nhanh và dễ vận hành
    • Hầu hết DB hiện đại cũng không có mặc định hoàn toàn an toàn
      Ví dụ, mức cô lập giao dịch mặc định của PostgreSQL là read committed
      Redis cũng mặc định fsync mỗi 1 giây
    • Redis cluster chỉ trả ack sau khi đã sao chép sang nhiều node
      Ngay cả với Redis standalone, cũng có thể cấu hình chỉ ack sau fsync, nhưng do OS buffering nên vẫn khó đảm bảo tuyệt đối
      Rốt cuộc, điều quan trọng là phải hiểu chính xác ý nghĩa của ack
    • Hầu hết hệ thống đều chọn sự đánh đổi giữa tốc độ và độ bền
      Nếu cứ khăng khăng dùng mặc định an toàn nhất thì hiệu năng sẽ giảm mạnh, và gánh nặng tự tuning sẽ dồn sang người dùng
      Ví dụ, mức cô lập mặc định của Postgres cũng yếu nên có thể phát sinh race condition
      Tham khảo: bài viết kiểm thử Hermitage
    • Vấn đề là fsync chỉ đưa ra những lựa chọn cực đoan
      Trong thời SSD, các mức trung gian như group-commit đã biến mất, và giờ chi phí chuyển syscall mới là nút thắt cổ chai
      2 phút là chu kỳ quá dài (cũng cần tính đến khác biệt giữa fdatasync và fsync)
  • Thành thật mà nói thì tôi cũng có đoán trước, nhưng không ngờ lại nghiêm trọng đến mức này
    Có lẽ cứ dùng Redpanda sẽ tốt hơn
  • Tôi từng tự hỏi liệu có thể cải thiện cảnh báo hiệu năng fsync của NATS hay không
    Nếu flush theo lô (batch flush) theo chu kỳ thì độ trễ sẽ tăng, nhưng có lẽ vẫn giữ được thông lượng
    • Thay vì chu kỳ cố định, chỉ cần xếp hàng các yêu cầu ghi trong lúc fsync đang chạy rồi xử lý cùng ở batch kế tiếp
      Cách này tương tự như việc gom các vòng Paxos lại
      Sau khi một vòng kết thúc thì phải bắt đầu ngay batch tiếp theo theo cách đó