Phần 41 đo tốc độ ghi tuần tự và thấy volume với bind mount gần như bằng nhau — 993,7 so với 966,6 MB/s. Điều đó có vẻ mâu thuẫn với danh tiếng "bind mount trên macOS chậm kinh khủng".

Bài này đo đúng loại tải mà danh tiếng đó nói tới. Bắt đầu bằng con số quan trọng nhất: giải nén một kho 4.401 tệp nhỏ, kiểu npm install hay git checkout.

Volume Bind mount
macOS / Docker Desktop 228 ms 3.027 ms
Linux 201 ms 234 ms

Trên macOS, bind mount chậm hơn 13,3 lần. Trên Linux, chênh lệch 16% — cùng cỡ với nhiễu giữa các lần chạy.

Đây là toàn bộ câu chuyện: vấn đề không nằm ở bind mount, nó nằm ở ranh giới giữa macOS và máy ảo Linux mà Docker Desktop phải bắc qua. Phần 41 đã thấy dấu hiệu: bên trong container, bind mount có hệ thống tệp tên fakeowner, còn volume là ext4 thẳng.

Chi tiết từng thao tác

3.000 tệp 512 byte, đơn vị mili giây.

macOS / Docker Desktop:

Phép đo Volume Bind tmpfs Bind chậm hơn
Tạo 3.000 tệp 46 713 29 15,5×
stat 3.000 tệp 3 262 3 87×
Đọc 3.000 tệp 20 332 18 16,6×
listdir 50 lần 26 483 18 18,6×
Đổi tên 3.000 tệp 18 807 8 45×
Xoá 3.000 tệp 9 481 3 53×
200 lần ghi+fsync 119 21 1 bind nhanh hơn 5,7×

Linux thật:

Phép đo Volume Bind tmpfs
Tạo 3.000 tệp 68 78 34
stat 3.000 tệp 3 3 3
Đọc 3.000 tệp 19 19 17
listdir 50 lần 25 24 15
Đổi tên 3.000 tệp 23 32 8
Xoá 3.000 tệp 9 12 3
200 lần ghi+fsync 139 126 1

Trên Linux, hai cột không phân biệt được. Trên macOS, mọi thao tác siêu dữ liệu đều đắt khủng khiếp — stat gấp 87 lần là con số tệ nhất, và stat chính là thứ mà trình biên dịch, bundler và trình theo dõi tệp gọi hàng chục nghìn lần.

Đó là lý do npm install trong container trên Mac chậm đến mức khó chịu, trong khi docker build tải về 500 MB image thì vẫn nhanh: một bên là hàng vạn thao tác nhỏ, một bên là một luồng byte lớn.

Dòng fsync đi ngược, và tôi không kết luận chắc

Có đúng một dòng bind mount thắng, và nó thắng đậm. Tôi đo lại riêng, 1.000 lần fsync, ba lần chạy:

Mili giây mỗi lần fsync
macOS, volume 0,620 / 0,686 / 0,662
macOS, bind 0,128 / 0,106 / 0,146
Linux, volume 0,669 / 0,613 / 0,603
Linux, bind 0,663 / 0,619 / 0,588

Ba trong bốn trường hợp cho khoảng 0,6 ms mỗi lần — con số hợp lý cho một rào chắn ghi thật xuống SSD. Riêng bind mount trên macOS rẻ hơn năm lần, gần với tmpfs hơn là với đĩa.

Điều đó gợi ý rằng fsync đi qua ranh giới đó không cho cùng một mức bảo đảm. Nhưng tôi không kiểm chứng được: muốn biết chắc thì phải cắt điện giữa chừng rồi xem dữ liệu còn không, mà tôi không có cách làm thí nghiệm đó cho đàng hoàng. Nên tôi để nguyên: con số là thật, cách giải thích là phỏng đoán.

Dù sao thì kết luận thực dụng không đổi — đừng chạy CSDL trên bind mount ở macOS. Nếu fsync rẻ vì nó không thật sự bền, đó là thứ tệ nhất có thể xảy ra với một CSDL.

:delegated:cached không còn tác dụng gì

