Phần 41 đo một phép: ghi tuần tự 512 MB rồi ép fsync, kết luận named volume nhanh hơn bind mount khoảng 1,4 lần trên Docker Desktop. Một phép đo không đủ để kết luận "volume luôn nhanh hơn" — I/O thật hiếm khi là ghi tuần tự một file lớn. Bài này đo thêm ba kiểu truy cập: ghi ngẫu nhiên 4K (giống pattern database), đọc ngẫu nhiên 4K, và tạo nhiều file nhỏ (giống node_modules hay thư mục cache). Kết quả không phải "cùng chiều khác số" — thứ tự đảo ngược hoàn toàn ở phép đo thứ hai.

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, dùng fio 3.36 cho các phép đo ngẫu nhiên và dd (GNU coreutils, cài thêm qua apk) cho phép đo tuần tự.

Container nối tới bind mount qua virtiofs và tới named volume qua ext4 trên VM của Docker Desktop, đặt cạnh Linux gốc nơi cả hai cùng đi qua một filesystem thật

Bốn phép đo, một cách chạy

Chuẩn bị giống nhau cho cả bind mount và volume: một named volume rỗng và một thư mục rỗng trên macOS, gắn lần lượt vào container qua -v.

docker volume create bench-vol-io
mkdir -p /tmp/bench-bind-io

Bốn phép đo, chạy riêng cho từng đích:

  1. Tuần tự lớn: dd if=/dev/zero of=/data/seq.img bs=1M count=512 conv=fsync
  2. Ngẫu nhiên 4K ghi: fio --rw=randwrite --bs=4k --size=100m --ioengine=sync --fsync=1
  3. Ngẫu nhiên 4K đọc: cùng tham số nhưng --rw=randread, chạy sau khi đã có sẵn file 100 MB
  4. Tạo nhiều file nhỏ: vòng lặp dd bs=1024 count=1 tạo 1000 file 1 KB, đo bằng date +%s%N (BusyBox date không có %N, phải cài thêm coreutils)

Phép 2 và 4 chạy 3 lần lấy trung bình vì dao động rõ; phép 1 và 3 chạy ít lần hơn vì lệch nhỏ.

Ghi tuần tự lớn: volume vẫn thắng, đúng như phần trước

Đo lại nhanh để đối chiếu với phần 41:

Kiểu Hôm nay (1 lần) Phần 41 (trung bình 3 lần)
Named volume 2,6 GB/s 3,0 GB/s
Bind mount (virtiofs) 1,7 GB/s 2,1 GB/s

Cùng chiều, số tuyệt đối lệch nhau vì máy đang chạy việc khác lúc đo hôm nay — nhắc lại đúng lý do phần 41 đã đo 3 lần lấy trung bình thay vì tin một lần chạy. Tỉ lệ volume nhanh hơn bind mount dao động quanh 1,4–1,5 lần ở cả hai lần đo, đủ ổn định để coi là thật.

Ghi ngẫu nhiên 4K có fsync: thứ tự đảo ngược hoàn toàn

Đây là phép đo khác biệt nhất so với phần 41 — không phải khác về con số, mà khác về ai thắng:

Kiểu Băng thông IOPS
Named volume 12,7 MiB/s (~13,3 MB/s) ~3.260
Bind mount (virtiofs) 53,6 MiB/s (~56,2 MB/s) ~13.700

Bind mount nhanh hơn volume khoảng 4,2 lần ở phép đo này — ngược hẳn với ghi tuần tự, và ngược với suy nghĩ phổ biến rằng "volume luôn nhanh hơn bind mount vì ext4 nhanh hơn virtiofs". Chạy 3 lần cho mỗi bên, sai lệch giữa các lần dưới 5% — không phải nhiễu đo.

Có một giả thuyết hợp lý cho khoảng cách này nhưng chưa kiểm chứng được trong phạm vi bài viết: fsync() gọi từ container đi qua virtiofs cuối cùng chạm vào lớp file của macOS, mà macOS mặc định không đảm bảo flush xuống đĩa vật lý cho một lời gọi fsync() thường (chỉ F_FULLFSYNC mới đảm bảo) — nên có thể virtiofs coi đó là một thao tác rẻ. Trong khi đó mỗi fsync() trên ext4 của VM phải chờ một lần commit journal thật trên đĩa ảo. Đây là suy đoán dựa trên tài liệu công khai về hành vi fsync của hai hệ điều hành, không phải điều đã đo trực tiếp trong bài này — nói rõ ra vì viết như thể đã kiểm chứng sẽ sai với quy ước của sê-ri này.

Đọc ngẫu nhiên 4K và tạo nhiều file nhỏ: hai bức tranh khác nhau

Đọc ngẫu nhiên 4K (100 MB, đọc ngay sau khi ghi, nên phần lớn nằm trong cache — số đo dưới đây phản ánh chi phí phần mềm của từng đường đi hơn là tốc độ đĩa vật lý):

Kiểu Băng thông IOPS
Named volume 123 MiB/s ~31.500
Bind mount (virtiofs) 243 MiB/s ~62.300

Bind mount nhanh hơn khoảng 2 lần. Cùng hướng với ghi ngẫu nhiên 4K, khác hướng với ghi tuần tự.

Tạo 1000 file 1 KB (trung bình 3 lần, đơn vị mili giây):

