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 và :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.