Lời khuyên rất phổ biến cho macOS là thêm :delegated hoặc :cached vào bind mount. Đo bằng chính phép giải nén 4.401 tệp, mỗi cấu hình hai lần:

Cấu hình Hai lần chạy
bind (không cờ) 3.382 ms / 3.065 ms
bind:delegated 3.179 ms / 3.278 ms
bind:cached 3.178 ms / 3.390 ms

Không có khác biệt nào. Dao động giữa hai lần chạy của cùng một cấu hình (3.382 so với 3.065) còn lớn hơn khoảng cách giữa các cấu hình.

Hai cờ này có ý nghĩa thật thời osxfs, cơ chế cũ của Docker Desktop. Docker Desktop hiện tại dùng VirtioFS và chấp nhận cờ mà không làm gì — không cảnh báo, không lỗi. Lời khuyên vẫn còn nguyên trên mạng, chỉ có tác dụng đã biến mất.

Cách vá thật: volume đè lên đúng thư mục nặng

Bạn vẫn cần bind mount mã nguồn — đó là cả điểm của việc phát triển trong container. Nhưng thư mục phụ thuộc thì không cần đồng bộ về máy:

services:
  app:
    volumes:
      - ./:/app                 # ma nguon: bind, de sua truc tiep
      - deps:/app/node_modules  # thu muc nang: volume
volumes:
  deps:

Đo bằng đúng phép giải nén ban đầu:

Cách gắn Thời gian
Toàn bộ là bind mount 3.027 ms
Bind mã nguồn + volume cho thư mục nặng 290 ms

Nhanh hơn 10,4 lần, và bạn vẫn sửa mã trên máy rồi thấy thay đổi ngay trong container.

Cách này áp dụng được cho node_modules, vendor của PHP, target của Maven và Rust, .venv của Python, thư mục .next/dist của bundler — tất cả những thư mục sinh ra bởi công cụ chứ không do bạn gõ tay, và bạn không cần nhìn thấy chúng trên máy.

Nhắc lại từ phần 41: volume rỗng gắn lên thư mục có sẵn nội dung sẽ được nạp mồi từ image. Nên nếu Dockerfile của bạn đã chạy npm install, volume sẽ có sẵn nội dung đó ở lần chạy đầu tiên.

Rút ra

Bạn đang ở Chọn gì
Linux, mọi trường hợp Cái nào cũng được — chọn theo ý nghĩa, không theo tốc độ
macOS, dữ liệu ứng dụng Volume, luôn luôn
macOS, mã nguồn khi phát triển Bind mount, kèm volume cho thư mục phụ thuộc
macOS, CSDL Volume, không bao giờ bind mount
Tệp tạm, bộ đệm tmpfs — nhanh hơn tất cả ở mọi phép đo

Và một điều dễ quên: đừng đo trên Mac rồi kết luận cho máy chủ. Kiến trúc chậm 13 lần trên máy bạn có thể hoàn toàn ổn trên production Linux, và ngược lại, một cấu hình chạy tốt trên Mac có thể đang giấu một vấn đề độ bền mà Linux sẽ phơi bày.

Thử ba mươi giây

Đo chính máy bạn:

docker volume create thu-vol
mkdir -p ./thu-bind
for d in vol bind; do
  echo -n "$d: "
  docker run --rm -v thu-vol:/vol -v "$PWD/thu-bind":/bind python:3.12-alpine \
    python -c "
import os,time,shutil
p='/$d/t'; shutil.rmtree(p,ignore_errors=True); os.makedirs(p)
t=time.perf_counter()
for i in range(3000): open(f'{p}/f{i}','w').write('x'*512)
for i in range(3000): os.stat(f'{p}/f{i}')
print(f'{(time.perf_counter()-t)*1000:.0f} ms')
shutil.rmtree(p,ignore_errors=True)"
done
docker volume rm thu-vol && rm -rf ./thu-bind

Hai con số gần nhau nghĩa là bạn đang ở Linux hoặc trên một cấu hình không có ranh giới máy ảo. Chênh nhau chục lần nghĩa là mọi thư mục phụ thuộc trong dự án của bạn nên chuyển sang volume.

Phần sau đo cách sao lưu và khôi phục volume — và cái bẫy khi sao lưu một CSDL đang chạy.