Kiểu Thời gian
Named volume 416 ms
Bind mount (virtiofs) 611 ms

Ở đây volume nhanh hơn khoảng 1,5 lần — quay lại đúng chiều với ghi tuần tự. Tạo file nhỏ đụng nhiều đến chi phí mỗi lần gọi hệ thống (open, write, close) hơn là băng thông thô, và ở khía cạnh này virtiofs tỏ ra đắt hơn ext4 nội bộ VM.

Bảng tổng hợp bốn phép đo

Phép đo Ai nhanh hơn Tỉ lệ
Ghi tuần tự 512 MB + fsync Volume ~1,4–1,5 lần
Ghi ngẫu nhiên 4K + fsync Bind mount ~4,2 lần
Đọc ngẫu nhiên 4K Bind mount ~2,0 lần
Tạo 1000 file nhỏ Volume ~1,5 lần

Không có một "người thắng chung cuộc". Ứng dụng ghi log tuần tự hoặc build cây thư mục nhiều file nhỏ hợp với volume; ứng dụng kiểu database ghi ngẫu nhiên từng trang 4K kèm fsync mỗi giao dịch lại hợp với bind mount hơn — trên chính Docker Desktop macOS, trái với lời khuyên phổ biến "luôn dùng volume cho dữ liệu database".

Trên Linux gốc: khoảng cách này gần như chắc chắn không tồn tại

Toàn bộ bảng trên chỉ đúng vì có một tầng dịch — virtiofs — nằm giữa VM Linux ẩn của Docker Desktop và filesystem thật của macOS. Named volume nằm hẳn trong VM nên không đi qua tầng đó.

Trên một máy chủ Linux thật, không có VM nào cả: container dùng thẳng kernel host. Theo tài liệu chính thức của Docker, cả named volume lẫn bind mount đều chỉ là một thư mục thật trên cùng một filesystem host, gắn vào container bằng cùng một cơ chế bind mount ở tầng kernel — không có tầng dịch nào để tạo ra khoảng cách như bốn phép đo trên.

Phần này là suy luận từ kiến trúc, không phải số đo trong bài — máy chạy bài viết chỉ có macOS, không có máy Linux trần để kiểm chứng lại, nên nói thẳng thay vì đoán ra một con số nghe có vẻ có căn cứ. WSL2 không tính là "Linux trần" vì cũng chạy trong VM và có tầng dịch riêng cho /mnt/c. Có máy Linux thật thì lặp lại đúng bốn lệnh trên là cách duy nhất để biết chắc.

Chọn theo khối lượng công việc, không chọn theo tin đồn

Áp dụng cho việc phát triển trên Docker Desktop macOS, nơi bốn số đo trên là thật:

  • PostgreSQL, MySQL hay bất kỳ thứ gì ghi ngẫu nhiên kèm fsync mỗi giao dịch: nếu I/O là điểm nghẽn khi dev trên Mac, đo thử bind mount trước khi mặc định chọn volume — số đo ở đây cho thấy nó có thể nhanh hơn hẳn, ngược trực giác.
  • Mount code nguồn để hot-reload: vẫn nên là bind mount, vì mục đích chính là sửa file bằng editor trên host chứ không phải tốc độ.
  • node_modules hay thư mục có hàng chục nghìn file nhỏ: named volume — kiểu I/O duy nhất trong bốn phép đo mà volume thắng rõ và dứt khoát.
  • Trên Linux thật (kể cả production): khác biệt hiệu năng gần như không đáng kể theo kiến trúc; chọn theo tiêu chí khác — volume dễ quản lý bằng docker volume, bind mount dễ debug vì xem trực tiếp được bằng công cụ trên host.

Tổng kết

Bốn kiểu I/O cho bốn kết quả không đồng nhất: volume thắng ở ghi tuần tự lớn (1,4–1,5 lần) và tạo nhiều file nhỏ (1,5 lần), nhưng thua rõ rệt ở ghi ngẫu nhiên 4K có fsync (thua 4,2 lần) và đọc ngẫu nhiên 4K (thua 2 lần). "Volume luôn nhanh hơn bind mount" là kết luận chỉ đúng một nửa, rút ra từ việc chỉ đo đúng một kiểu I/O. Khoảng cách này là sản phẩm phụ của virtiofs trên Docker Desktop, và được kỳ vọng biến mất trên Linux gốc — nhưng đó là suy luận, chưa phải số đo.

Thử ba mươi giây

Tái hiện phép đo gây bất ngờ nhất — ghi ngẫu nhiên 4K trên bind mount nhanh hơn volume:

docker volume create bench-vol && mkdir -p /tmp/bench-bind

docker run --rm -v bench-vol:/data alpine:3.20 sh -c \
  'apk add --no-cache fio >/dev/null 2>&1; fio --name=t --directory=/data --size=50m --bs=4k --rw=randwrite --ioengine=sync --fsync=1 2>&1 | grep IOPS'

docker run --rm -v /tmp/bench-bind:/data alpine:3.20 sh -c \
  'apk add --no-cache fio >/dev/null 2>&1; fio --name=t --directory=/data --size=50m --bs=4k --rw=randwrite --ioengine=sync --fsync=1 2>&1 | grep IOPS'

docker volume rm bench-vol; rm -rf /tmp/bench-bind

Bài viết liên quan