Như trong bài mở đầu series, máy MySQL trên Cloud VPS cấu hình nhỏ thường không bật binlog và không có replica. Khi đó, mọi sự an toàn của dữ liệu dựa vào đúng một thứ: backup database hằng đêm. Tài liệu này mô tả một luồng backup hoàn chỉnh: Jenkins điều phối, đọc thông tin kết nối từ .env của từng site, mysqldump chạy trên máy vận hành, nén, đẩy lên object storage tương thích S3 bằng rclone, xoá bản cũ theo chính sách lưu giữ, kèm các lỗi hay gặp và quy trình restore thử.
1. Backup database cho cụm không replica: yêu cầu thật sự là gì
Với kiến trúc một site một database, một máy MySQL chứa nhiều schema, bài toán backup database có vài ràng buộc cụ thể:
- RPO bằng chu kỳ backup: backup mỗi đêm một lần thì có thể mất gần một ngày dữ liệu nếu máy database hỏng ngay trước giờ backup. Không binlog nên không khôi phục về thời điểm bất kỳ được.
- Bản backup phải nằm ngoài cụm: dump chỉ nằm trên đĩa của chính máy database hay máy vận hành thì mất cùng máy, nên phải đẩy ra object storage.
- Không được làm chậm website: máy MySQL nhỏ vẫn đang phục vụ người dùng, nên dump chạy giờ thấp điểm và tuần tự, không song song hàng chục schema.
- Backup cả file: với site Laravel, dữ liệu nằm ở cả database lẫn thư mục upload (
storage/app,public/uploads…).
2. Luồng backup database: Jenkins, minion, máy vận hành, object storage
Jenkins (cron giờ thấp điểm, mỗi minion một job)
│
├─ ssh minion ── ls -d /var/www/* (danh sách site)
│ └─ đọc DB_HOST, DB_DATABASE, DB_USERNAME... từ .env từng site
│
├─ ssh minion ── nén thư mục upload ─▶ scp ─▶ máy vận hành:/backups/...
│
├─ ssh máy vận hành ── mysqldump -h <db> ... | gzip > <domain>.sql.gz
│ rclone copy ... ─▶ object storage tương thích S3
│ xoá thư mục tạm
│
└─ bước cuối: xoá các bản quá hạn trên storageĐiểm đáng chú ý: không SSH thẳng vào máy database. Minion chỉ được dùng để đọc .env và nén thư mục site. Mọi lệnh mysqldump chạy trên máy vận hành, kết nối tới máy database qua mạng private. Máy vận hành là nơi duy nhất cài client MySQL và rclone, nên khoá truy cập object storage chỉ nằm ở một chỗ. Cách tổ chức này đi cùng mô hình Jenkins điều khiển mọi máy bằng một credential SSH trong bài về Jenkins làm control plane.
3. Từng bước trong job backup database
Lấy thông tin kết nối từ .env
Mỗi site đã có thông tin database trong .env, nên job không cần danh sách riêng: thêm site mới là tự động có backup database, vì thư mục đã nằm trong thư mục site chung.
sites=($(ssh deploy@"$minion" "ls -d /var/www/*"))
for dir in "${sites[@]}"; do
domain=$(basename "$dir")
env=$(ssh deploy@"$minion" "cat '$dir/.env'")
db_host=$(grep -oP '^DB_HOST=\K\S+' <<< "$env")
db_name=$(grep -oP '^DB_DATABASE=\K\S+' <<< "$env")
db_user=$(grep -oP '^DB_USERNAME=\K\S+' <<< "$env")
# DB_PORT, DB_PASSWORD, DB_CONNECTION đọc tương tự
doneJob có thể xử lý thêm theo DB_CONNECTION: mysql dùng mysqldump, pgsql dùng pg_dump với file mật khẩu tạm quyền 600, xoá ngay sau khi dump.
Dump backup database bằng mysqldump, mật khẩu không nằm trên dòng lệnh
cnf=$(mktemp) && chmod 600 "$cnf"
printf '[client]\nhost=%s\nuser=%s\npassword="%s"\n' "$db_host" "$db_user" "$db_pass" > "$cnf"
mysqldump --defaults-extra-file="$cnf" --single-transaction --quick \
--no-tablespaces --skip-tz-utc "$db_name" \
| gzip > "$out/${domain}.sql.gz"
rm -f "$cnf"Ý nghĩa các cờ theo tài liệu mysqldump của MySQL 8.0:
--single-transaction: với bảng InnoDB, cho một bản dump nhất quán mà không khoá bảng. Thiếu cờ này, mysqldump khoá bảng trong lúc dump và có thể làm chậm đơn hàng đúng lúc đó.--quick: đọc từng dòng thay vì nạp cả bảng vào RAM, quan trọng trên máy vận hành cấu hình nhỏ.--no-tablespaces: bỏ phần dump tablespace, vốn đòi quyềnPROCESSmà user của từng site không có.--skip-tz-utc: giữ giá trịTIMESTAMPtheo múi giờ server thay vì ép về UTC, để thời gian hiển thị không lệch khi nạp lại.
File cấu hình tạm qua --defaults-extra-file thay cho -p<mật khẩu>, để mật khẩu không lọt vào danh sách tiến trình hay log khi bật set -x.
Đẩy bản backup database lên object storage bằng rclone
day=$(date +%Y%m%d)
rclone copy "$out/" "<remote>:<bucket>/${minion}/${day}/${domain}/"
rclone lsf "<remote>:<bucket>/${minion}/${day}/${domain}/" | grep -q "${domain}.sql.gz" \
&& rm -rf "$out"rclone là công cụ đồng bộ tệp với hàng chục loại storage, trong đó có mọi dịch vụ tương thích S3. rclone copy chỉ sao chép tệp chưa có ở đích, không xoá gì ở đích, nên một lỗi cấu hình không làm mất bản backup đã có. Bước rclone lsf xác nhận file đã thực sự lên storage rồi mới xoá bản tạm. Cấu trúc <minion>/<yyyymmdd>/<domain>/ giúp tìm bản backup database của một site chỉ cần biết minion và ngày.
Chính sách lưu giữ backup database: xoá theo tuổi, không theo danh sách ngày
Một cách hay gặp là tính tên thư mục của một khoảng ngày cũ (ví dụ từ 7 đến 19 ngày trước) rồi rclone purge từng thư mục. Cách này có lỗ hổng: thư mục nào cũ hơn khoảng đó (vì job ngừng chạy một thời gian) sẽ không bao giờ được dọn và dung lượng storage tăng dần. Nên xoá theo tuổi file:
# giữ 7 ngày gần nhất cho mọi thư mục của minion
rclone delete --min-age 7d "<remote>:<bucket>/${minion}/"
rclone rmdirs --leave-root "<remote>:<bucket>/${minion}/"Nếu storage hỗ trợ lifecycle rule, đặt luôn quy tắc hết hạn ở phía storage làm lớp bảo vệ thứ hai. Con số bảy ngày chỉ là ví dụ; hãy chọn theo yêu cầu khôi phục của từng loại site.
4. Lỗi thường gặp khi backup database lên object storage
Upload file lớn bị từ chối với mã 400
Khi file vượt một ngưỡng kích thước, rclone chuyển sang multipart upload: chia file thành nhiều phần, mỗi phần một request UploadPart, cuối cùng gọi CompleteMultipartUpload. Một số dịch vụ tương thích S3 xử lý multipart khác với S3 gốc, lỗi thường có dạng CreateMultipartUpload ... StatusCode: 400. Biểu hiện: site nhỏ backup đều, riêng site có file dump lớn thất bại. Cách xử lý:
- Kiểm tra
provider,--s3-chunk-size,--s3-upload-cutofftheo trang cấu hình S3 của rclone. - Nén bằng
gzipngay khi dump để file nhỏ hơn. - Chạy thử bằng tay với
-vvđể xem request nào bị 400.
Thư mục tạm đầy đĩa
Mỗi site đi qua hai nơi chứa tạm: minion (file nén thư mục site) và máy vận hành (file dump). Nếu job dừng giữa chừng vì scp hoặc rclone lỗi, lệnh rm phía sau không chạy và file tạm nằm lại. Sau vài đêm lỗi, đĩa của máy nhỏ sẽ đầy. Dùng trap 'rm -rf "$out"' EXIT, thêm job dọn thư mục tạm định kỳ, và đưa dung lượng đĩa máy vận hành vào giám sát.
Một site lỗi làm cả job backup database dừng
Nguồn thông tin duy nhất là .env. Site đổi mật khẩu database mà không cập nhật .env sẽ làm mysqldump báo Access denied. Với set -e, lỗi này dừng cả job, các site sau không được backup đêm đó. Redirect > file.sql còn tạo sẵn file 0 byte, trông như đã có backup. Ba biện pháp:
- Xử lý từng site riêng: lỗi thì ghi nhận, báo Slack, rồi
continue; cuối job mới trả mã lỗi nếu có site thất bại. - Sau dump, kiểm tra file không rỗng và dòng cuối có chuỗi
-- Dump completed(với gzip:zcat ... | tail -1). - Chỉ báo thành công khi file đã lên storage.
Máy đã gỡ nhưng job còn chạy
Khi một minion được dựng lại hoặc gỡ khỏi cụm (xem bài dựng lại bằng cloud image), quên gỡ job backup tương ứng thì mỗi đêm job báo Could not resolve hostname. Danh sách job backup database phải đi cùng danh sách máy.
5. Khôi phục: backup database chỉ có giá trị khi restore được
Job backup database chạy xanh chỉ chứng minh các lệnh đã chạy xong, không chứng minh file dump nạp lại được, đủ bảng, đủ dòng. Các việc sau nên là phần bắt buộc của quy trình backup database:
- Restore thử định kỳ: chọn vài site, tải bản mới nhất, nạp vào schema tạm trên máy MySQL thử, so số bảng và số dòng vài bảng chính với bản đang chạy.
- Đo thời gian restore để có con số thật khi cần trả lời "bao lâu có lại dữ liệu".
- Cảnh báo kích thước bất thường: file nhỏ hơn hẳn hôm trước là dấu hiệu dump hỏng hoặc bảng bị mất.
- Ghi lại quy trình restore: tải bản nào, nạp vào đâu, đổi
.envthế nào.
# restore thử trên máy thử
rclone copy "<remote>:<bucket>/app-php82-1/<yyyymmdd>/example.com/" /tmp/restore-test/
mysql --login-path=db-test -e "CREATE DATABASE restore_test CHARACTER SET utf8mb4"
zcat /tmp/restore-test/example.com.sql.gz | mysql --login-path=db-test restore_test
mysql --login-path=db-test restore_test -e "SELECT COUNT(*) FROM users"6. Checklist backup database
- Dump có
--single-transaction, mật khẩu qua file cấu hình quyền 600. - Mỗi site xử lý độc lập; lỗi một site không chặn site khác.
- Kiểm tra file dump hoàn chỉnh và đã lên storage trước khi xoá bản tạm.
- Bản backup database nằm ngoài cụm; khoá storage chỉ ở máy vận hành.
- Xoá bản cũ theo tuổi, có lifecycle rule phía storage.
- Cảnh báo khi job lỗi, khi đĩa tạm đầy, khi kích thước bất thường.
- Restore thử theo lịch cố định. Chưa restore thử là chưa có backup.
Bài tiếp theo
Self-healing tiết kiệm: tự phát hiện và khởi động lại dịch vụ chết. Các job quét dịch vụ, supervisor và đĩa chạy định kỳ trên Jenkins, và vì sao tự restart có thể che mất nguyên nhân gốc.
