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 mount và tmpfs. 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.
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ùngext4nằ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ú ý:
- 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
fsyncmới thấy con số thật — 3 GB/s, chỉ bằng khoảng 27% số buffer. - 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 |
Có | Có | Không |
Sống qua stop/start cùng container |
Có | Có | 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/startxoá 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 cpmớ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
- Ping 0,1 ms, curl treo 3004 ms: quy trình năm bước tìm ra luật iptables giấu trong container
- Container đứng trên hai mạng vẫn không tự thành router: dựng net-frontend và net-backend, đo cái gì gọi được cái gì
- Cùng container nhanh gần gấp đôi bridge ở tầng ping, nhưng HTTP thì gần hoà: đo độ trễ ba đường mạng Docker