Hình dung bạn phải phôtô một quyển sổ cái dày, trong khi người kế toán vẫn ngồi đó ghi tiếp từng dòng. Bạn chép trang 1 lúc 2 giờ, chép tới trang 400 đã là 2 giờ 3 phút — và trong ba phút đó anh ta đã thêm cả trăm dòng mới. Bản phôtô của bạn không ứng với bất kỳ khoảnh khắc nào: nửa đầu là quá khứ, nửa sau mới hơn. Nghe như một mớ hỗn độn. Nhưng quyển sổ này có một chỗ cứu: ở cuối sách có một nhật ký tạm ghi lại mọi bút toán đang chờ vào sổ. Ai cầm bản phôtô của bạn, chỉ cần phát lại cái nhật ký ấy là dựng lại được một trạng thái sạch, tự nhất quán — miễn là bạn đã phôtô cả nhật ký cùng trong một quyển. Sao lưu nóng một volume CSDL đúng là chuyện phôtô quyển sổ đó, và bài này đo tận nơi.

Container không giữ dữ liệu, volume mới giữ. Nhưng docker volume không có lệnh backup nào cả — bạn phải tự dựng. Có ba đường đi, và chúng khác nhau đủ nhiều để đáng đo.

Ba cách, cùng một volume 402 MB

Volume thử nghiệm chứa 200 MB dữ liệu nén được, 200 MB ngẫu nhiên, và 500 tệp nhỏ.

Cách Sao lưu Dung lượng Khôi phục
tar + gzip 5,9 s 200,2 MB 3,6 s
tar không nén 1,1 s 400,5 MB 0,9 s
docker cp 1,3 s 402,0 MB —
Chép thẳng thư mục volume 0,6 s 402,0 MB —

Cách chuẩn là mượn một container để đọc volume:

docker run --rm \
  -v v-du-lieu:/d:ro \
  -v "$PWD":/b \
  alpine tar czf /b/sao-luu.tgz -C /d .

Chạy được ở mọi nơi, không cần quyền root trên máy chủ, không cần container đích đang chạy. Chú ý :ro — không có lý do gì để tiến trình sao lưu ghi được vào dữ liệu gốc.

Khôi phục là chiều ngược lại:

docker volume create v-moi
docker run --rm -v v-moi:/d -v "$PWD":/b:ro \
  alpine tar xzf /b/sao-luu.tgz -C /d

gzip đắt hơn nhiều người tưởng. Nó chậm hơn 5,4 lần để đổi lấy một nửa dung lượng — và một nửa đó chỉ có vì tôi cố tình để 200 MB dữ liệu nén được. Với ảnh, video hay dữ liệu đã nén sẵn, bạn trả toàn bộ thời gian mà gần như không được gì. Đo thử trên dữ liệu thật của bạn trước khi mặc định bật z.

docker cp đòi một container đang tồn tại — nếu không có thì bạn phải dựng một cái tạm, và lúc đó cách tar gọn hơn. Nó cũng chỉ chép ra thư mục, không đóng gói.

Chép thẳng /var/lib/docker/volumes/<ten>/_data là nhanh nhất nhưng chỉ dùng được trên máy chủ Linux, cần quyền root, và trên Docker Desktop thì thư mục đó nằm trong máy ảo nên bạn không với tới. Nó cũng phụ thuộc vào chi tiết cài đặt mà Docker không cam kết giữ nguyên.

Cái bẫy sao lưu nóng — và phép đo không ủng hộ lời đồn

Lời khuyên phổ biến: tar một volume của CSDL đang chạy sẽ cho bản sao lưu hỏng.

Tôi thử cho ra nhẽ. PostgreSQL ghi 400.000 dòng bằng 200 giao dịch nhỏ liên tiếp. Giữa chừng, tôi chụp tar — và xác nhận nó thật sự rơi vào giữa lúc đang ghi:

so dong ngay TRUOC khi chup tar: 134.000
so dong ngay SAU khi chup xong : 224.000
so dong cuoi cung tren CSDL goc: 400.000

Chín mươi nghìn dòng được commit trong lúc tar đang đọc. Rồi tôi khôi phục vào một volume mới và khởi động PostgreSQL:

