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.

Ba cách sao lưu và khôi phục volume Docker: tar qua container tạm, docker cp, và pipe tar trực tiếp giữa hai volume, kèm thời gian và dung lượng đo được

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áp tar -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 tar thẳ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