Phần 40 đóng lại mảng mạng của sê-ri này. Từ bài này chuyển sang mảng lưu trữ: container tự nó không giữ được gì lâu dài — xoá container là mất writable layer. Docker cho ba cách gắn dữ liệu từ bên ngoài vào: volume, bind mounttmpfs. Ba cái nghe tương tự nhau vì cùng khai bằng cờ -v hoặc --mount, nhưng đi theo ba con đường vật lý hoàn toàn khác nhau, và bài này đo cả ba bằng số thật thay vì đoán.

Toàn bộ lệnh chạy trên Docker 29.7.2 (Docker Desktop, macOS, Apple Silicon, VM linux/aarch64), container nền alpine:3.20.

Container nối tới ba nơi lưu trữ khác nhau: bind mount qua virtiofs trên macOS host, volume trên đĩa ext4 của VM, và tmpfs trong RAM của VM

Ba kiểu, ba con đường vật lý

  • Named volume (-v ten-volume:/data): Docker tự tạo và quản lý, nằm trong thư mục Docker sở hữu. Trên Docker Desktop, chỗ đó là một phân vùng ext4 nằm trên đĩa ảo của VM Linux — không phải trên ổ đĩa macOS thật.
  • Bind mount (-v /duong/dan/tren/host:/data): trỏ thẳng vào một thư mục có sẵn của máy host. Trên Docker Desktop, "host" ở đây là macOS, còn container chạy trong VM, nên đường đi phải qua một tầng chia sẻ filesystem — virtiofs (mặc định từ Docker Desktop 4.6 trở lên).
  • tmpfs (--tmpfs /data:rw,size=1g): không phải file, mà là một vùng nhớ RAM được gắn vào như thể là filesystem. Không có đĩa nào liên quan.

Kiểm tra bằng mount bên trong container xác nhận đúng ba loại filesystem khác nhau:

/dev/vda1 on /data type ext4 (rw,relatime,discard)              # volume
virtiofs2 on /data type virtiofs (rw,nosuid,nodev,relatime,...)  # bind mount
tmpfs on /data type tmpfs (rw,nosuid,nodev,noexec,relatime,size=1048576k)  # tmpfs

Đo tốc độ ghi: ghi 512 MB bằng dd

Ghi bình thường (có bộ đệm hệ điều hành), lặp lại nhiều lần cho ổn định:

Kiểu Ghi có buffer Ghi ép fsync (trung bình 3 lần)
Named volume 11,0 GB/s ~3,0 GB/s (2,6 / 3,2 / 3,1)
Bind mount (virtiofs) 2,1 GB/s ~2,1 GB/s (2,0 / 2,1 / 2,1)
tmpfs 7,0 GB/s không áp dụng (không có đĩa để flush)

Hai điều đáng chú ý:

  1. Số "ghi có buffer" của volume đánh lừa nếu tưởng đó là tốc độ đĩa thật. 11 GB/s là tốc độ ghi vào page cache của VM, chưa chắc đã xuống đĩa. Ép fsync mới thấy con số thật — 3 GB/s, chỉ bằng khoảng 27% số buffer.
  2. Bind mount không có khoảng cách đó — buffer và fsync ra cùng một con số, khoảng 2,1 GB/s. Chưa rõ vì sao virtiofs không thể hiện độ trễ ghi kiểu writeback như ext4. Không đoán thêm ở đây vì không kiểm chứng được trong phạm vi bài này, chỉ ghi lại đúng số đo được.

Kết luận thực dụng: so cùng điều kiện ép flush, named volume nhanh hơn bind mount khoảng 1,4 lần trên máy này — ngược với suy nghĩ phổ biến rằng "cả hai đều chỉ là thư mục nên tốc độ như nhau".

Vì sao bind mount chậm hơn — và vì sao điều đó KHÔNG đúng trên Linux thật

Suy nghĩ "bind mount và volume tốc độ như nhau" đúng trên Linux gốc: không có VM nào cả, container chạy thẳng trên kernel host, bind mount chỉ là một bind syscall trỏ vào cùng filesystem mà volume cũng đang nằm trên đó — không tầng dịch nào ở giữa.

Trên Docker Desktop, container luôn chạy trong một VM Linux ẩn. Named volume sống trong VM đó nên đọc/ghi thẳng vào ext4 của VM. Bind mount phải xuyên ra ngoài VM để chạm filesystem thật của macOS, đi qua virtiofs — khoảng cách 2,1 so với 3 GB/s chính là cái giá của vòng đó, không phải giá của "bind mount" tự thân. Số đo trong bài này chỉ đúng cho máy chạy Docker Desktop; trên một máy chủ Linux thật, khoảng cách này có thể biến mất hoặc đảo chiều.

