Khi cluster chạy vài chục tới hàng trăm website, pipeline backup chạy tuần tự từng site không còn hợp lý: mỗi site phải nén thư mục web, dump database, đẩy lên object storage, và cộng dồn thời gian là quá dài cho một lượt. Tài liệu này hướng dẫn dùng parallel matrix của GitLab CI để chia việc backup thành nhiều job song song theo lô namespace, dùng resource_group để không dồn tải lên máy database, cách dump MySQL an toàn trên máy đang phục vụ, đẩy lên object storage bằng s3cmd, và các lỗi hay gặp trong chính pipeline backup.
1. Backup một website gồm những gì
Trước khi nói tới parallel matrix, cần rõ mỗi website trên cluster có hai phần dữ liệu cần giữ:
- Thư mục web trên NFS:
public,plugins,lang,themes(xem bài Helm chart với NFS PersistentVolume tĩnh, claimRef và ba loại probe). - Database trên VPS MySQL ngoài cluster (xem bài MySQL trong Kubernetes hay ngoài cluster: vì sao đưa database ra VPS).
Cả hai được đẩy lên object storage tương thích S3 theo đường dẫn <bucket>/<YYYYMMDD>/<domain>/. Pipeline khôi phục dùng đúng cấu trúc này, nên bản backup nào cũng nạp lại được bằng quy trình trong bài Migrate website lên Kubernetes: quy trình chuyển hàng loạt an toàn.
2. Từ vòng lặp tuần tự đến parallel matrix
Phiên bản đơn giản nhất là một job với vòng for qua mọi namespace có nhãn của chart (ví dụ app=web). Khi số site tăng, chuyển sang parallel matrix: nhân job thành nhiều bản, mỗi bản lo một lô namespace.
Chọn số job và kích thước lô cho parallel matrix là bài toán cân bằng. Nhiều job nhỏ thì mỗi job xong nhanh nhưng tốn thời gian khởi động container, nạp kubeconfig, và runner phải gánh nhiều job cùng lúc. Ít job lớn thì đỡ chi phí khởi động nhưng một job chậm kéo dài cả lượt. Hai mẹo giúp ích:
- Dùng một image CI dựng sẵn đủ công cụ (kubectl, s3cmd, zip, ssh) thay vì cài gói ở đầu mỗi job.
- Tách việc xoá backup cũ ra một stage riêng chạy sau cùng, thay vì xoá trong vòng lặp từng site.
3. Parallel matrix hoạt động thế nào
Từ khoá parallel:matrix nhân một job thành nhiều job, mỗi job ứng với một tổ hợp giá trị biến; GitLab tạo tối đa 200 job cho một matrix (xem tài liệu parallel:matrix). Ở đây parallel matrix chỉ dùng để đánh số lô. Danh sách namespace được lấy một lần ở job đầu và truyền qua artifact, để mọi job chia lô trên cùng một danh sách:
list_sites:
stage: prepare
script:
- kubectl get namespaces -l app=web -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | sort > namespaces.txt
- TOTAL=$(wc -l < namespaces.txt)
- '[ "$TOTAL" -le $((16 * 20)) ] || { echo "Vượt sức chứa của matrix: $TOTAL site"; exit 1; }'
artifacts:
paths: [namespaces.txt]
expire_in: 1 day
backup_cluster:
stage: backup
image: { name: "<registry>/ci-tools:<tag>", entrypoint: [""] }
needs: [list_sites]
resource_group: database_backup_$RG
parallel:
matrix:
- { RG: ["0"], LOOP: [0, 3, 6, 9, 12, 15], STEP: ["20"] }
- { RG: ["1"], LOOP: [1, 4, 7, 10, 13], STEP: ["20"] }
- { RG: ["2"], LOOP: [2, 5, 8, 11, 14], STEP: ["20"] }
script:
- START=$((LOOP * STEP + 1)); END=$((START + STEP - 1))
- sed -n "${START},${END}p" namespaces.txt > batch.txt
- '[ -s batch.txt ] || { echo "Lô trống"; exit 0; }'
- FAILED=0
- |
while read -r NAMESPACE; do
DOMAIN=$(kubectl get ingress -n "$NAMESPACE" -o jsonpath='{.items[0].spec.rules[0].host}')
backup_one_site "$NAMESPACE" "$DOMAIN" || { echo "LỖI: $DOMAIN"; FAILED=$((FAILED+1)); }
done < batch.txt
- '[ "$FAILED" -eq 0 ] || exit 1'Ví dụ trên chia 16 lô, mỗi lô 20 namespace, tức sức chứa 320 site; job list_sites báo lỗi sớm nếu vượt. Lô nào trống thì thoát với mã 0. Mỗi site lỗi được đếm, và job parallel matrix đỏ nếu có site không backup được. Hàm backup_one_site gói các bước ở mục 4 và 5.
4. Lấy thư mục web và thông tin database ngay từ pod
Với mỗi namespace trong lô, job parallel matrix chọn pod Running có container đã ready, rồi nén thư mục web ngay trong pod và stream thẳng về runner:
kubectl exec -n "$NAMESPACE" "$POD_NAME" -- \
sh -c "cd /var/www/html && zip -q -r - public plugins lang themes" > "$PATH_BACKUP_DATA"
DATABASE_HOST=$(kubectl exec $POD_NAME -n $NAMESPACE -- sh -c 'echo "$DB_HOST"')
DATABASE_NAME=$(kubectl exec $POD_NAME -n $NAMESPACE -- sh -c 'echo "$DB_DATABASE"')zip -r - ghi ra stdout, nên không tạo file tạm trong container hay trên NFS. Thông tin database đọc từ biến môi trường của pod, tức đúng cấu hình site đang dùng thật, không phụ thuộc file values có thể đã cũ.
5. Dump MySQL mà không làm chậm website
Máy database vẫn đang phục vụ website trong lúc các job parallel matrix chạy backup. Lệnh dump được viết để nhường tài nguyên và không khoá bảng:
timeout 1800 ssh backup@$DATABASE_HOST "nice -n 19 mysqldump \
--defaults-extra-file=/etc/mysql/backup.cnf \
--skip-tz-utc --single-transaction --quick --lock-tables=false \
--routines --triggers --no-tablespaces $DATABASE_NAME" > "$PATH_BACKUP_DATABASE" \
|| return 1
[ -s "$PATH_BACKUP_DATABASE" ] || { echo "file dump rỗng"; return 1; }--single-transaction: với bảng InnoDB, dump đọc trong một transaction nhất quán, website vẫn ghi bình thường (xem tài liệu mysqldump 8.4).--quick: đọc từng dòng thay vì nạp cả bảng vào bộ nhớ.--no-tablespaces: user không có quyềnPROCESSthì thiếu tuỳ chọn này mysqldump báo lỗi phần tablespace.nice -n 19: tiến trình dump nhận ưu tiên CPU thấp nhất.timeout 1800: một database treo không giữ cả job quá 30 phút.--defaults-extra-file: mật khẩu nằm trong option file quyền600trên máy database, không đi trên dòng lệnh đểpsnhìn thấy.- Kiểm tra
-s: file rỗng thì không đẩy lên storage, tránh có bản backup "thành công" mà không chứa gì.
6. resource_group: giới hạn số job cùng chạm vào database
resource_group đảm bảo trong cùng một nhóm, tại một thời điểm chỉ có một job chạy; các job khác cùng nhóm xếp hàng chờ. Chia các job của parallel matrix vào 3 nhóm thì tối đa 3 job dump cùng lúc: đủ song song mà không đè máy database.
Lỗi hay gặp là viết resource_group: database_backup_$((LOOP % 3)) với mong muốn chia nhóm theo phép chia dư. GitLab chỉ thay biến CI dạng $VAR trong resource_group, không chạy phép tính shell $((...)) trong YAML, và tên resource group chỉ nhận chữ, số cùng một số ký tự như -, _, /, $, {, }, ., dấu cách. Kết quả không phải ba nhóm như chú thích. Cách đúng là đưa nhóm thành một chiều của parallel matrix, như biến RG trong mẫu ở mục 3: mỗi phần tử của matrix sinh các job mang cả RG lẫn LOOP, và $RG là biến thật nên GitLab thay được.
7. Đẩy lên object storage và xoá bản cũ
Sau khi job parallel matrix tạo xong, mỗi site có hai file, <domain>_web_folders.zip và <domain>_backup.sql.zip, tải lên bằng s3cmd. Không dùng cờ -P (ACL public) rồi mới setacl --acl-private: giữa hai lệnh có một khoảng file dump database công khai, và nếu lệnh thứ hai lỗi thì file công khai luôn. Upload mặc định là private.
s3cmd put "$PATH_BACKUP_DATABASE" "$S3_BUCKET/$(date -u +%Y%m%d)/$DOMAIN/"Xoá bản cũ trong một stage riêng chạy sau cùng. Cách bền nhất là lifecycle rule của bucket (xoá object cũ hơn N ngày). Nếu phải xoá bằng script, hãy liệt kê thư mục ngày thực tế rồi so với mốc hạn giữ, thay vì vòng lặp cố định "từ 7 đến 19 ngày trước": pipeline ngừng chạy vài tuần thì các thư mục cũ hơn khoảng đó không bao giờ bị xoá. Dùng ngày UTC thống nhất để tên thư mục không phụ thuộc múi giờ runner.
CUTOFF=$(date -u -d "-7 days" +%Y%m%d)
s3cmd ls "$S3_BUCKET/" | awk '{print $2}' | while read -r DIR; do
D=$(basename "$DIR")
if [[ "$D" =~ ^[0-9]{8}$ ]] && [ "$D" -lt "$CUTOFF" ]; then
s3cmd del --recursive "$DIR"
fi
doneMột biến như BACKUP_FOREVER=true có thể đổi đích sang thư mục backup_projects/<domain>/ nằm ngoài vòng xoá, dùng để giữ bản backup lâu dài trước khi gỡ một site.
8. Lỗi thường gặp trong pipeline backup bằng parallel matrix
- Lấy danh sách namespace ở mỗi job của parallel matrix: khi job phải chờ theo resource group, các job bắt đầu ở những thời điểm khác nhau. Có site được tạo hoặc gỡ giữa chừng là chỉ số lệch: một site bị bỏ sót, một site bị backup hai lần. Lấy danh sách một lần, truyền qua artifact.
- Lỗi bị nuốt: mỗi lỗi chỉ dẫn tới
continue, job vẫn xanh dù vài site không có backup. Đếm site lỗi, cho job fail và gửi cảnh báo. - Vượt sức chứa của parallel matrix: số lô nhân kích thước lô nhỏ hơn số site thì các site sau lặng lẽ không được backup. Kiểm tra sớm như job
list_sites. - In mật khẩu ra log để debug, hoặc truyền mật khẩu qua
-pcủa mysqldump. - Tin host key mới mỗi lần chạy:
ssh-keyscanrồi tin luôn khoá là bỏ qua lớp bảo vệ chống giả mạo máy. Lưu sẵnknown_hoststrong biến CI. - Không bao giờ thử khôi phục: định kỳ lấy ngẫu nhiên vài bản backup và chạy pipeline restore lên một namespace thử.
9. Pipeline tự viết hay công cụ backup cho Kubernetes
Velero là lựa chọn phổ biến để sao lưu đối tượng Kubernetes và volume nằm trong cluster. Nhưng nếu dữ liệu cần giữ nằm ở hai nơi ngoài tầm với mặc định của nó, database trên VPS và thư mục trên NFS, một pipeline parallel matrix đi thẳng vào hai nơi đó, dùng đúng cấu trúc thư mục của pipeline khôi phục, sẽ đơn giản hơn.
| Được | Mất |
|---|---|
| Backup đúng hai thứ cần khôi phục, cùng định dạng với pipeline restore | Không sao lưu đối tượng Kubernetes; cấu hình site dựa vào repo values và pipeline riêng |
| Parallel matrix rút ngắn một lượt backup mà không cần thêm hạ tầng | Phải tự lo chia lô, giới hạn tải database, báo lỗi |
| Chạy trên runner GitLab sẵn có, lịch chạy bằng pipeline schedule | Kubeconfig và khoá SSH tới máy database nằm trong biến CI, phải bảo vệ kỹ |
Với parallel matrix, tốc độ chỉ là nửa bài toán; nửa còn lại là độ tin cậy: biết chắc site nào đã có backup, và kiểm tra định kỳ rằng bản backup khôi phục được. Mô hình tổ chức pipeline có ở bài Mỗi branch một pipeline vận hành: tự động hoá bằng GitLab CI.
Bài tiếp theo
PriorityClass và pod giữ chỗ: dự phòng tài nguyên cho cluster. Khi node bị xếp kín, làm sao giữ lại một phần CPU và RAM cho lúc cần.