trang thai : running
so dong    : 144.000
so dong hong: 0
LOG: database system was not properly shut down; automatic recovery in progress
LOG: redo done at 0/7FFFD40 ... elapsed: 0.13 s

Nó chạy. PostgreSQL coi đó như một lần mất điện, phát lại WAL, và cho ra một trạng thái nhất quán tại một mốc thời gian nào đó trong khoảng đang ghi. Không dòng nào hỏng. Đúng như bản phôtô quyển sổ: nó dựng lại được vì cái nhật ký tạm (WAL) đã được chép cùng.

Vậy lời đồn sai? Không hẳn — nó chỉ nói không đúng lý do. Ba điều phép đo này thật sự cho thấy:

Bạn nhận một mốc thời gian ngẫu nhiên. 144.000 dòng, trong khi lúc bắt đầu chụp đã có 134.000 và lúc chụp xong đã có 224.000. Con số đó do thứ tự tar đọc tệp quyết định, không do bạn. Với CSDL thì "một mốc nào đó" thường chấp nhận được; với hệ thống có nhiều thành phần phải khớp nhau thì không.

Nó chỉ hoạt động vì mọi thứ nằm trong MỘT volume. WAL và tệp dữ liệu được chụp cùng nhau nên phát lại được. Tách WAL sang volume khác — một cấu hình rất thường gặp để tăng hiệu năng — là hai bản chụp lệch nhau về thời gian và không còn gì bảo đảm. (Chính là phôtô cái nhật ký ở một quyển, còn sổ cái ở quyển khác, hai lần bấm máy khác thời điểm.)

Nó dựa vào khả năng phục hồi sau sự cố của CSDL. PostgreSQL, MySQL InnoDB, MongoDB đều có WAL nên chịu được. Một ứng dụng tự ghi tệp mà không có nhật ký ghi trước thì không.

Và pg_dump vẫn tốt hơn ở mọi mặt

Cùng một CSDL 300.000 dòng:

Dung lượng
tar czf cả volume 28,2 MB
pg_dump -Fc 7,4 MB

Nhỏ hơn 3,8 lần, vì nó xuất dữ liệu logic chứ không chép cả chỉ mục, khoảng trống và WAL. Nó cũng cho ảnh chụp nhất quán tại đúng một mốc do CSDL tự bảo đảm, và khôi phục được sang phiên bản PostgreSQL khác — điều mà bản tar thư mục dữ liệu không làm được.

docker exec pg pg_dump -U postgres -Fc -f /tmp/d.bin postgres
docker cp pg:/tmp/d.bin ./sao-luu-$(date +%F).bin

Quy tắc rút ra: CSDL thì dùng công cụ của chính nó, tar volume để dành cho tệp tải lên, ảnh, cấu hình — những thứ không có khái niệm giao dịch.

Nếu bắt buộc phải tar một CSDL, dừng nó trước:

docker stop pg
docker run --rm -v v-pg:/d:ro -v "$PWD":/b alpine tar czf /b/pg.tgz -C /d .
docker start pg

Vài giây ngừng dịch vụ đổi lấy một bản sao lưu bạn biết chắc là đúng.

Kiểm tra bản sao lưu, đừng chỉ tạo ra nó

Bản sao lưu chưa từng được khôi phục thì chưa phải bản sao lưu. Việc này rẻ hơn nhiều so với người ta tưởng, vì chỉ cần một volume tạm:

docker volume create v-kiem
docker run --rm -v v-kiem:/d -v "$PWD":/b:ro alpine tar xzf /b/sao-luu.tgz -C /d
docker run --rm -v v-kiem:/d alpine sh -c 'du -sh /d; ls /d | head'
docker volume rm v-kiem

Với CSDL thì thêm một bước: khởi động hẳn nó lên từ volume vừa khôi phục và đếm số dòng — đúng như phép đo trong bài này. Mất chưa tới một phút, và nó là khác biệt giữa "tôi có sao lưu" với "tôi khôi phục được".

Tự kiểm trên máy bạn

Liệt kê mọi volume kèm dung lượng, để biết cái nào đáng sao lưu:

