Phần 43 vá lỗi lệch quyền tệp giữa container và host. Bài này chuyển sang câu hỏi thực dụng hơn: dữ liệu trong volume chỉ nằm trên một máy — server đó chết hoặc đĩa hỏng là mất trắng. Có ba cách hay được nhắc tới để sao lưu volume: tar qua container tạm, docker cp, và sao chép thẳng giữa hai volume. Bài chạy thật cả ba, đo thời gian và dung lượng, rồi khôi phục lại để xác nhận dữ liệu còn nguyên. Toàn bộ lệnh chạy trên Docker 29.7.2 (Docker Desktop, macOS, VM linuxkit, kernel 7.0.12-linuxkit), container nền alpine:3.20.
Dữ liệu mẫu
Volume rỗng thì đo gì cũng vô nghĩa, nên trước tiên dựng một volume mô phỏng dữ liệu thật: nhiều file cấu hình nhỏ (dễ nén) và vài file nhị phân lớn (khó nén, giống trang dữ liệu database):
docker volume create demo-vol
docker run --rm -v demo-vol:/data alpine:3.20 sh -c "
mkdir -p /data/config /data/db
for i in \$(seq 1 4000); do
echo 'setting_'\$i'=true' > /data/config/file-\$i.conf
done
dd if=/dev/urandom of=/data/db/base.bin bs=1M count=80 2>/dev/null
dd if=/dev/urandom of=/data/db/wal.bin bs=1M count=40 2>/dev/null
du -sh /data
"
# 135.8M /data
Kết quả: 4002 file, 135,8 MB — 4000 file cấu hình vài chục byte mỗi cái, cộng hai file nhị phân 80 MB và 40 MB lấy từ /dev/urandom. Cần nói rõ: /dev/urandom là dữ liệu ngẫu nhiên thật sự, entropy cao nhất có thể — dữ liệu database thật (WAL, trang B-tree) thường nén được tốt hơn một chút vì vẫn còn cấu trúc lặp lại, nhưng cùng xu hướng: phần lớn dung lượng của một CSDL đang chạy không phải là văn bản thuần, nên đừng kỳ vọng gzip ăn được nhiều.
Cách 1: tar qua container tạm
Đây là cách được nhắc tới nhiều nhất trong tài liệu chính thức của Docker: mount volume nguồn (chỉ đọc) và một thư mục host vào cùng một container tạm, rồi tar từ trong đó ra:
mkdir -p /tmp/backup-demo
# không nén
time docker run --rm -v demo-vol:/data:ro -v /tmp/backup-demo:/backup \
alpine:3.20 tar cf /backup/backup.tar -C /data .
# có nén
time docker run --rm -v demo-vol:/data:ro -v /tmp/backup-demo:/backup \
alpine:3.20 tar czf /backup/backup.tar.gz -C /data .
Số đo:
| Biến thể | Thời gian | Dung lượng |
|---|---|---|
tar cf (không nén) |
0,469s | 123,9 MiB (129.928.704 byte) |
tar czf (có nén) |
2,769s | 120,1 MiB (125.936.454 byte) |
Nén chỉ ăn được 3,07% dung lượng nhưng tốn thêm gần 2,3 giây — chậm gấp khoảng 6 lần. Đây là điều đi ngược lời khuyên phổ biến "luôn czf cho gọn": lời khuyên đó đúng với dữ liệu văn bản thuần, nhưng 120 MB trong 135,8 MB dữ liệu mẫu đã có entropy cao, gzip gần như không còn gì để nén. Nếu volume của bạn chủ yếu là ảnh đã nén sẵn, video, hoặc file dữ liệu nhị phân của database, kết quả sẽ tương tự — bật nén chỉ để tốn thời gian.
Khôi phục ngược lại, vào một volume trống:
docker volume create demo-vol-restore1
time docker run --rm -v demo-vol-restore1:/data -v /tmp/backup-demo:/backup \
alpine:3.20 tar xf /backup/backup.tar -C /data # 0,303s
time docker run --rm -v demo-vol-restore1:/data -v /tmp/backup-demo:/backup \
alpine:3.20 tar xzf /backup/backup.tar.gz -C /data # 1,025s
Cả hai đều khôi phục đúng 4002 file, 135,8 MB — đối chiếu bằng du -sh và đếm file sau khi khôi phục. Vòng tròn sao lưu + khôi phục trọn vẹn: 0,772s (không nén) hoặc 3,794s (có nén).
Cách 2: docker cp
docker cp sao chép trực tiếp giữa container và host, không cần container tạm chỉ để chạy tar. Điều kiện: phải có một container đang gắn volume đó (chạy hay đã dừng đều được):
docker run -d --name demo-src -v demo-vol:/data alpine:3.20 sleep 3600
mkdir -p /tmp/backup-demo/cp-out
time docker cp demo-src:/data/. /tmp/backup-demo/cp-out/
# 1,469s — 136 MB, 4002 file trên đĩa host, không nén
Khôi phục là chiều ngược lại, copy từ host vào container đích đã gắn volume mới:
docker volume create demo-vol-restore2
docker run -d --name demo-dst -v demo-vol-restore2:/data alpine:3.20 sleep 3600
time docker cp /tmp/backup-demo/cp-out/. demo-dst:/data/
# 1,419s — khôi phục đúng 135,8 MB, 4002 file
Vòng tròn sao lưu + khôi phục: 2,888s — chậm hơn cách 1 không nén, nhanh hơn cách 1 có nén. docker cp tiện khi đã có sẵn container đang chạy (không cần dựng container tạm chỉ để tar), nhưng bất tiện hơn khi cần tự động hoá: phải giữ một container tồn tại trong lúc copy, và không có sẵn tuỳ chọn nén.
Cách 3: sao chép thẳng giữa hai volume, không qua đĩa host
Hai cách trên đều đi qua đĩa host như một bước trung gian. Khi mục đích chỉ là nhân bản một volume (dựng môi trường staging từ dữ liệu prod, hoặc chuyển volume sang một Docker host khác qua SSH) thì có thể ghép tar của container nguồn thẳng vào tar của container đích bằng pipe, không cần file trung gian nào:
docker volume create demo-vol-clone
time sh -c '
docker run --rm -v demo-vol:/from:ro alpine:3.20 tar cf - -C /from . \
| docker run --rm -i -v demo-vol-clone:/to alpine:3.20 tar xf - -C /to
'
# 2,337s — đã bao gồm cả đọc lẫn ghi, xác nhận lại đúng 135,8 MB, 4002 file
2,337 giây cho cả backup lẫn restore trong một lệnh — nhanh hơn tổng round-trip 2,888 giây của docker cp, và không tốn một byte nào trên đĩa host. Muốn chuyển sang máy khác thì thay container thứ hai bằng một lệnh SSH: docker run ... tar cf - -C /from . | ssh may-khac 'docker run --rm -i -v vol-dich:/to alpine:3.20 tar xf - -C /to'. Đánh đổi: cách này không tạo ra một file lưu trữ nào để cất giữ lâu dài — nó chỉ hữu ích cho tình huống "sao chép ngay bây giờ", còn muốn có bản backup nằm yên trên đĩa hoặc object storage thì vẫn phải dùng cách 1.
Bảng tổng hợp
| Cách | Backup | Restore | Round-trip | Tốn đĩa host? | Cần gì |
|---|---|---|---|---|---|
| 1. tar, không nén | 0,469s | 0,303s | 0,772s | Có (123,9 MiB) | không cần container đang chạy |
| 1. tar, có nén | 2,769s | 1,025s | 3,794s | Có (120,1 MiB, giảm 3,07%) | không cần container đang chạy |
| 2. docker cp | 1,469s | 1,419s | 2,888s | Có (136 MB, không nén) | container đang gắn volume |
| 3. pipe tar volume→volume | — | — | 2,337s (1 bước) | Không | hai container tạm cùng lúc |
Chọn cách nào
- Cần một file backup để cất giữ hoặc tải lên object storage → cách 1. Mặc định không nén trừ khi biết chắc dữ liệu là văn bản (log, cấu hình, mã nguồn) — lúc đó nén mới đáng thời gian bỏ ra.
- Chỉ cần lấy nhanh vài file ra khỏi container đang chạy để kiểm tra, không cần đóng gói →
docker cp, khỏi phải nhớ cú pháptar -C. - Nhân bản volume sang môi trường khác hoặc máy khác, không cần giữ file trung gian → pipe
tarthẳng giữa hai volume (hoặc qua SSH).
Tổng kết
Đo được ba điều đáng nhớ: nén không giúp nhiều khi phần lớn dữ liệu đã có entropy cao (giảm 3,07% mà chậm gấp 6 lần với dữ liệu mẫu ở đây); docker cp tiện dụng nhưng chậm hơn tar không nén vì không có tuỳ chọn đóng gói; và sao chép thẳng giữa hai volume qua pipe nhanh hơn tổng round-trip của docker cp đồng thời không đụng tới đĩa host — đổi lại nó không để lại một file lưu trữ nào. Không có cách nào thắng tuyệt đối, chọn theo việc bạn thật sự cần: một file lưu giữ lâu dài, một cú kiểm tra nhanh, hay một bản sao tức thời sang nơi khác.
Thử ba mươi giây
Sao lưu một volume ra file .tar.gz rồi khôi phục vào volume mới, xác nhận số file khớp nhau:
docker volume create demo && docker run --rm -v demo:/data alpine:3.20 \
sh -c "echo hello > /data/f.txt"
mkdir -p /tmp/bk
docker run --rm -v demo:/data:ro -v /tmp/bk:/backup alpine:3.20 \
tar czf /backup/demo.tar.gz -C /data .
docker volume create demo-restore
docker run --rm -v demo-restore:/data -v /tmp/bk:/backup alpine:3.20 \
tar xzf /backup/demo.tar.gz -C /data
docker run --rm -v demo-restore:/data alpine:3.20 cat /data/f.txt # hello
docker volume rm demo demo-restore && rm -rf /tmp/bk
Bài viết liên quan
- UID 1000 ghi được, UID 2000 đọc bị từ chối ngay lập tức: đo lệch quyền tệp giữa container và host, vá bằng ACL và group thay vì chown
- Bind mount ghi ngẫu nhiên 4K nhanh gấp 4 lần volume, nhưng thua khi ghi tuần tự: đo I/O theo bốn kiểu truy cập
- Chỉ stop rồi start lại, không cần rm, đã đủ xoá sạch tmpfs: đo tốc độ và độ bền của volume, bind mount, tmpfs