TATrương Tuấn AnhTuna / truongtuananh.vn
Tất cả bài viết
ngày ghi
phút đọc
6
nguồn dẫn
7

Sao lưu chưa mở lại đếm thì chưa phải sao lưu

Ba đêm nhật ký ghi 'đã sao lưu kho' trong khi kho thật 12.282 dòng, file được chép 9.027. Ba cổng chặn tôi dựng sau đó, mỗi cổng từ một sự cố có ngày tháng.

Chủ đề: vận hành, dữ liệu, cổng kiểm

Tôi vận hành một kho lãi suất ngân hàng cập nhật hằng ngày. Bảng quan trọng nhất trong đó tên là rate_snapshots. Nó ghi biểu lãi suất của từng ngân hàng theo từng ngày, chỉ được thêm, không bao giờ sửa hay xoá. Biểu hôm qua không còn nằm trên trang ngân hàng nào nữa. Mất bảng đó là mất vĩnh viễn, không cào lại được. Vì thế sao lưu không phải việc phụ. Và tôi đã làm hỏng nó theo cách im lặng nhất có thể.

Ba đêm "đã sao lưu kho" mà không có bản sao nào

Ngày 09/08/2026 tôi chuyển kho từ file SQLite trên máy lên Turso. Biến TURSO_REPLICA không được đặt, nên file data/laisuat.db đóng băng từ đúng hôm đó. Script chạy đêm vẫn giữ nguyên dòng cũ: cp data/laisuat.db sang thư mục sao lưu, rồi ghi nhật ký "đã sao lưu kho".

Ba đêm liền nó chép lại cùng một file chết. Đo ngày 11/08/2026:

BảngKho thật trên mâyFile đang được "sao lưu"
rate_snapshots12.2829.027
social_signals2.4492.029

Có nhật ký, có file, có cảm giác an toàn. Không có bản sao nào của kho thật. Tôi gọi đây là thành công giả: mỗi lệnh chạy hết, không báo lỗi, và kết quả sai.

Một ngày trước đó, 10/08/2026, dịch vụ đồng bộ tệp trên máy thay mất kho: file 5,5 MB còn 53 KB, mọi bảng rỗng. Cứu được nhờ một bản sao tình cờ có từ 01:05 cộng với 88 file bài thô. Tình cờ không phải là cơ chế. Hai sự cố dồn trong ba ngày là đủ để tôi viết lại script sao lưu từ đầu, với ba cổng chặn. Mỗi cổng gắn với một chuyện đã xảy ra.

Cổng 1: đọc kho đang chạy, không đọc file

Script mới không chép file nào. Nó mở kết nối tới kho đang chạy, bất kể là Turso hay SQLite local, đọc từng bảng rồi ghi ra một file SQLite mới. Manifest đi kèm ghi luôn loại kho đã đọc, để người mở bản sao lưu biết nó rút từ đâu.

Turso trả kết quả qua HTTP theo từng khúc. Đọc trọn một bảng lớn bằng một câu select * làm luồng đứt giữa chừng. Đêm 21/08/2026 lượt sao lưu chết đúng như vậy, và bản gần nhất lúc đó đã 5 ngày tuổi. Lỗi nặng dần theo kích thước kho, nên chạy lại cho may không phải cách chữa. Tôi chuyển sang đọc từng lô 2.000 dòng theo rowid, lô nào đứt thì thử lại một lần.

Cổng 2: kho co lại thì dừng

Ba bảng trong kho là append-only: rate_snapshots, social_signals, crawl_runs. Số dòng của chúng chỉ được tăng. Trước khi ghi bản mới, script đọc manifest bản trước và so từng bảng. Bản mới ít dòng hơn ở bất kỳ bảng nào trong ba bảng đó, script in ra bảng nào mất bao nhiêu dòng, trả mã khác 0 và không ghi gì cả.

Kho co lại có ba khả năng: kho bị thay như vụ 10/08, có người xoá nhầm, hoặc script đang đọc một kho khác kho hôm qua. Cả ba đều cần người nhìn vào trước khi bản cũ bị đè. Nếu thật sự cố ý, chạy lại kèm --du-biet. Cờ đó tồn tại để việc bỏ qua cổng phải là một quyết định có chữ ký, không phải mặc định.