docker system df -v | awk '/VOLUME NAME/,0' | head -20

Rồi sao lưu một cái:

V=<ten-volume>
docker run --rm -v "$V":/d:ro -v "$PWD":/b alpine \
  tar czf /b/"$V"-$(date +%F).tgz -C /d .
ls -lh "$V"-*.tgz

Ba câu hỏi nên trả lời được cho mỗi volume trong hệ thống của bạn:

  • Nó có dữ liệu không thể tạo lại không? (Nếu không, đừng sao lưu — dọn nó đi.)
  • Nếu là CSDL, bạn đang dùng công cụ của nó hay đang tar thư mục dữ liệu?
  • Lần cuối bạn khôi phục thử là khi nào?

Mẫu số chung

Bài học đầu tiên, chính là bản phôtô quyển sổ chép trang này lúc 2 giờ, trang kia lúc 2 giờ 3 phút: đọc trạng thái của một hệ thống đang thay đổi bằng cách quét tuần tự từng phần thì kết quả không ứng với một khoảnh khắc nào — muốn có "ảnh chụp tại một mốc" phải hoặc đóng băng nguồn, hoặc dùng đúng cơ chế snapshot mà nguồn tự bảo đảm. tar đọc tệp nọ đến tệp kia nên bắt được một mốc ngẫu nhiên; nó chỉ cứu được nhờ WAL của Postgres, không nhờ bản thân tar. Cùng cái bẫy "đọc rời rạc một thứ đang động" ở khắp nơi: trong CSDL, quét từng bảng một bằng nhiều câu lệnh rời sẽ thấy trạng thái xé lẻ, nên mới có ảnh chụp nhất quán (MVCC, REPEATABLE READ, pg_dump chạy trong một snapshot) để mọi bảng cùng một mốc; trong Java, duyệt một ArrayList trong khi luồng khác sửa nó ném thẳng ConcurrentModificationException, phải chụp bản sao hoặc dùng CopyOnWriteArrayList; trong Go, range trên một map đang bị ghi là data race, phải khoá lấy một snapshot rồi mới đọc; ở tầng đĩa, chép nhiều tệp rời khác hẳn một snapshot LVM/ZFS đông cứng cả cây thư mục trong một nhịp. Nguyên tắc: trước khi tin một "bản sao trạng thái", hỏi nó được đọc trong một khoảnh khắc hay quét dần qua thời gian — nếu là quét dần, thứ khiến nó dùng được không phải phép chép mà là cơ chế nhất quán của nguồn, và bạn phải chép trọn cơ chế ấy cùng nhau.

Điều thứ hai, đọc từ chuyện bản tar nóng chạy được, 0 dòng hỏng mà vẫn không nên tin: "nó chạy" không phải là "nó đúng như bạn định" — một thao tác sống sót nhờ may (đúng một volume + CSDL biết phục hồi) khác hẳn một thao tác có bảo đảm gọi tên được. Bản tar may mắn cho ra một mốc bạn không chọn; pg_dump cho ra mốc nhất quán do CSDL cam kết, lại nhỏ hơn 3,8 lần và khôi phục xuyên phiên bản. Cùng cái bẫy "chạy được nên tưởng đúng" ở khắp nơi: một test xanh dưới @Transactional che mất vi phạm khoá ngoại vì lệnh chưa bao giờ commit; một truy vấn trả về hàng nhưng từ một snapshot sai; một lần đọc "nhất quán sau cùng" thường đúng nên không ai thấy lỗi cho tới hôm nó sai. Nguyên tắc: hãy chọn thứ có bảo đảm bạn phát biểu được thành lời — công cụ riêng của CSDL, một mức cô lập giao dịch, một snapshot thật — thay vì một hành vi tình cờ đứng vững; và luôn nghiệm thu bằng kết quả (khôi phục thử, đếm lại) chứ không bằng việc lệnh chạy không báo lỗi, vì một bản sao lưu chưa khôi phục thử thì chưa phải bản sao lưu.

Phần sau chuyển sang Docker Compose: tệp compose.yaml thật sự làm gì khi bạn gõ up.