Đo độ bền: cái gì sống sót, cái gì không

Volume và bind mount đều sống sót qua việc xoá container — đúng như tài liệu Docker mô tả, đã kiểm chứng lại:

$ docker run --rm -v bench-vol:/data alpine:3.20 sh -c "echo hello > /data/f.txt"
$ docker run --rm -v bench-vol:/data alpine:3.20 cat /data/f.txt
hello

Bind mount còn đọc được thẳng từ macOS mà không cần container nào đang chạy — vì nó vốn dĩ chỉ là một thư mục thật.

tmpfs thì khác hẳn. Hiển nhiên: hai container tách biệt không chia sẻ tmpfs, vì không có khái niệm "tmpfs đặt tên" như volume để gắn lại. Điều không hiển nhiên và đã đo thật: dữ liệu tmpfs biến mất kể cả khi không xoá container, chỉ stop rồi start lại đúng container đó:

$ docker run -d --name t --tmpfs /data:rw,size=64m alpine:3.20 sleep 3600
$ docker exec t sh -c "echo hello > /data/f.txt"
$ docker stop t && docker start t
$ docker exec t cat /data/f.txt
cat: can't open '/data/f.txt': No such file or directory

Writable layer của container vẫn sống qua stop/start — chỉ tmpfs bị dọn sạch, vì nó là một mount tách rời được cấp phát lại mỗi lần container chuyển từ trạng thái dừng sang chạy. Ứng dụng nào giả định "container còn sống là dữ liệu tmpfs còn" sẽ sai ngay ở thao tác tưởng như vô hại nhất.

Thêm một điều đáng nhớ: size khai cho --tmpfs là giới hạn thật, không phải gợi ý. Ghi quá 100 MB vào tmpfs khai size=100m báo lỗi hết dung lượng như một ổ đĩa thật:

$ docker run --rm --tmpfs /data:rw,size=100m alpine:3.20 \
    sh -c "dd if=/dev/zero of=/data/big.img bs=1M count=200"
dd: error writing '/data/big.img': No space left on device
100+0 records out

Vì tmpfs ăn trực tiếp vào RAM của VM, một container quên đặt size (mặc định nửa RAM cấp cho VM) có thể chiếm hết bộ nhớ, kéo container khác cùng máy vào swap hoặc bị OOM-kill.

Bảng tổng hợp

Tiêu chí Volume Bind mount tmpfs
Tốc độ ghi (fsync) ~3,0 GB/s ~2,1 GB/s không cần fsync
Sống qua rm container Không
Sống qua stop/start cùng container Không
Sửa trực tiếp từ host không qua container Khó (Docker Desktop giấu VM) Dễ Không thể (RAM)
Có giới hạn dung lượng cứng Theo đĩa VM Theo đĩa host Theo size khai báo
Phù hợp Dữ liệu ứng dụng cần bền: DB, upload Mount code khi dev, file cấu hình từ host Cache tạm, secret không muốn ghi ra đĩa

Khi nào không nên dùng cái nào

  • Bind mount: tránh dùng cho production trên nhiều máy — đường dẫn host phải khớp tuyệt đối giữa các server, và UID/GID file trên host có thể lệch với user trong container.
  • tmpfs: tránh dùng cho bất cứ thứ gì cần sống qua một lần restart, kể cả restart do healthcheck tự động gây ra — bài này vừa đo stop/start xoá sạch tmpfs.
  • Volume: bất tiện nếu mục đích là sửa file bằng editor trên host lúc dev — nằm trong VM, phải qua docker exec/docker cp mới xem được, bind mount tiện hơn nhiều cho việc này.

Tổng kết

Ba kiểu lưu trữ của Docker không phải ba biến thể của cùng một thứ. Đo được: named volume nhanh hơn bind mount khoảng 1,4 lần khi ép ghi bền trên Docker Desktop, nhưng khoảng cách đó đến từ tầng virtiofs giữa VM và macOS chứ không phải bản chất bind mount — trên Linux thật có thể không còn. Volume và bind mount đều sống qua việc xoá container; tmpfs thì không, và bất ngờ nhất là nó cũng không sống nổi qua một lần stop/start bình thường, không cần ai gõ rm.

Thử ba mươi giây

Tái hiện đúng phát hiện bất ngờ nhất của bài — tmpfs mất dữ liệu chỉ vì stop rồi start, không cần xoá container:

docker run -d --name t --tmpfs /data:rw,size=64m alpine:3.20 sleep 60
docker exec t sh -c "echo hi > /data/f.txt && cat /data/f.txt"
docker stop t && docker start t
docker exec t cat /data/f.txt   # No such file or directory
docker rm -f t

Bài viết liên quan