Bảy cái bẫy khi sao lưu và chuyển dữ liệu — chúng tôi vấp đủ cả bảy
Một bản sao lưu nạp được ở máy mình không có nghĩa là nạp được ở máy chủ. Bảy chỗ hỏng thật, theo đúng thứ tự chúng tôi gặp.

Chúng tôi vừa dựng lại toàn bộ cơ sở dữ liệu production từ một bản sao lưu. Quá trình đó vấp bảy chỗ, và không chỗ nào trong số đó xuất hiện khi thử ở máy phát triển.
Bài này kể đủ bảy, theo đúng thứ tự gặp.
Bẫy 1: phiên bản PostgreSQL lệch nhau
Bản thảo ghi máy phát triển thuộc nhánh PostgreSQL 18, máy chủ thuộc nhánh 16. Chưa có đầu ra SELECT version() để xác nhận số bản vá.
Một dump tạo bởi công cụ mới hơn có thể chứa tham số mà đích cũ không hiểu, chẳng hạn:
SET transaction_timeout = 0;PostgreSQL 16 không biết tham số đó. Với ON_ERROR_STOP=1, quá trình nạp dừng khi gặp lệnh đó. Không nên sửa một dòng rồi mặc định toàn bộ dump đã tương thích.
Điều đáng sợ: ở máy phát triển, bản dump nạp hoàn hảo. Lỗi chỉ xuất hiện ở đúng nơi bạn không muốn nó xuất hiện.
Cách tránh: biết phiên bản máy chủ nguồn, đích và công cụ dump/restore. pg_dump không đọc được máy chủ mới hơn major của nó; restore về major cũ không được bảo đảm. Ưu tiên đích cùng major hoặc mới hơn, hoặc thiết kế một quy trình xuất/nhập tương thích đã được thử. Và nếu lệch, hãy nạp thử vào một database nháp trên chính máy chủ trước khi đụng tới database thật.
createdb db_nhap && psql -d db_nhap -v ON_ERROR_STOP=1 -f ddl.sql -f data.sqlChúng tôi làm đúng bước này và nó cứu chúng tôi khỏi việc xoá database thật rồi mới phát hiện bản dump không nạp được.
Bẫy 2: bí mật nằm trong cột bạn không ngờ
Lần quét bí mật đầu tiên, tôi tìm value trong bảng cấu hình. Sạch.
Lần quét thứ hai, kỹ hơn, tìm thấy khoá bí mật reCAPTCHA. Nó không nằm ở value — nó nằm ở value_encrypted, một cột nhị phân mà lần quét đầu không nhìn tới.
Rà lại toàn bộ lược đồ, có bốn cột nhị phân, và cả bốn đều chứa bí mật:
ai_providers.api_key khoá API đã mã hoá
push_keys.private_key khoá riêng thông báo đẩy
system_settings.value_encrypted secret reCAPTCHA
two_factor_secrets.secret mã 2FABản dump này sẽ vào git. Nếu lọt, chúng nằm đó vĩnh viễn trong lịch sử.
Cách tránh: đừng tìm theo tên cột bạn nhớ. Liệt kê mọi cột kiểu nhị phân trong lược đồ, rồi hỏi từng cột chứa gì:
SELECT table_name||'.'||column_name FROM information_schema.columns
WHERE data_type = 'bytea' AND table_schema = 'public';Bẫy 3: dump dữ liệu không giữ thứ tự khoá ngoại
pg_dump --data-only xuất các bảng theo thứ tự không bảo đảm thoả mãn ràng buộc khoá ngoại. Nạp vào là vi phạm ràng buộc.
Chúng tôi từng có một bản master_data.sql chết giữa chừng vì đúng lý do này — nạp được một phần rồi dừng, để lại một database nửa vời.
Cách tránh: khi dựng database mới, restore đầy đủ schema, data rồi post-data (index và ràng buộc) bằng một archive phù hợp. Custom format không tự giải quyết mọi lỗi data-only khi khoá ngoại đã tồn tại. Chỉ cân nhắc --disable-triggers trong quy trình được kiểm soát, đủ quyền và có kiểm tra toàn vẹn sau đó; không dùng nó như cách né ràng buộc mặc định.
Bẫy 4: "nạp được" không có nghĩa là "đúng"
Một bản dump có thể nạp sạch mà vẫn sai, nếu nó không khớp với mã nguồn đang chạy.
Phép kiểm tra thật sự không phải là "psql chạy xong không báo lỗi". Là:
go run ./cmd/api migrate
→ "no migrations to run. current version: 51"Nếu công cụ migration nói còn migration cần chạy, bản dump đang cũ hơn mã nguồn. Nếu nó nói phiên bản không nhận ra, bản dump mới hơn. Cần đối chiếu kế hoạch triển khai. Có migration chờ chạy có thể là bình thường khi nâng cấp; trạng thái không còn migration cũng chưa đủ chứng minh dữ liệu đúng. Kiểm tra số bản ghi, ràng buộc và luồng ứng dụng quan trọng.
Và đừng đọc mã thoát qua ống dẫn. Tôi từng chạy:
psql ... | tail -2 ; echo $?và thấy 0, rồi báo cáo là thành công. $? sau một ống dẫn là mã thoát của tail, không phải của psql. psql thực ra thoát với mã 3.
Đây có lẽ là bài học rẻ tiền nhất và hữu ích nhất trong cả bài: nếu bạn kiểm tra kết quả qua một ống dẫn, trong Bash hãy dùng set -o pipefail hoặc đọc ${PIPESTATUS[0]} ngay sau ống dẫn, hoặc bỏ ống dẫn đi. Cú pháp này không áp dụng nguyên xi cho PowerShell.
Bẫy 5: dữ liệu vận hành trộn với dữ liệu nội dung
Một bản dump đầy đủ mang theo cả nhật ký, phiên đăng nhập, hội thoại thử nghiệm, địa chỉ email test. Đưa lên máy chủ mới là đưa luôn cả rác.
Tệ hơn, nó mang theo dữ liệu cá nhân của người thật vào một nơi vốn không cần có.
Cách tránh: phân biệt bản backup phục hồi sự cố với bộ seed cho môi trường mới. Backup cần đủ dữ liệu theo mục tiêu phục hồi; seed có thể loại phiên và dữ liệu thử. Không xoá log hay dữ liệu vận hành khỏi backup một cách máy móc nếu chúng cần cho khôi phục hoặc truy vết.
Seed thường giữ | Seed cân nhắc loại; backup cần chính sách riêng |
|---|---|
Nội dung, cấu hình, người dùng, phân quyền | Nhật ký hoạt động |
Bản dịch, menu, danh mục | Phiên đăng nhập |
Hội thoại chat | |
Dữ liệu khách hàng thử nghiệm |
Bẫy 6: tệp trên đĩa không đi cùng cơ sở dữ liệu
Bản ghi trong bảng files trỏ tới một đường dẫn trên đĩa. Dump database mang theo bản ghi, không mang theo tệp.
Sau khi dựng lại, ảnh đại diện một thành viên hiện ra ô trống — bản ghi có, tệp không. Phải chép riêng.
Và khi chép, tôi mắc một lỗi nhỏ mà hậu quả không nhỏ:
install -d -o zukatek -g zukatek /var/lib/.../members/<id>/Trong lệnh này, không nên mặc định mọi thư mục cha đều có chủ sở hữu và quyền đúng chỉ vì đã chỉ định -o/-g. Thư mục members/ ở giữa được tạo ra thuộc về root, và ứng dụng không ghi vào đó được. Hệ quả: mọi lượt tải ảnh lên sau đó đều lỗi 500.
Lỗi này không lộ ra khi kiểm tra bằng mắt — quyền nhìn thì đúng. Nó chỉ lộ ra khi thật sự thử ghi bằng chính tài khoản của ứng dụng:
su -s /bin/sh zukatek -c 'mkdir -p /duong/dan/_thu && echo ok'Quy tắc: với quyền truy cập tệp, đừng suy luận — hãy thử ghi.
Bẫy 7: lệnh dọn dẹp xoá nhiều hơn bạn nghĩ
Sau khi kiểm thử xong, tôi dọn dữ liệu thử bằng:
TRUNCATE ai_messages, ai_conversations CASCADE;CASCADE lan sang bảng leads và lead_products. Tôi xoá luôn cả dữ liệu khách hàng — may là lúc đó bảng đang trống.
Tôi mắc đúng lỗi này hai lần trong cùng một ngày.
Cách tránh: khi dọn dữ liệu thử, xoá theo điều kiện nhận dạng dữ liệu của mình, đừng xoá cả bảng:
DELETE FROM leads WHERE email LIKE '%@test.local';Điều kiện vẫn có thể khớp nhầm và DELETE có thể kích hoạt cascade hoặc trigger. Tốt hơn là đánh dấu dữ liệu thử bằng ID/batch riêng, SELECT trước để xem phạm vi, dùng transaction và kiểm tra số dòng trước khi commit.
Quy trình rút gọn
Ghi lại phiên bản PostgreSQL ở cả hai đầu
Liệt kê cột nhị phân, quyết định cột nào bị loại khỏi dump
Chọn mục tiêu backup hay seed; chỉ loại dữ liệu theo chính sách rõ ràng
Dump, rồi quét bí mật bằng nhiều mẫu khác nhau
Sao lưu database đích trước khi đụng vào
Nạp thử vào database nháp trên chính máy chủ đích
Đối chiếu migration với bản triển khai, kiểm tra dữ liệu và luồng ứng dụng
Mới nạp thật
Chép tệp trên đĩa, rồi thử ghi bằng tài khoản ứng dụng
Dọn dữ liệu thử theo điều kiện, không
TRUNCATE
Bước 5 và 6 là hai bước hay bị bỏ nhất, và là hai bước cứu bạn.
Các sự cố được thuật lại từ bản thảo Zukatek tháng 10/2026, chưa có log gốc để kiểm chứng độc lập.
Nguồn đối chiếu: PostgreSQL — pg_dump, PostgreSQL — pg_restore.