Vì cổng này đếm dòng, mọi thứ khác trong script phải giữ số dòng nguyên vẹn. Bản đi ra khỏi máy cần bỏ dữ liệu cá nhân, tôi che cột chứ không xoá dòng. Xoá dòng ở đó là tự tay bịt mắt cái cổng này.

Cổng 3: ghi xong mở lại đếm

Sau khi nén và ghi file, script mở lại chính bản vừa ghi và làm năm việc: manifest phải có; sha256 của file phải khớp manifest; giải nén ra thư mục tạm; chạy integrity_check của SQLite; đếm từng bảng và so với số trong manifest. Sai một dòng là xoá cả file lẫn manifest, trả mã lỗi. Một file không mở được không phải bản sao lưu.

Cổng này bắt lỗi đầu tiên ngay trong chính script. Tôi dùng with_suffix('.json') để tìm manifest, nhưng hàm đó chỉ thay đuôi cuối, cho ra laisuat-….db.json trong khi manifest tên laisuat-….json. Bản sao lưu đầu tiên tự huỷ ngay sau khi ghi vì "không có manifest đi kèm". Bực, nhưng đúng: cổng làm việc của nó trước cả khi script làm việc của mình.

Manifest ghi thời điểm, loại kho, số dòng từng bảng, kích thước nén, sha256. Cờ --kiem chạy lại bài kiểm đó trên bản mới nhất bất cứ lúc nào. Script giữ 14 bản gần nhất cộng bản cũ nhất, vì lỗi phát hiện muộn cần một mốc xa hơn hai tuần.

Nhịp hằng ngày trên GitHub gọi sao lưu trước khi ghi bất cứ thứ gì vào kho, với cờ --khong-ca-nhan: bỏ bảng chứa số điện thoại người đọc để lại, che cột link bài gốc bằng một chuỗi cố ý không giống URL. Che bằng chuỗi chứ không phải NULL, vì cột đó khai NOT NULL và một bản dump không nạp lại được thì không phải bản sao lưu. Bước tải lên chỉ bắt file có đuôi -che; không có file khớp thì bước phải đỏ. README ghi giữ 90 ngày, workflow thực tế đặt 30, kèm chú thích: chính sách công bố hứa xoá dữ liệu cá nhân sau 90 ngày, giữ bản dump đúng 90 ngày là đặt hạn của bản sao bằng hạn của bản gốc.

Thành công giả thứ hai: trang phục vụ 16 ngân hàng khi kho có 29

Vụ sao lưu không phải lần duy nhất hệ thống báo xanh mà sai. Nhịp hằng ngày có bước 6 xuất JSON từng trang từ kho, và bước 7 dựng HTML tĩnh. Bước 7 đọc data/site/*.json, không đọc kho. Quên bước 6 là dựng lại trang bằng dữ liệu cũ, mọi thứ vẫn báo thành công. Đã xảy ra thật: trang phục vụ bản 16 ngân hàng trong khi kho đã có 29.

Cùng một họ với vụ sao lưu. Mỗi bước xanh theo tiêu chí của riêng nó. Không bước nào hỏi câu quan trọng: cái vừa làm ra có khớp kho không. Cách chữa cũng cùng một họ: ghi thứ tự bắt buộc vào README, và đặt bước đếm ở đầu ra thay vì tin vào mã trả về.

Điều tôi rút ra. Nhật ký "đã xong" chỉ chứng minh lệnh chạy hết, không chứng minh việc đã xong. Cổng kiểm phải đặt ở đầu ra và so với nguồn sự thật, không so với chính thứ vừa tạo ra nó. Mỗi cổng nên gắn với một sự cố có ngày tháng, vì cổng không có sự cố đứng sau thì khó bảo vệ khi ai đó muốn bỏ. Và bản sao lưu duy nhất đáng tin là bản đã từng được mở lại và đếm.

Nếu bạn cũng đang dựng một hệ thống chạy đêm mà chưa từng mở lại bản sao lưu của nó, ta nói chuyện 15 phút.

Bài này chạm đúng việc bạn đang làm? Mười lăm phút nói chuyện không mất gì.