♻️ Backup dữ liệu và rollback khi lỗi

By Better Wise

Bài trước bạn đã có log và uptime monitoring để biết app đang lỗi. Bài này trả lời câu hỏi khó hơn: lỗi rồi thì làm gì, và làm sao để luôn có đường lùi. Mình từng chạy một migration xóa cột vào lúc 11 giờ đêm, sáng hôm sau mất sạch danh sách người dùng đăng ký, chỉ vì nghĩ "app nhỏ, chưa cần backup".

Sau bài này bạn sẽ:

  • Đặt được lịch backup tự động và biết phải backup cái gì trước.
  • Kiểm tra được một bản backup thật sự khôi phục được, không chỉ nằm đó cho đẹp.
  • Rollback về phiên bản chạy tốt gần nhất mà không làm mất thêm dữ liệu.

Backup: sao lưu cái gì trước, bao lâu một lần

Nguyên tắc duy nhất cần nhớ: cái gì không tái tạo được thì backup trước. Thứ tự ưu tiên của bạn:

  1. Database: dữ liệu người dùng, đơn hàng, tài khoản. Mất là mất thật.
  2. File người dùng upload: ảnh, PDF, avatar. Hosting thường không backup giúp bạn phần này.
  3. Cấu hình và secrets: file .env để ở password manager hoặc nơi riêng, không nằm trong repo.
  4. Code: đã có Git, không cần backup kiểu copy thư mục, nhưng cần tag mỗi lần deploy (phần rollback bên dưới).

Về tần suất và nơi lưu:

  • Tần suất backup do RPO quyết định: _____. Backup mỗi ngày nghĩa là RPO 24 giờ. App có giao dịch thì nên 1 đến 6 giờ một lần.
  • RTO là _____. Không đo thì không biết mình chịu được sự cố bao lâu.
  • Lưu off-site: khác máy chủ, khác nhà cung cấp. Backup nằm cùng ổ với database thì mất cả hai cùng lúc.
  • Giữ nhiều thế hệ: 7 bản daily và 4 bản weekly là mức tối thiểu. Sự cố dữ liệu thường bị phát hiện muộn hơn 1 ngày.
  • Nguyên tắc 3-2-1: _____.

Ví dụ thực tế: phần lớn hosting managed (Supabase, Neon, Render, Railway) có automated backup sẵn, nhưng bạn phải vào bật và xem lại cửa sổ backup. VPS tự quản thì bạn tự đặt lịch:

# cron chạy 2:00 sáng mỗi ngày, nén rồi đẩy sang object storage
0 2 * * * pg_dump "$DATABASE_URL" | gzip > /backups/app-$(date +\%F).sql.gz

Bài tập 1 (5 phút): viết ra 4 dòng cho app của bạn: backup cái gì, tần suất, lưu ở đâu, giữ bao nhiêu bản.

Gợi ý đáp án

Ví dụ cho một app todo nhỏ:

  • Database Postgres: daily 2:00 sáng, giữ 7 bản, đẩy lên object storage (S3 hoặc Backblaze B2).
  • File upload: đồng bộ bucket sang bucket thứ hai ở region khác.
  • .env: lưu trong password manager, ghi chú ngày cập nhật.
  • Code: tag v1.x.y mỗi lần deploy.

Điều bắt buộc: bản backup phải nằm ở nơi khác với máy chủ đang chạy app.

Kiểm tra bản backup có khôi phục được

Một bản backup chưa được _____ thì chưa được tính là backup. File 2GB nằm im 6 tháng rất có thể đã hỏng, thiếu bảng, hoặc thiếu quyền đọc.

Quy trình test restore, làm được trong 15 phút:

  1. Tải bản backup mới nhất về máy hoặc về database tạm (staging), tuyệt đối không restore đè lên production.
  2. Restore vào database trống có tên kiểu restore_test.
  3. Đếm số dòng của 2 bảng chính và so với production: select count(*) from users;
  4. Chạy thử 1 đến 2 query quan trọng (đăng nhập, danh sách đơn hàng gần nhất).
  5. Ghi lại thời gian restore. Đó là RTO thật của bạn.

Đặt lịch test mỗi tháng một lần, và bắt buộc test một lần trước khi mở cho người dùng thật.

Bài tập 2 (10 phút): thực hiện quy trình trên với bản backup gần nhất của bạn. Ghi lại 3 con số: số dòng bảng users, thời gian restore, và lỗi gặp phải (nếu có).

Gợi ý đáp án

Kết quả tốt trông như sau: số dòng khớp với production (hoặc thấp hơn đúng bằng dữ liệu phát sinh sau giờ backup), restore xong dưới 10 phút, không có lỗi quyền.

Nếu restore lỗi vì thiếu extension, thiếu role, hay file backup rỗng: đó là phát hiện có giá trị nhất trong ngày. Sửa ngay bây giờ, đừng đợi đến lúc sập.

Rollback khi lỗi: chọn đúng thứ tự

Có hai kiểu rollback và rất nhiều người chọn sai kiểu:

  • Rollback code: đưa app về release trước. Nhanh, gần như không mất dữ liệu.
  • Rollback dữ liệu: restore backup. Chậm, và mất mọi dữ liệu phát sinh sau thời điểm backup.

Khi lỗi xuất hiện ngay sau deploy, thứ tự đúng là _____. Restore database chỉ để cứu dữ liệu bị hỏng, không phải để sửa lỗi code.

flowchart TD
    A["🚨<br/>Sự cố xảy ra"] --> B{"Lỗi xuất hiện ngay sau deploy?"}
    B -->|Có| C["Rollback code về release trước"]
    B -->|Không| D{"Dữ liệu sai hoặc mất?"}
    D -->|Có| E["Restore bản backup gần nhất"]
    D -->|Không| F["Xử lý theo log, không rollback"]
    C --> G["Xác nhận lại bằng log và uptime"]
    E --> G

