Kỹ sư của Facet khắc phục lỗi mất dữ liệu âm thầm trong hệ thống sao lưu SQLite đa thuê bao
Các nhà phát triển của Facet đã phát hiện ra một lỗi logic trong hệ thống khôi phục theo thời điểm (point-in-time restore), có thể trả về dữ liệu cũ. Họ đã sửa đổi cách xử lý epoch để ưu tiên độ chính xác về mặt thời gian thay vì dựa vào số thứ tự.
Bài được dịch tự động từ bản gốc tiếng Anh.
Facet, một nền tảng dành cho nhà sáng tạo được xây dựng trên Go và SQLite, gần đây đã xác định và giải quyết một lỗi nghiêm trọng trong cơ chế khôi phục theo thời điểm. Vấn đề này, được phát hiện vào tháng 10 năm 2026, liên quan đến việc hệ thống chọn sai các bản chụp (snapshot) cơ sở dữ liệu cũ trong quá trình khôi phục. Các kỹ sư đã triển khai một bản vá gồm ba phần để đảm bảo quá trình khôi phục dựa trên dấu thời gian (timestamp) thay vì chỉ dựa vào bộ đếm thứ tự đơn giản, qua đó ngăn chặn tình trạng hồi quy dữ liệu âm thầm đối với các thuê bao.
Chuyện gì đã xảy ra
Facet hoạt động như một nền tảng đa thuê bao, nơi mỗi nhà sáng tạo nhận được một cơ sở dữ liệu SQLite riêng biệt. Để cung cấp các tùy chọn khôi phục chi tiết mà không gây ra chi phí quá cao, đội ngũ đã xây dựng một công cụ gửi sao lưu (backup shipper) tùy chỉnh tích hợp trực tiếp vào quy trình ứng dụng của họ. Hệ thống này tải lên các phân đoạn bản chụp bất biến và nhật ký ghi trước (Write-Ahead Log - WAL) lên kho lưu trữ Cloudflare R2. Mỗi đối tượng bao gồm một tiêu đề đã ký với số thứ tự và mã hash của đối tượng trước đó, tạo ra một chuỗi chứng nhận có thể kiểm tra được cho dữ liệu của từng thuê bao.
Đội ngũ kỹ thuật gần đây phát hiện rằng một số trang demo đang trỏ tới các chuỗi dữ liệu cũ sau khi được di chuyển từ môi trường staging sang production. Các máy chủ staging đã duy trì các tệp trạng thái cục bộ với số epoch cao, trong khi các instance production mới bắt đầu lại từ epoch một. Do logic khôi phục ưu tiên số epoch cao nhất bất kể thời điểm dữ liệu được ghi, hệ thống sẽ khôi phục các trang về trạng thái từ cuối tháng Chín thay vì phiên bản trực tiếp hiện tại. Điều này xảy ra mặc dù tất cả các đối tượng sao lưu riêng lẻ đều vượt qua các bài kiểm tra tính toàn vẹn, vì lỗi nằm ở logic lựa chọn chứ không phải do dữ liệu bị hỏng.
Để giải quyết vấn đề này, nhóm đã triển khai ba thay đổi cụ thể vào môi trường production. Thứ nhất, họ sửa đổi cách phân bổ epoch để kiểm tra lịch sử từ xa trước khi gán số thứ tự mới, ngăn chặn các tệp trạng thái cục bộ tạo ra những chuỗi xung đột. Thứ hai, bộ lập kế hoạch khôi phục giờ đây chọn epoch dựa trên mức độ liên quan về thời gian so với thời điểm khôi phục mục tiêu, chứ không chỉ dựa vào giá trị số học lớn hơn. Cuối cùng, hệ thống hiện từ chối rõ ràng việc khôi phục nếu phát hiện trật tự thời gian bị đảo ngược, buộc các nhà vận hành phải giải quyết các dòng thời gian mơ hồ thay vì đoán mò.
Chi tiết chính
- Hệ thống sao lưu tốn khoảng $0.0034 mỗi thuê bao mỗi tháng nhờ sử dụng phương pháp tải lên dựa trên thay đổi, bỏ qua các cơ sở dữ liệu nhàn rỗi.
- Mỗi đối tượng sao lưu là bất biến và chứa mã hash SHA-256 của đối tượng trước đó, cho phép xác minh mà không cần chỉ mục trung tâm.
- Lỗi khiến bộ lập kế hoạch khôi phục chọn các chuỗi cũ với số epoch cao hơn từ các môi trường staging trước đó.
- Các bản vá bao gồm việc phân bổ epoch dựa trên lịch sử từ xa và từ chối khôi phục khi trật tự thời gian bị đảo ngược.
- Kiểm thử sau khi sửa lỗi cho thấy một trang demo với 79 phân đoạn WAL đã được khôi phục thành công trong 2 phút 34 giây.
- Hệ thống vô hiệu hóa tính năng checkpointing tự động của SQLite cho các thuê bao đã gửi sao lưu để ngăn ngừa xung đột ghi với công cụ gửi nội bộ.
Bối cảnh
Khôi phục theo thời điểm (PITR) cho phép quản trị viên khôi phục cơ sở dữ liệu về bất kỳ khoảnh khắc cụ thể nào, thay vì chỉ bản sao lưu đầy đủ gần nhất. Trong SQLite, điều này thường liên quan đến việc kết hợp một bản chụp cơ sở với một loạt các phân đoạn WAL ghi lại mọi thay đổi được thực hiện kể từ bản chụp đó. Cách tiếp cận của Facet tránh sử dụng các công cụ bên ngoài như Litestream bằng cách chạy logic sao lưu trong cùng một tiến trình Go phục vụ cơ sở dữ liệu. Điều này đảm bảo hệ thống sao lưu nắm bắt được các sự kiện vòng đời của thuê bao như ngủ đông hoặc di chuyển, những điều mà các sidecar bên ngoài có thể bỏ sót.
Khái niệm "epoch" trong bối cảnh này đề cập đến một giai đoạn riêng biệt trong chuỗi sao lưu. Epoch được đặt lại khi tính liên tục không thể được đảm bảo, chẳng hạn như sau khi di chuyển máy chủ. Bằng cách liên kết các epoch này với mã hash mật mã, hệ thống đảm bảo rằng bất kỳ khoảng trống hay sự giả mạo nào cũng có thể phát hiện được. Tuy nhiên, việc quản lý các epoch này giữa các môi trường khác nhau đòi hỏi sự phối hợp cẩn thận để đảm bảo rằng các số mới luôn tương ứng với dữ liệu mới, một giả định đã thất bại trong sự cố gần đây.
Tại sao điều này quan trọng
Đối với các nhóm chạy phần mềm tự host hoặc quản lý kiến trúc đa thuê bao, sự cố này làm nổi bật rủi ro khi phụ thuộc vào trạng thái cục bộ cho các hệ thống phân tán. Khi ứng dụng di chuyển giữa các máy chủ hoặc môi trường, siêu dữ liệu còn sót lại có thể tạo ra xung đột khó phát hiện cho đến khi thử khôi phục. Kinh nghiệm của đội ngũ Facet cho thấy ngay cả các kiểm tra tính toàn vẹn mạnh mẽ trên từng tệp riêng lẻ cũng không thể ngăn chặn các lỗi logic trong cách ghép nối các tệp đó trong quá trình khôi phục.
Hiệu quả chi phí của mô hình của họ cũng cung cấp một khuôn mẫu cho các công ty vừa và nhỏ. Bằng cách chỉ tính phí cho các thao tác ghi thực tế và bỏ qua các thuê bao nhàn rỗi, họ đạt được mức giá giúp PITR khả thi cho mọi người dùng, không chỉ khách hàng doanh nghiệp. Điều này chứng minh rằng các chiến lược sao lưu chi tiết không nhất thiết đòi hỏi hạ tầng đắt đỏ nếu kiến trúc phần mềm phù hợp với mô hình định giá của nhà cung cấp lưu trữ.
Hơn nữa, quyết định báo lỗi rõ ràng khi gặp dữ liệu mơ hồ là một bài học quan trọng trong kỹ thuật độ tin cậy. Nhiều hệ thống có thể cố gắng đoán chuỗi đúng, tiềm ẩn nguy cơ phục vụ dữ liệu bị hỏng hoặc cũ mà không cảnh báo người dùng. Lựa chọn của Facet là chặn việc khôi phục và yêu cầu can thiệp thủ công đảm bảo tính toàn vẹn dữ liệu không bao giờ bị đánh đổi vì tính sẵn sàng.
Bạn có thể làm gì
- Kiểm toán logic lựa chọn sao lưu của bạn để đảm bảo nó ưu tiên dấu thời gian hơn số thứ tự khi giải quyết xung đột.
- Triển khai cơ chế tải lên dựa trên thay đổi để giảm chi phí lưu trữ cho các cơ sở dữ liệu nhàn rỗi trong môi trường đa thuê bao.
- Xác minh tính toàn vẹn của sao lưu bằng cách so sánh số lượng hàng trong bảng đã khôi phục với cơ sở dữ liệu trực tiếp trong các đợt diễn tập định kỳ.
- Đảm bảo quy trình sao lưu nắm bắt được các sự kiện vòng đời của thuê bao như di chuyển để ngăn ngừa mất đồng bộ trạng thái.
- Cấu hình hệ thống từ chối các lần khôi phục mơ hồ thay vì cố gắng đoán chuỗi dữ liệu đúng.
- Lưu trữ siêu dữ liệu sao lưu từ xa thay vì chỉ phụ thuộc vào các tệp trạng thái cục bộ có thể trở nên lỗi thời sau khi di chuyển.