Để rollback code nhanh, chuẩn bị trước 3 thứ: tag phiên bản mỗi lần deploy (git tag v1.4.0 && git push --tags), giữ release trước đó trên hosting (Vercel, Netlify, Render đều có nút rollback về deployment cũ), và biết chính xác release nào đang chạy.

Cái bẫy lớn nhất là migration phá hủy. Nếu deploy kèm migration drop column hoặc đổi kiểu dữ liệu, rollback code không mang dữ liệu đã xóa trở lại. Vì vậy: trước mọi migration có thể xóa hoặc đổi kiểu dữ liệu, luôn _____. Cách viết migration an toàn là backward-compatible: thêm cột mới, deploy, chuyển dữ liệu, rồi mới xóa cột cũ ở lần deploy sau.

Thứ tự xử lý sự cố, viết sẵn và dán vào đâu đó:

  1. Chặn thiệt hại: tắt tính năng lỗi bằng feature flag, hoặc bật maintenance page.
  2. Rollback code về release chạy tốt gần nhất.
  3. Kiểm tra log và uptime để xác nhận lỗi đã hết.
  4. Chỉ restore dữ liệu nếu dữ liệu thật sự hỏng, và thông báo cho người dùng nếu mất dữ liệu.
  5. Ghi lại 5 dòng post-mortem: lỗi gì, phát hiện lúc nào, mất gì, sửa thế nào, thêm bước nào để không lặp lại.

Bài tập 3 (5 phút): 10 phút sau khi deploy v1.5, log đầy lỗi 500 và người dùng báo không đăng nhập được. Viết 4 bước bạn làm ngay, theo đúng thứ tự.

Gợi ý đáp án

  1. Bật maintenance page hoặc tắt tính năng đăng nhập để chặn thiệt hại.
  2. Rollback về v1.4 (nút rollback về deployment cũ, hoặc git checkout v1.4.0 rồi deploy lại).
  3. Xem log 5 phút sau đó, xác nhận lỗi 500 đã hết và uptime trở lại xanh.
  4. Đọc log của v1.5 để tìm nguyên nhân, sửa ở nhánh mới, không sửa trực tiếp trên production đang chạy.

Không restore database ở bước nào cả, vì đây là lỗi code chứ không phải lỗi dữ liệu.

Ba cái bẫy biến backup thành đồ trang trí

  • Backup chưa test: đến lúc cần mới biết file rỗng. Test restore mỗi tháng, và luôn test trước go-live.
  • Backup cùng chỗ với app: cùng ổ, cùng tài khoản hosting thì một sự cố xóa sạch cả hai. Luôn có một bản off-site.
  • Restore để sửa lỗi code: bạn đổi một sự cố nhỏ thành việc mất dữ liệu thật. Rollback code trước, luôn luôn.

Quy tắc của mình: không có bản backup nào được restore thử thì chưa được deploy migration nào cả.

Chốt lại và bước tiếp theo

Ba điều mang đi:

  • Backup theo thứ tự database, file người dùng, secrets; lưu off-site; giữ nhiều thế hệ bản.
  • Chỉ tin bản backup đã restore thử, và ghi lại RTO thật.
  • Sự cố xảy ra thì chặn thiệt hại trước, rollback code sau, restore dữ liệu chỉ khi thật cần.

Ôn nhanh bằng bộ thẻ dưới đây trước khi sang bài checkpoint, nơi bạn sẽ tự chạy toàn bộ quy trình này trên app thật của mình.

Front: Một bản backup chưa từng được restore thử thì được coi là gì? Back: Chưa có backup. Phải test restore mới tính.

Front: Nguyên tắc 3-2-1 trong backup là gì? Back: 3 bản sao, 2 nơi lưu khác nhau, 1 bản off-site.

Front: RPO là gì? Back: Lượng dữ liệu tối đa bạn chấp nhận mất, quyết định bởi tần suất backup.

Front: RTO là gì? Back: Thời gian cần để khôi phục xong dịch vụ, đo được qua lần restore thử.

Front: Thứ tự ưu tiên backup? Back: Database, file người dùng upload, cấu hình và secrets. Code đã có Git.

Front: Vì sao không được lưu backup cùng máy chủ với app? Back: Một sự cố xóa hoặc mất máy chủ sẽ xóa luôn cả app lẫn backup.

Front: Lỗi xuất hiện ngay sau deploy thì rollback kiểu gì trước? Back: Rollback code về release trước, không restore database.

Front: Khi nào mới restore database từ backup? Back: Khi dữ liệu thật sự sai hoặc mất, và chấp nhận mất dữ liệu sau thời điểm backup.

Front: Vì sao migration drop column nguy hiểm khi rollback? Back: Rollback code không mang dữ liệu đã xóa trở lại. Phải backup trước migration.

Front: Cách viết migration an toàn để rollback được? Back: Backward-compatible: thêm cột mới, deploy, chuyển dữ liệu, xóa cột cũ ở lần sau.

Front: Cần chuẩn bị gì để rollback code nhanh? Back: Tag phiên bản mỗi lần deploy và giữ release trước đó trên hosting.

Front: Kiểm tra gì ngay sau khi restore một bản backup? Back: Đếm số dòng bảng chính, chạy vài query quan trọng, ghi lại thời gian restore.

Front: Bước đầu tiên khi phát hiện sự cố production? Back: Chặn thiệt hại: tắt tính năng lỗi hoặc bật maintenance page.